Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View file

@ -0,0 +1,63 @@
CHANGELOG
=========
2.8.0
-----
* deprecated FileDumper::format(), overwrite FileDumper::formatCatalogue() instead.
* deprecated Translator::getMessages(), rely on TranslatorBagInterface::getCatalogue() instead.
* added `FileDumper::formatCatalogue` which allows format the catalogue without dumping it into file.
* added option `json_encoding` to JsonFileDumper
* added options `as_tree`, `inline` to YamlFileDumper
* added support for XLIFF 2.0.
* added support for XLIFF target and tool attributes.
* added message parameters to DataCollectorTranslator.
* [DEPRECATION] The `DiffOperation` class has been deprecated and
will be removed in Symfony 3.0, since its operation has nothing to do with 'diff',
so the class name is misleading. The `TargetOperation` class should be used for
this use-case instead.
2.7.0
-----
* added DataCollectorTranslator for collecting the translated messages.
2.6.0
-----
* added possibility to cache catalogues
* added TranslatorBagInterface
* added LoggingTranslator
* added Translator::getMessages() for retrieving the message catalogue as an array
2.5.0
-----
* added relative file path template to the file dumpers
* added optional backup to the file dumpers
* changed IcuResFileDumper to extend FileDumper
2.3.0
-----
* added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues)
* added Translator::getFallbackLocales()
* deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method
2.2.0
-----
* QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3.
* [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now
throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found
and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid.
* changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException
(IcuDatFileLoader, IcuResFileLoader and QtFileLoader)
2.1.0
-----
* added support for more than one fallback locale
* added support for extracting translation messages from templates (Twig and PHP)
* added dumpers for translation catalogs
* added support for QT, gettext, and ResourceBundles

View file

@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Base catalogues binary operation class.
*
* A catalogue binary operation performs operation on
* source (the left argument) and target (the right argument) catalogues.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
abstract class AbstractOperation implements OperationInterface
{
/**
* @var MessageCatalogueInterface The source catalogue
*/
protected $source;
/**
* @var MessageCatalogueInterface The target catalogue
*/
protected $target;
/**
* @var MessageCatalogue The result catalogue
*/
protected $result;
/**
* @var null|array The domains affected by this operation
*/
private $domains;
/**
* This array stores 'all', 'new' and 'obsolete' messages for all valid domains.
*
* The data structure of this array is as follows:
* ```php
* array(
* 'domain 1' => array(
* 'all' => array(...),
* 'new' => array(...),
* 'obsolete' => array(...)
* ),
* 'domain 2' => array(
* 'all' => array(...),
* 'new' => array(...),
* 'obsolete' => array(...)
* ),
* ...
* )
* ```
*
* @var array The array that stores 'all', 'new' and 'obsolete' messages
*/
protected $messages;
/**
* @param MessageCatalogueInterface $source The source catalogue
* @param MessageCatalogueInterface $target The target catalogue
*
* @throws \LogicException
*/
public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target)
{
if ($source->getLocale() !== $target->getLocale()) {
throw new \LogicException('Operated catalogues must belong to the same locale.');
}
$this->source = $source;
$this->target = $target;
$this->result = new MessageCatalogue($source->getLocale());
$this->domains = null;
$this->messages = array();
}
/**
* {@inheritdoc}
*/
public function getDomains()
{
if (null === $this->domains) {
$this->domains = array_values(array_unique(array_merge($this->source->getDomains(), $this->target->getDomains())));
}
return $this->domains;
}
/**
* {@inheritdoc}
*/
public function getMessages($domain)
{
if (!in_array($domain, $this->getDomains())) {
throw new \InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
}
if (!isset($this->messages[$domain]['all'])) {
$this->processDomain($domain);
}
return $this->messages[$domain]['all'];
}
/**
* {@inheritdoc}
*/
public function getNewMessages($domain)
{
if (!in_array($domain, $this->getDomains())) {
throw new \InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
}
if (!isset($this->messages[$domain]['new'])) {
$this->processDomain($domain);
}
return $this->messages[$domain]['new'];
}
/**
* {@inheritdoc}
*/
public function getObsoleteMessages($domain)
{
if (!in_array($domain, $this->getDomains())) {
throw new \InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
}
if (!isset($this->messages[$domain]['obsolete'])) {
$this->processDomain($domain);
}
return $this->messages[$domain]['obsolete'];
}
/**
* {@inheritdoc}
*/
public function getResult()
{
foreach ($this->getDomains() as $domain) {
if (!isset($this->messages[$domain])) {
$this->processDomain($domain);
}
}
return $this->result;
}
/**
* Performs operation on source and target catalogues for the given domain and
* stores the results.
*
* @param string $domain The domain which the operation will be performed for
*/
abstract protected function processDomain($domain);
}

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
@trigger_error('The '.__NAMESPACE__.'\DiffOperation class is deprecated since version 2.8 and will be removed in 3.0. Use the TargetOperation class in the same namespace instead.', E_USER_DEPRECATED);
/**
* Diff operation between two catalogues.
*
* The name of 'Diff' is misleading because the operation
* has nothing to do with diff:
*
* intersection = source target = {x: x source x target}
* all = intersection (target intersection) = target
* new = all source = {x: x target x source}
* obsolete = source all = source target = {x: x source x target}
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @deprecated since version 2.8, to be removed in 3.0. Use TargetOperation instead.
*/
class DiffOperation extends TargetOperation
{
}

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
/**
* Merge operation between two catalogues as follows:
* all = source target = {x: x source x target}
* new = all source = {x: x target x source}
* obsolete = source all = {x: x source x source x target} =
* Basically, the result contains messages from both catalogues.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class MergeOperation extends AbstractOperation
{
/**
* {@inheritdoc}
*/
protected function processDomain($domain)
{
$this->messages[$domain] = array(
'all' => array(),
'new' => array(),
'obsolete' => array(),
);
foreach ($this->source->all($domain) as $id => $message) {
$this->messages[$domain]['all'][$id] = $message;
$this->result->add(array($id => $message), $domain);
if (null !== $keyMetadata = $this->source->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
}
foreach ($this->target->all($domain) as $id => $message) {
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$this->result->add(array($id => $message), $domain);
if (null !== $keyMetadata = $this->target->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
}
}
}
}

View file

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Represents an operation on catalogue(s).
*
* An instance of this interface performs an operation on one or more catalogues and
* stores intermediate and final results of the operation.
*
* The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and
* the following results are stored:
*
* Messages: also called 'all', are valid messages for the given domain after the operation is performed.
*
* New Messages: also called 'new' (new = all source = {x: x all x source}).
*
* Obsolete Messages: also called 'obsolete' (obsolete = source all = {x: x source x all}).
*
* Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
interface OperationInterface
{
/**
* Returns domains affected by operation.
*
* @return array
*/
public function getDomains();
/**
* Returns all valid messages ('all') after operation.
*
* @param string $domain
*
* @return array
*/
public function getMessages($domain);
/**
* Returns new messages ('new') after operation.
*
* @param string $domain
*
* @return array
*/
public function getNewMessages($domain);
/**
* Returns obsolete messages ('obsolete') after operation.
*
* @param string $domain
*
* @return array
*/
public function getObsoleteMessages($domain);
/**
* Returns resulting catalogue ('result').
*
* @return MessageCatalogueInterface
*/
public function getResult();
}

View file

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
/**
* Target operation between two catalogues:
* intersection = source target = {x: x source x target}
* all = intersection (target intersection) = target
* new = all source = {x: x target x source}
* obsolete = source all = source target = {x: x source x target}
* Basically, the result contains messages from the target catalogue.
*
* @author Michael Lee <michael.lee@zerustech.com>
*/
class TargetOperation extends AbstractOperation
{
/**
* {@inheritdoc}
*/
protected function processDomain($domain)
{
$this->messages[$domain] = array(
'all' => array(),
'new' => array(),
'obsolete' => array(),
);
// For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``,
// because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
//
// For 'new' messages, the code can't be simplied as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));``
// because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback}
//
// For 'obsolete' messages, the code can't be simplifed as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))``
// because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
foreach ($this->source->all($domain) as $id => $message) {
if ($this->target->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->result->add(array($id => $message), $domain);
if (null !== $keyMetadata = $this->source->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
} else {
$this->messages[$domain]['obsolete'][$id] = $message;
}
}
foreach ($this->target->all($domain) as $id => $message) {
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$this->result->add(array($id => $message), $domain);
if (null !== $keyMetadata = $this->target->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
}
}
}
}

View file

@ -0,0 +1,150 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Translation\DataCollectorTranslator;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var DataCollectorTranslator
*/
private $translator;
/**
* @param DataCollectorTranslator $translator
*/
public function __construct(DataCollectorTranslator $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
$messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages());
$this->data = $this->computeCount($messages);
$this->data['messages'] = $messages;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
}
/**
* @return array
*/
public function getMessages()
{
return isset($this->data['messages']) ? $this->data['messages'] : array();
}
/**
* @return int
*/
public function getCountMissings()
{
return isset($this->data[DataCollectorTranslator::MESSAGE_MISSING]) ? $this->data[DataCollectorTranslator::MESSAGE_MISSING] : 0;
}
/**
* @return int
*/
public function getCountFallbacks()
{
return isset($this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK]) ? $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] : 0;
}
/**
* @return int
*/
public function getCountDefines()
{
return isset($this->data[DataCollectorTranslator::MESSAGE_DEFINED]) ? $this->data[DataCollectorTranslator::MESSAGE_DEFINED] : 0;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'translation';
}
private function sanitizeCollectedMessages($messages)
{
$result = array();
foreach ($messages as $key => $message) {
$messageId = $message['locale'].$message['domain'].$message['id'];
if (!isset($result[$messageId])) {
$message['count'] = 1;
$message['parameters'] = !empty($message['parameters']) ? array($message['parameters']) : array();
$messages[$key]['translation'] = $this->sanitizeString($message['translation']);
$result[$messageId] = $message;
} else {
if (!empty($message['parameters'])) {
$result[$messageId]['parameters'][] = $message['parameters'];
}
++$result[$messageId]['count'];
}
unset($messages[$key]);
}
return $result;
}
private function computeCount($messages)
{
$count = array(
DataCollectorTranslator::MESSAGE_DEFINED => 0,
DataCollectorTranslator::MESSAGE_MISSING => 0,
DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0,
);
foreach ($messages as $message) {
++$count[$message['state']];
}
return $count;
}
private function sanitizeString($string, $length = 80)
{
$string = trim(preg_replace('/\s+/', ' ', $string));
if (false !== $encoding = mb_detect_encoding($string, null, true)) {
if (mb_strlen($string, $encoding) > $length) {
return mb_substr($string, 0, $length - 3, $encoding).'...';
}
} elseif (strlen($string) > $length) {
return substr($string, 0, $length - 3).'...';
}
return $string;
}
}

View file

@ -0,0 +1,166 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface
{
const MESSAGE_DEFINED = 0;
const MESSAGE_MISSING = 1;
const MESSAGE_EQUALS_FALLBACK = 2;
/**
* @var TranslatorInterface|TranslatorBagInterface
*/
private $translator;
/**
* @var array
*/
private $messages = array();
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
*/
public function __construct(TranslatorInterface $translator)
{
if (!$translator instanceof TranslatorBagInterface) {
throw new \InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', get_class($translator)));
}
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
$trans = $this->translator->trans($id, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters);
return $trans;
}
/**
* {@inheritdoc}
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters, $number);
return $trans;
}
/**
* {@inheritdoc}
*/
public function setLocale($locale)
{
$this->translator->setLocale($locale);
}
/**
* {@inheritdoc}
*/
public function getLocale()
{
return $this->translator->getLocale();
}
/**
* {@inheritdoc}
*/
public function getCatalogue($locale = null)
{
return $this->translator->getCatalogue($locale);
}
/**
* Gets the fallback locales.
*
* @return array $locales The fallback locales
*/
public function getFallbackLocales()
{
if ($this->translator instanceof Translator) {
return $this->translator->getFallbackLocales();
}
return array();
}
/**
* Passes through all unknown calls onto the translator object.
*/
public function __call($method, $args)
{
return call_user_func_array(array($this->translator, $method), $args);
}
/**
* @return array
*/
public function getCollectedMessages()
{
return $this->messages;
}
/**
* @param string|null $locale
* @param string|null $domain
* @param string $id
* @param string $translation
* @param array|null $parameters
* @param int|null $number
*/
private function collectMessage($locale, $domain, $id, $translation, $parameters = array(), $number = null)
{
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->translator->getCatalogue($locale);
$locale = $catalogue->getLocale();
if ($catalogue->defines($id, $domain)) {
$state = self::MESSAGE_DEFINED;
} elseif ($catalogue->has($id, $domain)) {
$state = self::MESSAGE_EQUALS_FALLBACK;
$fallbackCatalogue = $catalogue->getFallbackCatalogue();
while ($fallbackCatalogue) {
if ($fallbackCatalogue->defines($id, $domain)) {
$locale = $fallbackCatalogue->getLocale();
break;
}
$fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
}
} else {
$state = self::MESSAGE_MISSING;
}
$this->messages[] = array(
'locale' => $locale,
'domain' => $domain,
'id' => $id,
'translation' => $translation,
'parameters' => $parameters,
'transChoiceNumber' => $number,
'state' => $state,
);
}
}

