Upgrade 1-11.38

This commit is contained in:
xesmyd
2026-03-30 14:10:30 +02:00
parent f2a7e6d1fc
commit ac648ef29d
24665 changed files with 69682 additions and 2205004 deletions
+32
View File
@@ -1,6 +1,38 @@
CHANGELOG
=========
4.3.0
-----
* Improved Xliff 1.2 loader to load the original file's metadata
* Added `TranslatorPathsPass`
4.2.0
-----
* Started using ICU parent locales as fallback locales.
* allow using the ICU message format using domains with the "+intl-icu" suffix
* deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter
* deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface`
* deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead
* Added `IntlFormatter` and `IntlFormatterInterface`
* added support for multiple files and directories in `XliffLintCommand`
* Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal
4.1.0
-----
* The `FileDumper::setBackup()` method is deprecated.
* The `TranslationWriter::disableBackup()` method is deprecated.
* The `XliffFileDumper` will write "name" on the "unit" node when dumping XLIFF 2.0.
4.0.0
-----
* removed the backup feature of the `FileDumper` class
* removed `TranslationWriter::writeTranslations()` method
* removed support for passing `MessageSelector` instances to the constructor of the `Translator` class
3.4.0
-----
+3 -3
View File
@@ -91,7 +91,7 @@ abstract class AbstractOperation implements OperationInterface
public function getMessages($domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
throw new InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
}
if (!isset($this->messages[$domain]['all'])) {
@@ -107,7 +107,7 @@ abstract class AbstractOperation implements OperationInterface
public function getNewMessages($domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
throw new InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
}
if (!isset($this->messages[$domain]['new'])) {
@@ -123,7 +123,7 @@ abstract class AbstractOperation implements OperationInterface
public function getObsoleteMessages($domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
throw new InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
}
if (!isset($this->messages[$domain]['obsolete'])) {
+5 -2
View File
@@ -11,6 +11,8 @@
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Merge operation between two catalogues as follows:
* all = source target = {x: x ∈ source x ∈ target}
@@ -32,10 +34,11 @@ class MergeOperation extends AbstractOperation
'new' => [],
'obsolete' => [],
];
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
foreach ($this->source->all($domain) as $id => $message) {
$this->messages[$domain]['all'][$id] = $message;
$this->result->add([$id => $message], $domain);
$this->result->add([$id => $message], $this->source->defines($id, $intlDomain) ? $intlDomain : $domain);
if (null !== $keyMetadata = $this->source->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
@@ -45,7 +48,7 @@ class MergeOperation extends AbstractOperation
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$this->result->add([$id => $message], $domain);
$this->result->add([$id => $message], $this->target->defines($id, $intlDomain) ? $intlDomain : $domain);
if (null !== $keyMetadata = $this->target->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
+5 -2
View File
@@ -11,6 +11,8 @@
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Target operation between two catalogues:
* intersection = source ∩ target = {x: x ∈ source ∧ x ∈ target}
@@ -33,6 +35,7 @@ class TargetOperation extends AbstractOperation
'new' => [],
'obsolete' => [],
];
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
// 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}
@@ -46,7 +49,7 @@ class TargetOperation extends AbstractOperation
foreach ($this->source->all($domain) as $id => $message) {
if ($this->target->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->result->add([$id => $message], $domain);
$this->result->add([$id => $message], $this->target->defines($id, $intlDomain) ? $intlDomain : $domain);
if (null !== $keyMetadata = $this->source->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
@@ -59,7 +62,7 @@ class TargetOperation extends AbstractOperation
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$this->result->add([$id => $message], $domain);
$this->result->add([$id => $message], $this->target->defines($id, $intlDomain) ? $intlDomain : $domain);
if (null !== $keyMetadata = $this->target->getMetadata($id, $domain)) {
$this->result->setMetadata($id, $keyMetadata, $domain);
}
+60 -28
View File
@@ -13,11 +13,12 @@ namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Util\XliffUtils;
/**
* Validates XLIFF files syntax and outputs encountered errors.
@@ -34,13 +35,15 @@ class XliffLintCommand extends Command
private $displayCorrectFiles;
private $directoryIteratorProvider;
private $isReadableProvider;
private $requireStrictFileNames;
public function __construct($name = null, $directoryIteratorProvider = null, $isReadableProvider = null)
public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null, bool $requireStrictFileNames = true)
{
parent::__construct($name);
$this->directoryIteratorProvider = $directoryIteratorProvider;
$this->isReadableProvider = $isReadableProvider;
$this->requireStrictFileNames = $requireStrictFileNames;
}
/**
@@ -50,7 +53,7 @@ class XliffLintCommand extends Command
{
$this
->setDescription('Lints a XLIFF file and outputs encountered errors')
->addArgument('filename', null, 'A file or a directory or STDIN')
->addArgument('filename', InputArgument::IS_ARRAY, 'A file or a directory or STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
->setHelp(<<<EOF
The <info>%command.name%</info> command lints a XLIFF file and outputs to STDOUT
@@ -77,11 +80,11 @@ EOF
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$filename = $input->getArgument('filename');
$filenames = (array) $input->getArgument('filename');
$this->format = $input->getOption('format');
$this->displayCorrectFiles = $output->isVerbose();
if (!$filename) {
if (0 === \count($filenames)) {
if (!$stdin = $this->getStdin()) {
throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
}
@@ -89,13 +92,15 @@ EOF
return $this->display($io, [$this->validate($stdin)]);
}
if (!$this->isReadable($filename)) {
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
}
$filesInfo = [];
foreach ($this->getFiles($filename) as $file) {
$filesInfo[] = $this->validate(file_get_contents($file), $file);
foreach ($filenames as $filename) {
if (!$this->isReadable($filename)) {
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
}
foreach ($this->getFiles($filename) as $file) {
$filesInfo[] = $this->validate(file_get_contents($file), $file);
}
}
return $this->display($io, $filesInfo);
@@ -103,31 +108,47 @@ EOF
private function validate($content, $file = null)
{
$errors = [];
// Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input
if ('' === trim($content)) {
return ['file' => $file, 'valid' => true];
}
libxml_use_internal_errors(true);
$internal = libxml_use_internal_errors(true);
$document = new \DOMDocument();
$document->loadXML($content);
if ($document->schemaValidate(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd')) {
return ['file' => $file, 'valid' => true];
if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) {
$normalizedLocale = preg_quote(str_replace('-', '_', $targetLanguage), '/');
// strict file names require translation files to be named '____.locale.xlf'
// otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed
// also, the regexp matching must be case-insensitive, as defined for 'target-language' values
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
$expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.xlf/', $normalizedLocale) : sprintf('/^(.*\.(?i:%s)\.xlf|(?i:%s)\..*\.xlf)/', $normalizedLocale, $normalizedLocale);
if (0 === preg_match($expectedFilenamePattern, basename($file))) {
$errors[] = [
'line' => -1,
'column' => -1,
'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
];
}
}
$errorMessages = array_map(function ($error) {
return [
'line' => $error->line,
'column' => $error->column,
'message' => trim($error->message),
foreach (XliffUtils::validateSchema($document) as $xmlError) {
$errors[] = [
'line' => $xmlError['line'],
'column' => $xmlError['column'],
'message' => $xmlError['message'],
];
}, libxml_get_errors());
}
libxml_clear_errors();
libxml_use_internal_errors(false);
libxml_use_internal_errors($internal);
return ['file' => $file, 'valid' => false, 'messages' => $errorMessages];
return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors];
}
private function display(SymfonyStyle $io, array $files)
@@ -180,7 +201,7 @@ EOF
}
});
$io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
$io->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return min($errors, 1);
}
@@ -207,13 +228,13 @@ EOF
*/
private function getStdin()
{
if (0 !== ftell(\STDIN)) {
if (0 !== ftell(STDIN)) {
return null;
}
$inputs = '';
while (!feof(\STDIN)) {
$inputs .= fread(\STDIN, 1024);
while (!feof(STDIN)) {
$inputs .= fread(STDIN, 1024);
}
return $inputs;
@@ -229,7 +250,7 @@ EOF
};
if (null !== $this->directoryIteratorProvider) {
return \call_user_func($this->directoryIteratorProvider, $directory, $default);
return ($this->directoryIteratorProvider)($directory, $default);
}
return $default($directory);
@@ -242,9 +263,20 @@ EOF
};
if (null !== $this->isReadableProvider) {
return \call_user_func($this->isReadableProvider, $fileOrDirectory, $default);
return ($this->isReadableProvider)($fileOrDirectory, $default);
}
return $default($fileOrDirectory);
}
private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string
{
foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) {
if ('target-language' === $attribute->nodeName) {
return $attribute->nodeValue;
}
}
return null;
}
}
@@ -37,12 +37,9 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
{
$messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages());
$this->data = $this->computeCount($messages);
$this->data += $this->computeCount($messages);
$this->data['messages'] = $messages;
$this->data['locale'] = $this->translator->getLocale();
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
$this->data = $this->cloneVar($this->data);
}
@@ -51,6 +48,8 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
$this->data['locale'] = $this->translator->getLocale();
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
}
/**
@@ -98,6 +97,9 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
return !empty($this->data['locale']) ? $this->data['locale'] : null;
}
/**
* @internal since Symfony 4.2
*/
public function getFallbackLocales()
{
return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : [];
+22 -10
View File
@@ -13,11 +13,14 @@ namespace Symfony\Component\Translation;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, WarmableInterface
class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface, WarmableInterface
{
const MESSAGE_DEFINED = 0;
const MESSAGE_MISSING = 1;
@@ -33,10 +36,13 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
*/
public function __construct(TranslatorInterface $translator)
public function __construct($translator)
{
if (!$translator instanceof TranslatorBagInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', \get_class($translator)));
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', \get_class($translator)));
}
$this->translator = $translator;
@@ -55,11 +61,18 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
{
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters, $number);
if ($this->translator instanceof TranslatorInterface) {
$trans = $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale);
} else {
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
}
$this->collectMessage($locale, $domain, $id, $trans, ['%count%' => $number] + $parameters);
return $trans;
}
@@ -117,7 +130,7 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
*/
public function __call($method, $args)
{
return \call_user_func_array([$this->translator, $method], $args);
return $this->translator->{$method}(...$args);
}
/**
@@ -134,9 +147,8 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
* @param string $id
* @param string $translation
* @param array|null $parameters
* @param int|null $number
*/
private function collectMessage($locale, $domain, $id, $translation, $parameters = [], $number = null)
private function collectMessage($locale, $domain, $id, $translation, $parameters = [])
{
if (null === $domain) {
$domain = 'messages';
@@ -170,8 +182,8 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
'id' => $id,
'translation' => $translation,
'parameters' => $parameters,
'transChoiceNumber' => $number,
'state' => $state,
'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null,
];
}
}
@@ -23,7 +23,7 @@ class TranslationDumperPass implements CompilerPassInterface
private $writerServiceId;
private $dumperTag;
public function __construct($writerServiceId = 'translation.writer', $dumperTag = 'translation.dumper')
public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper')
{
$this->writerServiceId = $writerServiceId;
$this->dumperTag = $dumperTag;
@@ -24,7 +24,7 @@ class TranslationExtractorPass implements CompilerPassInterface
private $extractorServiceId;
private $extractorTag;
public function __construct($extractorServiceId = 'translation.extractor', $extractorTag = 'translation.extractor')
public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor')
{
$this->extractorServiceId = $extractorServiceId;
$this->extractorTag = $extractorTag;
@@ -24,12 +24,8 @@ class TranslatorPass implements CompilerPassInterface
private $debugCommandServiceId;
private $updateCommandServiceId;
public function __construct($translatorServiceId = 'translator.default', $readerServiceId = 'translation.loader', $loaderTag = 'translation.loader', $debugCommandServiceId = 'console.command.translation_debug', $updateCommandServiceId = 'console.command.translation_update')
public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update')
{
if ('translation.loader' === $readerServiceId && 2 > \func_num_args()) {
@trigger_error(sprintf('The default value for $readerServiceId in "%s()" will change in 4.0 to "translation.reader".', __METHOD__), \E_USER_DEPRECATED);
}
$this->translatorServiceId = $translatorServiceId;
$this->readerServiceId = $readerServiceId;
$this->loaderTag = $loaderTag;
@@ -62,18 +58,6 @@ class TranslatorPass implements CompilerPassInterface
}
}
// Duplicated code to support "translation.reader", to be removed in 4.0
if ('translation.reader' !== $this->readerServiceId) {
if ($container->hasDefinition('translation.reader')) {
$definition = $container->getDefinition('translation.reader');
foreach ($loaders as $id => $formats) {
foreach ($formats as $format) {
$definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]);
}
}
}
}
$container
->findDefinition($this->translatorServiceId)
->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs))
@@ -84,12 +68,22 @@ class TranslatorPass implements CompilerPassInterface
return;
}
$paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(2));
if ($container->hasDefinition($this->debugCommandServiceId)) {
$container->getDefinition($this->debugCommandServiceId)->replaceArgument(4, $container->getParameter('twig.default_path'));
}
$definition = $container->getDefinition($this->debugCommandServiceId);
$definition->replaceArgument(4, $container->getParameter('twig.default_path'));
if (\count($definition->getArguments()) > 6) {
$definition->replaceArgument(6, $paths);
}
}
if ($container->hasDefinition($this->updateCommandServiceId)) {
$container->getDefinition($this->updateCommandServiceId)->replaceArgument(5, $container->getParameter('twig.default_path'));
$definition = $container->getDefinition($this->updateCommandServiceId);
$definition->replaceArgument(5, $container->getParameter('twig.default_path'));
if (\count($definition->getArguments()) > 7) {
$definition->replaceArgument(7, $paths);
}
}
}
}
+29 -23
View File
@@ -17,7 +17,6 @@ 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
@@ -33,13 +32,6 @@ abstract class FileDumper implements DumperInterface
*/
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.
*
@@ -54,10 +46,16 @@ abstract class FileDumper implements DumperInterface
* Sets backup flag.
*
* @param bool $backup
*
* @deprecated since Symfony 4.1
*/
public function setBackup($backup)
{
$this->backup = $backup;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), E_USER_DEPRECATED);
if (false !== $backup) {
throw new \LogicException('The backup feature is no longer supported.');
}
}
/**
@@ -71,20 +69,33 @@ abstract class FileDumper implements DumperInterface
// 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) {
@trigger_error('Creating a backup while dumping a message catalogue is deprecated since Symfony 3.1 and will be removed in 4.0. Use TranslationWriter::disableBackup() to disable the backup.', \E_USER_DEPRECATED);
copy($fullpath, $fullpath.'~');
}
} else {
if (!file_exists($fullpath)) {
$directory = \dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory));
}
}
// save file
$intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX;
$intlMessages = $messages->all($intlDomain);
if ($intlMessages) {
$intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale());
file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options));
$messages->replace([], $intlDomain);
try {
if ($messages->all($domain)) {
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
continue;
} finally {
$messages->replace($intlMessages, $intlDomain);
}
}
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
}
@@ -107,13 +118,8 @@ abstract class FileDumper implements DumperInterface
/**
* 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)
private function getRelativePath(string $domain, string $locale): string
{
return strtr($this->relativePathTemplate, [
'%domain%' => $domain,
+1 -5
View File
@@ -25,11 +25,7 @@ class JsonFileDumper extends FileDumper
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
{
if (isset($options['json_encoding'])) {
$flags = $options['json_encoding'];
} else {
$flags = \JSON_PRETTY_PRINT;
}
$flags = $options['json_encoding'] ?? JSON_PRETTY_PRINT;
return json_encode($messages->all($domain), $flags);
}
+23
View File
@@ -39,6 +39,18 @@ class PoFileDumper extends FileDumper
} else {
$newLine = true;
}
$metadata = $messages->getMetadata($source, $domain);
if (isset($metadata['comments'])) {
$output .= $this->formatComments($metadata['comments']);
}
if (isset($metadata['flags'])) {
$output .= $this->formatComments(implode(',', (array) $metadata['flags']), ',');
}
if (isset($metadata['sources'])) {
$output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':');
}
$output .= sprintf('msgid "%s"'."\n", $this->escape($source));
$output .= sprintf('msgstr "%s"'."\n", $this->escape($target));
}
@@ -58,4 +70,15 @@ class PoFileDumper extends FileDumper
{
return addcslashes($str, "\0..\37\42\134");
}
private function formatComments($comments, string $prefix = ''): ?string
{
$output = null;
foreach ((array) $comments as $comment) {
$output .= sprintf('#%s %s'."\n", $prefix, $comment);
}
return $output;
}
}
+11
View File
@@ -33,6 +33,17 @@ class QtFileDumper extends FileDumper
foreach ($messages->all($domain) as $source => $target) {
$message = $context->appendChild($dom->createElement('message'));
$metadata = $messages->getMetadata($source, $domain);
if (isset($metadata['sources'])) {
foreach ((array) $metadata['sources'] as $location) {
$loc = explode(':', $location, 2);
$location = $message->appendChild($dom->createElement('location'));
$location->setAttribute('filename', $loc[0]);
if (isset($loc[1])) {
$location->setAttribute('line', $loc[1]);
}
}
}
$message->appendChild($dom->createElement('source', $source));
$message->appendChild($dom->createElement('translation', $target));
}
+10 -1
View File
@@ -141,11 +141,20 @@ class XliffFileDumper extends FileDumper
$xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale()));
$xliffFile = $xliff->appendChild($dom->createElement('file'));
$xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale());
if (MessageCatalogue::INTL_DOMAIN_SUFFIX === substr($domain, -($suffixLength = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)))) {
$xliffFile->setAttribute('id', substr($domain, 0, -$suffixLength).'.'.$messages->getLocale());
} else {
$xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale());
}
foreach ($messages->all($domain) as $source => $target) {
$translation = $dom->createElement('unit');
$translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._'));
$name = $source;
if (\strlen($source) > 80) {
$name = substr(md5($source), -7);
}
$translation->setAttribute('name', $name);
$metadata = $messages->getMetadata($source, $domain);
// Add notes section
+1 -1
View File
@@ -25,7 +25,7 @@ class YamlFileDumper extends FileDumper
{
private $extension;
public function __construct(/**string */$extension = 'yml')
public function __construct(string $extension = 'yml')
{
$this->extension = $extension;
}
@@ -16,6 +16,6 @@ namespace Symfony\Component\Translation\Exception;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ExceptionInterface
interface ExceptionInterface extends \Throwable
{
}
@@ -27,7 +27,7 @@ abstract class AbstractFileExtractor
*/
protected function extractFiles($resource)
{
if (\is_array($resource) || $resource instanceof \Traversable) {
if (is_iterable($resource)) {
$files = [];
foreach ($resource as $file) {
if ($this->canBeExtracted($file)) {
@@ -43,14 +43,9 @@ abstract class AbstractFileExtractor
return $files;
}
/**
* @param string $file
*
* @return \SplFileInfo
*/
private function toSplFileInfo($file)
private function toSplFileInfo(string $file): \SplFileInfo
{
return ($file instanceof \SplFileInfo) ? $file : new \SplFileInfo($file);
return new \SplFileInfo($file);
}
/**
+21 -14
View File
@@ -81,12 +81,9 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
{
$files = $this->extractFiles($resource);
foreach ($files as $file) {
$this->parseTokens(token_get_all(file_get_contents($file)), $catalog);
$this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file);
if (\PHP_VERSION_ID >= 70000) {
// PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098
gc_mem_caches();
}
gc_mem_caches();
}
}
@@ -121,7 +118,7 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
{
for (; $tokenIterator->valid(); $tokenIterator->next()) {
$t = $tokenIterator->current();
if (\T_WHITESPACE !== $t[0]) {
if (T_WHITESPACE !== $t[0]) {
break;
}
}
@@ -169,23 +166,23 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
}
switch ($t[0]) {
case \T_START_HEREDOC:
case T_START_HEREDOC:
$docToken = $t[1];
break;
case \T_ENCAPSED_AND_WHITESPACE:
case \T_CONSTANT_ENCAPSED_STRING:
case T_ENCAPSED_AND_WHITESPACE:
case T_CONSTANT_ENCAPSED_STRING:
if ('' === $docToken) {
$message .= PhpStringTokenParser::parse($t[1]);
} else {
$docPart = $t[1];
}
break;
case \T_END_HEREDOC:
case T_END_HEREDOC:
$message .= PhpStringTokenParser::parseDocString($docToken, $docPart);
$docToken = '';
$docPart = '';
break;
case \T_WHITESPACE:
case T_WHITESPACE:
break;
default:
break 2;
@@ -198,10 +195,16 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
/**
* Extracts trans message from PHP tokens.
*
* @param array $tokens
* @param array $tokens
* @param string $filename
*/
protected function parseTokens($tokens, MessageCatalogue $catalog)
protected function parseTokens($tokens, MessageCatalogue $catalog/*, string $filename*/)
{
if (\func_num_args() < 3 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) {
@trigger_error(sprintf('The "%s()" method will have a new "string $filename" argument in version 5.0, not defining it is deprecated since Symfony 4.3.', __METHOD__), E_USER_DEPRECATED);
}
$filename = 2 < \func_num_args() ? func_get_arg(2) : '';
$tokenIterator = new \ArrayIterator($tokens);
for ($key = 0; $key < $tokenIterator->count(); ++$key) {
@@ -238,6 +241,10 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
if ($message) {
$catalog->set($message, $this->prefix.$message, $domain);
$metadata = $catalog->getMetadata($message, $domain) ?? [];
$normalizedFilename = preg_replace('{[\\\\/]+}', '/', $filename);
$metadata['sources'][] = $normalizedFilename.':'.$tokens[$key][2];
$catalog->setMetadata($message, $metadata, $domain);
break;
}
}
@@ -253,7 +260,7 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
*/
protected function canBeExtracted($file)
{
return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION);
return $this->isFile($file) && 'php' === pathinfo($file, PATHINFO_EXTENSION);
}
/**
@@ -13,6 +13,8 @@ namespace Symfony\Component\Translation\Formatter;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @deprecated since Symfony 4.2, use MessageFormatterInterface::format() with a %count% parameter instead
*/
interface ChoiceMessageFormatterInterface
{
+38 -7
View File
@@ -11,21 +11,32 @@
namespace Symfony\Component\Translation\Formatter;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormatterInterface
class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface, ChoiceMessageFormatterInterface
{
private $selector;
private $translator;
private $intlFormatter;
/**
* @param MessageSelector|null $selector The message selector for pluralization
* @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization
*/
public function __construct(MessageSelector $selector = null)
public function __construct($translator = null, IntlFormatterInterface $intlFormatter = null)
{
$this->selector = $selector ?: new MessageSelector();
if ($translator instanceof MessageSelector) {
$translator = new IdentityTranslator($translator);
} elseif (null !== $translator && !$translator instanceof TranslatorInterface && !$translator instanceof LegacyTranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator ?? new IdentityTranslator();
$this->intlFormatter = $intlFormatter ?? new IntlFormatter();
}
/**
@@ -33,16 +44,36 @@ class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormat
*/
public function format($message, $locale, array $parameters = [])
{
if ($this->translator instanceof TranslatorInterface) {
return $this->translator->trans($message, $parameters, null, $locale);
}
return strtr($message, $parameters);
}
/**
* {@inheritdoc}
*/
public function formatIntl(string $message, string $locale, array $parameters = []): string
{
return $this->intlFormatter->formatIntl($message, $locale, $parameters);
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use format() with a %count% parameter instead
*/
public function choiceFormat($message, $number, $locale, array $parameters = [])
{
$parameters = array_merge(['%count%' => $number], $parameters);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the format() one instead with a %%count%% parameter.', __METHOD__), E_USER_DEPRECATED);
return $this->format($this->selector->choose($message, (int) $number, $locale), $locale, $parameters);
$parameters = ['%count%' => $number] + $parameters;
if ($this->translator instanceof TranslatorInterface) {
return $this->format($message, $locale, $parameters);
}
return $this->format($this->translator->transChoice($message, $number, [], null, $locale), $locale, $parameters);
}
}
+26 -28
View File
@@ -11,53 +11,51 @@
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
/**
* IdentityTranslator does not translate anything.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IdentityTranslator implements TranslatorInterface
class IdentityTranslator implements LegacyTranslatorInterface, TranslatorInterface
{
use TranslatorTrait;
private $selector;
private $locale;
/**
* @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 = [], $domain = null, $locale = null)
{
return strtr((string) $id, $parameters);
$this->selector = $selector;
if (__CLASS__ !== \get_class($this)) {
@trigger_error(sprintf('Calling "%s()" is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED);
}
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
{
return strtr($this->selector->choose((string) $id, (int) $number, $locale ?: $this->getLocale()), $parameters);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), E_USER_DEPRECATED);
if ($this->selector) {
return strtr($this->selector->choose((string) $id, $number, $locale ?: $this->getLocale()), $parameters);
}
return $this->trans($id, ['%count%' => $number] + $parameters, $domain, $locale);
}
private function getPluralizationRule(int $number, string $locale): int
{
return PluralizationRules::get($number, $locale, false);
}
}
+3
View File
@@ -11,6 +11,8 @@
namespace Symfony\Component\Translation;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', Interval::class), E_USER_DEPRECATED);
use Symfony\Component\Translation\Exception\InvalidArgumentException;
/**
@@ -32,6 +34,7 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException;
* @author Fabien Potencier <fabien@symfony.com>
*
* @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation
* @deprecated since Symfony 4.2, use IdentityTranslator instead
*/
class Interval
{
+1 -2
View File
@@ -39,12 +39,11 @@ class IcuDatFileLoader extends IcuResFileLoader
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));
throw new InvalidResourceException(sprintf('Cannot load resource "%s"', $resource));
} elseif (intl_is_failure($rb->getErrorCode())) {
throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
}
+1 -2
View File
@@ -39,12 +39,11 @@ class IcuResFileLoader implements LoaderInterface
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));
throw new InvalidResourceException(sprintf('Cannot load resource "%s"', $resource));
} elseif (intl_is_failure($rb->getErrorCode())) {
throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
}
+6 -6
View File
@@ -30,7 +30,7 @@ class JsonFileLoader extends FileLoader
$messages = json_decode($data, true);
if (0 < $errorCode = json_last_error()) {
throw new InvalidResourceException('Error parsing JSON: '.$this->getJSONErrorMessage($errorCode));
throw new InvalidResourceException(sprintf('Error parsing JSON - %s', $this->getJSONErrorMessage($errorCode)));
}
}
@@ -47,15 +47,15 @@ class JsonFileLoader extends FileLoader
private function getJSONErrorMessage($errorCode)
{
switch ($errorCode) {
case \JSON_ERROR_DEPTH:
case JSON_ERROR_DEPTH:
return 'Maximum stack depth exceeded';
case \JSON_ERROR_STATE_MISMATCH:
case JSON_ERROR_STATE_MISMATCH:
return 'Underflow or the modes mismatch';
case \JSON_ERROR_CTRL_CHAR:
case JSON_ERROR_CTRL_CHAR:
return 'Unexpected control character found';
case \JSON_ERROR_SYNTAX:
case JSON_ERROR_SYNTAX:
return 'Syntax error, malformed JSON';
case \JSON_ERROR_UTF8:
case JSON_ERROR_UTF8:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
default:
return 'Unknown error';
+1 -4
View File
@@ -129,11 +129,8 @@ class MoFileLoader extends FileLoader
* Reads an unsigned long from stream respecting endianness.
*
* @param resource $stream
* @param bool $isBigEndian
*
* @return int
*/
private function readLong($stream, $isBigEndian)
private function readLong($stream, bool $isBigEndian): int
{
$result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4));
$result = current($result);
+1 -1
View File
@@ -25,7 +25,7 @@ class PhpFileLoader extends FileLoader
*/
protected function loadResource($resource)
{
if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) {
if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(ini_get('opcache.enable_cli'), FILTER_VALIDATE_BOOLEAN))) {
self::$cache = null;
}
+48 -179
View File
@@ -13,10 +13,10 @@ namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Util\XliffUtils;
/**
* XliffFileLoader loads translations from XLIFF files.
@@ -53,11 +53,13 @@ class XliffFileLoader implements LoaderInterface
try {
$dom = XmlUtils::loadFile($resource);
} catch (\InvalidArgumentException $e) {
throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $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));
$xliffVersion = XliffUtils::getVersionNumber($dom);
if ($errors = XliffUtils::validateSchema($dom)) {
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $resource, XliffUtils::getErrorsAsString($errors)));
}
if ('1.2' === $xliffVersion) {
$this->extractXliff1($dom, $catalogue, $domain);
@@ -75,50 +77,60 @@ class XliffFileLoader implements LoaderInterface
* @param MessageCatalogue $catalogue Catalogue where we'll collect messages and metadata
* @param string $domain The domain
*/
private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, $domain)
private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $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();
$namespace = 'urn:oasis:names:tc:xliff:document:1.2';
$xml->registerXPathNamespace('xliff', $namespace);
if (!(isset($attributes['resname']) || isset($translation->source))) {
continue;
}
foreach ($xml->xpath('//xliff:file') as $file) {
$fileAttributes = $file->attributes();
$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 : $translation->source), $encoding);
$file->registerXPathNamespace('xliff', $namespace);
$catalogue->set((string) $source, $target, $domain);
foreach ($file->xpath('.//xliff:trans-unit') as $translation) {
$attributes = $translation->attributes();
$metadata = [];
if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) {
$metadata['notes'] = $notes;
}
if (isset($translation->target) && $translation->target->attributes()) {
$metadata['target-attributes'] = [];
foreach ($translation->target->attributes() as $key => $value) {
$metadata['target-attributes'][$key] = (string) $value;
if (!(isset($attributes['resname']) || isset($translation->source))) {
continue;
}
}
if (isset($attributes['id'])) {
$metadata['id'] = (string) $attributes['id'];
}
$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) ($translation->target ?? $translation->source), $encoding);
$catalogue->setMetadata((string) $source, $metadata, $domain);
$catalogue->set((string) $source, $target, $domain);
$metadata = [
'source' => (string) $translation->source,
'file' => [
'original' => (string) $fileAttributes['original'],
],
];
if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) {
$metadata['notes'] = $notes;
}
if (isset($translation->target) && $translation->target->attributes()) {
$metadata['target-attributes'] = [];
foreach ($translation->target->attributes() as $key => $value) {
$metadata['target-attributes'][$key] = (string) $value;
}
}
if (isset($attributes['id'])) {
$metadata['id'] = (string) $attributes['id'];
}
$catalogue->setMetadata((string) $source, $metadata, $domain);
}
}
}
/**
* @param string $domain
*/
private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, $domain)
private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain)
{
$xml = simplexml_import_dom($dom);
$encoding = strtoupper($dom->encoding);
@@ -162,13 +174,8 @@ class XliffFileLoader implements LoaderInterface
/**
* 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)
private function utf8ToCharset(string $content, string $encoding = null): string
{
if ('UTF-8' !== $encoding && !empty($encoding)) {
return mb_convert_encoding($content, $encoding, 'UTF-8');
@@ -177,145 +184,7 @@ class XliffFileLoader implements LoaderInterface
return $content;
}
/**
* Validates and parses the given file into a DOMDocument.
*
* @param string $file
* @param string $schema source of the schema
*
* @throws InvalidResourceException
*/
private function validateSchema($file, \DOMDocument $dom, $schema)
{
$internalErrors = libxml_use_internal_errors(true);
if (\LIBXML_VERSION < 20900) {
$disableEntities = libxml_disable_entity_loader(false);
$isValid = @$dom->schemaValidateSource($schema);
libxml_disable_entity_loader($disableEntities);
} else {
$isValid = @$dom->schemaValidateSource($schema);
}
if (!$isValid) {
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $file).implode("\n", $this->getXmlErrors($internalErrors)));
}
$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);
$locationstart = 'file:///';
if (0 === stripos($newPath, 'phar://')) {
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
if ($tmpfile) {
copy($newPath, $tmpfile);
$parts = explode('/', str_replace('\\', '/', $tmpfile));
} else {
array_shift($parts);
$locationstart = 'phar:///';
}
}
$drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$newPath = $locationstart.$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 = [];
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.
*
* @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 (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) {
throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s".', $namespace));
}
return substr($namespace, 34);
}
}
// Falls back to v1.2
return '1.2';
}
/**
* @param string|null $encoding
*
* @return array
*/
private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, $encoding = null)
private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, string $encoding = null): array
{
$notes = [];
+3 -10
View File
@@ -15,6 +15,7 @@ use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\LogicException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser as YamlParser;
use Symfony\Component\Yaml\Yaml;
/**
* YamlFileLoader loads translations from Yaml files.
@@ -38,18 +39,10 @@ class YamlFileLoader extends FileLoader
$this->yamlParser = new YamlParser();
}
$prevErrorHandler = set_error_handler(function ($level, $message, $script, $line) use ($resource, &$prevErrorHandler) {
$message = \E_USER_DEPRECATED === $level ? preg_replace('/ on line \d+/', ' in "'.$resource.'"$0', $message) : $message;
return $prevErrorHandler ? $prevErrorHandler($level, $message, $script, $line) : false;
});
try {
$messages = $this->yamlParser->parseFile($resource);
$messages = $this->yamlParser->parseFile($resource, Yaml::PARSE_CONSTANT);
} catch (ParseException $e) {
throw new InvalidResourceException(sprintf('The file "%s" does not contain valid YAML: ', $resource).$e->getMessage(), 0, $e);
} finally {
restore_error_handler();
throw new InvalidResourceException(sprintf('Error parsing YAML, invalid file "%s"', $resource), 0, $e);
}
if (null !== $messages && !\is_array($messages)) {
File diff suppressed because it is too large Load Diff
@@ -1,411 +0,0 @@
<?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>
@@ -1,309 +0,0 @@
<?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>
+27 -6
View File
@@ -13,11 +13,14 @@ namespace Symfony\Component\Translation;
use Psr\Log\LoggerInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface
class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterface, TranslatorBagInterface
{
/**
* @var TranslatorInterface|TranslatorBagInterface
@@ -29,10 +32,13 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
*/
public function __construct(TranslatorInterface $translator, LoggerInterface $logger)
public function __construct($translator, LoggerInterface $logger)
{
if (!$translator instanceof TranslatorBagInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', \get_class($translator)));
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', \get_class($translator)));
}
$this->translator = $translator;
@@ -52,10 +58,19 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
{
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), E_USER_DEPRECATED);
if ($this->translator instanceof TranslatorInterface) {
$trans = $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale);
} else {
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
}
$this->log($id, $domain, $locale);
return $trans;
@@ -66,7 +81,13 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface
*/
public function setLocale($locale)
{
$prev = $this->translator->getLocale();
$this->translator->setLocale($locale);
if ($prev === $locale) {
return;
}
$this->logger->debug(sprintf('The locale of the translator has changed from "%s" to "%s".', $prev, $locale));
}
/**
@@ -104,7 +125,7 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface
*/
public function __call($method, $args)
{
return \call_user_func_array([$this->translator, $method], $args);
return $this->translator->{$method}(...$args);
}
/**
+39 -9
View File
@@ -30,7 +30,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
* @param string $locale The locale
* @param array $messages An array of messages classified by domain
*/
public function __construct($locale, array $messages = [])
public function __construct(?string $locale, array $messages = [])
{
$this->locale = $locale;
$this->messages = $messages;
@@ -49,7 +49,17 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
*/
public function getDomains()
{
return array_keys($this->messages);
$domains = [];
$suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX);
foreach ($this->messages as $domain => $messages) {
if (\strlen($domain) > $suffixLength && false !== $i = strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) {
$domain = substr($domain, 0, $i);
}
$domains[$domain] = $domain;
}
return array_values($domains);
}
/**
@@ -57,11 +67,23 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
*/
public function all($domain = null)
{
if (null === $domain) {
return $this->messages;
if (null !== $domain) {
return ($this->messages[$domain.self::INTL_DOMAIN_SUFFIX] ?? []) + ($this->messages[$domain] ?? []);
}
return isset($this->messages[$domain]) ? $this->messages[$domain] : [];
$allMessages = [];
$suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX);
foreach ($this->messages as $domain => $messages) {
if (\strlen($domain) > $suffixLength && false !== $i = strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) {
$domain = substr($domain, 0, $i);
$allMessages[$domain] = $messages + ($allMessages[$domain] ?? []);
} else {
$allMessages[$domain] = ($allMessages[$domain] ?? []) + $messages;
}
}
return $allMessages;
}
/**
@@ -77,7 +99,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
*/
public function has($id, $domain = 'messages')
{
if (isset($this->messages[$domain][$id])) {
if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) {
return true;
}
@@ -93,7 +115,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
*/
public function defines($id, $domain = 'messages')
{
return isset($this->messages[$domain][$id]);
return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]);
}
/**
@@ -101,6 +123,10 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
*/
public function get($id, $domain = 'messages')
{
if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) {
return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id];
}
if (isset($this->messages[$domain][$id])) {
return $this->messages[$domain][$id];
}
@@ -117,7 +143,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
*/
public function replace($messages, $domain = 'messages')
{
$this->messages[$domain] = [];
unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]);
$this->add($messages, $domain);
}
@@ -142,10 +168,14 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
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));
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) {
if ($intlMessages = $catalogue->all($domain.self::INTL_DOMAIN_SUFFIX)) {
$this->add($intlMessages, $domain.self::INTL_DOMAIN_SUFFIX);
$messages = array_diff_key($messages, $intlMessages);
}
$this->add($messages, $domain);
}
@@ -20,6 +20,8 @@ use Symfony\Component\Config\Resource\ResourceInterface;
*/
interface MessageCatalogueInterface
{
const INTL_DOMAIN_SUFFIX = '+intl-icu';
/**
* Gets the catalogue locale.
*
+7 -3
View File
@@ -11,6 +11,8 @@
namespace Symfony\Component\Translation;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', MessageSelector::class), E_USER_DEPRECATED);
use Symfony\Component\Translation\Exception\InvalidArgumentException;
/**
@@ -18,6 +20,8 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.2, use IdentityTranslator instead.
*/
class MessageSelector
{
@@ -39,9 +43,9 @@ class MessageSelector
* 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
* @param string $message The message being translated
* @param int|float $number The number of items represented for the message
* @param string $locale The locale to use for choosing
*
* @return string
*
+10 -2
View File
@@ -15,6 +15,8 @@ namespace Symfony\Component\Translation;
* Returns the plural rules for a given locale.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Symfony 4.2, use IdentityTranslator instead
*/
class PluralizationRules
{
@@ -28,8 +30,12 @@ class PluralizationRules
*
* @return int The plural position
*/
public static function get($number, $locale)
public static function get($number, $locale/*, bool $triggerDeprecation = true*/)
{
if (3 > \func_num_args() || func_get_arg(2)) {
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), E_USER_DEPRECATED);
}
if ('pt_BR' === $locale) {
// temporary set a locale for brazilian
$locale = 'xbr';
@@ -40,7 +46,7 @@ class PluralizationRules
}
if (isset(self::$rules[$locale])) {
$return = \call_user_func(self::$rules[$locale], $number);
$return = self::$rules[$locale]($number);
if (!\is_int($return) || $return < 0) {
return 0;
@@ -196,6 +202,8 @@ class PluralizationRules
*/
public static function set(callable $rule, $locale)
{
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), E_USER_DEPRECATED);
if ('pt_BR' === $locale) {
// temporary set a locale for brazilian
$locale = 'xbr';
+1 -19
View File
@@ -3,28 +3,10 @@ Translation Component
The Translation component provides tools to internationalize your application.
Getting Started
---------------
```
$ composer require symfony/translation
```
```php
use Symfony\Component\Translation\Translator;
$translator = new Translator('fr_FR');
$translator->addResource('array', [
'Hello World!' => 'Bonjour !',
], 'fr_FR');
echo $translator->trans('Hello World!'); // outputs « Bonjour ! »
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/translation.html)
* [Documentation](https://symfony.com/doc/current/components/translation.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)
@@ -61,7 +61,7 @@ foreach (array_slice($argv, 1) as $argumentOrOption) {
foreach ($config['original_files'] as $originalFilePath) {
if (!file_exists($originalFilePath)) {
echo sprintf('The following file does not exist. Make sure that you execute this command at the root dir of the Symfony code repository.%s %s', \PHP_EOL, $originalFilePath);
echo sprintf('The following file does not exist. Make sure that you execute this command at the root dir of the Symfony code repository.%s %s', PHP_EOL, $originalFilePath);
exit(1);
}
}
@@ -89,7 +89,7 @@ function findTranslationFiles($originalFilePath, $localeToAnalyze)
$originalFileName = basename($originalFilePath);
$translationFileNamePattern = str_replace('.en.', '.*.', $originalFileName);
$translationFiles = glob($translationsDir.'/'.$translationFileNamePattern, \GLOB_NOSORT);
$translationFiles = glob($translationsDir.'/'.$translationFileNamePattern, GLOB_NOSORT);
sort($translationFiles);
foreach ($translationFiles as $filePath) {
$locale = extractLocaleFromFilePath($filePath);
@@ -127,7 +127,7 @@ function printTranslationStatus($originalFilePath, $translationStatus, $verboseO
{
printTitle($originalFilePath);
printTable($translationStatus, $verboseOutput);
echo \PHP_EOL.\PHP_EOL;
echo PHP_EOL.PHP_EOL;
}
function extractLocaleFromFilePath($filePath)
@@ -154,8 +154,8 @@ function extractTranslationKeys($filePath)
function printTitle($title)
{
echo $title.\PHP_EOL;
echo str_repeat('=', strlen($title)).\PHP_EOL.\PHP_EOL;
echo $title.PHP_EOL;
echo str_repeat('=', strlen($title)).PHP_EOL.PHP_EOL;
}
function printTable($translations, $verboseOutput)
@@ -174,19 +174,19 @@ function printTable($translations, $verboseOutput)
textColorGreen();
}
echo sprintf('| Locale: %-'.$longestLocaleNameLength.'s | Translated: %d/%d', $locale, $translation['translated'], $translation['total']).\PHP_EOL;
echo sprintf('| Locale: %-'.$longestLocaleNameLength.'s | Translated: %d/%d', $locale, $translation['translated'], $translation['total']).PHP_EOL;
textColorNormal();
if (true === $verboseOutput && count($translation['missingKeys']) > 0) {
echo str_repeat('-', 80).\PHP_EOL;
echo '| Missing Translations:'.\PHP_EOL;
echo str_repeat('-', 80).PHP_EOL;
echo '| Missing Translations:'.PHP_EOL;
foreach ($translation['missingKeys'] as $id => $content) {
echo sprintf('| (id=%s) %s', $id, $content).\PHP_EOL;
echo sprintf('| (id=%s) %s', $id, $content).PHP_EOL;
}
echo str_repeat('-', 80).\PHP_EOL;
echo str_repeat('-', 80).PHP_EOL;
}
}
}
@@ -53,6 +53,20 @@ class MergeOperationTest extends AbstractOperationTest
);
}
public function testGetResultFromIntlDomain()
{
$this->assertEquals(
new MessageCatalogue('en', [
'messages' => ['a' => 'old_a', 'b' => 'old_b'],
'messages+intl-icu' => ['d' => 'old_d', 'c' => 'new_c'],
]),
$this->createOperation(
new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b'], 'messages+intl-icu' => ['d' => 'old_d']]),
new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'new_a', 'c' => 'new_c']])
)->getResult()
);
}
public function testGetResultWithMetadata()
{
$leftCatalogue = new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]);
@@ -53,6 +53,20 @@ class TargetOperationTest extends AbstractOperationTest
);
}
public function testGetResultFromIntlDomain()
{
$this->assertEquals(
new MessageCatalogue('en', [
'messages' => ['a' => 'old_a'],
'messages+intl-icu' => ['c' => 'new_c'],
]),
$this->createOperation(
new MessageCatalogue('en', ['messages' => ['a' => 'old_a'], 'messages+intl-icu' => ['b' => 'old_b']]),
new MessageCatalogue('en', ['messages' => ['a' => 'new_a'], 'messages+intl-icu' => ['c' => 'new_c']])
)->getResult()
);
}
public function testGetResultWithMetadata()
{
$leftCatalogue = new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]);
@@ -17,7 +17,7 @@ use Symfony\Component\Translation\DataCollectorTranslator;
class TranslationDataCollectorTest extends TestCase
{
protected function setUp()
protected function setUp(): void
{
if (!class_exists('Symfony\Component\HttpKernel\DataCollector\DataCollector')) {
$this->markTestSkipped('The "DataCollector" is not available');
@@ -25,7 +25,7 @@ class DataCollectorTranslatorTest extends TestCase
$collector->trans('foo');
$collector->trans('bar');
$collector->transChoice('choice', 0);
$collector->trans('choice', ['%count%' => 0]);
$collector->trans('bar_ru');
$collector->trans('bar_ru', ['foo' => 'bar']);
@@ -57,7 +57,7 @@ class DataCollectorTranslatorTest extends TestCase
'fallbackLocale' => null,
'domain' => 'messages',
'state' => DataCollectorTranslator::MESSAGE_MISSING,
'parameters' => [],
'parameters' => ['%count%' => 0],
'transChoiceNumber' => 0,
];
$expectedMessages[] = [
@@ -84,6 +84,31 @@ class DataCollectorTranslatorTest extends TestCase
$this->assertEquals($expectedMessages, $collector->getCollectedMessages());
}
/**
* @group legacy
*/
public function testCollectMessagesTransChoice()
{
$collector = $this->createCollector();
$collector->setFallbackLocales(['fr', 'ru']);
$collector->transChoice('choice', 0);
$expectedMessages = [];
$expectedMessages[] = [
'id' => 'choice',
'translation' => 'choice',
'locale' => 'en',
'fallbackLocale' => null,
'domain' => 'messages',
'state' => DataCollectorTranslator::MESSAGE_MISSING,
'parameters' => ['%count%' => 0],
'transChoiceNumber' => 0,
];
$this->assertEquals($expectedMessages, $collector->getCollectedMessages());
}
private function createCollector()
{
$translator = new Translator('en');
@@ -55,49 +55,68 @@ class TranslationPassTest extends TestCase
$this->assertEquals($expected, $container->getDefinition((string) $translator->getArgument(0))->getArgument(0));
}
/**
* @group legacy
* @expectedDeprecation The default value for $readerServiceId in "Symfony\Component\Translation\DependencyInjection\TranslatorPass::__construct()" will change in 4.0 to "translation.reader".
*
* A test that verifies the deprecated "translation.loader" gets the LoaderInterfaces added.
*
* This test should be removed in 4.0.
*/
public function testValidCollectorWithDeprecatedTranslationLoader()
public function testValidCommandsViewPathsArgument()
{
$loader = (new Definition())
->addTag('translation.loader', ['alias' => 'xliff', 'legacy-alias' => 'xlf']);
$legacyReader = new Definition();
$reader = new Definition();
$translator = (new Definition())
->setArguments([null, null, null, null]);
$container = new ContainerBuilder();
$container->setDefinition('translator.default', $translator);
$container->setDefinition('translation.loader', $legacyReader);
$container->setDefinition('translation.reader', $reader);
$container->setDefinition('translation.xliff_loader', $loader);
$container->register('translator.default')
->setArguments([null, null, null, null])
;
$debugCommand = $container->register('console.command.translation_debug')
->setArguments([null, null, null, null, null, [], []])
;
$updateCommand = $container->register('console.command.translation_update')
->setArguments([null, null, null, null, null, null, [], []])
;
$container->register('twig.template_iterator')
->setArguments([null, null, ['other/templates' => null, 'tpl' => 'App']])
;
$container->setParameter('twig.default_path', 'templates');
$pass = new TranslatorPass();
$pass = new TranslatorPass('translator.default');
$pass->process($container);
$expectedReader = (new Definition())
->addMethodCall('addLoader', ['xliff', new Reference('translation.xliff_loader')])
->addMethodCall('addLoader', ['xlf', new Reference('translation.xliff_loader')])
$expectedViewPaths = ['other/templates', 'tpl'];
$this->assertSame('templates', $debugCommand->getArgument(4));
$this->assertSame('templates', $updateCommand->getArgument(5));
$this->assertSame($expectedViewPaths, $debugCommand->getArgument(6));
$this->assertSame($expectedViewPaths, $updateCommand->getArgument(7));
}
public function testCommandsViewPathsArgumentsAreIgnoredWithOldServiceDefinitions()
{
$container = new ContainerBuilder();
$container->register('translator.default')
->setArguments([null, null, null, null])
;
$this->assertEquals($expectedReader, $legacyReader);
$this->assertEquals($expectedReader, $reader);
$expectedLoader = (new Definition())
->addTag('translation.loader', ['alias' => 'xliff', 'legacy-alias' => 'xlf'])
$debugCommand = $container->register('console.command.translation_debug')
->setArguments([
new Reference('translator'),
new Reference('translation.reader'),
new Reference('translation.extractor'),
'%translator.default_path%',
null,
])
;
$this->assertEquals($expectedLoader, $loader);
$updateCommand = $container->register('console.command.translation_update')
->setArguments([
new Reference('translation.writer'),
new Reference('translation.reader'),
new Reference('translation.extractor'),
'%kernel.default_locale%',
'%translator.default_path%',
null,
])
;
$container->register('twig.template_iterator')
->setArguments([null, null, ['other/templates' => null, 'tpl' => 'App']])
;
$container->setParameter('twig.default_path', 'templates');
$this->assertSame(['translation.xliff_loader' => ['xliff', 'xlf']], $translator->getArgument(3));
$pass = new TranslatorPass('translator.default');
$pass->process($container);
$expected = ['translation.xliff_loader' => new ServiceClosureArgument(new Reference('translation.xliff_loader'))];
$this->assertEquals($expected, $container->getDefinition((string) $translator->getArgument(0))->getArgument(0));
$this->assertSame('templates', $debugCommand->getArgument(4));
$this->assertSame('templates', $updateCommand->getArgument(5));
}
}
+14 -13
View File
@@ -32,27 +32,28 @@ class FileDumperTest extends TestCase
@unlink($tempDir.'/messages.en.concrete');
}
/**
* @group legacy
*/
public function testDumpBackupsFileIfExisting()
public function testDumpIntl()
{
$tempDir = sys_get_temp_dir();
$file = $tempDir.'/messages.en.concrete';
$backupFile = $file.'~';
@touch($file);
$catalogue = new MessageCatalogue('en');
$catalogue->add(['foo' => 'bar']);
$catalogue->add(['foo' => 'bar'], 'd1');
$catalogue->add(['bar' => 'foo'], 'd1+intl-icu');
$catalogue->add(['bar' => 'foo'], 'd2+intl-icu');
$dumper = new ConcreteFileDumper();
@unlink($tempDir.'/d2.en.concrete');
$dumper->dump($catalogue, ['path' => $tempDir]);
$this->assertFileExists($backupFile);
$this->assertStringEqualsFile($tempDir.'/d1.en.concrete', 'foo=bar');
@unlink($tempDir.'/d1.en.concrete');
@unlink($file);
@unlink($backupFile);
$this->assertStringEqualsFile($tempDir.'/d1+intl-icu.en.concrete', 'bar=foo');
@unlink($tempDir.'/d1+intl-icu.en.concrete');
$this->assertFileNotExists($tempDir.'/d2.en.concrete');
$this->assertStringEqualsFile($tempDir.'/d2+intl-icu.en.concrete', 'bar=foo');
@unlink($tempDir.'/d2+intl-icu.en.concrete');
}
public function testDumpCreatesNestedDirectoriesAndFile()
@@ -79,7 +80,7 @@ class ConcreteFileDumper extends FileDumper
{
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
{
return '';
return http_build_query($messages->all($domain), '', '&');
}
protected function getExtension()
@@ -34,6 +34,6 @@ class JsonFileDumperTest extends TestCase
$dumper = new JsonFileDumper();
$this->assertStringEqualsFile(__DIR__.'/../fixtures/resources.dump.json', $dumper->formatCatalogue($catalogue, 'messages', ['json_encoding' => \JSON_HEX_QUOT]));
$this->assertStringEqualsFile(__DIR__.'/../fixtures/resources.dump.json', $dumper->formatCatalogue($catalogue, 'messages', ['json_encoding' => JSON_HEX_QUOT]));
}
}
@@ -20,7 +20,26 @@ class PoFileDumperTest extends TestCase
public function testFormatCatalogue()
{
$catalogue = new MessageCatalogue('en');
$catalogue->add(['foo' => 'bar', 'bar' => 'foo']);
$catalogue->add(['foo' => 'bar', 'bar' => 'foo', 'foo_bar' => 'foobar', 'bar_foo' => 'barfoo']);
$catalogue->setMetadata('foo_bar', [
'comments' => [
'Comment 1',
'Comment 2',
],
'flags' => [
'fuzzy',
'another',
],
'sources' => [
'src/file_1',
'src/file_2:50',
],
]);
$catalogue->setMetadata('bar_foo', [
'comments' => 'Comment',
'flags' => 'fuzzy',
'sources' => 'src/file_1',
]);
$dumper = new PoFileDumper();
@@ -20,7 +20,26 @@ class QtFileDumperTest extends TestCase
public function testFormatCatalogue()
{
$catalogue = new MessageCatalogue('en');
$catalogue->add(['foo' => 'bar'], 'resources');
$catalogue->add(['foo' => 'bar', 'foo_bar' => 'foobar', 'bar_foo' => 'barfoo'], 'resources');
$catalogue->setMetadata('foo_bar', [
'comments' => [
'Comment 1',
'Comment 2',
],
'flags' => [
'fuzzy',
'another',
],
'sources' => [
'src/file_1',
'src/file_2:50',
],
], 'resources');
$catalogue->setMetadata('bar_foo', [
'comments' => 'Comment',
'flags' => 'fuzzy',
'sources' => 'src/file_1',
], 'resources');
$dumper = new QtFileDumper();
@@ -54,6 +54,21 @@ class XliffFileDumperTest extends TestCase
);
}
public function testFormatIcuCatalogueXliff2()
{
$catalogue = new MessageCatalogue('en_US');
$catalogue->add([
'foo' => 'bar',
], 'messages'.MessageCatalogue::INTL_DOMAIN_SUFFIX);
$dumper = new XliffFileDumper();
$this->assertStringEqualsFile(
__DIR__.'/../fixtures/resources-2.0+intl-icu.xlf',
$dumper->formatCatalogue($catalogue, 'messages'.MessageCatalogue::INTL_DOMAIN_SUFFIX, ['default_locale' => 'fr_FR', 'xliff_version' => '2.0'])
);
}
public function testFormatCatalogueWithCustomToolInfo()
{
$options = [
@@ -69,6 +69,10 @@ EOF;
$actualCatalogue = $catalogue->all();
$this->assertEquals($expectedCatalogue, $actualCatalogue);
$filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../fixtures/extractor/translation.html.php';
$this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('single-quoted key'));
$this->assertEquals(['sources' => [$filename.':43']], $catalogue->getMetadata('other-domain-test-no-params-short-array', 'not_messages'));
}
public function resourcesProvider()
@@ -26,6 +26,7 @@ class MessageFormatterTest extends TestCase
/**
* @dataProvider getTransChoiceMessages
* @group legacy
*/
public function testFormatPlural($expected, $message, $number, $parameters)
{
+6 -79
View File
@@ -11,102 +11,29 @@
namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Contracts\Translation\Test\TranslatorTest;
class IdentityTranslatorTest extends TestCase
class IdentityTranslatorTest extends TranslatorTest
{
private $defaultLocale;
protected function setUp()
protected function setUp(): void
{
parent::setUp();
$this->defaultLocale = \Locale::getDefault();
}
protected function tearDown()
protected function tearDown(): void
{
parent::tearDown();
\Locale::setDefault($this->defaultLocale);
}
/**
* @dataProvider getTransTests
*/
public function testTrans($expected, $id, $parameters)
public function getTranslator()
{
$translator = new IdentityTranslator();
$this->assertEquals($expected, $translator->trans($id, $parameters));
}
/**
* @dataProvider getTransChoiceTests
*/
public function testTransChoiceWithExplicitLocale($expected, $id, $number, $parameters)
{
$translator = new IdentityTranslator();
$translator->setLocale('en');
$this->assertEquals($expected, $translator->transChoice($id, $number, $parameters));
}
/**
* @dataProvider getTransChoiceTests
*/
public function testTransChoiceWithDefaultLocale($expected, $id, $number, $parameters)
{
\Locale::setDefault('en');
$translator = new IdentityTranslator();
$this->assertEquals($expected, $translator->transChoice($id, $number, $parameters));
}
public function testGetSetLocale()
{
$translator = new IdentityTranslator();
$translator->setLocale('en');
$this->assertEquals('en', $translator->getLocale());
}
public function testGetLocaleReturnsDefaultLocaleIfNotSet()
{
// in order to test with "pt_BR"
IntlTestHelper::requireFullIntl($this, false);
$translator = new IdentityTranslator();
\Locale::setDefault('en');
$this->assertEquals('en', $translator->getLocale());
\Locale::setDefault('pt_BR');
$this->assertEquals('pt_BR', $translator->getLocale());
}
public function getTransTests()
{
return [
['Symfony is great!', 'Symfony is great!', []],
['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']],
];
}
public function getTransChoiceTests()
{
return [
['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0, ['%count%' => 0]],
['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1, ['%count%' => 1]],
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10, ['%count%' => 10]],
['There are 0 apples', 'There is 1 apple|There are %count% apples', 0, ['%count%' => 0]],
['There is 1 apple', 'There is 1 apple|There are %count% apples', 1, ['%count%' => 1]],
['There are 10 apples', 'There is 1 apple|There are %count% apples', 10, ['%count%' => 10]],
// custom validation messages may be coded with a fixed value
['There are 2 apples', 'There are 2 apples', 2, ['%count%' => 2]],
];
return new IdentityTranslator();
}
}
+3
View File
@@ -14,6 +14,9 @@ namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Interval;
/**
* @group legacy
*/
class IntervalTest extends TestCase
{
/**
@@ -50,7 +50,7 @@ class JsonFileLoaderTest extends TestCase
public function testParseException()
{
$this->expectException('Symfony\Component\Translation\Exception\InvalidResourceException');
$this->expectExceptionMessage('Error parsing JSON: Syntax error, malformed JSON');
$this->expectExceptionMessage('Error parsing JSON - Syntax error, malformed JSON');
$loader = new JsonFileLoader();
$resource = __DIR__.'/../fixtures/malformed.json';
$loader->load($resource, 'en', 'domain1');
@@ -15,7 +15,7 @@ use PHPUnit\Framework\TestCase;
abstract class LocalizedTestCase extends TestCase
{
protected function setUp()
protected function setUp(): void
{
if (!\extension_loaded('intl')) {
$this->markTestSkipped('Extension intl is required.');
@@ -23,7 +23,11 @@ class QtFileLoaderTest extends TestCase
$resource = __DIR__.'/../fixtures/resources.ts';
$catalogue = $loader->load($resource, 'en', 'resources');
$this->assertEquals(['foo' => 'bar'], $catalogue->all('resources'));
$this->assertEquals([
'foo' => 'bar',
'foo_bar' => 'foobar',
'bar_foo' => 'barfoo',
], $catalogue->all('resources'));
$this->assertEquals('en', $catalogue->getLocale());
$this->assertEquals([new FileResource($resource)], $catalogue->getResources());
}
@@ -49,17 +49,13 @@ class XliffFileLoaderTest extends TestCase
public function testLoadWithExternalEntitiesDisabled()
{
if (\LIBXML_VERSION < 20900) {
$disableEntities = libxml_disable_entity_loader(true);
}
$disableEntities = libxml_disable_entity_loader(true);
$loader = new XliffFileLoader();
$resource = __DIR__.'/../fixtures/resources.xlf';
$catalogue = $loader->load($resource, 'en', 'domain1');
if (\LIBXML_VERSION < 20900) {
libxml_disable_entity_loader($disableEntities);
}
libxml_disable_entity_loader($disableEntities);
$this->assertEquals('en', $catalogue->getLocale());
$this->assertEquals([new FileResource($resource)], $catalogue->getResources());
@@ -88,7 +84,17 @@ class XliffFileLoaderTest extends TestCase
$this->assertEquals(utf8_decode('föö'), $catalogue->get('bar', 'domain1'));
$this->assertEquals(utf8_decode('bär'), $catalogue->get('foo', 'domain1'));
$this->assertEquals(['notes' => [['content' => utf8_decode('bäz')]], 'id' => '1'], $catalogue->getMetadata('foo', 'domain1'));
$this->assertEquals(
[
'source' => 'foo',
'notes' => [['content' => utf8_decode('bäz')]],
'id' => '1',
'file' => [
'original' => 'file.ext',
],
],
$catalogue->getMetadata('foo', 'domain1')
);
}
public function testTargetAttributesAreStoredCorrectly()
@@ -154,11 +160,44 @@ class XliffFileLoaderTest extends TestCase
$loader = new XliffFileLoader();
$catalogue = $loader->load(__DIR__.'/../fixtures/withnote.xlf', 'en', 'domain1');
$this->assertEquals(['notes' => [['priority' => 1, 'content' => 'foo']], 'id' => '1'], $catalogue->getMetadata('foo', 'domain1'));
$this->assertEquals(
[
'source' => 'foo',
'notes' => [['priority' => 1, 'content' => 'foo']],
'id' => '1',
'file' => [
'original' => 'file.ext',
],
],
$catalogue->getMetadata('foo', 'domain1')
);
// message without target
$this->assertEquals(['notes' => [['content' => 'bar', 'from' => 'foo']], 'id' => '2'], $catalogue->getMetadata('extra', 'domain1'));
$this->assertEquals(
[
'source' => 'extrasource',
'notes' => [['content' => 'bar', 'from' => 'foo']],
'id' => '2',
'file' => [
'original' => 'file.ext',
],
],
$catalogue->getMetadata('extra', 'domain1')
);
// message with empty target
$this->assertEquals(['notes' => [['content' => 'baz'], ['priority' => 2, 'from' => 'bar', 'content' => 'qux']], 'id' => '123'], $catalogue->getMetadata('key', 'domain1'));
$this->assertEquals(
[
'source' => 'key',
'notes' => [
['content' => 'baz'],
['priority' => 2, 'from' => 'bar', 'content' => 'qux'],
],
'id' => '123',
'file' => [
'original' => 'file.ext',
],
],
$catalogue->getMetadata('key', 'domain1')
);
}
public function testLoadVersion2()
@@ -247,4 +286,32 @@ class XliffFileLoaderTest extends TestCase
$this->assertSame('processed', $metadata['notes'][0]['category']);
$this->assertSame('true', $metadata['notes'][0]['content']);
}
public function testLoadWithMultipleFileNodes()
{
$loader = new XliffFileLoader();
$catalogue = $loader->load(__DIR__.'/../fixtures/resources-multi-files.xlf', 'en', 'domain1');
$this->assertEquals(
[
'source' => 'foo',
'id' => '1',
'file' => [
'original' => 'file.ext',
],
],
$catalogue->getMetadata('foo', 'domain1')
);
$this->assertEquals(
[
'source' => 'test',
'notes' => [['content' => 'note']],
'id' => '4',
'file' => [
'original' => 'otherfile.ext',
],
],
$catalogue->getMetadata('test', 'domain1')
);
}
}
+20 -2
View File
@@ -21,17 +21,19 @@ class LoggingTranslatorTest extends TestCase
public function testTransWithNoTranslationIsLogged()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
$logger->expects($this->exactly(2))
$logger->expects($this->exactly(1))
->method('warning')
->with('Translation not found.')
;
$translator = new Translator('ar');
$loggableTranslator = new LoggingTranslator($translator, $logger);
$loggableTranslator->transChoice('some_message2', 10, ['%count%' => 10]);
$loggableTranslator->trans('bar');
}
/**
* @group legacy
*/
public function testTransChoiceFallbackIsLogged()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
@@ -47,4 +49,20 @@ class LoggingTranslatorTest extends TestCase
$loggableTranslator = new LoggingTranslator($translator, $logger);
$loggableTranslator->transChoice('some_message2', 10, ['%count%' => 10]);
}
/**
* @group legacy
*/
public function testTransChoiceWithNoTranslationIsLogged()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
$logger->expects($this->exactly(1))
->method('warning')
->with('Translation not found.')
;
$translator = new Translator('ar');
$loggableTranslator = new LoggingTranslator($translator, $logger);
$loggableTranslator->transChoice('some_message2', 10, ['%count%' => 10]);
}
}
+28 -7
View File
@@ -25,9 +25,9 @@ class MessageCatalogueTest extends TestCase
public function testGetDomains()
{
$catalogue = new MessageCatalogue('en', ['domain1' => [], 'domain2' => []]);
$catalogue = new MessageCatalogue('en', ['domain1' => [], 'domain2' => [], 'domain2+intl-icu' => [], 'domain3+intl-icu' => []]);
$this->assertEquals(['domain1', 'domain2'], $catalogue->getDomains());
$this->assertEquals(['domain1', 'domain2', 'domain3'], $catalogue->getDomains());
}
public function testAll()
@@ -37,24 +37,43 @@ class MessageCatalogueTest extends TestCase
$this->assertEquals(['foo' => 'foo'], $catalogue->all('domain1'));
$this->assertEquals([], $catalogue->all('domain88'));
$this->assertEquals($messages, $catalogue->all());
$messages = ['domain1+intl-icu' => ['foo' => 'bar']] + $messages + [
'domain2+intl-icu' => ['bar' => 'foo'],
'domain3+intl-icu' => ['biz' => 'biz'],
];
$catalogue = new MessageCatalogue('en', $messages);
$this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1'));
$this->assertEquals(['bar' => 'foo'], $catalogue->all('domain2'));
$this->assertEquals(['biz' => 'biz'], $catalogue->all('domain3'));
$messages = [
'domain1' => ['foo' => 'bar'],
'domain2' => ['bar' => 'foo'],
'domain3' => ['biz' => 'biz'],
];
$this->assertEquals($messages, $catalogue->all());
}
public function testHas()
{
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]);
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2+intl-icu' => ['bar' => 'bar']]);
$this->assertTrue($catalogue->has('foo', 'domain1'));
$this->assertTrue($catalogue->has('bar', 'domain2'));
$this->assertFalse($catalogue->has('bar', 'domain1'));
$this->assertFalse($catalogue->has('foo', 'domain88'));
}
public function testGetSet()
{
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]);
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar'], 'domain2+intl-icu' => ['bar' => 'foo']]);
$catalogue->set('foo1', 'foo1', 'domain1');
$this->assertEquals('foo', $catalogue->get('foo', 'domain1'));
$this->assertEquals('foo1', $catalogue->get('foo1', 'domain1'));
$this->assertEquals('foo', $catalogue->get('bar', 'domain2'));
}
public function testAdd()
@@ -75,7 +94,7 @@ class MessageCatalogueTest extends TestCase
public function testReplace()
{
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]);
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain1+intl-icu' => ['bar' => 'bar']]);
$catalogue->replace($messages = ['foo1' => 'foo1'], 'domain1');
$this->assertEquals($messages, $catalogue->all('domain1'));
@@ -89,16 +108,18 @@ class MessageCatalogueTest extends TestCase
$r1 = $this->getMockBuilder('Symfony\Component\Config\Resource\ResourceInterface')->getMock();
$r1->expects($this->any())->method('__toString')->willReturn('r1');
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]);
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo']]);
$catalogue->addResource($r);
$catalogue1 = new MessageCatalogue('en', ['domain1' => ['foo1' => 'foo1']]);
$catalogue1 = new MessageCatalogue('en', ['domain1' => ['foo1' => 'foo1'], 'domain2+intl-icu' => ['bar' => 'bar']]);
$catalogue1->addResource($r1);
$catalogue->addCatalogue($catalogue1);
$this->assertEquals('foo', $catalogue->get('foo', 'domain1'));
$this->assertEquals('foo1', $catalogue->get('foo1', 'domain1'));
$this->assertEquals('bar', $catalogue->get('bar', 'domain2'));
$this->assertEquals('bar', $catalogue->get('bar', 'domain2+intl-icu'));
$this->assertEquals([$r, $r1], $catalogue->getResources());
}
@@ -14,6 +14,9 @@ namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\MessageSelector;
/**
* @group legacy
*/
class MessageSelectorTest extends TestCase
{
/**
@@ -26,6 +26,8 @@ use Symfony\Component\Translation\PluralizationRules;
* The goal to cover all languages is to far fetched so this test case is smaller.
*
* @author Clemens Tolboom clemens@build2be.nl
*
* @group legacy
*/
class PluralizationRulesTest extends TestCase
{
@@ -100,9 +102,9 @@ class PluralizationRulesTest extends TestCase
foreach ($matrix as $langCode => $data) {
$indexes = array_flip($data);
if ($expectSuccess) {
$this->assertCount((int) $nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms.");
$this->assertEquals($nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms.");
} else {
$this->assertNotCount((int) $nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms.");
$this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms.");
}
}
}
+26 -3
View File
@@ -23,13 +23,13 @@ class TranslatorCacheTest extends TestCase
{
protected $tmpDir;
protected function setUp()
protected function setUp(): void
{
$this->tmpDir = sys_get_temp_dir().'/sf2_translation';
$this->tmpDir = sys_get_temp_dir().'/sf_translation';
$this->deleteTmpDir();
}
protected function tearDown()
protected function tearDown(): void
{
$this->deleteTmpDir();
}
@@ -67,13 +67,17 @@ class TranslatorCacheTest extends TestCase
$translator = new Translator($locale, null, $this->tmpDir, $debug);
$translator->addLoader($format, new ArrayLoader());
$translator->addResource($format, [$msgid => 'OK'], $locale);
$translator->addResource($format, [$msgid.'+intl' => 'OK'], $locale, 'messages+intl-icu');
$translator->trans($msgid);
$translator->trans($msgid.'+intl', [], 'messages+intl-icu');
// Try again and see we get a valid result whilst no loader can be used
$translator = new Translator($locale, null, $this->tmpDir, $debug);
$translator->addLoader($format, $this->createFailingLoader());
$translator->addResource($format, [$msgid => 'OK'], $locale);
$translator->addResource($format, [$msgid.'+intl' => 'OK'], $locale, 'messages+intl-icu');
$this->assertEquals('OK', $translator->trans($msgid), '-> caching does not work in '.($debug ? 'debug' : 'production'));
$this->assertEquals('OK', $translator->trans($msgid.'+intl', [], 'messages+intl-icu'));
}
public function testCatalogueIsReloadedWhenResourcesAreNoLongerFresh()
@@ -213,6 +217,7 @@ class TranslatorCacheTest extends TestCase
$translator->addResource('array', ['foo' => 'foo (a)'], 'a');
$translator->addResource('array', ['foo' => 'foo (b)'], 'b');
$translator->addResource('array', ['bar' => 'bar (b)'], 'b');
$translator->addResource('array', ['baz' => 'baz (b)'], 'b', 'messages+intl-icu');
$catalogue = $translator->getCatalogue('a');
$this->assertFalse($catalogue->defines('bar')); // Sure, the "a" catalogue does not contain that message.
@@ -231,12 +236,14 @@ class TranslatorCacheTest extends TestCase
$translator->addResource('array', ['foo' => 'foo (a)'], 'a');
$translator->addResource('array', ['foo' => 'foo (b)'], 'b');
$translator->addResource('array', ['bar' => 'bar (b)'], 'b');
$translator->addResource('array', ['baz' => 'baz (b)'], 'b', 'messages+intl-icu');
$catalogue = $translator->getCatalogue('a');
$this->assertFalse($catalogue->defines('bar'));
$fallback = $catalogue->getFallbackCatalogue();
$this->assertTrue($fallback->defines('foo'));
$this->assertTrue($fallback->defines('baz', 'messages+intl-icu'));
}
public function testRefreshCacheWhenResourcesAreNoLongerFresh()
@@ -262,6 +269,22 @@ class TranslatorCacheTest extends TestCase
$translator->trans('foo');
}
public function testCachedCatalogueIsReDumpedWhenCacheVaryChange()
{
$translator = new Translator('a', null, $this->tmpDir, false, []);
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', ['foo' => 'bar'], 'a', 'messages');
// Cached catalogue is dumped
$this->assertSame('bar', $translator->trans('foo', [], 'messages', 'a'));
$translator = new Translator('a', null, $this->tmpDir, false, ['vary']);
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', ['foo' => 'ccc'], 'a', 'messages');
$this->assertSame('ccc', $translator->trans('foo', [], 'messages', 'a'));
}
protected function getCatalogue($locale, $messages, $resources = [])
{
$catalogue = new MessageCatalogue($locale);
+67 -15
View File
@@ -12,7 +12,6 @@
namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Exception\RuntimeException;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Translator;
@@ -235,6 +234,42 @@ class TranslatorTest extends TestCase
$this->assertEquals('bar', $translator->trans('foo', [], 'resources'));
}
public function testTransWithIcuFallbackLocale()
{
$translator = new Translator('en_GB');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', ['foo' => 'foofoo'], 'en_GB');
$translator->addResource('array', ['bar' => 'foobar'], 'en_001');
$translator->addResource('array', ['baz' => 'foobaz'], 'en');
$this->assertSame('foofoo', $translator->trans('foo'));
$this->assertSame('foobar', $translator->trans('bar'));
$this->assertSame('foobaz', $translator->trans('baz'));
}
public function testTransWithIcuVariantFallbackLocale()
{
$translator = new Translator('en_GB_scouse');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', ['foo' => 'foofoo'], 'en_GB_scouse');
$translator->addResource('array', ['bar' => 'foobar'], 'en_GB');
$translator->addResource('array', ['baz' => 'foobaz'], 'en_001');
$translator->addResource('array', ['qux' => 'fooqux'], 'en');
$this->assertSame('foofoo', $translator->trans('foo'));
$this->assertSame('foobar', $translator->trans('bar'));
$this->assertSame('foobaz', $translator->trans('baz'));
$this->assertSame('fooqux', $translator->trans('qux'));
}
public function testTransWithIcuRootFallbackLocale()
{
$translator = new Translator('az_Cyrl');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', ['foo' => 'foofoo'], 'az_Cyrl');
$translator->addResource('array', ['bar' => 'foobar'], 'az');
$this->assertSame('foofoo', $translator->trans('foo'));
$this->assertSame('bar', $translator->trans('bar'));
}
/**
* @dataProvider getFallbackLocales
*/
@@ -319,12 +354,12 @@ class TranslatorTest extends TestCase
$resources = $translator->getCatalogue('en')->getResources();
$this->assertCount(1, $resources);
$this->assertContainsEquals(__DIR__.\DIRECTORY_SEPARATOR.'fixtures'.\DIRECTORY_SEPARATOR.'resources.yml', $resources);
$this->assertContains(__DIR__.\DIRECTORY_SEPARATOR.'fixtures'.\DIRECTORY_SEPARATOR.'resources.yml', $resources);
$resources = $translator->getCatalogue('en_GB')->getResources();
$this->assertCount(2, $resources);
$this->assertContainsEquals(__DIR__.\DIRECTORY_SEPARATOR.'fixtures'.\DIRECTORY_SEPARATOR.'empty.yml', $resources);
$this->assertContainsEquals(__DIR__.\DIRECTORY_SEPARATOR.'fixtures'.\DIRECTORY_SEPARATOR.'resources.yml', $resources);
$this->assertContains(__DIR__.\DIRECTORY_SEPARATOR.'fixtures'.\DIRECTORY_SEPARATOR.'empty.yml', $resources);
$this->assertContains(__DIR__.\DIRECTORY_SEPARATOR.'fixtures'.\DIRECTORY_SEPARATOR.'resources.yml', $resources);
}
/**
@@ -379,6 +414,7 @@ class TranslatorTest extends TestCase
/**
* @dataProvider getTransChoiceTests
* @group legacy
*/
public function testTransChoice($expected, $id, $translation, $number, $parameters, $locale, $domain)
{
@@ -391,6 +427,7 @@ class TranslatorTest extends TestCase
/**
* @dataProvider getInvalidLocalesTests
* @group legacy
*/
public function testTransChoiceInvalidLocale($locale)
{
@@ -404,6 +441,7 @@ class TranslatorTest extends TestCase
/**
* @dataProvider getValidLocalesTests
* @group legacy
*/
public function testTransChoiceValidLocale($locale)
{
@@ -485,7 +523,7 @@ class TranslatorTest extends TestCase
['Il y a 0 pomme', new StringClass('{0} There are no appless|{1} There is one apple|]1,Inf] There is %count% apples'), '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, [], 'fr', ''],
// Override %count% with a custom value
['Il y a quelques pommes', 'one: There is one apple|more: There are %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 2, ['%count%' => 'quelques'], 'fr', ''],
['Il y a quelques pommes', 'one: There is one apple|more: There are %count% apples', 'one: Il y a %count% pomme|more: Il y a quelques pommes', 2, ['%count%' => 'quelques'], 'fr', ''],
];
}
@@ -523,6 +561,24 @@ class TranslatorTest extends TestCase
];
}
/**
* @requires extension intl
*/
public function testIntlFormattedDomain()
{
$translator = new Translator('en');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', ['some_message' => 'Hello %name%'], 'en');
$this->assertSame('Hello Bob', $translator->trans('some_message', ['%name%' => 'Bob']));
$translator->addResource('array', ['some_message' => 'Hi {name}'], 'en', 'messages+intl-icu');
$this->assertSame('Hi Bob', $translator->trans('some_message', ['%name%' => 'Bob']));
}
/**
* @group legacy
*/
public function testTransChoiceFallback()
{
$translator = new Translator('ru');
@@ -533,6 +589,9 @@ class TranslatorTest extends TestCase
$this->assertEquals('10 things', $translator->transChoice('some_message2', 10, ['%count%' => 10]));
}
/**
* @group legacy
*/
public function testTransChoiceFallbackBis()
{
$translator = new Translator('ru');
@@ -543,6 +602,9 @@ class TranslatorTest extends TestCase
$this->assertEquals('10 things', $translator->transChoice('some_message2', 10, ['%count%' => 10]));
}
/**
* @group legacy
*/
public function testTransChoiceFallbackWithNoTranslation()
{
$translator = new Translator('ru');
@@ -553,16 +615,6 @@ class TranslatorTest extends TestCase
// unchanged if it can't be found
$this->assertEquals('some_message2', $translator->transChoice('some_message2', 10, ['%count%' => 10]));
}
public function testMissingLoaderForResourceError()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('No loader is registered for the "twig" format when loading the "messages.en.twig" resource.');
$translator = new Translator('en');
$translator->addResource('twig', 'messages.en.twig', 'en');
$translator->getCatalogue('en');
}
}
class StringClass
@@ -18,22 +18,6 @@ use Symfony\Component\Translation\Writer\TranslationWriter;
class TranslationWriterTest extends TestCase
{
/**
* @group legacy
* @expectedDeprecation The "Symfony\Component\Translation\Writer\TranslationWriter::writeTranslations()" method is deprecated since Symfony 3.4 and will be removed in 4.0. Use write() instead.
*/
public function testWriteTranslations()
{
$dumper = $this->getMockBuilder('Symfony\Component\Translation\Dumper\DumperInterface')->getMock();
$dumper
->expects($this->once())
->method('dump');
$writer = new TranslationWriter();
$writer->addDumper('test', $dumper);
$writer->writeTranslations(new MessageCatalogue('en'), 'test');
}
public function testWrite()
{
$dumper = $this->getMockBuilder('Symfony\Component\Translation\Dumper\DumperInterface')->getMock();
@@ -43,9 +27,12 @@ class TranslationWriterTest extends TestCase
$writer = new TranslationWriter();
$writer->addDumper('test', $dumper);
$writer->write(new MessageCatalogue([]), 'test');
$writer->write(new MessageCatalogue('en'), 'test');
}
/**
* @group legacy
*/
public function testDisableBackup()
{
$nonBackupDumper = new NonBackupDumper();
@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="fr-FR" trgLang="en-US">
<file id="messages.en_US">
<unit id="LCa0a2j">
<unit id="LCa0a2j" name="foo">
<segment>
<source>foo</source>
<target>bar</target>
</segment>
</unit>
<unit id="LHDhK3o">
<unit id="LHDhK3o" name="key">
<segment>
<source>key</source>
<target order="1"></target>
</segment>
</unit>
<unit id="2DA_bnh">
<unit id="2DA_bnh" name="key.with.cdata">
<segment>
<source>key.with.cdata</source>
<target><![CDATA[<source> & <target>]]></target>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="fr-FR" trgLang="en-US">
<file id="messages.en_US">
<unit id="LCa0a2j">
<unit id="LCa0a2j" name="foo">
<notes>
<note category="state">new</note>
<note category="approved">true</note>
@@ -12,7 +12,7 @@
<target>bar</target>
</segment>
</unit>
<unit id="uqWglk0">
<unit id="uqWglk0" name="baz">
<notes>
<note id="x">x_content</note>
<note appliesTo="target" category="quality">Fuzzy</note>
+13
View File
@@ -9,3 +9,16 @@ msgstr "bar"
msgid "bar"
msgstr "foo"
# Comment 1
# Comment 2
#, fuzzy,another
#: src/file_1 src/file_2:50
msgid "foo_bar"
msgstr "foobar"
# Comment
#, fuzzy
#: src/file_1
msgid "bar_foo"
msgstr "barfoo"
+11
View File
@@ -6,5 +6,16 @@
<source>foo</source>
<translation>bar</translation>
</message>
<message>
<location filename="src/file_1"/>
<location filename="src/file_2" line="50"/>
<source>foo_bar</source>
<translation>foobar</translation>
</message>
<message>
<location filename="src/file_1"/>
<source>bar_foo</source>
<translation>barfoo</translation>
</message>
</context>
</TS>
+2 -2
View File
@@ -7,8 +7,8 @@
<target>bar</target>
<note priority="1">foo</note>
</trans-unit>
<trans-unit id="2">
<source>extra</source>
<trans-unit id="2" resname="extra">
<source>extrasource</source>
<note from="foo">bar</note>
</trans-unit>
<trans-unit id="123">
+99 -44
View File
@@ -19,14 +19,17 @@ use Symfony\Component\Translation\Exception\LogicException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Translation\Exception\RuntimeException;
use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface;
use Symfony\Component\Translation\Formatter\IntlFormatterInterface;
use Symfony\Component\Translation\Formatter\MessageFormatter;
use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Translator implements TranslatorInterface, TranslatorBagInterface
class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface
{
/**
* @var MessageCatalogueInterface[]
@@ -68,33 +71,36 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
*/
private $debug;
private $cacheVary;
/**
* @var ConfigCacheFactoryInterface|null
*/
private $configCacheFactory;
/**
* @param string $locale The locale
* @param MessageFormatterInterface|null $formatter The message formatter
* @param string|null $cacheDir The directory to use for the cache
* @param bool $debug Use cache in debug mode ?
*
* @var array|null
*/
private $parentLocales;
private $hasIntlFormatter;
/**
* @throws InvalidArgumentException If a locale contains invalid characters
*/
public function __construct($locale, $formatter = null, $cacheDir = null, $debug = false)
public function __construct(?string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = [])
{
$this->setLocale($locale);
if ($formatter instanceof MessageSelector) {
$formatter = new MessageFormatter($formatter);
@trigger_error(sprintf('Passing a "%s" instance into the "%s()" method as a second argument is deprecated since Symfony 3.4 and will be removed in 4.0. Inject a "%s" implementation instead.', MessageSelector::class, __METHOD__, MessageFormatterInterface::class), \E_USER_DEPRECATED);
} elseif (null === $formatter) {
if (null === $formatter) {
$formatter = new MessageFormatter();
}
$this->formatter = $formatter;
$this->cacheDir = $cacheDir;
$this->debug = $debug;
$this->cacheVary = $cacheVary;
$this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface;
}
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
@@ -173,12 +179,14 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
$this->assertValidLocale($locale);
}
$this->fallbackLocales = $locales;
$this->fallbackLocales = $this->cacheVary['fallback_locales'] = $locales;
}
/**
* Gets the fallback locales.
*
* @internal since Symfony 4.2
*
* @return array The fallback locales
*/
public function getFallbackLocales()
@@ -195,14 +203,34 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
$domain = 'messages';
}
return $this->formatter->format($this->getCatalogue($locale)->get((string) $id, $domain), $locale, $parameters);
$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;
}
}
if ($this->hasIntlFormatter && $catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters);
}
return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters);
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), E_USER_DEPRECATED);
if (!$this->formatter instanceof ChoiceMessageFormatterInterface) {
throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter)));
}
@@ -223,6 +251,10 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
}
}
if ($this->hasIntlFormatter && $catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, ['%count%' => $number] + $parameters);
}
return $this->formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters);
}
@@ -283,10 +315,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
$this->loadFallbackCatalogues($locale);
}
/**
* @param string $locale
*/
private function initializeCacheCatalogue($locale)
private function initializeCacheCatalogue(string $locale): void
{
if (isset($this->catalogues[$locale])) {
/* Catalogue already initialized. */
@@ -309,7 +338,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
$this->catalogues[$locale] = include $cache->getPath();
}
private function dumpCatalogue($locale, ConfigCacheInterface $cache)
private function dumpCatalogue($locale, ConfigCacheInterface $cache): void
{
$this->initializeCatalogue($locale);
$fallbackContent = $this->getFallbackContent($this->catalogues[$locale]);
@@ -327,14 +356,14 @@ return \$catalogue;
EOF
,
$locale,
var_export($this->catalogues[$locale]->all(), true),
var_export($this->getAllMessages($this->catalogues[$locale]), true),
$fallbackContent
);
$cache->write($content, $this->catalogues[$locale]->getResources());
}
private function getFallbackContent(MessageCatalogue $catalogue)
private function getFallbackContent(MessageCatalogue $catalogue): string
{
$fallbackContent = '';
$current = '';
@@ -353,7 +382,7 @@ EOF
,
$fallbackSuffix,
$fallback,
var_export($fallbackCatalogue->all(), true),
var_export($this->getAllMessages($fallbackCatalogue), true),
$currentSuffix,
$fallbackSuffix
);
@@ -366,28 +395,27 @@ EOF
private function getCatalogueCachePath($locale)
{
return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->fallbackLocales), true)), 0, 7), '/', '_').'.php';
return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php';
}
private function doLoadCatalogue($locale)
/**
* @internal
*/
protected function doLoadCatalogue($locale): void
{
$this->catalogues[$locale] = new MessageCatalogue($locale);
if (isset($this->resources[$locale])) {
foreach ($this->resources[$locale] as $resource) {
if (!isset($this->loaders[$resource[0]])) {
if (\is_string($resource[1])) {
throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1]));
}
throw new RuntimeException(sprintf('No loader is registered for the "%s" format.', $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)
private function loadFallbackCatalogues($locale): void
{
$current = $this->catalogues[$locale];
@@ -396,7 +424,7 @@ EOF
$this->initializeCatalogue($fallback);
}
$fallbackCatalogue = new MessageCatalogue($fallback, $this->catalogues[$fallback]->all());
$fallbackCatalogue = new MessageCatalogue($fallback, $this->getAllMessages($this->catalogues[$fallback]));
foreach ($this->catalogues[$fallback]->getResources() as $resource) {
$fallbackCatalogue->addResource($resource);
}
@@ -407,6 +435,10 @@ EOF
protected function computeFallbackLocales($locale)
{
if (null === $this->parentLocales) {
$parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true);
}
$locales = [];
foreach ($this->fallbackLocales as $fallback) {
if ($fallback === $locale) {
@@ -416,19 +448,27 @@ EOF
$locales[] = $fallback;
}
if (\function_exists('locale_parse')) {
$localeSubTags = locale_parse($locale);
if (1 < \count($localeSubTags)) {
array_pop($localeSubTags);
$fallback = locale_compose($localeSubTags);
if (false !== $fallback) {
array_unshift($locales, $fallback);
while ($locale) {
$parent = $parentLocales[$locale] ?? null;
if ($parent) {
$locale = 'root' !== $parent ? $parent : null;
} elseif (\function_exists('locale_parse')) {
$localeSubTags = locale_parse($locale);
$locale = null;
if (1 < \count($localeSubTags)) {
array_pop($localeSubTags);
$locale = locale_compose($localeSubTags) ?: null;
}
} elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) {
$locale = substr($locale, 0, $i);
} else {
$locale = null;
}
if (null !== $locale) {
array_unshift($locales, $locale);
}
} elseif (false !== strrchr($locale, '_')) {
array_unshift($locales, substr($locale, 0, -\strlen(strrchr($locale, '_'))));
} elseif (false !== strrchr($locale, '-')) {
array_unshift($locales, substr($locale, 0, -\strlen(strrchr($locale, '-'))));
}
return array_unique($locales);
@@ -451,10 +491,8 @@ EOF
/**
* Provides the ConfigCache factory implementation, falling back to a
* default implementation if necessary.
*
* @return ConfigCacheFactoryInterface $configCacheFactory
*/
private function getConfigCacheFactory()
private function getConfigCacheFactory(): ConfigCacheFactoryInterface
{
if (!$this->configCacheFactory) {
$this->configCacheFactory = new ConfigCacheFactory($this->debug);
@@ -462,4 +500,21 @@ EOF
return $this->configCacheFactory;
}
private function getAllMessages(MessageCatalogueInterface $catalogue): array
{
$allMessages = [];
foreach ($catalogue->all() as $domain => $messages) {
if ($intlMessages = $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
$allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages;
$messages = array_diff_key($messages, $intlMessages);
}
if ($messages) {
$allMessages[$domain] = $messages;
}
}
return $allMessages;
}
}
+4 -1
View File
@@ -12,13 +12,16 @@
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Contracts\Translation\LocaleAwareInterface;
/**
* TranslatorInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Symfony 4.2, use Symfony\Contracts\Translation\TranslatorInterface instead
*/
interface TranslatorInterface
interface TranslatorInterface extends LocaleAwareInterface
{
/**
* Translates the given message.
+5 -18
View File
@@ -38,9 +38,13 @@ class TranslationWriter implements TranslationWriterInterface
/**
* Disables dumper backup.
*
* @deprecated since Symfony 4.1
*/
public function disableBackup()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), E_USER_DEPRECATED);
foreach ($this->dumpers as $dumper) {
if (method_exists($dumper, 'setBackup')) {
$dumper->setBackup(false);
@@ -77,27 +81,10 @@ class TranslationWriter implements TranslationWriterInterface
$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']));
throw new RuntimeException(sprintf('Translation Writer was not able to create directory "%s"', $options['path']));
}
// save
$dumper->dump($catalogue, $options);
}
/**
* Writes translation from the catalogue according to the selected format.
*
* @param MessageCatalogue $catalogue The message catalogue to write
* @param string $format The format to use to dump the messages
* @param array $options Options that are passed to the dumper
*
* @throws InvalidArgumentException
*
* @deprecated since 3.4 will be removed in 4.0. Use write instead.
*/
public function writeTranslations(MessageCatalogue $catalogue, $format, $options = [])
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 3.4 and will be removed in 4.0. Use write() instead.', __METHOD__), \E_USER_DEPRECATED);
$this->write($catalogue, $format, $options);
}
}
+17 -6
View File
@@ -16,24 +16,30 @@
}
],
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/polyfill-mbstring": "~1.0"
"php": "^7.1.3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^1.1.6"
},
"require-dev": {
"symfony/config": "~2.8|~3.0|~4.0",
"symfony/config": "~3.4|~4.0",
"symfony/console": "~3.4|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/http-kernel": "~3.4|~4.0",
"symfony/intl": "^2.8.18|^3.2.5|~4.0",
"symfony/intl": "~3.4|~4.0",
"symfony/service-contracts": "^1.1.2",
"symfony/var-dumper": "~3.4|~4.0",
"symfony/yaml": "~3.4|~4.0",
"symfony/finder": "~2.8|~3.0|~4.0",
"psr/log": "~1.0"
},
"conflict": {
"symfony/config": "<2.8",
"symfony/config": "<3.4",
"symfony/dependency-injection": "<3.4",
"symfony/yaml": "<3.4"
},
"provide": {
"symfony/translation-implementation": "1.0"
},
"suggest": {
"symfony/config": "",
"symfony/yaml": "",
@@ -45,5 +51,10 @@
"/Tests/"
]
},
"minimum-stability": "dev"
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "4.3-dev"
}
}
}