View file

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* CsvFileDumper generates a csv formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class CsvFileDumper extends FileDumper
{
private $delimiter = ';';
private $enclosure = '"';
/**
* {@inheritdoc}
*/
public function format(MessageCatalogue $messages, $domain = 'messages')
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
$handle = fopen('php://memory', 'rb+');
foreach ($messages->all($domain) as $source => $target) {
fputcsv($handle, array($source, $target), $this->delimiter, $this->enclosure);
}
rewind($handle);
$output = stream_get_contents($handle);
fclose($handle);
return $output;
}
/**
* Sets the delimiter and escape character for CSV.
*
* @param string $delimiter delimiter character
* @param string $enclosure enclosure character
*/
public function setCsvControl($delimiter = ';', $enclosure = '"')
{
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'csv';
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* DumperInterface is the interface implemented by all translation dumpers.
* There is no common option.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
interface DumperInterface
{
/**
* Dumps the message catalogue.
*
* @param MessageCatalogue $messages The message catalogue
* @param array $options Options that are used by the dumper
*/
public function dump(MessageCatalogue $messages, $options = array());
}

View file

@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s).
* Performs backup of already existing files.
*
* Options:
* - path (mandatory): the directory where the files should be saved
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
abstract class FileDumper implements DumperInterface
{
/**
* A template for the relative paths to files.
*
* @var string
*/
protected $relativePathTemplate = '%domain%.%locale%.%extension%';
/**
* Make file backup before the dump.
*
* @var bool
*/
private $backup = true;
/**
* Sets the template for the relative paths to files.
*
* @param string $relativePathTemplate A template for the relative paths to files
*/
public function setRelativePathTemplate($relativePathTemplate)
{
$this->relativePathTemplate = $relativePathTemplate;
}
/**
* Sets backup flag.
*
* @param bool
*/
public function setBackup($backup)
{
$this->backup = $backup;
}
/**
* {@inheritdoc}
*/
public function dump(MessageCatalogue $messages, $options = array())
{
if (!array_key_exists('path', $options)) {
throw new \InvalidArgumentException('The file dumper needs a path option.');
}
// save a file for each domain
foreach ($messages->getDomains() as $domain) {
// backup
$fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale());
if (file_exists($fullpath)) {
if ($this->backup) {
copy($fullpath, $fullpath.'~');
}
} else {
$directory = dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
throw new \RuntimeException(sprintf('Unable to create directory "%s".', $directory));
}
}
// save file
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
}
/**
* Transforms a domain of a message catalogue to its string representation.
*
* Override this function in child class if $options is used for message formatting.
*
* @param MessageCatalogue $messages
* @param string $domain
* @param array $options
*
* @return string representation
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
@trigger_error('The '.__METHOD__.' method will replace the format method in 3.0. You should overwrite it instead of overwriting format instead.', E_USER_DEPRECATED);
return $this->format($messages, $domain);
}
/**
* Transforms a domain of a message catalogue to its string representation.
*
* @param MessageCatalogue $messages
* @param string $domain
*
* @return string representation
*
* @deprecated since version 2.8, to be removed in 3.0. Overwrite formatCatalogue() instead.
*/
protected function format(MessageCatalogue $messages, $domain)
{
throw new \LogicException('The "FileDumper::format" method needs to be overwritten, you should implement either "format" or "formatCatalogue".');
}
/**
* Gets the file extension of the dumper.
*
* @return string file extension
*/
abstract protected function getExtension();
/**
* Gets the relative file path using the template.
*
* @param string $domain The domain
* @param string $locale The locale
*
* @return string The relative file path
*/
private function getRelativePath($domain, $locale)
{
return strtr($this->relativePathTemplate, array(
'%domain%' => $domain,
'%locale%' => $locale,
'%extension%' => $this->getExtension(),
));
}
}

View file

@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class IcuResFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
protected $relativePathTemplate = '%domain%/%locale%.%extension%';
/**
* {@inheritdoc}
*/
public function format(MessageCatalogue $messages, $domain = 'messages')
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
$data = $indexes = $resources = '';
foreach ($messages->all($domain) as $source => $target) {
$indexes .= pack('v', strlen($data) + 28);
$data .= $source."\0";
}
$data .= $this->writePadding($data);
$keyTop = $this->getPosition($data);
foreach ($messages->all($domain) as $source => $target) {
$resources .= pack('V', $this->getPosition($data));
$data .= pack('V', strlen($target))
.mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8')
.$this->writePadding($data)
;
}
$resOffset = $this->getPosition($data);
$data .= pack('v', count($messages))
.$indexes
.$this->writePadding($data)
.$resources
;
$bundleTop = $this->getPosition($data);
$root = pack('V7',
$resOffset + (2 << 28), // Resource Offset + Resource Type
6, // Index length
$keyTop, // Index keys top
$bundleTop, // Index resources top
$bundleTop, // Index bundle top
count($messages), // Index max table length
0 // Index attributes
);
$header = pack('vC2v4C12@32',
32, // Header size
0xDA, 0x27, // Magic number 1 and 2
20, 0, 0, 2, // Rest of the header, ..., Size of a char
0x52, 0x65, 0x73, 0x42, // Data format identifier
1, 2, 0, 0, // Data version
1, 4, 0, 0 // Unicode version
);
return $header.$root.$data;
}
private function writePadding($data)
{
$padding = strlen($data) % 4;
if ($padding) {
return str_repeat("\xAA", 4 - $padding);
}
}
private function getPosition($data)
{
return (strlen($data) + 28) / 4;
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'res';
}
}

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* IniFileDumper generates an ini formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class IniFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
public function format(MessageCatalogue $messages, $domain = 'messages')
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
$output = '';
foreach ($messages->all($domain) as $source => $target) {
$escapeTarget = str_replace('"', '\"', $target);
$output .= $source.'="'.$escapeTarget."\"\n";
}
return $output;
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'ini';
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* JsonFileDumper generates an json formatted string representation of a message catalogue.
*
* @author singles
*/
class JsonFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
public function format(MessageCatalogue $messages, $domain = 'messages')
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
if (isset($options['json_encoding'])) {
$flags = $options['json_encoding'];
} else {
$flags = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
}
return json_encode($messages->all($domain), $flags);
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'json';
}
}

View file

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Loader\MoFileLoader;
/**
* MoFileDumper generates a gettext formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class MoFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
public function format(MessageCatalogue $messages, $domain = 'messages')
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
$sources = $targets = $sourceOffsets = $targetOffsets = '';
$offsets = array();
$size = 0;
foreach ($messages->all($domain) as $source => $target) {
$offsets[] = array_map('strlen', array($sources, $source, $targets, $target));
$sources .= "\0".$source;
$targets .= "\0".$target;
++$size;
}
$header = array(
'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC,
'formatRevision' => 0,
'count' => $size,
'offsetId' => MoFileLoader::MO_HEADER_SIZE,
'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size),
'sizeHashes' => 0,
'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size),
);
$sourcesSize = strlen($sources);
$sourcesStart = $header['offsetHashes'] + 1;
foreach ($offsets as $offset) {
$sourceOffsets .= $this->writeLong($offset[1])
.$this->writeLong($offset[0] + $sourcesStart);
$targetOffsets .= $this->writeLong($offset[3])
.$this->writeLong($offset[2] + $sourcesStart + $sourcesSize);
}
$output = implode(array_map(array($this, 'writeLong'), $header))
.$sourceOffsets
.$targetOffsets
.$sources
.$targets
;
return $output;
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'mo';
}
private function writeLong($str)
{
return pack('V*', $str);
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* PhpFileDumper generates PHP files from a message catalogue.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
class PhpFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
protected function format(MessageCatalogue $messages, $domain)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
return "<?php\n\nreturn ".var_export($messages->all($domain), true).";\n";
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'php';
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* PoFileDumper generates a gettext formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class PoFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
public function format(MessageCatalogue $messages, $domain = 'messages')
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
$output = 'msgid ""'."\n";
$output .= 'msgstr ""'."\n";
$output .= '"Content-Type: text/plain; charset=UTF-8\n"'."\n";
$output .= '"Content-Transfer-Encoding: 8bit\n"'."\n";
$output .= '"Language: '.$messages->getLocale().'\n"'."\n";
$output .= "\n";
$newLine = false;
foreach ($messages->all($domain) as $source => $target) {
if ($newLine) {
$output .= "\n";
} else {
$newLine = true;
}
$output .= sprintf('msgid "%s"'."\n", $this->escape($source));
$output .= sprintf('msgstr "%s"', $this->escape($target));
}
return $output;
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'po';
}
private function escape($str)
{
return addcslashes($str, "\0..\37\42\134");
}
}

View file

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* QtFileDumper generates ts files from a message catalogue.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class QtFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
public function format(MessageCatalogue $messages, $domain)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
$dom = new \DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$ts = $dom->appendChild($dom->createElement('TS'));
$context = $ts->appendChild($dom->createElement('context'));
$context->appendChild($dom->createElement('name', $domain));
foreach ($messages->all($domain) as $source => $target) {
$message = $context->appendChild($dom->createElement('message'));
$message->appendChild($dom->createElement('source', $source));
$message->appendChild($dom->createElement('translation', $target));
}
return $dom->saveXML();
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'ts';
}
}

View file

@ -0,0 +1,193 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* XliffFileDumper generates xliff files from a message catalogue.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
class XliffFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
$xliffVersion = '1.2';
if (array_key_exists('xliff_version', $options)) {
$xliffVersion = $options['xliff_version'];
}
if (array_key_exists('default_locale', $options)) {
$defaultLocale = $options['default_locale'];
} else {
$defaultLocale = \Locale::getDefault();
}
if ('1.2' === $xliffVersion) {
return $this->dumpXliff1($defaultLocale, $messages, $domain, $options);
}
if ('2.0' === $xliffVersion) {
return $this->dumpXliff2($defaultLocale, $messages, $domain, $options);
}
throw new \InvalidArgumentException(sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion));
}
/**
* {@inheritdoc}
*/
protected function format(MessageCatalogue $messages, $domain)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'xlf';
}
private function dumpXliff1($defaultLocale, MessageCatalogue $messages, $domain, array $options = array())
{
$toolInfo = array('tool-id' => 'symfony', 'tool-name' => 'Symfony');
if (array_key_exists('tool_info', $options)) {
$toolInfo = array_merge($toolInfo, $options['tool_info']);
}
$dom = new \DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$xliff = $dom->appendChild($dom->createElement('xliff'));
$xliff->setAttribute('version', '1.2');
$xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2');
$xliffFile = $xliff->appendChild($dom->createElement('file'));
$xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale));
$xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale()));
$xliffFile->setAttribute('datatype', 'plaintext');
$xliffFile->setAttribute('original', 'file.ext');
$xliffHead = $xliffFile->appendChild($dom->createElement('header'));
$xliffTool = $xliffHead->appendChild($dom->createElement('tool'));
foreach ($toolInfo as $id => $value) {
$xliffTool->setAttribute($id, $value);
}
$xliffBody = $xliffFile->appendChild($dom->createElement('body'));
foreach ($messages->all($domain) as $source => $target) {
$translation = $dom->createElement('trans-unit');
$translation->setAttribute('id', md5($source));
$translation->setAttribute('resname', $source);
$s = $translation->appendChild($dom->createElement('source'));
$s->appendChild($dom->createTextNode($source));
// Does the target contain characters requiring a CDATA section?
$text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target);
$targetElement = $dom->createElement('target');
$metadata = $messages->getMetadata($source, $domain);
if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) {
foreach ($metadata['target-attributes'] as $name => $value) {
$targetElement->setAttribute($name, $value);
}
}
$t = $translation->appendChild($targetElement);
$t->appendChild($text);
if ($this->hasMetadataArrayInfo('notes', $metadata)) {
foreach ($metadata['notes'] as $note) {
if (!isset($note['content'])) {
continue;
}
$n = $translation->appendChild($dom->createElement('note'));
$n->appendChild($dom->createTextNode($note['content']));
if (isset($note['priority'])) {
$n->setAttribute('priority', $note['priority']);
}
if (isset($note['from'])) {
$n->setAttribute('from', $note['from']);
}
}
}
$xliffBody->appendChild($translation);
}
return $dom->saveXML();
}
private function dumpXliff2($defaultLocale, MessageCatalogue $messages, $domain, array $options = array())
{
$dom = new \DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$xliff = $dom->appendChild($dom->createElement('xliff'));
$xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0');
$xliff->setAttribute('version', '2.0');
$xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale));
$xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale()));
$xliffFile = $xliff->appendChild($dom->createElement('file'));
$xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale());
foreach ($messages->all($domain) as $source => $target) {
$translation = $dom->createElement('unit');
$translation->setAttribute('id', md5($source));
$segment = $translation->appendChild($dom->createElement('segment'));
$s = $segment->appendChild($dom->createElement('source'));
$s->appendChild($dom->createTextNode($source));
// Does the target contain characters requiring a CDATA section?
$text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target);
$targetElement = $dom->createElement('target');
$metadata = $messages->getMetadata($source, $domain);
if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) {
foreach ($metadata['target-attributes'] as $name => $value) {
$targetElement->setAttribute($name, $value);
}
}
$t = $segment->appendChild($targetElement);
$t->appendChild($text);
$xliffFile->appendChild($translation);
}
return $dom->saveXML();
}
/**
* @param string $key
* @param array|null $metadata
*
* @return bool
*/
private function hasMetadataArrayInfo($key, $metadata = null)
{
return null !== $metadata && array_key_exists($key, $metadata) && ($metadata[$key] instanceof \Traversable || is_array($metadata[$key]));
}
}

View file

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Util\ArrayConverter;
use Symfony\Component\Yaml\Yaml;
/**
* YamlFileDumper generates yaml files from a message catalogue.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
class YamlFileDumper extends FileDumper
{
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
if (!class_exists('Symfony\Component\Yaml\Yaml')) {
throw new \LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.');
}
$data = $messages->all($domain);
if (isset($options['as_tree']) && $options['as_tree']) {
$data = ArrayConverter::expandToTree($data);
}
if (isset($options['inline']) && ($inline = (int) $options['inline']) > 0) {
return Yaml::dump($data, $inline);
}
return Yaml::dump($data);
}
/**
* {@inheritdoc}
*/
protected function format(MessageCatalogue $messages, $domain)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the formatCatalogue() method instead.', E_USER_DEPRECATED);
return $this->formatCatalogue($messages, $domain);
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'yml';
}
}

View file

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
/**
* Exception interface for all exceptions thrown by the component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ExceptionInterface
{
}

View file

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
/**
* Thrown when a resource cannot be loaded.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class InvalidResourceException extends \InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
/**
* Thrown when a resource does not exist.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class NotFoundResourceException extends \InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Extractor;
/**
* Base class used by classes that extract translation messages from files.
*
* @author Marcos D. Sánchez <marcosdsanchez@gmail.com>
*/
abstract class AbstractFileExtractor
{
/**
* @param string|array $resource files, a file or a directory
*
* @return array
*/
protected function extractFiles($resource)
{
if (is_array($resource) || $resource instanceof \Traversable) {
$files = array();
foreach ($resource as $file) {
if ($this->canBeExtracted($file)) {
$files[] = $this->toSplFileInfo($file);
}
}
} elseif (is_file($resource)) {
$files = $this->canBeExtracted($resource) ? array($this->toSplFileInfo($resource)) : array();
} else {
$files = $this->extractFromDirectory($resource);
}
return $files;
}
/**
* @param string $file
*
* @return \SplFileInfo
*/
private function toSplFileInfo($file)
{
return ($file instanceof \SplFileInfo) ? $file : new \SplFileInfo($file);
}
/**
* @param string $file
*
* @return bool
*
* @throws \InvalidArgumentException
*/
protected function isFile($file)
{
if (!is_file($file)) {
throw new \InvalidArgumentException(sprintf('The "%s" file does not exist.', $file));
}
return true;
}
/**
* @param string $file
*
* @return bool
*/
abstract protected function canBeExtracted($file);
/**
* @param string|array $resource files, a file or a directory
*
* @return array files to be extracted
*/
abstract protected function extractFromDirectory($resource);
}

View file

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Extractor;
use Symfony\Component\Translation\MessageCatalogue;
/**
* ChainExtractor extracts translation messages from template files.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
class ChainExtractor implements ExtractorInterface
{
/**
* The extractors.
*
* @var ExtractorInterface[]
*/
private $extractors = array();
/**
* Adds a loader to the translation extractor.
*
* @param string $format The format of the loader
* @param ExtractorInterface $extractor The loader
*/
public function addExtractor($format, ExtractorInterface $extractor)
{
$this->extractors[$format] = $extractor;
}
/**
* {@inheritdoc}
*/
public function setPrefix($prefix)
{
foreach ($this->extractors as $extractor) {
$extractor->setPrefix($prefix);
}
}
/**
* {@inheritdoc}
*/
public function extract($directory, MessageCatalogue $catalogue)
{
foreach ($this->extractors as $extractor) {
$extractor->extract($directory, $catalogue);
}
}
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Extractor;
use Symfony\Component\Translation\MessageCatalogue;
/**
* Extracts translation messages from a directory or files to the catalogue.
* New found messages are injected to the catalogue using the prefix.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
interface ExtractorInterface
{
/**
* Extracts translation messages from files, a file or a directory to the catalogue.
*
* @param string|array $resource files, a file or a directory
* @param MessageCatalogue $catalogue The catalogue
*/
public function extract($resource, MessageCatalogue $catalogue);
/**
* Sets the prefix that should be used for new found messages.
*
* @param string $prefix The prefix
*/
public function setPrefix($prefix);
}

View file

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* IdentityTranslator does not translate anything.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IdentityTranslator implements TranslatorInterface
{
private $selector;
private $locale;
/**
* Constructor.
*
* @param MessageSelector|null $selector The message selector for pluralization
*/
public function __construct(MessageSelector $selector = null)
{
$this->selector = $selector ?: new MessageSelector();
}
/**
* {@inheritdoc}
*/
public function setLocale($locale)
{
$this->locale = $locale;
}
/**
* {@inheritdoc}
*/
public function getLocale()
{
return $this->locale ?: \Locale::getDefault();
}
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
return strtr((string) $id, $parameters);
}
/**
* {@inheritdoc}
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
return strtr($this->selector->choose((string) $id, (int) $number, $locale ?: $this->getLocale()), $parameters);
}
}

View file

@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* Tests if a given number belongs to a given math interval.
*
* An interval can represent a finite set of numbers:
*
* {1,2,3,4}
*
* An interval can represent numbers between two numbers:
*
* [1, +Inf]
* ]-1,2[
*
* The left delimiter can be [ (inclusive) or ] (exclusive).
* The right delimiter can be [ (exclusive) or ] (inclusive).
* Beside numbers, you can use -Inf and +Inf for the infinite.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation
*/
class Interval
{
/**
* Tests if the given number is in the math interval.
*
* @param int $number A number
* @param string $interval An interval
*
* @return bool
*
* @throws \InvalidArgumentException
*/
public static function test($number, $interval)
{
$interval = trim($interval);
if (!preg_match('/^'.self::getIntervalRegexp().'$/x', $interval, $matches)) {
throw new \InvalidArgumentException(sprintf('"%s" is not a valid interval.', $interval));
}
if ($matches[1]) {
foreach (explode(',', $matches[2]) as $n) {
if ($number == $n) {
return true;
}
}
} else {
$leftNumber = self::convertNumber($matches['left']);
$rightNumber = self::convertNumber($matches['right']);
return
('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber)
&& (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber)
;
}
return false;
}
/**
* Returns a Regexp that matches valid intervals.
*
* @return string A Regexp (without the delimiters)
*/
public static function getIntervalRegexp()
{
return <<<EOF
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
|
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
EOF;
}
private static function convertNumber($number)
{
if ('-Inf' === $number) {
return log(0);
} elseif ('+Inf' === $number || 'Inf' === $number) {
return -log(0);
}
return (float) $number;
}
}

19
web/vendor/symfony/translation/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2004-2017 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
/**
* ArrayLoader loads translations from a PHP array.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ArrayLoader implements LoaderInterface
{
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
{
$this->flatten($resource);
$catalogue = new MessageCatalogue($locale);
$catalogue->add($resource, $domain);
return $catalogue;
}
/**
* Flattens an nested array of translations.
*
* The scheme used is:
* 'key' => array('key2' => array('key3' => 'value'))
* Becomes:
* 'key.key2.key3' => 'value'
*
* This function takes an array by reference and will modify it
*
* @param array &$messages The array that will be flattened
* @param array $subnode Current subnode being parsed, used internally for recursive calls
* @param string $path Current path being parsed, used internally for recursive calls
*/
private function flatten(array &$messages, array $subnode = null, $path = null)
{
if (null === $subnode) {
$subnode = &$messages;
}
foreach ($subnode as $key => $value) {
if (is_array($value)) {
$nodePath = $path ? $path.'.'.$key : $key;
$this->flatten($messages, $value, $nodePath);
if (null === $path) {
unset($messages[$key]);
}
} elseif (null !== $path) {
$messages[$path.'.'.$key] = $value;
}
}
}
}

View file

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
/**
* CsvFileLoader loads translations from CSV files.
*
* @author Saša Stamenković <umpirsky@gmail.com>
*/
class CsvFileLoader extends FileLoader
{
private $delimiter = ';';
private $enclosure = '"';
private $escape = '\\';
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
{
$messages = array();
try {
$file = new \SplFileObject($resource, 'rb');
} catch (\RuntimeException $e) {
throw new NotFoundResourceException(sprintf('Error opening file "%s".', $resource), 0, $e);
}
$file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY);
$file->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
foreach ($file as $data) {
if ('#' !== substr($data[0], 0, 1) && isset($data[1]) && 2 === count($data)) {
$messages[$data[0]] = $data[1];
}
}
return $messages;
}
/**
* Sets the delimiter, enclosure, and escape character for CSV.
*
* @param string $delimiter delimiter character
* @param string $enclosure enclosure character
* @param string $escape escape character
*/
public function setCsvControl($delimiter = ';', $enclosure = '"', $escape = '\\')
{
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->escape = $escape;
}
}

View file

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Config\Resource\FileResource;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
abstract class FileLoader extends ArrayLoader
{
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
}
if (!file_exists($resource)) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
}
$messages = $this->loadResource($resource);
// empty resource
if (null === $messages) {
$messages = array();
}
// not an array
if (!is_array($messages)) {
throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource));
}
$catalogue = parent::load($messages, $locale, $domain);
if (class_exists('Symfony\Component\Config\Resource\FileResource')) {
$catalogue->addResource(new FileResource($resource));
}
return $catalogue;
}
/*
* @param string $resource
*
* @return array
*
* @throws InvalidResourceException If stream content has an invalid format.
*/
abstract protected function loadResource($resource);
}

View file

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Config\Resource\FileResource;
/**
* IcuResFileLoader loads translations from a resource bundle.
*
* @author stealth35
*/
class IcuDatFileLoader extends IcuResFileLoader
{
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
{
if (!stream_is_local($resource.'.dat')) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
}
if (!file_exists($resource.'.dat')) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
}
try {
$rb = new \ResourceBundle($locale, $resource);
} catch (\Exception $e) {
// HHVM compatibility: constructor throws on invalid resource
$rb = null;
}
if (!$rb) {
throw new InvalidResourceException(sprintf('Cannot load resource "%s"', $resource));
} elseif (intl_is_failure($rb->getErrorCode())) {
throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
}
$messages = $this->flatten($rb);
$catalogue = new MessageCatalogue($locale);
$catalogue->add($messages, $domain);
if (class_exists('Symfony\Component\Config\Resource\FileResource')) {
$catalogue->addResource(new FileResource($resource.'.dat'));
}
return $catalogue;
}
}

View file

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Config\Resource\DirectoryResource;
/**
* IcuResFileLoader loads translations from a resource bundle.
*
* @author stealth35
*/
class IcuResFileLoader implements LoaderInterface
{
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
}
if (!is_dir($resource)) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
}
try {
$rb = new \ResourceBundle($locale, $resource);
} catch (\Exception $e) {
// HHVM compatibility: constructor throws on invalid resource
$rb = null;
}
if (!$rb) {
throw new InvalidResourceException(sprintf('Cannot load resource "%s"', $resource));
} elseif (intl_is_failure($rb->getErrorCode())) {
throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
}
$messages = $this->flatten($rb);
$catalogue = new MessageCatalogue($locale);
$catalogue->add($messages, $domain);
if (class_exists('Symfony\Component\Config\Resource\DirectoryResource')) {
$catalogue->addResource(new DirectoryResource($resource));
}
return $catalogue;
}
/**
* Flattens an ResourceBundle.
*
* The scheme used is:
* key { key2 { key3 { "value" } } }
* Becomes:
* 'key.key2.key3' => 'value'
*
* This function takes an array by reference and will modify it
*
* @param \ResourceBundle $rb the ResourceBundle that will be flattened
* @param array $messages used internally for recursive calls
* @param string $path current path being parsed, used internally for recursive calls
*
* @return array the flattened ResourceBundle
*/
protected function flatten(\ResourceBundle $rb, array &$messages = array(), $path = null)
{
foreach ($rb as $key => $value) {
$nodePath = $path ? $path.'.'.$key : $key;
if ($value instanceof \ResourceBundle) {
$this->flatten($value, $messages, $nodePath);
} else {
$messages[$nodePath] = $value;
}
}
return $messages;
}
}

View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
/**
* IniFileLoader loads translations from an ini file.
*
* @author stealth35
*/
class IniFileLoader extends FileLoader
{
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
{
return parse_ini_file($resource, true);
}
}

View file

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\Exception\InvalidResourceException;
/**
* JsonFileLoader loads translations from an json file.
*
* @author singles
*/
class JsonFileLoader extends FileLoader
{
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
{
$messages = array();
if ($data = file_get_contents($resource)) {
$messages = json_decode($data, true);
if (0 < $errorCode = json_last_error()) {
throw new InvalidResourceException(sprintf('Error parsing JSON - %s', $this->getJSONErrorMessage($errorCode)));
}
}
return $messages;
}
/**
* Translates JSON_ERROR_* constant into meaningful message.
*
* @param int $errorCode Error code returned by json_last_error() call
*
* @return string Message string
*/
private function getJSONErrorMessage($errorCode)
{
switch ($errorCode) {
case JSON_ERROR_DEPTH:
return 'Maximum stack depth exceeded';
case JSON_ERROR_STATE_MISMATCH:
return 'Underflow or the modes mismatch';
case JSON_ERROR_CTRL_CHAR:
return 'Unexpected control character found';
case JSON_ERROR_SYNTAX:
return 'Syntax error, malformed JSON';
case JSON_ERROR_UTF8:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
default:
return 'Unknown error';
}
}
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
/**
* LoaderInterface is the interface implemented by all translation loaders.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface LoaderInterface
{
/**
* Loads a locale.
*
* @param mixed $resource A resource
* @param string $locale A locale
* @param string $domain The domain
*
* @return MessageCatalogue A MessageCatalogue instance
*
* @throws NotFoundResourceException when the resource cannot be found
* @throws InvalidResourceException when the resource cannot be loaded
*/
public function load($resource, $locale, $domain = 'messages');
}

View file

@ -0,0 +1,154 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\Exception\InvalidResourceException;
/**
* @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
*/
class MoFileLoader extends FileLoader
{
/**
* Magic used for validating the format of a MO file as well as
* detecting if the machine used to create that file was little endian.
*
* @var float
*/
const MO_LITTLE_ENDIAN_MAGIC = 0x950412de;
/**
* Magic used for validating the format of a MO file as well as
* detecting if the machine used to create that file was big endian.
*
* @var float
*/
const MO_BIG_ENDIAN_MAGIC = 0xde120495;
/**
* The size of the header of a MO file in bytes.
*
* @var int Number of bytes
*/
const MO_HEADER_SIZE = 28;
/**
* Parses machine object (MO) format, independent of the machine's endian it
* was created on. Both 32bit and 64bit systems are supported.
*
* {@inheritdoc}
*/
protected function loadResource($resource)
{
$stream = fopen($resource, 'r');
$stat = fstat($stream);
if ($stat['size'] < self::MO_HEADER_SIZE) {
throw new InvalidResourceException('MO stream content has an invalid format.');
}
$magic = unpack('V1', fread($stream, 4));
$magic = hexdec(substr(dechex(current($magic)), -8));
if ($magic == self::MO_LITTLE_ENDIAN_MAGIC) {
$isBigEndian = false;
} elseif ($magic == self::MO_BIG_ENDIAN_MAGIC) {
$isBigEndian = true;
} else {
throw new InvalidResourceException('MO stream content has an invalid format.');
}
// formatRevision
$this->readLong($stream, $isBigEndian);
$count = $this->readLong($stream, $isBigEndian);
$offsetId = $this->readLong($stream, $isBigEndian);
$offsetTranslated = $this->readLong($stream, $isBigEndian);
// sizeHashes
$this->readLong($stream, $isBigEndian);
// offsetHashes
$this->readLong($stream, $isBigEndian);
$messages = array();
for ($i = 0; $i < $count; ++$i) {
$pluralId = null;
$translated = null;
fseek($stream, $offsetId + $i * 8);
$length = $this->readLong($stream, $isBigEndian);
$offset = $this->readLong($stream, $isBigEndian);
if ($length < 1) {
continue;
}
fseek($stream, $offset);
$singularId = fread($stream, $length);
if (strpos($singularId, "\000") !== false) {
list($singularId, $pluralId) = explode("\000", $singularId);
}
fseek($stream, $offsetTranslated + $i * 8);
$length = $this->readLong($stream, $isBigEndian);
$offset = $this->readLong($stream, $isBigEndian);
if ($length < 1) {
continue;
}
fseek($stream, $offset);
$translated = fread($stream, $length);
if (strpos($translated, "\000") !== false) {
$translated = explode("\000", $translated);
}
$ids = array('singular' => $singularId, 'plural' => $pluralId);
$item = compact('ids', 'translated');
if (is_array($item['translated'])) {
$messages[$item['ids']['singular']] = stripcslashes($item['translated'][0]);
if (isset($item['ids']['plural'])) {
$plurals = array();
foreach ($item['translated'] as $plural => $translated) {
$plurals[] = sprintf('{%d} %s', $plural, $translated);
}
$messages[$item['ids']['plural']] = stripcslashes(implode('|', $plurals));
}
} elseif (!empty($item['ids']['singular'])) {
$messages[$item['ids']['singular']] = stripcslashes($item['translated']);
}
}
fclose($stream);
return array_filter($messages);
}
/**
* Reads an unsigned long from stream respecting endianess.
*
* @param resource $stream
* @param bool $isBigEndian
*
* @return int
*/
private function readLong($stream, $isBigEndian)
{
$result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4));
$result = current($result);
return (int) substr($result, -8);
}
}

View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
/**
* PhpFileLoader loads translations from PHP files returning an array of translations.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class PhpFileLoader extends FileLoader
{
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
{
return require $resource;
}
}

View file

@ -0,0 +1,151 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
/**
* @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
* @copyright Copyright (c) 2012, Clemens Tolboom
*/
class PoFileLoader extends FileLoader
{
/**
* Parses portable object (PO) format.
*
* From http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
* we should be able to parse files having:
*
* white-space
* # translator-comments
* #. extracted-comments
* #: reference...
* #, flag...
* #| msgid previous-untranslated-string
* msgid untranslated-string
* msgstr translated-string
*
* extra or different lines are:
*
* #| msgctxt previous-context
* #| msgid previous-untranslated-string
* msgctxt context
*
* #| msgid previous-untranslated-string-singular
* #| msgid_plural previous-untranslated-string-plural
* msgid untranslated-string-singular
* msgid_plural untranslated-string-plural
* msgstr[0] translated-string-case-0
* ...
* msgstr[N] translated-string-case-n
*
* The definition states:
* - white-space and comments are optional.
* - msgid "" that an empty singleline defines a header.
*
* This parser sacrifices some features of the reference implementation the
* differences to that implementation are as follows.
* - No support for comments spanning multiple lines.
* - Translator and extracted comments are treated as being the same type.
* - Message IDs are allowed to have other encodings as just US-ASCII.
*
* Items with an empty id are ignored.
*
* {@inheritdoc}
*/
protected function loadResource($resource)
{
$stream = fopen($resource, 'r');
$defaults = array(
'ids' => array(),
'translated' => null,
);
$messages = array();
$item = $defaults;
$flags = array();
while ($line = fgets($stream)) {
$line = trim($line);
if ($line === '') {
// Whitespace indicated current item is done
if (!in_array('fuzzy', $flags)) {
$this->addMessage($messages, $item);
}
$item = $defaults;
$flags = array();
} elseif (substr($line, 0, 2) === '#,') {
$flags = array_map('trim', explode(',', substr($line, 2)));
} elseif (substr($line, 0, 7) === 'msgid "') {
// We start a new msg so save previous
// TODO: this fails when comments or contexts are added
$this->addMessage($messages, $item);
$item = $defaults;
$item['ids']['singular'] = substr($line, 7, -1);
} elseif (substr($line, 0, 8) === 'msgstr "') {
$item['translated'] = substr($line, 8, -1);
} elseif ($line[0] === '"') {
$continues = isset($item['translated']) ? 'translated' : 'ids';
if (is_array($item[$continues])) {
end($item[$continues]);
$item[$continues][key($item[$continues])] .= substr($line, 1, -1);
} else {
$item[$continues] .= substr($line, 1, -1);
}
} elseif (substr($line, 0, 14) === 'msgid_plural "') {
$item['ids']['plural'] = substr($line, 14, -1);
} elseif (substr($line, 0, 7) === 'msgstr[') {
$size = strpos($line, ']');
$item['translated'][(int) substr($line, 7, 1)] = substr($line, $size + 3, -1);
}
}
// save last item
if (!in_array('fuzzy', $flags)) {
$this->addMessage($messages, $item);
}
fclose($stream);
return $messages;
}
/**
* Save a translation item to the messages.
*
* A .po file could contain by error missing plural indexes. We need to
* fix these before saving them.
*
* @param array $messages
* @param array $item
*/
private function addMessage(array &$messages, array $item)
{
if (is_array($item['translated'])) {
$messages[stripcslashes($item['ids']['singular'])] = stripcslashes($item['translated'][0]);
if (isset($item['ids']['plural'])) {
$plurals = $item['translated'];
// PO are by definition indexed so sort by index.
ksort($plurals);
// Make sure every index is filled.
end($plurals);
$count = key($plurals);
// Fill missing spots with '-'.
$empties = array_fill(0, $count + 1, '-');
$plurals += $empties;
ksort($plurals);
$messages[stripcslashes($item['ids']['plural'])] = stripcslashes(implode('|', $plurals));
}
} elseif (!empty($item['ids']['singular'])) {
$messages[stripcslashes($item['ids']['singular'])] = stripcslashes($item['translated']);
}
}
}

View file

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Config\Resource\FileResource;
/**
* QtFileLoader loads translations from QT Translations XML files.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class QtFileLoader implements LoaderInterface
{
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
}
if (!file_exists($resource)) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
}
try {
$dom = XmlUtils::loadFile($resource);
} catch (\InvalidArgumentException $e) {
throw new InvalidResourceException(sprintf('Unable to load "%s".', $resource), $e->getCode(), $e);
}
$internalErrors = libxml_use_internal_errors(true);
libxml_clear_errors();
$xpath = new \DOMXPath($dom);
$nodes = $xpath->evaluate('//TS/context/name[text()="'.$domain.'"]');
$catalogue = new MessageCatalogue($locale);
if ($nodes->length == 1) {
$translations = $nodes->item(0)->nextSibling->parentNode->parentNode->getElementsByTagName('message');
foreach ($translations as $translation) {
$translationValue = (string) $translation->getElementsByTagName('translation')->item(0)->nodeValue;
if (!empty($translationValue)) {
$catalogue->set(
(string) $translation->getElementsByTagName('source')->item(0)->nodeValue,
$translationValue,
$domain
);
}
$translation = $translation->nextSibling;
}
if (class_exists('Symfony\Component\Config\Resource\FileResource')) {
$catalogue->addResource(new FileResource($resource));
}
}
libxml_use_internal_errors($internalErrors);
return $catalogue;
}
}

View file

@ -0,0 +1,322 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Config\Resource\FileResource;
/**
* XliffFileLoader loads translations from XLIFF files.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class XliffFileLoader implements LoaderInterface
{
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
}
if (!file_exists($resource)) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
}
$catalogue = new MessageCatalogue($locale);
$this->extract($resource, $catalogue, $domain);
if (class_exists('Symfony\Component\Config\Resource\FileResource')) {
$catalogue->addResource(new FileResource($resource));
}
return $catalogue;
}
private function extract($resource, MessageCatalogue $catalogue, $domain)
{
try {
$dom = XmlUtils::loadFile($resource);
} catch (\InvalidArgumentException $e) {
throw new InvalidResourceException(sprintf('Unable to load "%s": %s', $resource, $e->getMessage()), $e->getCode(), $e);
}
$xliffVersion = $this->getVersionNumber($dom);
$this->validateSchema($xliffVersion, $dom, $this->getSchema($xliffVersion));
if ('1.2' === $xliffVersion) {
$this->extractXliff1($dom, $catalogue, $domain);
}
if ('2.0' === $xliffVersion) {
$this->extractXliff2($dom, $catalogue, $domain);
}
}
/**
* Extract messages and metadata from DOMDocument into a MessageCatalogue.
*
* @param \DOMDocument $dom Source to extract messages and metadata
* @param MessageCatalogue $catalogue Catalogue where we'll collect messages and metadata
* @param string $domain The domain
*/
private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, $domain)
{
$xml = simplexml_import_dom($dom);
$encoding = strtoupper($dom->encoding);
$xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
foreach ($xml->xpath('//xliff:trans-unit') as $translation) {
$attributes = $translation->attributes();
if (!(isset($attributes['resname']) || isset($translation->source))) {
continue;
}
$source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source;
// If the xlf file has another encoding specified, try to convert it because
// simple_xml will always return utf-8 encoded values
$target = $this->utf8ToCharset((string) (isset($translation->target) ? $translation->target : $source), $encoding);
$catalogue->set((string) $source, $target, $domain);
$metadata = array();
if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) {
$metadata['notes'] = $notes;
}
if (isset($translation->target) && $translation->target->attributes()) {
$metadata['target-attributes'] = array();
foreach ($translation->target->attributes() as $key => $value) {
$metadata['target-attributes'][$key] = (string) $value;
}
}
$catalogue->setMetadata((string) $source, $metadata, $domain);
}
}
/**
* @param \DOMDocument $dom
* @param MessageCatalogue $catalogue
* @param string $domain
*/
private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, $domain)
{
$xml = simplexml_import_dom($dom);
$encoding = strtoupper($dom->encoding);
$xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0');
foreach ($xml->xpath('//xliff:unit/xliff:segment') as $segment) {
$source = $segment->source;
// If the xlf file has another encoding specified, try to convert it because
// simple_xml will always return utf-8 encoded values
$target = $this->utf8ToCharset((string) (isset($segment->target) ? $segment->target : $source), $encoding);
$catalogue->set((string) $source, $target, $domain);
$metadata = array();
if (isset($segment->target) && $segment->target->attributes()) {
$metadata['target-attributes'] = array();
foreach ($segment->target->attributes() as $key => $value) {
$metadata['target-attributes'][$key] = (string) $value;
}
}
$catalogue->setMetadata((string) $source, $metadata, $domain);
}
}
/**
* Convert a UTF8 string to the specified encoding.
*
* @param string $content String to decode
* @param string $encoding Target encoding
*
* @return string
*/
private function utf8ToCharset($content, $encoding = null)
{
if ('UTF-8' !== $encoding && !empty($encoding)) {
return mb_convert_encoding($content, $encoding, 'UTF-8');
}
return $content;
}
/**
* Validates and parses the given file into a DOMDocument.
*
* @param string $file
* @param \DOMDocument $dom
* @param string $schema source of the schema
*
* @throws \RuntimeException
* @throws InvalidResourceException
*/
private function validateSchema($file, \DOMDocument $dom, $schema)
{
$internalErrors = libxml_use_internal_errors(true);
$disableEntities = libxml_disable_entity_loader(false);
if (!@$dom->schemaValidateSource($schema)) {
libxml_disable_entity_loader($disableEntities);
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $file, implode("\n", $this->getXmlErrors($internalErrors))));
}
libxml_disable_entity_loader($disableEntities);
$dom->normalizeDocument();
libxml_clear_errors();
libxml_use_internal_errors($internalErrors);
}
private function getSchema($xliffVersion)
{
if ('1.2' === $xliffVersion) {
$schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd');
$xmlUri = 'http://www.w3.org/2001/xml.xsd';
} elseif ('2.0' === $xliffVersion) {
$schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-2.0.xsd');
$xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd';
} else {
throw new \InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion));
}
return $this->fixXmlLocation($schemaSource, $xmlUri);
}
/**
* Internally changes the URI of a dependent xsd to be loaded locally.
*
* @param string $schemaSource Current content of schema file
* @param string $xmlUri External URI of XML to convert to local
*
* @return string
*/
private function fixXmlLocation($schemaSource, $xmlUri)
{
$newPath = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd';
$parts = explode('/', $newPath);
if (0 === stripos($newPath, 'phar://')) {
$tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
if ($tmpfile) {
copy($newPath, $tmpfile);
$parts = explode('/', str_replace('\\', '/', $tmpfile));
}
}
$drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$newPath = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
return str_replace($xmlUri, $newPath, $schemaSource);
}
/**
* Returns the XML errors of the internal XML parser.
*
* @param bool $internalErrors
*
* @return array An array of errors
*/
private function getXmlErrors($internalErrors)
{
$errors = array();
foreach (libxml_get_errors() as $error) {
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
$error->code,
trim($error->message),
$error->file ?: 'n/a',
$error->line,
$error->column
);
}
libxml_clear_errors();
libxml_use_internal_errors($internalErrors);
return $errors;
}
/**
* Gets xliff file version based on the root "version" attribute.
* Defaults to 1.2 for backwards compatibility.
*
* @param \DOMDocument $dom
*
* @throws \InvalidArgumentException
*
* @return string
*/
private function getVersionNumber(\DOMDocument $dom)
{
/** @var \DOMNode $xliff */
foreach ($dom->getElementsByTagName('xliff') as $xliff) {
$version = $xliff->attributes->getNamedItem('version');
if ($version) {
return $version->nodeValue;
}
$namespace = $xliff->attributes->getNamedItem('xmlns');
if ($namespace) {
if (substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34) !== 0) {
throw new \InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s"', $namespace));
}
return substr($namespace, 34);
}
}
// Falls back to v1.2
return '1.2';
}
/*
* @param \SimpleXMLElement|null $noteElement
* @param string|null $encoding
*
* @return array
*/
private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, $encoding = null)
{
$notes = array();
if (null === $noteElement) {
return $notes;
}
foreach ($noteElement as $xmlNote) {
$noteAttributes = $xmlNote->attributes();
$note = array('content' => $this->utf8ToCharset((string) $xmlNote, $encoding));
if (isset($noteAttributes['priority'])) {
$note['priority'] = (int) $noteAttributes['priority'];
}
if (isset($noteAttributes['from'])) {
$note['from'] = (string) $noteAttributes['from'];
}
$notes[] = $note;
}
return $notes;
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Yaml\Parser as YamlParser;
use Symfony\Component\Yaml\Exception\ParseException;
/**
* YamlFileLoader loads translations from Yaml files.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class YamlFileLoader extends FileLoader
{
private $yamlParser;
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
{
if (null === $this->yamlParser) {
if (!class_exists('Symfony\Component\Yaml\Parser')) {
throw new \LogicException('Loading translations from the YAML format requires the Symfony Yaml component.');
}
$this->yamlParser = new YamlParser();
}
try {
$messages = $this->yamlParser->parse(file_get_contents($resource));
} catch (ParseException $e) {
throw new InvalidResourceException(sprintf('Error parsing YAML, invalid file "%s"', $resource), 0, $e);
}
return $messages;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,411 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
XLIFF Version 2.0
OASIS Standard
05 August 2014
Copyright (c) OASIS Open 2014. All rights reserved.
Source: http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/schemas/
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
xmlns:xlf="urn:oasis:names:tc:xliff:document:2.0"
targetNamespace="urn:oasis:names:tc:xliff:document:2.0">
<!-- Import -->
<xs:import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="informativeCopiesOf3rdPartySchemas/w3c/xml.xsd"/>
<!-- Element Group -->
<xs:group name="inline">
<xs:choice>
<xs:element ref="xlf:cp"/>
<xs:element ref="xlf:ph"/>
<xs:element ref="xlf:pc"/>
<xs:element ref="xlf:sc"/>
<xs:element ref="xlf:ec"/>
<xs:element ref="xlf:mrk"/>
<xs:element ref="xlf:sm"/>
<xs:element ref="xlf:em"/>
</xs:choice>
</xs:group>
<!-- Attribute Types -->
<xs:simpleType name="yesNo">
<xs:restriction base="xs:string">
<xs:enumeration value="yes"/>
<xs:enumeration value="no"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="yesNoFirstNo">
<xs:restriction base="xs:string">
<xs:enumeration value="yes"/>
<xs:enumeration value="firstNo"/>
<xs:enumeration value="no"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="dirValue">
<xs:restriction base="xs:string">
<xs:enumeration value="ltr"/>
<xs:enumeration value="rtl"/>
<xs:enumeration value="auto"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="appliesTo">
<xs:restriction base="xs:string">
<xs:enumeration value="source"/>
<xs:enumeration value="target"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="userDefinedValue">
<xs:restriction base="xs:string">
<xs:pattern value="[^\s:]+:[^\s:]+"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="attrType_type">
<xs:restriction base="xs:string">
<xs:enumeration value="fmt"/>
<xs:enumeration value="ui"/>
<xs:enumeration value="quote"/>
<xs:enumeration value="link"/>
<xs:enumeration value="image"/>
<xs:enumeration value="other"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="typeForMrkValues">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="generic"/>
<xs:enumeration value="comment"/>
<xs:enumeration value="term"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="attrType_typeForMrk">
<xs:union memberTypes="xlf:typeForMrkValues xlf:userDefinedValue"/>
</xs:simpleType>
<xs:simpleType name="priorityValue">
<xs:restriction base="xs:positiveInteger">
<xs:minInclusive value="1"/>
<xs:maxInclusive value="10"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="stateType">
<xs:restriction base="xs:string">
<xs:enumeration value="initial"/>
<xs:enumeration value="translated"/>
<xs:enumeration value="reviewed"/>
<xs:enumeration value="final"/>
</xs:restriction>
</xs:simpleType>
<!-- Structural Elements -->
<xs:element name="xliff">
<xs:complexType mixed="false">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="unbounded" ref="xlf:file"/>
</xs:sequence>
<xs:attribute name="version" use="required"/>
<xs:attribute name="srcLang" use="required"/>
<xs:attribute name="trgLang" use="optional"/>
<xs:attribute ref="xml:space" use="optional" default="default"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="file">
<xs:complexType mixed="false">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="1" ref="xlf:skeleton"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"
processContents="lax"/>
<xs:element minOccurs="0" maxOccurs="1" ref="xlf:notes"/>
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element ref="xlf:unit"/>
<xs:element ref="xlf:group"/>
</xs:choice>
</xs:sequence>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="canResegment" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="original" use="optional"/>
<xs:attribute name="translate" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="srcDir" use="optional" type="xlf:dirValue" default="auto"/>
<xs:attribute name="trgDir" use="optional" type="xlf:dirValue" default="auto"/>
<xs:attribute ref="xml:space" use="optional"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="skeleton">
<xs:complexType mixed="true">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"
processContents="lax"/>
</xs:sequence>
<xs:attribute name="href" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="group">
<xs:complexType mixed="false">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"
processContents="lax"/>
<xs:element minOccurs="0" maxOccurs="1" ref="xlf:notes"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="xlf:unit"/>
<xs:element ref="xlf:group"/>
</xs:choice>
</xs:sequence>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="name" use="optional"/>
<xs:attribute name="canResegment" use="optional" type="xlf:yesNo"/>
<xs:attribute name="translate" use="optional" type="xlf:yesNo"/>
<xs:attribute name="srcDir" use="optional" type="xlf:dirValue"/>
<xs:attribute name="trgDir" use="optional" type="xlf:dirValue"/>
<xs:attribute name="type" use="optional" type="xlf:userDefinedValue"/>
<xs:attribute ref="xml:space" use="optional"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="unit">
<xs:complexType mixed="false">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"
processContents="lax"/>
<xs:element minOccurs="0" maxOccurs="1" ref="xlf:notes"/>
<xs:element minOccurs="0" maxOccurs="1" ref="xlf:originalData"/>
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element ref="xlf:segment"/>
<xs:element ref="xlf:ignorable"/>
</xs:choice>
</xs:sequence>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="name" use="optional"/>
<xs:attribute name="canResegment" use="optional" type="xlf:yesNo"/>
<xs:attribute name="translate" use="optional" type="xlf:yesNo"/>
<xs:attribute name="srcDir" use="optional" type="xlf:dirValue"/>
<xs:attribute name="trgDir" use="optional" type="xlf:dirValue"/>
<xs:attribute ref="xml:space" use="optional"/>
<xs:attribute name="type" use="optional" type="xlf:userDefinedValue"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="segment">
<xs:complexType mixed="false">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="1" ref="xlf:source"/>
<xs:element minOccurs="0" maxOccurs="1" ref="xlf:target"/>
</xs:sequence>
<xs:attribute name="id" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="canResegment" use="optional" type="xlf:yesNo"/>
<xs:attribute name="state" use="optional" type="xlf:stateType" default="initial"/>
<xs:attribute name="subState" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="ignorable">
<xs:complexType mixed="false">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="1" ref="xlf:source"/>
<xs:element minOccurs="0" maxOccurs="1" ref="xlf:target"/>
</xs:sequence>
<xs:attribute name="id" use="optional" type="xs:NMTOKEN"/>
</xs:complexType>
</xs:element>
<xs:element name="notes">
<xs:complexType mixed="false">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="unbounded" ref="xlf:note"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="note">
<xs:complexType mixed="true">
<xs:attribute name="id" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="appliesTo" use="optional" type="xlf:appliesTo"/>
<xs:attribute name="category" use="optional"/>
<xs:attribute name="priority" use="optional" type="xlf:priorityValue" default="1"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="originalData">
<xs:complexType mixed="false">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="unbounded" ref="xlf:data"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="data">
<xs:complexType mixed="true">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xlf:cp"/>
</xs:sequence>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="dir" use="optional" type="xlf:dirValue" default="auto"/>
<xs:attribute ref="xml:space" use="optional" fixed="preserve"/>
</xs:complexType>
</xs:element>
<xs:element name="source">
<xs:complexType mixed="true">
<xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/>
<xs:attribute ref="xml:lang" use="optional"/>
<xs:attribute ref="xml:space" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="target">
<xs:complexType mixed="true">
<xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/>
<xs:attribute ref="xml:lang" use="optional"/>
<xs:attribute ref="xml:space" use="optional"/>
<xs:attribute name="order" use="optional" type="xs:positiveInteger"/>
</xs:complexType>
</xs:element>
<!-- Inline Elements -->
<xs:element name="cp">
<!-- Code Point -->
<xs:complexType mixed="false">
<xs:attribute name="hex" use="required" type="xs:hexBinary"/>
</xs:complexType>
</xs:element>
<xs:element name="ph">
<!-- Placeholder -->
<xs:complexType mixed="false">
<xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/>
<xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="disp" use="optional"/>
<xs:attribute name="equiv" use="optional"/>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="dataRef" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="subFlows" use="optional" type="xs:NMTOKENS"/>
<xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/>
<xs:attribute name="type" use="optional" type="xlf:attrType_type"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="pc">
<!-- Paired Code -->
<xs:complexType mixed="true">
<xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/>
<xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canOverlap" use="optional" type="xlf:yesNo"/>
<xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/>
<xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="dispEnd" use="optional"/>
<xs:attribute name="dispStart" use="optional"/>
<xs:attribute name="equivEnd" use="optional"/>
<xs:attribute name="equivStart" use="optional"/>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="dataRefEnd" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="dataRefStart" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="subFlowsEnd" use="optional" type="xs:NMTOKENS"/>
<xs:attribute name="subFlowsStart" use="optional" type="xs:NMTOKENS"/>
<xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/>
<xs:attribute name="type" use="optional" type="xlf:attrType_type"/>
<xs:attribute name="dir" use="optional" type="xlf:dirValue"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="sc">
<!-- Start Code -->
<xs:complexType mixed="false">
<xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canOverlap" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/>
<xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="dataRef" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="dir" use="optional" type="xlf:dirValue"/>
<xs:attribute name="disp" use="optional"/>
<xs:attribute name="equiv" use="optional"/>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="isolated" use="optional" type="xlf:yesNo" default="no"/>
<xs:attribute name="subFlows" use="optional" type="xs:NMTOKENS"/>
<xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/>
<xs:attribute name="type" use="optional" type="xlf:attrType_type"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="ec">
<!-- End Code -->
<xs:complexType mixed="false">
<xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canOverlap" use="optional" type="xlf:yesNo" default="yes"/>
<xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/>
<xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="dataRef" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="dir" use="optional" type="xlf:dirValue"/>
<xs:attribute name="disp" use="optional"/>
<xs:attribute name="equiv" use="optional"/>
<xs:attribute name="id" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="isolated" use="optional" type="xlf:yesNo" default="no"/>
<xs:attribute name="startRef" use="optional" type="xs:NMTOKEN"/>
<xs:attribute name="subFlows" use="optional" type="xs:NMTOKENS"/>
<xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/>
<xs:attribute name="type" use="optional" type="xlf:attrType_type"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="mrk">
<!-- Annotation Marker -->
<xs:complexType mixed="true">
<xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/>
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="translate" use="optional" type="xlf:yesNo"/>
<xs:attribute name="type" use="optional" type="xlf:attrType_typeForMrk"/>
<xs:attribute name="ref" use="optional" type="xs:anyURI"/>
<xs:attribute name="value" use="optional"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="sm">
<!-- Start Annotation Marker -->
<xs:complexType mixed="false">
<xs:attribute name="id" use="required" type="xs:NMTOKEN"/>
<xs:attribute name="translate" use="optional" type="xlf:yesNo"/>
<xs:attribute name="type" use="optional" type="xlf:attrType_typeForMrk"/>
<xs:attribute name="ref" use="optional" type="xs:anyURI"/>
<xs:attribute name="value" use="optional"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="em">
<!-- End Annotation Marker -->
<xs:complexType mixed="false">
<xs:attribute name="startRef" use="required" type="xs:NMTOKEN"/>
</xs:complexType>
</xs:element>
</xs:schema>

View file

@ -0,0 +1,309 @@
<?xml version='1.0'?>
<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns ="http://www.w3.org/1999/xhtml"
xml:lang="en">
<xs:annotation>
<xs:documentation>
<div>
<h1>About the XML namespace</h1>
<div class="bodytext">
<p>
This schema document describes the XML namespace, in a form
suitable for import by other schema documents.
</p>
<p>
See <a href="http://www.w3.org/XML/1998/namespace.html">
http://www.w3.org/XML/1998/namespace.html</a> and
<a href="http://www.w3.org/TR/REC-xml">
http://www.w3.org/TR/REC-xml</a> for information
about this namespace.
</p>
<p>
Note that local names in this namespace are intended to be
defined only by the World Wide Web Consortium or its subgroups.
The names currently defined in this namespace are listed below.
They should not be used with conflicting semantics by any Working
Group, specification, or document instance.
</p>
<p>
See further below in this document for more information about <a
href="#usage">how to refer to this schema document from your own
XSD schema documents</a> and about <a href="#nsversioning">the
namespace-versioning policy governing this schema document</a>.
</p>
</div>
</div>
</xs:documentation>
</xs:annotation>
<xs:attribute name="lang">
<xs:annotation>
<xs:documentation>
<div>
<h3>lang (as an attribute name)</h3>
<p>
denotes an attribute whose value
is a language code for the natural language of the content of
any element; its value is inherited. This name is reserved
by virtue of its definition in the XML specification.</p>
</div>
<div>
<h4>Notes</h4>
<p>
Attempting to install the relevant ISO 2- and 3-letter
codes as the enumerated possible values is probably never
going to be a realistic possibility.
</p>
<p>
See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
and the IANA language subtag registry at
<a href="http://www.iana.org/assignments/language-subtag-registry">
http://www.iana.org/assignments/language-subtag-registry</a>
for further information.
</p>
<p>
The union allows for the 'un-declaration' of xml:lang with
the empty string.
</p>
</div>
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:union memberTypes="xs:language">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value=""/>
</xs:restriction>
</xs:simpleType>
</xs:union>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="space">
<xs:annotation>
<xs:documentation>
<div>
<h3>space (as an attribute name)</h3>
<p>
denotes an attribute whose
value is a keyword indicating what whitespace processing
discipline is intended for the content of the element; its
value is inherited. This name is reserved by virtue of its
definition in the XML specification.</p>
</div>
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:NCName">
<xs:enumeration value="default"/>
<xs:enumeration value="preserve"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
<xs:documentation>
<div>
<h3>base (as an attribute name)</h3>
<p>
denotes an attribute whose value
provides a URI to be used as the base for interpreting any
relative URIs in the scope of the element on which it
appears; its value is inherited. This name is reserved
by virtue of its definition in the XML Base specification.</p>
<p>
See <a
href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
for information about this attribute.
</p>
</div>
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="id" type="xs:ID">
<xs:annotation>
<xs:documentation>
<div>
<h3>id (as an attribute name)</h3>
<p>
denotes an attribute whose value
should be interpreted as if declared to be of type ID.
This name is reserved by virtue of its definition in the
xml:id specification.</p>
<p>
See <a
href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
for information about this attribute.
</p>
</div>
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup name="specialAttrs">
<xs:attribute ref="xml:base"/>
<xs:attribute ref="xml:lang"/>
<xs:attribute ref="xml:space"/>
<xs:attribute ref="xml:id"/>
</xs:attributeGroup>
<xs:annotation>
<xs:documentation>
<div>
<h3>Father (in any context at all)</h3>
<div class="bodytext">
<p>
denotes Jon Bosak, the chair of
the original XML Working Group. This name is reserved by
the following decision of the W3C XML Plenary and
XML Coordination groups:
</p>
<blockquote>
<p>
In appreciation for his vision, leadership and
dedication the W3C XML Plenary on this 10th day of
February, 2000, reserves for Jon Bosak in perpetuity
the XML name "xml:Father".
</p>
</blockquote>
</div>
</div>
</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>
<div xml:id="usage" id="usage">
<h2><a name="usage">About this schema document</a></h2>
<div class="bodytext">
<p>
This schema defines attributes and an attribute group suitable
for use by schemas wishing to allow <code>xml:base</code>,
<code>xml:lang</code>, <code>xml:space</code> or
<code>xml:id</code> attributes on elements they define.
</p>
<p>
To enable this, such a schema must import this schema for
the XML namespace, e.g. as follows:
</p>
<pre>
&lt;schema.. .>
.. .
&lt;import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2001/xml.xsd"/>
</pre>
<p>
or
</p>
<pre>
&lt;import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
</pre>
<p>
Subsequently, qualified reference to any of the attributes or the
group defined below will have the desired effect, e.g.
</p>
<pre>
&lt;type.. .>
.. .
&lt;attributeGroup ref="xml:specialAttrs"/>
</pre>
<p>
will define a type which will schema-validate an instance element
with any of those attributes.
</p>
</div>
</div>
</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>
<div id="nsversioning" xml:id="nsversioning">
<h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
<div class="bodytext">
<p>
In keeping with the XML Schema WG's standard versioning
policy, this schema document will persist at
<a href="http://www.w3.org/2009/01/xml.xsd">
http://www.w3.org/2009/01/xml.xsd</a>.
</p>
<p>
At the date of issue it can also be found at
<a href="http://www.w3.org/2001/xml.xsd">
http://www.w3.org/2001/xml.xsd</a>.
</p>
<p>
The schema document at that URI may however change in the future,
in order to remain compatible with the latest version of XML
Schema itself, or with the XML namespace itself. In other words,
if the XML Schema or XML namespaces change, the version of this
document at <a href="http://www.w3.org/2001/xml.xsd">
http://www.w3.org/2001/xml.xsd
</a>
will change accordingly; the version at
<a href="http://www.w3.org/2009/01/xml.xsd">
http://www.w3.org/2009/01/xml.xsd
</a>
will not change.
</p>
<p>
Previous dated (and unchanging) versions of this schema
document are at:
</p>
<ul>
<li><a href="http://www.w3.org/2009/01/xml.xsd">
http://www.w3.org/2009/01/xml.xsd</a></li>
<li><a href="http://www.w3.org/2007/08/xml.xsd">
http://www.w3.org/2007/08/xml.xsd</a></li>
<li><a href="http://www.w3.org/2004/10/xml.xsd">
http://www.w3.org/2004/10/xml.xsd</a></li>
<li><a href="http://www.w3.org/2001/03/xml.xsd">
http://www.w3.org/2001/03/xml.xsd</a></li>
</ul>
</div>
</div>
</xs:documentation>
</xs:annotation>
</xs:schema>

View file

@ -0,0 +1,138 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Psr\Log\LoggerInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface
{
/**
* @var TranslatorInterface|TranslatorBagInterface
*/
private $translator;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
* @param LoggerInterface $logger
*/
public function __construct(TranslatorInterface $translator, LoggerInterface $logger)
{
if (!$translator instanceof TranslatorBagInterface) {
throw new \InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', get_class($translator)));
}
$this->translator = $translator;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
$trans = $this->translator->trans($id, $parameters, $domain, $locale);
$this->log($id, $domain, $locale);
return $trans;
}
/**
* {@inheritdoc}
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
$this->log($id, $domain, $locale);
return $trans;
}
/**
* {@inheritdoc}
*/
public function setLocale($locale)
{
$this->translator->setLocale($locale);
}
/**
* {@inheritdoc}
*/
public function getLocale()
{
return $this->translator->getLocale();
}
/**
* {@inheritdoc}
*/
public function getCatalogue($locale = null)
{
return $this->translator->getCatalogue($locale);
}
/**
* Gets the fallback locales.
*
* @return array $locales The fallback locales
*/
public function getFallbackLocales()
{
if ($this->translator instanceof Translator) {
return $this->translator->getFallbackLocales();
}
return array();
}
/**
* Passes through all unknown calls onto the translator object.
*/
public function __call($method, $args)
{
return call_user_func_array(array($this->translator, $method), $args);
}
/**
* Logs for missing translations.
*
* @param string $id
* @param string|null $domain
* @param string|null $locale
*/
private function log($id, $domain, $locale)
{
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->translator->getCatalogue($locale);
if ($catalogue->defines($id, $domain)) {
return;
}
if ($catalogue->has($id, $domain)) {
$this->logger->debug('Translation use fallback catalogue.', array('id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()));
} else {
$this->logger->warning('Translation not found.', array('id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()));
}
}
}

View file

@ -0,0 +1,274 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Component\Config\Resource\ResourceInterface;
/**
* MessageCatalogue.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface
{
private $messages = array();
private $metadata = array();
private $resources = array();
private $locale;
private $fallbackCatalogue;
private $parent;
/**
* Constructor.
*
* @param string $locale The locale
* @param array $messages An array of messages classified by domain
*/
public function __construct($locale, array $messages = array())
{
$this->locale = $locale;
$this->messages = $messages;
}
/**
* {@inheritdoc}
*/
public function getLocale()
{
return $this->locale;
}
/**
* {@inheritdoc}
*/
public function getDomains()
{
return array_keys($this->messages);
}
/**
* {@inheritdoc}
*/
public function all($domain = null)
{
if (null === $domain) {
return $this->messages;
}
return isset($this->messages[$domain]) ? $this->messages[$domain] : array();
}
/**
* {@inheritdoc}
*/
public function set($id, $translation, $domain = 'messages')
{
$this->add(array($id => $translation), $domain);
}
/**
* {@inheritdoc}
*/
public function has($id, $domain = 'messages')
{
if (isset($this->messages[$domain][$id])) {
return true;
}
if (null !== $this->fallbackCatalogue) {
return $this->fallbackCatalogue->has($id, $domain);
}
return false;
}
/**
* {@inheritdoc}
*/
public function defines($id, $domain = 'messages')
{
return isset($this->messages[$domain][$id]);
}
/**
* {@inheritdoc}
*/
public function get($id, $domain = 'messages')
{
if (isset($this->messages[$domain][$id])) {
return $this->messages[$domain][$id];
}
if (null !== $this->fallbackCatalogue) {
return $this->fallbackCatalogue->get($id, $domain);
}
return $id;
}
/**
* {@inheritdoc}
*/
public function replace($messages, $domain = 'messages')
{
$this->messages[$domain] = array();
$this->add($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function add($messages, $domain = 'messages')
{
if (!isset($this->messages[$domain])) {
$this->messages[$domain] = $messages;
} else {
$this->messages[$domain] = array_replace($this->messages[$domain], $messages);
}
}
/**
* {@inheritdoc}
*/
public function addCatalogue(MessageCatalogueInterface $catalogue)
{
if ($catalogue->getLocale() !== $this->locale) {
throw new \LogicException(sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s"', $catalogue->getLocale(), $this->locale));
}
foreach ($catalogue->all() as $domain => $messages) {
$this->add($messages, $domain);
}
foreach ($catalogue->getResources() as $resource) {
$this->addResource($resource);
}
if ($catalogue instanceof MetadataAwareInterface) {
$metadata = $catalogue->getMetadata('', '');
$this->addMetadata($metadata);
}
}
/**
* {@inheritdoc}
*/
public function addFallbackCatalogue(MessageCatalogueInterface $catalogue)
{
// detect circular references
$c = $catalogue;
while ($c = $c->getFallbackCatalogue()) {
if ($c->getLocale() === $this->getLocale()) {
throw new \LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
}
}
$c = $this;
do {
if ($c->getLocale() === $catalogue->getLocale()) {
throw new \LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
}
foreach ($catalogue->getResources() as $resource) {
$c->addResource($resource);
}
} while ($c = $c->parent);
$catalogue->parent = $this;
$this->fallbackCatalogue = $catalogue;
foreach ($catalogue->getResources() as $resource) {
$this->addResource($resource);
}
}
/**
* {@inheritdoc}
*/
public function getFallbackCatalogue()
{
return $this->fallbackCatalogue;
}
/**
* {@inheritdoc}
*/
public function getResources()
{
return array_values($this->resources);
}
/**
* {@inheritdoc}
*/
public function addResource(ResourceInterface $resource)
{
$this->resources[$resource->__toString()] = $resource;
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = '', $domain = 'messages')
{
if ('' == $domain) {
return $this->metadata;
}
if (isset($this->metadata[$domain])) {
if ('' == $key) {
return $this->metadata[$domain];
}
if (isset($this->metadata[$domain][$key])) {
return $this->metadata[$domain][$key];
}
}
}
/**
* {@inheritdoc}
*/
public function setMetadata($key, $value, $domain = 'messages')
{
$this->metadata[$domain][$key] = $value;
}
/**
* {@inheritdoc}
*/
public function deleteMetadata($key = '', $domain = 'messages')
{
if ('' == $domain) {
$this->metadata = array();
} elseif ('' == $key) {
unset($this->metadata[$domain]);
} else {
unset($this->metadata[$domain][$key]);
}
}
/**
* Adds current values with the new values.
*
* @param array $values Values to add
*/
private function addMetadata(array $values)
{
foreach ($values as $domain => $keys) {
foreach ($keys as $key => $value) {
$this->setMetadata($key, $value, $domain);
}
}
}
}

View file

@ -0,0 +1,142 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Component\Config\Resource\ResourceInterface;
/**
* MessageCatalogueInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface MessageCatalogueInterface
{
/**
* Gets the catalogue locale.
*
* @return string The locale
*/
public function getLocale();
/**
* Gets the domains.
*
* @return array An array of domains
*/
public function getDomains();
/**
* Gets the messages within a given domain.
*
* If $domain is null, it returns all messages.
*
* @param string $domain The domain name
*
* @return array An array of messages
*/
public function all($domain = null);
/**
* Sets a message translation.
*
* @param string $id The message id
* @param string $translation The messages translation
* @param string $domain The domain name
*/
public function set($id, $translation, $domain = 'messages');
/**
* Checks if a message has a translation.
*
* @param string $id The message id
* @param string $domain The domain name
*
* @return bool true if the message has a translation, false otherwise
*/
public function has($id, $domain = 'messages');
/**
* Checks if a message has a translation (it does not take into account the fallback mechanism).
*
* @param string $id The message id
* @param string $domain The domain name
*
* @return bool true if the message has a translation, false otherwise
*/
public function defines($id, $domain = 'messages');
/**
* Gets a message translation.
*
* @param string $id The message id
* @param string $domain The domain name
*
* @return string The message translation
*/
public function get($id, $domain = 'messages');
/**
* Sets translations for a given domain.
*
* @param array $messages An array of translations
* @param string $domain The domain name
*/
public function replace($messages, $domain = 'messages');
/**
* Adds translations for a given domain.
*
* @param array $messages An array of translations
* @param string $domain The domain name
*/
public function add($messages, $domain = 'messages');
/**
* Merges translations from the given Catalogue into the current one.
*
* The two catalogues must have the same locale.
*
* @param self $catalogue
*/
public function addCatalogue(MessageCatalogueInterface $catalogue);
/**
* Merges translations from the given Catalogue into the current one
* only when the translation does not exist.
*
* This is used to provide default translations when they do not exist for the current locale.
*
* @param self $catalogue
*/
public function addFallbackCatalogue(MessageCatalogueInterface $catalogue);
/**
* Gets the fallback catalogue.
*
* @return self|null A MessageCatalogueInterface instance or null when no fallback has been set
*/
public function getFallbackCatalogue();
/**
* Returns an array of resources loaded to build this collection.
*
* @return ResourceInterface[] An array of resources
*/
public function getResources();
/**
* Adds a resource for this collection.
*
* @param ResourceInterface $resource A resource instance
*/
public function addResource(ResourceInterface $resource);
}

View file

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* MessageSelector.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MessageSelector
{
/**
* Given a message with different plural translations separated by a
* pipe (|), this method returns the correct portion of the message based
* on the given number, locale and the pluralization rules in the message
* itself.
*
* The message supports two different types of pluralization rules:
*
* interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
* indexed: There is one apple|There are %count% apples
*
* The indexed solution can also contain labels (e.g. one: There is one apple).
* This is purely for making the translations more clear - it does not
* affect the functionality.
*
* The two methods can also be mixed:
* {0} There are no apples|one: There is one apple|more: There are %count% apples
*
* @param string $message The message being translated
* @param int $number The number of items represented for the message
* @param string $locale The locale to use for choosing
*
* @return string
*
* @throws \InvalidArgumentException
*/
public function choose($message, $number, $locale)
{
$parts = explode('|', $message);
$explicitRules = array();
$standardRules = array();
foreach ($parts as $part) {
$part = trim($part);
if (preg_match('/^(?P<interval>'.Interval::getIntervalRegexp().')\s*(?P<message>.*?)$/xs', $part, $matches)) {
$explicitRules[$matches['interval']] = $matches['message'];
} elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) {
$standardRules[] = $matches[1];
} else {
$standardRules[] = $part;
}
}
// try to match an explicit rule, then fallback to the standard ones
foreach ($explicitRules as $interval => $m) {
if (Interval::test($number, $interval)) {
return $m;
}
}
$position = PluralizationRules::get($number, $locale);
if (!isset($standardRules[$position])) {
// when there's exactly one rule given, and that rule is a standard
// rule, use this rule
if (1 === count($parts) && isset($standardRules[0])) {
return $standardRules[0];
}
throw new \InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $message, $locale, $number));
}
return $standardRules[$position];
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* MetadataAwareInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface MetadataAwareInterface
{
/**
* Gets metadata for the given domain and key.
*
* Passing an empty domain will return an array with all metadata indexed by
* domain and then by key. Passing an empty key will return an array with all
* metadata for the given domain.
*
* @param string $key The key
* @param string $domain The domain name
*
* @return mixed The value that was set or an array with the domains/keys or null
*/
public function getMetadata($key = '', $domain = 'messages');
/**
* Adds metadata to a message domain.
*
* @param string $key The key
* @param mixed $value The value
* @param string $domain The domain name
*/
public function setMetadata($key, $value, $domain = 'messages');
/**
* Deletes metadata for the given key and domain.
*
* Passing an empty domain will delete all metadata. Passing an empty key will
* delete all metadata for the given domain.
*
* @param string $key The key
* @param string $domain The domain name
*/
public function deleteMetadata($key = '', $domain = 'messages');
}

View file

@ -0,0 +1,215 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* Returns the plural rules for a given locale.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class PluralizationRules
{
private static $rules = array();
/**
* Returns the plural position to use for the given locale and number.
*
* @param int $number The number
* @param string $locale The locale
*
* @return int The plural position
*/
public static function get($number, $locale)
{
if ('pt_BR' === $locale) {
// temporary set a locale for brazilian
$locale = 'xbr';
}
if (strlen($locale) > 3) {
$locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
}
if (isset(self::$rules[$locale])) {
$return = call_user_func(self::$rules[$locale], $number);
if (!is_int($return) || $return < 0) {
return 0;
}
return $return;
}
/*
* The plural rules are derived from code of the Zend Framework (2010-09-25),
* which is subject to the new BSD license (http://framework.zend.com/license/new-bsd).
* Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
*/
switch ($locale) {
case 'az':
case 'bo':
case 'dz':
case 'id':
case 'ja':
case 'jv':
case 'ka':
case 'km':
case 'kn':
case 'ko':
case 'ms':
case 'th':
case 'tr':
case 'vi':
case 'zh':
return 0;
break;
case 'af':
case 'bn':
case 'bg':
case 'ca':
case 'da':
case 'de':
case 'el':
case 'en':
case 'eo':
case 'es':
case 'et':
case 'eu':
case 'fa':
case 'fi':
case 'fo':
case 'fur':
case 'fy':
case 'gl':
case 'gu':
case 'ha':
case 'he':
case 'hu':
case 'is':
case 'it':
case 'ku':
case 'lb':
case 'ml':
case 'mn':
case 'mr':
case 'nah':
case 'nb':
case 'ne':
case 'nl':
case 'nn':
case 'no':
case 'om':
case 'or':
case 'pa':
case 'pap':
case 'ps':
case 'pt':
case 'so':
case 'sq':
case 'sv':
case 'sw':
case 'ta':
case 'te':
case 'tk':
case 'ur':
case 'zu':
return ($number == 1) ? 0 : 1;
case 'am':
case 'bh':
case 'fil':
case 'fr':
case 'gun':
case 'hi':
case 'hy':
case 'ln':
case 'mg':
case 'nso':
case 'xbr':
case 'ti':
case 'wa':
return (($number == 0) || ($number == 1)) ? 0 : 1;
case 'be':
case 'bs':
case 'hr':
case 'ru':
case 'sr':
case 'uk':
return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
case 'cs':
case 'sk':
return ($number == 1) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2);
case 'ga':
return ($number == 1) ? 0 : (($number == 2) ? 1 : 2);
case 'lt':
return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
case 'sl':
return ($number % 100 == 1) ? 0 : (($number % 100 == 2) ? 1 : ((($number % 100 == 3) || ($number % 100 == 4)) ? 2 : 3));
case 'mk':
return ($number % 10 == 1) ? 0 : 1;
case 'mt':
return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3));
case 'lv':
return ($number == 0) ? 0 : ((($number % 10 == 1) && ($number % 100 != 11)) ? 1 : 2);
case 'pl':
return ($number == 1) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2);
case 'cy':
return ($number == 1) ? 0 : (($number == 2) ? 1 : ((($number == 8) || ($number == 11)) ? 2 : 3));
case 'ro':
return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2);
case 'ar':
return ($number == 0) ? 0 : (($number == 1) ? 1 : (($number == 2) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5))));
default:
return 0;
}
}
/**
* Overrides the default plural rule for a given locale.
*
* @param callable $rule A PHP callable
* @param string $locale The locale
*
* @throws \LogicException
*/
public static function set($rule, $locale)
{
if ('pt_BR' === $locale) {
// temporary set a locale for brazilian
$locale = 'xbr';
}
if (strlen($locale) > 3) {
$locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
}
if (!is_callable($rule)) {
throw new \LogicException('The given rule can not be called');
}
self::$rules[$locale] = $rule;
}
}

View file

@ -0,0 +1,13 @@
Translation Component
=====================
The Translation component provides tools to internationalize your application.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/translation/index.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View file

@ -0,0 +1,485 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\Config\ConfigCacheFactory;
/**
* Translator.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Translator implements TranslatorInterface, TranslatorBagInterface
{
/**
* @var MessageCatalogueInterface[]
*/
protected $catalogues = array();
/**
* @var string
*/
protected $locale;
/**
* @var array
*/
private $fallbackLocales = array();
/**
* @var LoaderInterface[]
*/
private $loaders = array();
/**
* @var array
*/
private $resources = array();
/**
* @var MessageSelector
*/
private $selector;
/**
* @var string
*/
private $cacheDir;
/**
* @var bool
*/
private $debug;
/**
* @var ConfigCacheFactoryInterface|null
*/
private $configCacheFactory;
/**
* Constructor.
*
* @param string $locale The locale
* @param MessageSelector|null $selector The message selector for pluralization
* @param string|null $cacheDir The directory to use for the cache
* @param bool $debug Use cache in debug mode ?
*
* @throws \InvalidArgumentException If a locale contains invalid characters
*/
public function __construct($locale, MessageSelector $selector = null, $cacheDir = null, $debug = false)
{
$this->setLocale($locale);
$this->selector = $selector ?: new MessageSelector();
$this->cacheDir = $cacheDir;
$this->debug = $debug;
}
/**
* Sets the ConfigCache factory to use.
*
* @param ConfigCacheFactoryInterface $configCacheFactory
*/
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
{
$this->configCacheFactory = $configCacheFactory;
}
/**
* Adds a Loader.
*
* @param string $format The name of the loader (@see addResource())
* @param LoaderInterface $loader A LoaderInterface instance
*/
public function addLoader($format, LoaderInterface $loader)
{
$this->loaders[$format] = $loader;
}
/**
* Adds a Resource.
*
* @param string $format The name of the loader (@see addLoader())
* @param mixed $resource The resource name
* @param string $locale The locale
* @param string $domain The domain
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function addResource($format, $resource, $locale, $domain = null)
{
if (null === $domain) {
$domain = 'messages';
}
$this->assertValidLocale($locale);
$this->resources[$locale][] = array($format, $resource, $domain);
if (in_array($locale, $this->fallbackLocales)) {
$this->catalogues = array();
} else {
unset($this->catalogues[$locale]);
}
}
/**
* {@inheritdoc}
*/
public function setLocale($locale)
{
$this->assertValidLocale($locale);
$this->locale = $locale;
}
/**
* {@inheritdoc}
*/
public function getLocale()
{
return $this->locale;
}
/**
* Sets the fallback locale(s).
*
* @param string|array $locales The fallback locale(s)
*
* @throws \InvalidArgumentException If a locale contains invalid characters
*
* @deprecated since version 2.3, to be removed in 3.0. Use setFallbackLocales() instead
*/
public function setFallbackLocale($locales)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0. Use the setFallbackLocales() method instead.', E_USER_DEPRECATED);
$this->setFallbackLocales(is_array($locales) ? $locales : array($locales));
}
/**
* Sets the fallback locales.
*
* @param array $locales The fallback locales
*
* @throws \InvalidArgumentException If a locale contains invalid characters
*/
public function setFallbackLocales(array $locales)
{
// needed as the fallback locales are linked to the already loaded catalogues
$this->catalogues = array();
foreach ($locales as $locale) {
$this->assertValidLocale($locale);
}
$this->fallbackLocales = $locales;
}
/**
* Gets the fallback locales.
*
* @return array $locales The fallback locales
*/
public function getFallbackLocales()
{
return $this->fallbackLocales;
}
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
if (null === $domain) {
$domain = 'messages';
}
return strtr($this->getCatalogue($locale)->get((string) $id, $domain), $parameters);
}
/**
* {@inheritdoc}
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->getCatalogue($locale);
$locale = $catalogue->getLocale();
while (!$catalogue->defines($id, $domain)) {
if ($cat = $catalogue->getFallbackCatalogue()) {
$catalogue = $cat;
$locale = $catalogue->getLocale();
} else {
break;
}
}
return strtr($this->selector->choose($catalogue->get($id, $domain), (int) $number, $locale), $parameters);
}
/**
* {@inheritdoc}
*/
public function getCatalogue($locale = null)
{
if (null === $locale) {
$locale = $this->getLocale();
} else {
$this->assertValidLocale($locale);
}
if (!isset($this->catalogues[$locale])) {
$this->loadCatalogue($locale);
}
return $this->catalogues[$locale];
}
/**
* Gets the loaders.
*
* @return array LoaderInterface[]
*/
protected function getLoaders()
{
return $this->loaders;
}
/**
* Collects all messages for the given locale.
*
* @param string|null $locale Locale of translations, by default is current locale
*
* @return array[array] indexed by catalog
*
* @deprecated since version 2.8, to be removed in 3.0. Use TranslatorBagInterface::getCatalogue() method instead.
*/
public function getMessages($locale = null)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use TranslatorBagInterface::getCatalogue() method instead.', E_USER_DEPRECATED);
$catalogue = $this->getCatalogue($locale);
$messages = $catalogue->all();
while ($catalogue = $catalogue->getFallbackCatalogue()) {
$messages = array_replace_recursive($catalogue->all(), $messages);
}
return $messages;
}
/**
* @param string $locale
*/
protected function loadCatalogue($locale)
{
if (null === $this->cacheDir) {
$this->initializeCatalogue($locale);
} else {
$this->initializeCacheCatalogue($locale);
}
}
/**
* @param string $locale
*/
protected function initializeCatalogue($locale)
{
$this->assertValidLocale($locale);
try {
$this->doLoadCatalogue($locale);
} catch (NotFoundResourceException $e) {
if (!$this->computeFallbackLocales($locale)) {
throw $e;
}
}
$this->loadFallbackCatalogues($locale);
}
/**
* @param string $locale
*/
private function initializeCacheCatalogue($locale)
{
if (isset($this->catalogues[$locale])) {
/* Catalogue already initialized. */
return;
}
$this->assertValidLocale($locale);
$self = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0.
$cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale),
function (ConfigCacheInterface $cache) use ($self, $locale) {
$self->dumpCatalogue($locale, $cache);
}
);
if (isset($this->catalogues[$locale])) {
/* Catalogue has been initialized as it was written out to cache. */
return;
}
/* Read catalogue from cache. */
$this->catalogues[$locale] = include $cache->getPath();
}
/**
* This method is public because it needs to be callable from a closure in PHP 5.3. It should be made protected (or even private, if possible) in 3.0.
*
* @internal
*/
public function dumpCatalogue($locale, ConfigCacheInterface $cache)
{
$this->initializeCatalogue($locale);
$fallbackContent = $this->getFallbackContent($this->catalogues[$locale]);
$content = sprintf(<<<EOF
<?php
use Symfony\Component\Translation\MessageCatalogue;
\$catalogue = new MessageCatalogue('%s', %s);
%s
return \$catalogue;
EOF
,
$locale,
var_export($this->catalogues[$locale]->all(), true),
$fallbackContent
);
$cache->write($content, $this->catalogues[$locale]->getResources());
}
private function getFallbackContent(MessageCatalogue $catalogue)
{
$fallbackContent = '';
$current = '';
$replacementPattern = '/[^a-z0-9_]/i';
$fallbackCatalogue = $catalogue->getFallbackCatalogue();
while ($fallbackCatalogue) {
$fallback = $fallbackCatalogue->getLocale();
$fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback));
$currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current));
$fallbackContent .= sprintf(<<<'EOF'
$catalogue%s = new MessageCatalogue('%s', %s);
$catalogue%s->addFallbackCatalogue($catalogue%s);
EOF
,
$fallbackSuffix,
$fallback,
var_export($fallbackCatalogue->all(), true),
$currentSuffix,
$fallbackSuffix
);
$current = $fallbackCatalogue->getLocale();
$fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
}
return $fallbackContent;
}
private function getCatalogueCachePath($locale)
{
return $this->cacheDir.'/catalogue.'.$locale.'.'.sha1(serialize($this->fallbackLocales)).'.php';
}
private function doLoadCatalogue($locale)
{
$this->catalogues[$locale] = new MessageCatalogue($locale);
if (isset($this->resources[$locale])) {
foreach ($this->resources[$locale] as $resource) {
if (!isset($this->loaders[$resource[0]])) {
throw new \RuntimeException(sprintf('The "%s" translation loader is not registered.', $resource[0]));
}
$this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2]));
}
}
}
private function loadFallbackCatalogues($locale)
{
$current = $this->catalogues[$locale];
foreach ($this->computeFallbackLocales($locale) as $fallback) {
if (!isset($this->catalogues[$fallback])) {
$this->loadCatalogue($fallback);
}
$fallbackCatalogue = new MessageCatalogue($fallback, $this->catalogues[$fallback]->all());
foreach ($this->catalogues[$fallback]->getResources() as $resource) {
$fallbackCatalogue->addResource($resource);
}
$current->addFallbackCatalogue($fallbackCatalogue);
$current = $fallbackCatalogue;
}
}
protected function computeFallbackLocales($locale)
{
$locales = array();
foreach ($this->fallbackLocales as $fallback) {
if ($fallback === $locale) {
continue;
}
$locales[] = $fallback;
}
if (strrchr($locale, '_') !== false) {
array_unshift($locales, substr($locale, 0, -strlen(strrchr($locale, '_'))));
}
return array_unique($locales);
}
/**
* Asserts that the locale is valid, throws an Exception if not.
*
* @param string $locale Locale to tests
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
protected function assertValidLocale($locale)
{
if (1 !== preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) {
throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
}
}
/**
* Provides the ConfigCache factory implementation, falling back to a
* default implementation if necessary.
*
* @return ConfigCacheFactoryInterface $configCacheFactory
*/
private function getConfigCacheFactory()
{
if (!$this->configCacheFactory) {
$this->configCacheFactory = new ConfigCacheFactory($this->debug);
}
return $this->configCacheFactory;
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* TranslatorBagInterface.
*
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
interface TranslatorBagInterface
{
/**
* Gets the catalogue by locale.
*
* @param string|null $locale The locale or null to use the default
*
* @return MessageCatalogueInterface
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function getCatalogue($locale = null);
}

View file

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* TranslatorInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TranslatorInterface
{
/**
* Translates the given message.
*
* @param string $id The message id (may also be an object that can be cast to string)
* @param array $parameters An array of parameters for the message
* @param string|null $domain The domain for the message or null to use the default
* @param string|null $locale The locale or null to use the default
*
* @return string The translated string
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null);
/**
* Translates the given choice message by choosing a translation according to a number.
*
* @param string $id The message id (may also be an object that can be cast to string)
* @param int $number The number to use to find the indice of the message
* @param array $parameters An array of parameters for the message
* @param string|null $domain The domain for the message or null to use the default
* @param string|null $locale The locale or null to use the default
*
* @return string The translated string
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null);
/**
* Sets the current locale.
*
* @param string $locale The locale
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function setLocale($locale);
/**
* Returns the current locale.
*
* @return string The locale
*/
public function getLocale();
}

View file

@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Util;
/**
* ArrayConverter generates tree like structure from a message catalogue.
* e.g. this
* 'foo.bar1' => 'test1',
* 'foo.bar2' => 'test2'
* converts to follows:
* foo:
* bar1: test1
* bar2: test2.
*
* @author Gennady Telegin <gtelegin@gmail.com>
*/
class ArrayConverter
{
/**
* Converts linear messages array to tree-like array.
* For example this rray('foo.bar' => 'value') will be converted to array('foo' => array('bar' => 'value')).
*
* @param array $messages Linear messages array
*
* @return array Tree-like messages array
*/
public static function expandToTree(array $messages)
{
$tree = array();
foreach ($messages as $id => $value) {
$referenceToElement = &self::getElementByPath($tree, explode('.', $id));
$referenceToElement = $value;
unset($referenceToElement);
}
return $tree;
}
private static function &getElementByPath(array &$tree, array $parts)
{
$elem = &$tree;
$parentOfElem = null;
foreach ($parts as $i => $part) {
if (isset($elem[$part]) && is_string($elem[$part])) {
/* Process next case:
* 'foo': 'test1',
* 'foo.bar': 'test2'
*
* $tree['foo'] was string before we found array {bar: test2}.
* Treat new element as string too, e.g. add $tree['foo.bar'] = 'test2';
*/
$elem = &$elem[ implode('.', array_slice($parts, $i)) ];
break;
}
$parentOfElem = &$elem;
$elem = &$elem[$part];
}
if (is_array($elem) && count($elem) > 0 && $parentOfElem) {
/* Process next case:
* 'foo.bar': 'test1'
* 'foo': 'test2'
*
* $tree['foo'] was array = {bar: 'test1'} before we found string constant `foo`.
* Cancel treating $tree['foo'] as array and cancel back it expansion,
* e.g. make it $tree['foo.bar'] = 'test1' again.
*/
self::cancelExpand($parentOfElem, $part, $elem);
}
return $elem;
}
private static function cancelExpand(array &$tree, $prefix, array $node)
{
$prefix .= '.';
foreach ($node as $id => $value) {
if (is_string($value)) {
$tree[$prefix.$id] = $value;
} else {
self::cancelExpand($tree, $prefix.$id, $value);
}
}
}
}

View file

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Writer;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Dumper\DumperInterface;
/**
* TranslationWriter writes translation messages.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
class TranslationWriter
{
/**
* Dumpers used for export.
*
* @var array
*/
private $dumpers = array();
/**
* Adds a dumper to the writer.
*
* @param string $format The format of the dumper
* @param DumperInterface $dumper The dumper
*/
public function addDumper($format, DumperInterface $dumper)
{
$this->dumpers[$format] = $dumper;
}
/**
* Disables dumper backup.
*/
public function disableBackup()
{
foreach ($this->dumpers as $dumper) {
if (method_exists($dumper, 'setBackup')) {
$dumper->setBackup(false);
}
}
}
/**
* Obtains the list of supported formats.
*
* @return array
*/
public function getFormats()
{
return array_keys($this->dumpers);
}
/**
* Writes translation from the catalogue according to the selected format.
*
* @param MessageCatalogue $catalogue The message catalogue to dump
* @param string $format The format to use to dump the messages
* @param array $options Options that are passed to the dumper
*
* @throws \InvalidArgumentException
*/
public function writeTranslations(MessageCatalogue $catalogue, $format, $options = array())
{
if (!isset($this->dumpers[$format])) {
throw new \InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format));
}
// get the right dumper
$dumper = $this->dumpers[$format];
if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0777, true) && !is_dir($options['path'])) {
throw new \RuntimeException(sprintf('Translation Writer was not able to create directory "%s"', $options['path']));
}
// save
$dumper->dump($catalogue, $options);
}
}

View file

@ -0,0 +1,48 @@
{
"name": "symfony/translation",
"type": "library",
"description": "Symfony Translation Component",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.9",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
"symfony/config": "~2.8",
"symfony/intl": "~2.4|~3.0.0",
"symfony/yaml": "~2.2|~3.0.0",
"psr/log": "~1.0"
},
"conflict": {
"symfony/config": "<2.7"
},
"suggest": {
"symfony/config": "",
"symfony/yaml": "",
"psr/log": "To use logging capability in translator"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Translation\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
}
}

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony Translation Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>