upgraded dependencies

This commit is contained in:
RafficMohammed
2023-01-08 01:59:16 +05:30
parent 51056e3aad
commit f9ae387337
6895 changed files with 133617 additions and 178680 deletions

View File

@@ -1,6 +1,49 @@
CHANGELOG
=========
5.4
---
* Add `github` format & autodetection to render errors as annotations when
running the XLIFF linter command in a Github Actions environment.
* Translation providers are not experimental anymore
5.3
---
* Add `translation:pull` and `translation:push` commands to manage translations with third-party providers
* Add `TranslatorBagInterface::getCatalogues` method
* Add support to load XLIFF string in `XliffFileLoader`
5.2.0
-----
* added support for calling `trans` with ICU formatted messages
* added `PseudoLocalizationTranslator`
* added `TranslatableMessage` objects that represent a message that can be translated
* added the `t()` function to easily create `TranslatableMessage` objects
* Added support for extracting messages from `TranslatableMessage` objects
5.1.0
-----
* added support for `name` attribute on `unit` element from xliff2 to be used as a translation key instead of always the `source` element
5.0.0
-----
* removed support for using `null` as the locale in `Translator`
* removed `TranslatorInterface`
* removed `MessageSelector`
* removed `ChoiceMessageFormatterInterface`
* removed `PluralizationRule`
* removed `Interval`
* removed `transChoice()` methods, use the trans() method instead with a %count% parameter
* removed `FileDumper::setBackup()` and `TranslationWriter::disableBackup()`
* removed `MessageFormatter::choiceFormat()`
* added argument `$filename` to `PhpExtractor::parseTokens()`
* removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit.
4.4.0
-----

View File

@@ -26,6 +26,10 @@ use Symfony\Component\Translation\MessageCatalogueInterface;
*/
abstract class AbstractOperation implements OperationInterface
{
public const OBSOLETE_BATCH = 'obsolete';
public const NEW_BATCH = 'new';
public const ALL_BATCH = 'all';
protected $source;
protected $target;
protected $result;
@@ -79,7 +83,18 @@ abstract class AbstractOperation implements OperationInterface
public function getDomains()
{
if (null === $this->domains) {
$this->domains = array_values(array_unique(array_merge($this->source->getDomains(), $this->target->getDomains())));
$domains = [];
foreach ([$this->source, $this->target] as $catalogue) {
foreach ($catalogue->getDomains() as $domain) {
$domains[$domain] = $domain;
if ($catalogue->all($domainIcu = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)) {
$domains[$domainIcu] = $domainIcu;
}
}
}
$this->domains = array_values($domains);
}
return $this->domains;
@@ -88,49 +103,49 @@ abstract class AbstractOperation implements OperationInterface
/**
* {@inheritdoc}
*/
public function getMessages($domain)
public function getMessages(string $domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain]['all'])) {
if (!isset($this->messages[$domain][self::ALL_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain]['all'];
return $this->messages[$domain][self::ALL_BATCH];
}
/**
* {@inheritdoc}
*/
public function getNewMessages($domain)
public function getNewMessages(string $domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain]['new'])) {
if (!isset($this->messages[$domain][self::NEW_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain]['new'];
return $this->messages[$domain][self::NEW_BATCH];
}
/**
* {@inheritdoc}
*/
public function getObsoleteMessages($domain)
public function getObsoleteMessages(string $domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain]['obsolete'])) {
if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain]['obsolete'];
return $this->messages[$domain][self::OBSOLETE_BATCH];
}
/**
@@ -147,11 +162,42 @@ abstract class AbstractOperation implements OperationInterface
return $this->result;
}
/**
* @param self::*_BATCH $batch
*/
public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void
{
// If MessageFormatter class does not exists, intl domains are not supported.
if (!class_exists(\MessageFormatter::class)) {
return;
}
foreach ($this->getDomains() as $domain) {
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
switch ($batch) {
case self::OBSOLETE_BATCH: $messages = $this->getObsoleteMessages($domain); break;
case self::NEW_BATCH: $messages = $this->getNewMessages($domain); break;
case self::ALL_BATCH: $messages = $this->getMessages($domain); break;
default: throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH));
}
if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) {
continue;
}
$result = $this->getResult();
$allIntlMessages = $result->all($intlDomain);
$currentMessages = array_diff_key($messages, $result->all($domain));
$result->replace($currentMessages, $domain);
$result->replace($allIntlMessages + $messages, $intlDomain);
}
}
/**
* Performs operation on source and target catalogues for the given domain and
* stores the results.
*
* @param string $domain The domain which the operation will be performed for
*/
abstract protected function processDomain($domain);
abstract protected function processDomain(string $domain);
}

View File

@@ -27,7 +27,7 @@ class MergeOperation extends AbstractOperation
/**
* {@inheritdoc}
*/
protected function processDomain($domain)
protected function processDomain(string $domain)
{
$this->messages[$domain] = [
'all' => [],

View File

@@ -44,29 +44,23 @@ interface OperationInterface
/**
* Returns all valid messages ('all') after operation.
*
* @param string $domain
*
* @return array
*/
public function getMessages($domain);
public function getMessages(string $domain);
/**
* Returns new messages ('new') after operation.
*
* @param string $domain
*
* @return array
*/
public function getNewMessages($domain);
public function getNewMessages(string $domain);
/**
* Returns obsolete messages ('obsolete') after operation.
*
* @param string $domain
*
* @return array
*/
public function getObsoleteMessages($domain);
public function getObsoleteMessages(string $domain);
/**
* Returns resulting catalogue ('result').

View File

@@ -28,7 +28,7 @@ class TargetOperation extends AbstractOperation
/**
* {@inheritdoc}
*/
protected function processDomain($domain)
protected function processDomain(string $domain)
{
$this->messages[$domain] = [
'all' => [],

View File

@@ -0,0 +1,188 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
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\Catalogue\TargetOperation;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
final class TranslationPullCommand extends Command
{
use TranslationTrait;
protected static $defaultName = 'translation:pull';
protected static $defaultDescription = 'Pull translations from a given provider.';
private $providerCollection;
private $writer;
private $reader;
private $defaultLocale;
private $transPaths;
private $enabledLocales;
public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = [])
{
$this->providerCollection = $providerCollection;
$this->writer = $writer;
$this->reader = $reader;
$this->defaultLocale = $defaultLocale;
$this->transPaths = $transPaths;
$this->enabledLocales = $enabledLocales;
parent::__construct();
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('provider')) {
$suggestions->suggestValues($this->providerCollection->keys());
return;
}
if ($input->mustSuggestOptionValuesFor('domains')) {
$provider = $this->providerCollection->get($input->getArgument('provider'));
if ($provider && method_exists($provider, 'getDomains')) {
$domains = $provider->getDomains();
$suggestions->suggestValues($domains);
}
return;
}
if ($input->mustSuggestOptionValuesFor('locales')) {
$suggestions->suggestValues($this->enabledLocales);
return;
}
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']);
}
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$keys = $this->providerCollection->keys();
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
$this
->setDefinition([
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pulls translations from the given provider. Only
new translations are pulled, existing ones are not overwritten.
You can overwrite existing translations (and remove the missing ones on local side) by using the <comment>--force</> flag:
<info>php %command.full_name% --force provider</>
Full example:
<info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</>
This command pulls all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case.
Local translations for others domains and locales are ignored.
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$provider = $this->providerCollection->get($input->getArgument('provider'));
$force = $input->getOption('force');
$intlIcu = $input->getOption('intl-icu');
$locales = $input->getOption('locales') ?: $this->enabledLocales;
$domains = $input->getOption('domains');
$format = $input->getOption('format');
$xliffVersion = '1.2';
if ($intlIcu && !$force) {
$io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.');
}
switch ($format) {
case 'xlf20': $xliffVersion = '2.0';
// no break
case 'xlf12': $format = 'xlf';
}
$writeOptions = [
'path' => end($this->transPaths),
'xliff_version' => $xliffVersion,
'default_locale' => $this->defaultLocale,
];
if (!$domains) {
$domains = $provider->getDomains();
}
$providerTranslations = $provider->read($domains, $locales);
if ($force) {
foreach ($providerTranslations->getCatalogues() as $catalogue) {
$operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue);
if ($intlIcu) {
$operation->moveMessagesToIntlDomainsIfPossible();
}
$this->writer->write($operation->getResult(), $format, $writeOptions);
}
$io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
// Append pulled translations to local ones.
$localTranslations->addBag($providerTranslations->diff($localTranslations));
foreach ($localTranslations->getCatalogues() as $catalogue) {
$this->writer->write($catalogue, $format, $writeOptions);
}
$io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
}

View File

@@ -0,0 +1,189 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidArgumentException;
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\Provider\FilteringProvider;
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\TranslatorBag;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
final class TranslationPushCommand extends Command
{
use TranslationTrait;
protected static $defaultName = 'translation:push';
protected static $defaultDescription = 'Push translations to a given provider.';
private $providers;
private $reader;
private $transPaths;
private $enabledLocales;
public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = [])
{
$this->providers = $providers;
$this->reader = $reader;
$this->transPaths = $transPaths;
$this->enabledLocales = $enabledLocales;
parent::__construct();
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('provider')) {
$suggestions->suggestValues($this->providers->keys());
return;
}
if ($input->mustSuggestOptionValuesFor('domains')) {
$provider = $this->providers->get($input->getArgument('provider'));
if ($provider && method_exists($provider, 'getDomains')) {
$domains = $provider->getDomains();
$suggestions->suggestValues($domains);
}
return;
}
if ($input->mustSuggestOptionValuesFor('locales')) {
$suggestions->suggestValues($this->enabledLocales);
}
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$keys = $this->providers->keys();
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
$this
->setDefinition([
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pushes translations to the given provider. Only new
translations are pushed, existing ones are not overwritten.
You can overwrite existing translations by using the <comment>--force</> flag:
<info>php %command.full_name% --force provider</>
You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
<info>php %command.full_name% --delete-missing provider</>
Full example:
<info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</>
This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
Provider translations for others domains and locales are ignored.
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$provider = $this->providers->get($input->getArgument('provider'));
if (!$this->enabledLocales) {
throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME)));
}
$io = new SymfonyStyle($input, $output);
$domains = $input->getOption('domains');
$locales = $input->getOption('locales');
$force = $input->getOption('force');
$deleteMissing = $input->getOption('delete-missing');
if (!$domains && $provider instanceof FilteringProvider) {
$domains = $provider->getDomains();
}
// Reading local translations must be done after retrieving the domains from the provider
// in order to manage only translations from configured domains
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
if (!$domains) {
$domains = $this->getDomainsFromTranslatorBag($localTranslations);
}
if (!$deleteMissing && $force) {
$provider->write($localTranslations);
$io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
$providerTranslations = $provider->read($domains, $locales);
if ($deleteMissing) {
$provider->delete($providerTranslations->diff($localTranslations));
$io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
// Read provider translations again, after missing translations deletion,
// to avoid push freshly deleted translations.
$providerTranslations = $provider->read($domains, $locales);
}
$translationsToWrite = $localTranslations->diff($providerTranslations);
if ($force) {
$translationsToWrite->addBag($localTranslations->intersect($providerTranslations));
}
$provider->write($translationsToWrite);
$io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array
{
$domains = [];
foreach ($translatorBag->getCatalogues() as $catalogue) {
$domains += $catalogue->getDomains();
}
return array_unique($domains);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\TranslatorBag;
/**
* @internal
*/
trait TranslationTrait
{
private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag
{
$bag = new TranslatorBag();
foreach ($locales as $locale) {
$catalogue = new MessageCatalogue($locale);
foreach ($transPaths as $path) {
$this->reader->read($path, $catalogue);
}
if ($domains) {
foreach ($domains as $domain) {
$bag->addCatalogue($this->filterCatalogue($catalogue, $domain));
}
} else {
$bag->addCatalogue($catalogue);
}
}
return $bag;
}
private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
{
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
// extract intl-icu messages only
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
if ($intlMessages = $catalogue->all($intlDomain)) {
$filteredCatalogue->add($intlMessages, $intlDomain);
}
// extract all messages and subtract intl-icu messages
if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
$filteredCatalogue->add($messages, $domain);
}
foreach ($catalogue->getResources() as $resource) {
$filteredCatalogue->addResource($resource);
}
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
foreach ($metadata as $k => $v) {
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
}
}
if ($metadata = $catalogue->getMetadata('', $domain)) {
foreach ($metadata as $k => $v) {
$filteredCatalogue->setMetadata($k, $v, $domain);
}
}
return $filteredCatalogue;
}
}

View File

@@ -11,7 +11,10 @@
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -31,6 +34,7 @@ use Symfony\Component\Translation\Util\XliffUtils;
class XliffLintCommand extends Command
{
protected static $defaultName = 'lint:xliff';
protected static $defaultDescription = 'Lint an XLIFF file and outputs encountered errors';
private $format;
private $displayCorrectFiles;
@@ -53,9 +57,9 @@ class XliffLintCommand extends Command
protected function configure()
{
$this
->setDescription('Lint an XLIFF file and outputs encountered errors')
->setDescription(self::$defaultDescription)
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format')
->setHelp(<<<EOF
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
@@ -82,22 +86,15 @@ EOF
{
$io = new SymfonyStyle($input, $output);
$filenames = (array) $input->getArgument('filename');
$this->format = $input->getOption('format');
$this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt');
$this->displayCorrectFiles = $output->isVerbose();
if (['-'] === $filenames) {
return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]);
}
// @deprecated to be removed in 5.0
if (!$filenames) {
if (0 !== ftell(\STDIN)) {
throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
}
@trigger_error('Piping content from STDIN to the "lint:xliff" command without passing the dash symbol "-" as argument is deprecated since Symfony 4.4.', \E_USER_DEPRECATED);
return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]);
throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
}
$filesInfo = [];
@@ -166,15 +163,18 @@ EOF
return $this->displayTxt($io, $files);
case 'json':
return $this->displayJson($io, $files);
case 'github':
return $this->displayTxt($io, $files, true);
default:
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format));
}
}
private function displayTxt(SymfonyStyle $io, array $filesInfo)
private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false)
{
$countFiles = \count($filesInfo);
$erroredFiles = 0;
$githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null;
foreach ($filesInfo as $info) {
if ($info['valid'] && $this->displayCorrectFiles) {
@@ -182,9 +182,15 @@ EOF
} elseif (!$info['valid']) {
++$erroredFiles;
$io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
$io->listing(array_map(function ($error) {
$io->listing(array_map(function ($error) use ($info, $githubReporter) {
// general document errors have a '-1' line number
return -1 === $error['line'] ? $error['message'] : sprintf('Line %d, Column %d: %s', $error['line'], $error['column'], $error['message']);
$line = -1 === $error['line'] ? null : $error['line'];
if ($githubReporter) {
$githubReporter->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null);
}
return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']);
}, $info['messages']));
}
}
@@ -270,4 +276,11 @@ EOF
return null;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['txt', 'json', 'github']);
}
}
}

View File

@@ -21,7 +21,7 @@ use Symfony\Component\VarDumper\Cloner\Data;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @final since Symfony 4.4
* @final
*/
class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface
{
@@ -47,10 +47,8 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
/**
* {@inheritdoc}
*
* @param \Throwable|null $exception
*/
public function collect(Request $request, Response $response/* , \Throwable $exception = null */)
public function collect(Request $request, Response $response, \Throwable $exception = null)
{
$this->data['locale'] = $this->translator->getLocale();
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
@@ -72,26 +70,17 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
return $this->data['messages'] ?? [];
}
/**
* @return int
*/
public function getCountMissings()
public function getCountMissings(): int
{
return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0;
}
/**
* @return int
*/
public function getCountFallbacks()
public function getCountFallbacks(): int
{
return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0;
}
/**
* @return int
*/
public function getCountDefines()
public function getCountDefines(): int
{
return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0;
}
@@ -102,7 +91,7 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
}
/**
* @internal since Symfony 4.2
* @internal
*/
public function getFallbackLocales()
{
@@ -112,7 +101,7 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return 'translation';
}

View File

@@ -13,36 +13,28 @@ 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 LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface, WarmableInterface
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface
{
public const MESSAGE_DEFINED = 0;
public const MESSAGE_MISSING = 1;
public const MESSAGE_EQUALS_FALLBACK = 2;
/**
* @var TranslatorInterface|TranslatorBagInterface
*/
private $translator;
private $messages = [];
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
* @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator
*/
public function __construct($translator)
public function __construct(TranslatorInterface $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)));
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator)));
}
$this->translator = $translator;
@@ -51,9 +43,9 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorIn
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
{
$trans = $this->translator->trans($id, $parameters, $domain, $locale);
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters);
return $trans;
@@ -61,26 +53,8 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorIn
/**
* {@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)
{
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;
}
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function setLocale(string $locale)
{
$this->translator->setLocale($locale);
}
@@ -96,7 +70,7 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorIn
/**
* {@inheritdoc}
*/
public function getCatalogue($locale = null)
public function getCatalogue(string $locale = null)
{
return $this->translator->getCatalogue($locale);
}
@@ -104,17 +78,29 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorIn
/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
public function getCatalogues(): array
{
return $this->translator->getCatalogues();
}
/**
* {@inheritdoc}
*
* @return string[]
*/
public function warmUp(string $cacheDir)
{
if ($this->translator instanceof WarmableInterface) {
$this->translator->warmUp($cacheDir);
return (array) $this->translator->warmUp($cacheDir);
}
return [];
}
/**
* Gets the fallback locales.
*
* @return array The fallback locales
* @return array
*/
public function getFallbackLocales()
{
@@ -128,7 +114,7 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorIn
/**
* Passes through all unknown calls onto the translator object.
*/
public function __call($method, $args)
public function __call(string $method, array $args)
{
return $this->translator->{$method}(...$args);
}
@@ -141,13 +127,12 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorIn
return $this->messages;
}
private function collectMessage(?string $locale, ?string $domain, ?string $id, string $translation, ?array $parameters = [])
private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = [])
{
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->translator->getCatalogue($locale);
$locale = $catalogue->getLocale();
$fallbackLocale = null;

View File

@@ -25,6 +25,10 @@ class TranslationDumperPass implements CompilerPassInterface
public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper')
{
if (1 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->writerServiceId = $writerServiceId;
$this->dumperTag = $dumperTag;
}

View File

@@ -26,6 +26,10 @@ class TranslationExtractorPass implements CompilerPassInterface
public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->extractorServiceId = $extractorServiceId;
$this->extractorTag = $extractorTag;
}

View File

@@ -24,8 +24,12 @@ class TranslatorPass implements CompilerPassInterface
private $debugCommandServiceId;
private $updateCommandServiceId;
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')
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_extract')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->translatorServiceId = $translatorServiceId;
$this->readerServiceId = $readerServiceId;
$this->loaderTag = $loaderTag;
@@ -68,7 +72,7 @@ class TranslatorPass implements CompilerPassInterface
return;
}
$paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(2));
$paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1));
if ($container->hasDefinition($this->debugCommandServiceId)) {
$definition = $container->getDefinition($this->debugCommandServiceId);
$definition->replaceArgument(4, $container->getParameter('twig.default_path'));

View File

@@ -27,12 +27,28 @@ class TranslatorPathsPass extends AbstractRecursivePass
private $updateCommandServiceId;
private $resolverServiceId;
private $level = 0;
/**
* @var array<string, bool>
*/
private $paths = [];
/**
* @var array<int, Definition>
*/
private $definitions = [];
/**
* @var array<string, array<string, bool>>
*/
private $controllers = [];
public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update', string $resolverServiceId = 'argument_resolver.service')
public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_extract', string $resolverServiceId = 'argument_resolver.service')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->translatorServiceId = $translatorServiceId;
$this->debugCommandServiceId = $debugCommandServiceId;
$this->updateCommandServiceId = $updateCommandServiceId;
@@ -82,7 +98,7 @@ class TranslatorPathsPass extends AbstractRecursivePass
}
}
protected function processValue($value, $isRoot = false)
protected function processValue($value, bool $isRoot = false)
{
if ($value instanceof Reference) {
if ((string) $value === $this->translatorServiceId) {

View File

@@ -26,7 +26,7 @@ class CsvFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$handle = fopen('php://memory', 'r+');
@@ -43,11 +43,8 @@ class CsvFileDumper extends FileDumper
/**
* Sets the delimiter and escape character for CSV.
*
* @param string $delimiter Delimiter character
* @param string $enclosure Enclosure character
*/
public function setCsvControl($delimiter = ';', $enclosure = '"')
public function setCsvControl(string $delimiter = ';', string $enclosure = '"')
{
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;

View File

@@ -26,5 +26,5 @@ interface DumperInterface
*
* @param array $options Options that are used by the dumper
*/
public function dump(MessageCatalogue $messages, $options = []);
public function dump(MessageCatalogue $messages, array $options = []);
}

View File

@@ -37,31 +37,15 @@ abstract class FileDumper implements DumperInterface
*
* @param string $relativePathTemplate A template for the relative paths to files
*/
public function setRelativePathTemplate($relativePathTemplate)
public function setRelativePathTemplate(string $relativePathTemplate)
{
$this->relativePathTemplate = $relativePathTemplate;
}
/**
* Sets backup flag.
*
* @param bool $backup
*
* @deprecated since Symfony 4.1
*/
public function setBackup($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.');
}
}
/**
* {@inheritdoc}
*/
public function dump(MessageCatalogue $messages, $options = [])
public function dump(MessageCatalogue $messages, array $options = [])
{
if (!\array_key_exists('path', $options)) {
throw new InvalidArgumentException('The file dumper needs a path option.');
@@ -103,16 +87,14 @@ abstract class FileDumper implements DumperInterface
/**
* Transforms a domain of a message catalogue to its string representation.
*
* @param string $domain
*
* @return string representation
* @return string
*/
abstract public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []);
abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []);
/**
* Gets the file extension of the dumper.
*
* @return string file extension
* @return string
*/
abstract protected function getExtension();

View File

@@ -28,7 +28,7 @@ class IcuResFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$data = $indexes = $resources = '';

View File

@@ -23,7 +23,7 @@ class IniFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$output = '';

View File

@@ -23,7 +23,7 @@ class JsonFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT;

View File

@@ -24,7 +24,7 @@ class MoFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$sources = $targets = $sourceOffsets = $targetOffsets = '';
$offsets = [];

View File

@@ -23,7 +23,7 @@ class PhpFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
return "<?php\n\nreturn ".var_export($messages->all($domain), true).";\n";
}

View File

@@ -23,7 +23,7 @@ class PoFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$output = 'msgid ""'."\n";
$output .= 'msgstr ""'."\n";

View File

@@ -23,7 +23,7 @@ class QtFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$dom = new \DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;

View File

@@ -24,7 +24,7 @@ class XliffFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
$xliffVersion = '1.2';
if (\array_key_exists('xliff_version', $options)) {

View File

@@ -33,7 +33,7 @@ class YamlFileDumper extends FileDumper
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = [])
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
{
if (!class_exists(Yaml::class)) {
throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.');

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
class IncompleteDsnException extends InvalidArgumentException
{
public function __construct(string $message, string $dsn = null, \Throwable $previous = null)
{
if ($dsn) {
$message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message;
}
parent::__construct($message, 0, $previous);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
/**
* @author Oskar Stark <oskarstark@googlemail.com>
*/
class MissingRequiredOptionException extends IncompleteDsnException
{
public function __construct(string $option, string $dsn = null, \Throwable $previous = null)
{
$message = sprintf('The option "%s" is required but missing.', $option);
parent::__construct($message, $dsn, $previous);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class ProviderException extends RuntimeException implements ProviderExceptionInterface
{
private $response;
private $debug;
public function __construct(string $message, ResponseInterface $response, int $code = 0, \Exception $previous = null)
{
$this->response = $response;
$this->debug = $response->getInfo('debug') ?? '';
parent::__construct($message, $code, $previous);
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
public function getDebug(): string
{
return $this->debug;
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ProviderExceptionInterface extends ExceptionInterface
{
/*
* Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface
*/
public function getDebug(): string;
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
use Symfony\Component\Translation\Bridge;
use Symfony\Component\Translation\Provider\Dsn;
class UnsupportedSchemeException extends LogicException
{
private const SCHEME_TO_PACKAGE_MAP = [
'crowdin' => [
'class' => Bridge\Crowdin\CrowdinProviderFactory::class,
'package' => 'symfony/crowdin-translation-provider',
],
'loco' => [
'class' => Bridge\Loco\LocoProviderFactory::class,
'package' => 'symfony/loco-translation-provider',
],
'lokalise' => [
'class' => Bridge\Lokalise\LokaliseProviderFactory::class,
'package' => 'symfony/lokalise-translation-provider',
],
];
public function __construct(Dsn $dsn, string $name = null, array $supported = [])
{
$provider = $dsn->getScheme();
if (false !== $pos = strpos($provider, '+')) {
$provider = substr($provider, 0, $pos);
}
$package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null;
if ($package && !class_exists($package['class'])) {
parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed; try running "composer require %s".', $provider, $package['package']));
return;
}
$message = sprintf('The "%s" scheme is not supported', $dsn->getScheme());
if ($name && $supported) {
$message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported));
}
parent::__construct($message.'.');
}
}

View File

@@ -49,13 +49,11 @@ abstract class AbstractFileExtractor
}
/**
* @param string $file
*
* @return bool
*
* @throws InvalidArgumentException
*/
protected function isFile($file)
protected function isFile(string $file)
{
if (!is_file($file)) {
throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file));
@@ -65,16 +63,14 @@ abstract class AbstractFileExtractor
}
/**
* @param string $file
*
* @return bool
*/
abstract protected function canBeExtracted($file);
abstract protected function canBeExtracted(string $file);
/**
* @param string|array $resource Files, a file or a directory
*
* @return iterable files to be extracted
* @return iterable
*/
abstract protected function extractFromDirectory($resource);
}

View File

@@ -29,10 +29,8 @@ class ChainExtractor implements ExtractorInterface
/**
* Adds a loader to the translation extractor.
*
* @param string $format The format of the loader
*/
public function addExtractor($format, ExtractorInterface $extractor)
public function addExtractor(string $format, ExtractorInterface $extractor)
{
$this->extractors[$format] = $extractor;
}
@@ -40,7 +38,7 @@ class ChainExtractor implements ExtractorInterface
/**
* {@inheritdoc}
*/
public function setPrefix($prefix)
public function setPrefix(string $prefix)
{
foreach ($this->extractors as $extractor) {
$extractor->setPrefix($prefix);

View File

@@ -30,8 +30,6 @@ interface ExtractorInterface
/**
* Sets the prefix that should be used for new found messages.
*
* @param string $prefix The prefix
*/
public function setPrefix($prefix);
public function setPrefix(string $prefix);
}

View File

@@ -27,15 +27,11 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
/**
* Prefix for new found message.
*
* @var string
*/
private $prefix = '';
/**
* The sequence that captures translation messages.
*
* @var array
*/
protected $sequences = [
[
@@ -50,25 +46,83 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
],
[
'->',
'transChoice',
'trans',
'(',
self::MESSAGE_TOKEN,
],
[
'new',
'TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
'->',
'trans',
'new',
'TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
],
[
'->',
'transChoice',
'new',
'\\',
'Symfony',
'\\',
'Component',
'\\',
'Translation',
'\\',
'TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
'new',
'\Symfony\Component\Translation\TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
'new',
'\\',
'Symfony',
'\\',
'Component',
'\\',
'Translation',
'\\',
'TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
],
[
'new',
'\Symfony\Component\Translation\TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
],
[
't',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
't',
'(',
self::MESSAGE_TOKEN,
],
@@ -90,7 +144,7 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
/**
* {@inheritdoc}
*/
public function setPrefix($prefix)
public function setPrefix(string $prefix)
{
$this->prefix = $prefix;
}
@@ -207,17 +261,9 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
/**
* Extracts trans message from PHP tokens.
*
* @param array $tokens
* @param string $filename
*/
protected function parseTokens($tokens, MessageCatalogue $catalog/* , string $filename */)
protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename)
{
if (\func_num_args() < 3 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
@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) {
@@ -265,13 +311,11 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
}
/**
* @param string $file
*
* @return bool
*
* @throws \InvalidArgumentException
*/
protected function canBeExtracted($file)
protected function canBeExtracted(string $file)
{
return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION);
}
@@ -281,6 +325,10 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
*/
protected function extractFromDirectory($directory)
{
if (!class_exists(Finder::class)) {
throw new \LogicException(sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class));
}
$finder = new Finder();
return $finder->files()->name('*.php')->in($directory);

View File

@@ -65,9 +65,9 @@ class PhpStringTokenParser
*
* @param string $str String token content
*
* @return string The parsed string
* @return string
*/
public static function parse($str)
public static function parse(string $str)
{
$bLength = 0;
if ('b' === $str[0]) {
@@ -91,9 +91,9 @@ class PhpStringTokenParser
* @param string $str String without quotes
* @param string|null $quote Quote type
*
* @return string String with escape sequences parsed
* @return string
*/
public static function parseEscapeSequences($str, $quote)
public static function parseEscapeSequences(string $str, string $quote = null)
{
if (null !== $quote) {
$str = str_replace('\\'.$quote, $quote, $str);
@@ -125,9 +125,9 @@ class PhpStringTokenParser
* @param string $startToken Doc string start token content (<<<SMTHG)
* @param string $str String token content
*
* @return string Parsed string
* @return string
*/
public static function parseDocString($startToken, $str)
public static function parseDocString(string $startToken, string $str)
{
// strip last newline (thanks tokenizer for sticking it into the string!)
$str = preg_replace('~(\r\n|\n|\r)$~', '', $str);

View File

@@ -1,32 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Formatter;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @deprecated since Symfony 4.2, use MessageFormatterInterface::format() with a %count% parameter instead
*/
interface ChoiceMessageFormatterInterface
{
/**
* Formats a localized message pattern with given arguments.
*
* @param string $message The message (may also be an object that can be cast to string)
* @param int $number The number to use to find the indice of the message
* @param string $locale The message locale
* @param array $parameters An array of parameters for the message
*
* @return string
*/
public function choiceFormat($message, $number, $locale, array $parameters = []);
}

View File

@@ -12,8 +12,6 @@
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;
// Help opcache.preload discover always-needed symbols
@@ -22,7 +20,7 @@ class_exists(IntlFormatter::class);
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface, ChoiceMessageFormatterInterface
class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface
{
private $translator;
private $intlFormatter;
@@ -30,14 +28,8 @@ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterf
/**
* @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization
*/
public function __construct($translator = null, IntlFormatterInterface $intlFormatter = null)
public function __construct(TranslatorInterface $translator = null, IntlFormatterInterface $intlFormatter = null)
{
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();
}
@@ -45,7 +37,7 @@ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterf
/**
* {@inheritdoc}
*/
public function format($message, $locale, array $parameters = [])
public function format(string $message, string $locale, array $parameters = [])
{
if ($this->translator instanceof TranslatorInterface) {
return $this->translator->trans($message, $parameters, null, $locale);
@@ -61,22 +53,4 @@ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterf
{
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 = [])
{
@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);
$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);
}
}

View File

@@ -26,5 +26,5 @@ interface MessageFormatterInterface
*
* @return string
*/
public function format($message, $locale, array $parameters = []);
public function format(string $message, string $locale, array $parameters = []);
}

View File

@@ -11,7 +11,7 @@
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
@@ -20,58 +20,7 @@ use Symfony\Contracts\Translation\TranslatorTrait;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IdentityTranslator implements LegacyTranslatorInterface, TranslatorInterface
class IdentityTranslator implements TranslatorInterface, LocaleAwareInterface
{
use TranslatorTrait {
trans as private doTrans;
setLocale as private doSetLocale;
}
private $selector;
public function __construct(MessageSelector $selector = null)
{
$this->selector = $selector;
if (__CLASS__ !== static::class) {
@trigger_error(sprintf('Calling "%s()" is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED);
}
}
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
{
return $this->doTrans($id, $parameters, $domain, $locale);
}
/**
* {@inheritdoc}
*/
public function setLocale($locale)
{
$this->doSetLocale($locale);
}
/**
* {@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->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(float $number, string $locale): int
{
return PluralizationRules::get($number, $locale, false);
}
use TranslatorTrait;
}

View File

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

View File

@@ -23,7 +23,7 @@ class ArrayLoader implements LoaderInterface
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
public function load($resource, string $locale, string $domain = 'messages')
{
$resource = $this->flatten($resource);
$catalogue = new MessageCatalogue($locale);

View File

@@ -27,7 +27,7 @@ class CsvFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
protected function loadResource(string $resource)
{
$messages = [];
@@ -55,12 +55,8 @@ class CsvFileLoader extends FileLoader
/**
* Sets the delimiter, enclosure, and escape character for CSV.
*
* @param string $delimiter Delimiter character
* @param string $enclosure Enclosure character
* @param string $escape Escape character
*/
public function setCsvControl($delimiter = ';', $enclosure = '"', $escape = '\\')
public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = '\\')
{
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;

View File

@@ -23,7 +23,7 @@ abstract class FileLoader extends ArrayLoader
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
public function load($resource, string $locale, string $domain = 'messages')
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
@@ -55,11 +55,9 @@ abstract class FileLoader extends ArrayLoader
}
/**
* @param string $resource
*
* @return array
*
* @throws InvalidResourceException if stream content has an invalid format
*/
abstract protected function loadResource($resource);
abstract protected function loadResource(string $resource);
}

View File

@@ -26,7 +26,7 @@ class IcuDatFileLoader extends IcuResFileLoader
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
public function load($resource, string $locale, string $domain = 'messages')
{
if (!stream_is_local($resource.'.dat')) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));

View File

@@ -26,7 +26,7 @@ class IcuResFileLoader implements LoaderInterface
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
public function load($resource, string $locale, string $domain = 'messages')
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
@@ -73,9 +73,9 @@ class IcuResFileLoader implements LoaderInterface
* @param array $messages Used internally for recursive calls
* @param string $path Current path being parsed, used internally for recursive calls
*
* @return array the flattened ResourceBundle
* @return array
*/
protected function flatten(\ResourceBundle $rb, array &$messages = [], $path = null)
protected function flatten(\ResourceBundle $rb, array &$messages = [], string $path = null)
{
foreach ($rb as $key => $value) {
$nodePath = $path ? $path.'.'.$key : $key;

View File

@@ -21,7 +21,7 @@ class IniFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
protected function loadResource(string $resource)
{
return parse_ini_file($resource, true);
}

View File

@@ -23,7 +23,7 @@ class JsonFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
protected function loadResource(string $resource)
{
$messages = [];
if ($data = file_get_contents($resource)) {

View File

@@ -29,10 +29,10 @@ interface LoaderInterface
* @param string $locale A locale
* @param string $domain The domain
*
* @return MessageCatalogue A MessageCatalogue instance
* @return MessageCatalogue
*
* @throws NotFoundResourceException when the resource cannot be found
* @throws InvalidResourceException when the resource cannot be loaded
*/
public function load($resource, $locale, $domain = 'messages');
public function load($resource, string $locale, string $domain = 'messages');
}

View File

@@ -41,7 +41,7 @@ class MoFileLoader extends FileLoader
*
* {@inheritdoc}
*/
protected function loadResource($resource)
protected function loadResource(string $resource)
{
$stream = fopen($resource, 'r');

View File

@@ -23,7 +23,7 @@ class PhpFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
protected function loadResource(string $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))) {
self::$cache = null;

View File

@@ -60,7 +60,7 @@ class PoFileLoader extends FileLoader
*
* {@inheritdoc}
*/
protected function loadResource($resource)
protected function loadResource(string $resource)
{
$stream = fopen($resource, 'r');

View File

@@ -15,6 +15,7 @@ use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Translation\Exception\RuntimeException;
use Symfony\Component\Translation\MessageCatalogue;
/**
@@ -27,8 +28,12 @@ class QtFileLoader implements LoaderInterface
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
public function load($resource, string $locale, string $domain = 'messages')
{
if (!class_exists(XmlUtils::class)) {
throw new RuntimeException('Loading translations from the QT format requires the Symfony Config component.');
}
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
}

View File

@@ -12,9 +12,12 @@
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\Exception\InvalidXmlException;
use Symfony\Component\Config\Util\Exception\XmlParsingException;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Translation\Exception\RuntimeException;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Util\XliffUtils;
@@ -28,38 +31,53 @@ class XliffFileLoader implements LoaderInterface
/**
* {@inheritdoc}
*/
public function load($resource, $locale, $domain = 'messages')
public function load($resource, string $locale, string $domain = 'messages')
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
if (!class_exists(XmlUtils::class)) {
throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.');
}
if (!file_exists($resource)) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
if (!$this->isXmlString($resource)) {
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
}
if (!file_exists($resource)) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
}
if (!is_file($resource)) {
throw new InvalidResourceException(sprintf('This is neither a file nor an XLIFF string "%s".', $resource));
}
}
try {
if ($this->isXmlString($resource)) {
$dom = XmlUtils::parse($resource);
} else {
$dom = XmlUtils::loadFile($resource);
}
} catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) {
throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e);
}
if ($errors = XliffUtils::validateSchema($dom)) {
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors));
}
$catalogue = new MessageCatalogue($locale);
$this->extract($resource, $catalogue, $domain);
$this->extract($dom, $catalogue, $domain);
if (class_exists(FileResource::class)) {
if (is_file($resource) && class_exists(FileResource::class)) {
$catalogue->addResource(new FileResource($resource));
}
return $catalogue;
}
private function extract($resource, MessageCatalogue $catalogue, string $domain)
private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain)
{
try {
$dom = XmlUtils::loadFile($resource);
} catch (\InvalidArgumentException $e) {
throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e);
}
$xliffVersion = XliffUtils::getVersionNumber($dom);
if ($errors = XliffUtils::validateSchema($dom)) {
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors));
}
if ('1.2' === $xliffVersion) {
$this->extractXliff1($dom, $catalogue, $domain);
@@ -135,11 +153,12 @@ class XliffFileLoader implements LoaderInterface
foreach ($xml->xpath('//xliff:unit') as $unit) {
foreach ($unit->segment as $segment) {
$source = $segment->source;
$attributes = $unit->attributes();
$source = $attributes['name'] ?? $segment->source;
// If the xlf file has another encoding specified, try to convert it because
// simple_xml will always return utf-8 encoded values
$target = $this->utf8ToCharset((string) ($segment->target ?? $source), $encoding);
$target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding);
$catalogue->set((string) $source, $target, $domain);
@@ -205,4 +224,9 @@ class XliffFileLoader implements LoaderInterface
return $notes;
}
private function isXmlString(string $resource): bool
{
return 0 === strpos($resource, '<?xml');
}
}

View File

@@ -29,7 +29,7 @@ class YamlFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
protected function loadResource($resource)
protected function loadResource(string $resource)
{
if (null === $this->yamlParser) {
if (!class_exists(\Symfony\Component\Yaml\Parser::class)) {

View File

@@ -13,32 +13,24 @@ 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, LegacyTranslatorInterface, TranslatorBagInterface
class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
{
/**
* @var TranslatorInterface|TranslatorBagInterface
*/
private $translator;
private $logger;
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
* @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator The translator must implement TranslatorBagInterface
*/
public function __construct($translator, LoggerInterface $logger)
public function __construct(TranslatorInterface $translator, LoggerInterface $logger)
{
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)));
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator)));
}
$this->translator = $translator;
@@ -48,29 +40,9 @@ class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterfac
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
{
$trans = $this->translator->trans($id, $parameters, $domain, $locale);
$this->log($id, $domain, $locale);
return $trans;
}
/**
* {@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->translator instanceof TranslatorInterface) {
$trans = $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale);
} else {
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
}
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);
$this->log($id, $domain, $locale);
return $trans;
@@ -79,7 +51,7 @@ class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterfac
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function setLocale(string $locale)
{
$prev = $this->translator->getLocale();
$this->translator->setLocale($locale);
@@ -101,15 +73,23 @@ class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterfac
/**
* {@inheritdoc}
*/
public function getCatalogue($locale = null)
public function getCatalogue(string $locale = null)
{
return $this->translator->getCatalogue($locale);
}
/**
* {@inheritdoc}
*/
public function getCatalogues(): array
{
return $this->translator->getCatalogues();
}
/**
* Gets the fallback locales.
*
* @return array The fallback locales
* @return array
*/
public function getFallbackLocales()
{
@@ -123,7 +103,7 @@ class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterfac
/**
* Passes through all unknown calls onto the translator object.
*/
public function __call($method, $args)
public function __call(string $method, array $args)
{
return $this->translator->{$method}(...$args);
}
@@ -131,13 +111,12 @@ class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterfac
/**
* Logs for missing translations.
*/
private function log(?string $id, ?string $domain, ?string $locale)
private function log(string $id, ?string $domain, ?string $locale)
{
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->translator->getCatalogue($locale);
if ($catalogue->defines($id, $domain)) {
return;

View File

@@ -29,12 +29,8 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* @param array $messages An array of messages classified by domain
*/
public function __construct(?string $locale, array $messages = [])
public function __construct(string $locale, array $messages = [])
{
if (null === $locale) {
@trigger_error(sprintf('Passing "null" to the first argument of the "%s" method has been deprecated since Symfony 4.4 and will throw an error in 5.0.', __METHOD__), \E_USER_DEPRECATED);
}
$this->locale = $locale;
$this->messages = $messages;
}
@@ -67,7 +63,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function all($domain = null)
public function all(string $domain = null)
{
if (null !== $domain) {
// skip messages merge if intl-icu requested explicitly
@@ -95,7 +91,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function set($id, $translation, $domain = 'messages')
public function set(string $id, string $translation, string $domain = 'messages')
{
$this->add([$id => $translation], $domain);
}
@@ -103,7 +99,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function has($id, $domain = 'messages')
public function has(string $id, string $domain = 'messages')
{
if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) {
return true;
@@ -119,7 +115,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function defines($id, $domain = 'messages')
public function defines(string $id, string $domain = 'messages')
{
return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]);
}
@@ -127,7 +123,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function get($id, $domain = 'messages')
public function get(string $id, string $domain = 'messages')
{
if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) {
return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id];
@@ -147,7 +143,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function replace($messages, $domain = 'messages')
public function replace(array $messages, string $domain = 'messages')
{
unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]);
@@ -157,7 +153,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function add($messages, $domain = 'messages')
public function add(array $messages, string $domain = 'messages')
{
$altDomain = str_ends_with($domain, self::INTL_DOMAIN_SUFFIX) ? substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)) : $domain.self::INTL_DOMAIN_SUFFIX;
foreach ($messages as $id => $message) {
@@ -256,7 +252,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function getMetadata($key = '', $domain = 'messages')
public function getMetadata(string $key = '', string $domain = 'messages')
{
if ('' == $domain) {
return $this->metadata;
@@ -278,7 +274,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function setMetadata($key, $value, $domain = 'messages')
public function setMetadata(string $key, $value, string $domain = 'messages')
{
$this->metadata[$domain][$key] = $value;
}
@@ -286,7 +282,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
/**
* {@inheritdoc}
*/
public function deleteMetadata($key = '', $domain = 'messages')
public function deleteMetadata(string $key = '', string $domain = 'messages')
{
if ('' == $domain) {
$this->metadata = [];

View File

@@ -25,14 +25,14 @@ interface MessageCatalogueInterface
/**
* Gets the catalogue locale.
*
* @return string The locale
* @return string
*/
public function getLocale();
/**
* Gets the domains.
*
* @return array An array of domains
* @return array
*/
public function getDomains();
@@ -43,9 +43,9 @@ interface MessageCatalogueInterface
*
* @param string $domain The domain name
*
* @return array An array of messages
* @return array
*/
public function all($domain = null);
public function all(string $domain = null);
/**
* Sets a message translation.
@@ -54,7 +54,7 @@ interface MessageCatalogueInterface
* @param string $translation The messages translation
* @param string $domain The domain name
*/
public function set($id, $translation, $domain = 'messages');
public function set(string $id, string $translation, string $domain = 'messages');
/**
* Checks if a message has a translation.
@@ -62,9 +62,9 @@ interface MessageCatalogueInterface
* @param string $id The message id
* @param string $domain The domain name
*
* @return bool true if the message has a translation, false otherwise
* @return bool
*/
public function has($id, $domain = 'messages');
public function has(string $id, string $domain = 'messages');
/**
* Checks if a message has a translation (it does not take into account the fallback mechanism).
@@ -72,9 +72,9 @@ interface MessageCatalogueInterface
* @param string $id The message id
* @param string $domain The domain name
*
* @return bool true if the message has a translation, false otherwise
* @return bool
*/
public function defines($id, $domain = 'messages');
public function defines(string $id, string $domain = 'messages');
/**
* Gets a message translation.
@@ -82,9 +82,9 @@ interface MessageCatalogueInterface
* @param string $id The message id
* @param string $domain The domain name
*
* @return string The message translation
* @return string
*/
public function get($id, $domain = 'messages');
public function get(string $id, string $domain = 'messages');
/**
* Sets translations for a given domain.
@@ -92,7 +92,7 @@ interface MessageCatalogueInterface
* @param array $messages An array of translations
* @param string $domain The domain name
*/
public function replace($messages, $domain = 'messages');
public function replace(array $messages, string $domain = 'messages');
/**
* Adds translations for a given domain.
@@ -100,7 +100,7 @@ interface MessageCatalogueInterface
* @param array $messages An array of translations
* @param string $domain The domain name
*/
public function add($messages, $domain = 'messages');
public function add(array $messages, string $domain = 'messages');
/**
* Merges translations from the given Catalogue into the current one.
@@ -120,14 +120,14 @@ interface MessageCatalogueInterface
/**
* Gets the fallback catalogue.
*
* @return self|null A MessageCatalogueInterface instance or null when no fallback has been set
* @return self|null
*/
public function getFallbackCatalogue();
/**
* Returns an array of resources loaded to build this collection.
*
* @return ResourceInterface[] An array of resources
* @return ResourceInterface[]
*/
public function getResources();

View File

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

View File

@@ -25,30 +25,22 @@ interface MetadataAwareInterface
* domain and then by key. Passing an empty key will return an array with all
* metadata for the given domain.
*
* @param string $key The key
* @param string $domain The domain name
*
* @return mixed The value that was set or an array with the domains/keys or null
*/
public function getMetadata($key = '', $domain = 'messages');
public function getMetadata(string $key = '', string $domain = 'messages');
/**
* Adds metadata to a message domain.
*
* @param string $key The key
* @param mixed $value The value
* @param string $domain The domain name
* @param mixed $value
*/
public function setMetadata($key, $value, $domain = 'messages');
public function setMetadata(string $key, $value, string $domain = 'messages');
/**
* Deletes metadata for the given key and domain.
*
* Passing an empty domain will delete all metadata. Passing an empty key will
* delete all metadata for the given domain.
*
* @param string $key The key
* @param string $domain The domain name
*/
public function deleteMetadata($key = '', $domain = 'messages');
public function deleteMetadata(string $key = '', string $domain = 'messages');
}

View File

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

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\Exception\IncompleteDsnException;
abstract class AbstractProviderFactory implements ProviderFactoryInterface
{
public function supports(Dsn $dsn): bool
{
return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true);
}
/**
* @return string[]
*/
abstract protected function getSupportedSchemes(): array;
protected function getUser(Dsn $dsn): string
{
if (null === $user = $dsn->getUser()) {
throw new IncompleteDsnException('User is not set.', $dsn->getOriginalDsn());
}
return $user;
}
protected function getPassword(Dsn $dsn): string
{
if (null === $password = $dsn->getPassword()) {
throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn());
}
return $password;
}
}

View File

@@ -0,0 +1,110 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\MissingRequiredOptionException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Oskar Stark <oskarstark@googlemail.com>
*/
final class Dsn
{
private $scheme;
private $host;
private $user;
private $password;
private $port;
private $path;
private $options;
private $originalDsn;
public function __construct(string $dsn)
{
$this->originalDsn = $dsn;
if (false === $parsedDsn = parse_url($dsn)) {
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn));
}
if (!isset($parsedDsn['scheme'])) {
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn));
}
$this->scheme = $parsedDsn['scheme'];
if (!isset($parsedDsn['host'])) {
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn));
}
$this->host = $parsedDsn['host'];
$this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null;
$this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null;
$this->port = $parsedDsn['port'] ?? null;
$this->path = $parsedDsn['path'] ?? null;
parse_str($parsedDsn['query'] ?? '', $this->options);
}
public function getScheme(): string
{
return $this->scheme;
}
public function getHost(): string
{
return $this->host;
}
public function getUser(): ?string
{
return $this->user;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getPort(int $default = null): ?int
{
return $this->port ?? $default;
}
public function getOption(string $key, $default = null)
{
return $this->options[$key] ?? $default;
}
public function getRequiredOption(string $key)
{
if (!\array_key_exists($key, $this->options) || '' === trim($this->options[$key])) {
throw new MissingRequiredOptionException($key);
}
return $this->options[$key];
}
public function getOptions(): array
{
return $this->options;
}
public function getPath(): ?string
{
return $this->path;
}
public function getOriginalDsn(): string
{
return $this->originalDsn;
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\TranslatorBag;
use Symfony\Component\Translation\TranslatorBagInterface;
/**
* Filters domains and locales between the Translator config values and those specific to each provider.
*
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
class FilteringProvider implements ProviderInterface
{
private $provider;
private $locales;
private $domains;
public function __construct(ProviderInterface $provider, array $locales, array $domains = [])
{
$this->provider = $provider;
$this->locales = $locales;
$this->domains = $domains;
}
public function __toString(): string
{
return (string) $this->provider;
}
/**
* {@inheritdoc}
*/
public function write(TranslatorBagInterface $translatorBag): void
{
$this->provider->write($translatorBag);
}
public function read(array $domains, array $locales): TranslatorBag
{
$domains = !$this->domains ? $domains : array_intersect($this->domains, $domains);
$locales = array_intersect($this->locales, $locales);
return $this->provider->read($domains, $locales);
}
public function delete(TranslatorBagInterface $translatorBag): void
{
$this->provider->delete($translatorBag);
}
public function getDomains(): array
{
return $this->domains;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\TranslatorBag;
use Symfony\Component\Translation\TranslatorBagInterface;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
class NullProvider implements ProviderInterface
{
public function __toString(): string
{
return 'null';
}
public function write(TranslatorBagInterface $translatorBag, bool $override = false): void
{
}
public function read(array $domains, array $locales): TranslatorBag
{
return new TranslatorBag();
}
public function delete(TranslatorBagInterface $translatorBag): void
{
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
final class NullProviderFactory extends AbstractProviderFactory
{
public function create(Dsn $dsn): ProviderInterface
{
if ('null' === $dsn->getScheme()) {
return new NullProvider();
}
throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes());
}
protected function getSupportedSchemes(): array
{
return ['null'];
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\Exception\IncompleteDsnException;
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
interface ProviderFactoryInterface
{
/**
* @throws UnsupportedSchemeException
* @throws IncompleteDsnException
*/
public function create(Dsn $dsn): ProviderInterface;
public function supports(Dsn $dsn): bool;
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\TranslatorBag;
use Symfony\Component\Translation\TranslatorBagInterface;
interface ProviderInterface
{
public function __toString(): string;
/**
* Translations available in the TranslatorBag only must be created.
* Translations available in both the TranslatorBag and on the provider
* must be overwritten.
* Translations available on the provider only must be kept.
*/
public function write(TranslatorBagInterface $translatorBag): void;
public function read(array $domains, array $locales): TranslatorBag;
public function delete(TranslatorBagInterface $translatorBag): void;
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
final class TranslationProviderCollection
{
/**
* @var array<string, ProviderInterface>
*/
private $providers;
/**
* @param array<string, ProviderInterface> $providers
*/
public function __construct(iterable $providers)
{
$this->providers = \is_array($providers) ? $providers : iterator_to_array($providers);
}
public function __toString(): string
{
return '['.implode(',', array_keys($this->providers)).']';
}
public function has(string $name): bool
{
return isset($this->providers[$name]);
}
public function get(string $name): ProviderInterface
{
if (!$this->has($name)) {
throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this));
}
return $this->providers[$name];
}
public function keys(): array
{
return array_keys($this->providers);
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
class TranslationProviderCollectionFactory
{
private $factories;
private $enabledLocales;
/**
* @param iterable<mixed, ProviderFactoryInterface> $factories
*/
public function __construct(iterable $factories, array $enabledLocales)
{
$this->factories = $factories;
$this->enabledLocales = $enabledLocales;
}
public function fromConfig(array $config): TranslationProviderCollection
{
$providers = [];
foreach ($config as $name => $currentConfig) {
$providers[$name] = $this->fromDsnObject(
new Dsn($currentConfig['dsn']),
!$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'],
!$currentConfig['domains'] ? [] : $currentConfig['domains']
);
}
return new TranslationProviderCollection($providers);
}
public function fromDsnObject(Dsn $dsn, array $locales, array $domains = []): ProviderInterface
{
foreach ($this->factories as $factory) {
if ($factory->supports($dsn)) {
return new FilteringProvider($factory->create($dsn), $locales, $domains);
}
}
throw new UnsupportedSchemeException($dsn);
}
}

View File

@@ -0,0 +1,368 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* This translator should only be used in a development environment.
*/
final class PseudoLocalizationTranslator implements TranslatorInterface
{
private const EXPANSION_CHARACTER = '~';
private $translator;
private $accents;
private $expansionFactor;
private $brackets;
private $parseHTML;
/**
* @var string[]
*/
private $localizableHTMLAttributes;
/**
* Available options:
* * accents:
* type: boolean
* default: true
* description: replace ASCII characters of the translated string with accented versions or similar characters
* example: if true, "foo" => "ƒöö".
*
* * expansion_factor:
* type: float
* default: 1
* validation: it must be greater than or equal to 1
* description: expand the translated string by the given factor with spaces and tildes
* example: if 2, "foo" => "~foo ~"
*
* * brackets:
* type: boolean
* default: true
* description: wrap the translated string with brackets
* example: if true, "foo" => "[foo]"
*
* * parse_html:
* type: boolean
* default: false
* description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML
* warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo <div>bar" => "foo <div>bar</div>"
*
* * localizable_html_attributes:
* type: string[]
* default: []
* description: the list of HTML attributes whose values can be altered - it is only useful when the "parse_html" option is set to true
* example: if ["title"], and with the "accents" option set to true, "<a href="#" title="Go to your profile">Profile</a>" => "<a href="#" title="Ĝö ţö ýöûŕ þŕöƒîļé">Þŕöƒîļé</a>" - if "title" was not in the "localizable_html_attributes" list, the title attribute data would be left unchanged.
*/
public function __construct(TranslatorInterface $translator, array $options = [])
{
$this->translator = $translator;
$this->accents = $options['accents'] ?? true;
if (1.0 > ($this->expansionFactor = $options['expansion_factor'] ?? 1.0)) {
throw new \InvalidArgumentException('The expansion factor must be greater than or equal to 1.');
}
$this->brackets = $options['brackets'] ?? true;
$this->parseHTML = $options['parse_html'] ?? false;
if ($this->parseHTML && !$this->accents && 1.0 === $this->expansionFactor) {
$this->parseHTML = false;
}
$this->localizableHTMLAttributes = $options['localizable_html_attributes'] ?? [];
}
/**
* {@inheritdoc}
*/
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string
{
$trans = '';
$visibleText = '';
foreach ($this->getParts($this->translator->trans($id, $parameters, $domain, $locale)) as [$visible, $localizable, $text]) {
if ($visible) {
$visibleText .= $text;
}
if (!$localizable) {
$trans .= $text;
continue;
}
$this->addAccents($trans, $text);
}
$this->expand($trans, $visibleText);
$this->addBrackets($trans);
return $trans;
}
public function getLocale(): string
{
return $this->translator->getLocale();
}
private function getParts(string $originalTrans): array
{
if (!$this->parseHTML) {
return [[true, true, $originalTrans]];
}
$html = mb_encode_numericentity($originalTrans, [0x80, 0xFFFF, 0, 0xFFFF], mb_detect_encoding($originalTrans, null, true) ?: 'UTF-8');
$useInternalErrors = libxml_use_internal_errors(true);
$dom = new \DOMDocument();
$dom->loadHTML('<trans>'.$html.'</trans>');
libxml_clear_errors();
libxml_use_internal_errors($useInternalErrors);
return $this->parseNode($dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0));
}
private function parseNode(\DOMNode $node): array
{
$parts = [];
foreach ($node->childNodes as $childNode) {
if (!$childNode instanceof \DOMElement) {
$parts[] = [true, true, $childNode->nodeValue];
continue;
}
$parts[] = [false, false, '<'.$childNode->tagName];
/** @var \DOMAttr $attribute */
foreach ($childNode->attributes as $attribute) {
$parts[] = [false, false, ' '.$attribute->nodeName.'="'];
$localizableAttribute = \in_array($attribute->nodeName, $this->localizableHTMLAttributes, true);
foreach (preg_split('/(&(?:amp|quot|#039|lt|gt);+)/', htmlspecialchars($attribute->nodeValue, \ENT_QUOTES, 'UTF-8'), -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $match) {
if ('' === $match) {
continue;
}
$parts[] = [false, $localizableAttribute && 0 === $i % 2, $match];
}
$parts[] = [false, false, '"'];
}
$parts[] = [false, false, '>'];
$parts = array_merge($parts, $this->parseNode($childNode, $parts));
$parts[] = [false, false, '</'.$childNode->tagName.'>'];
}
return $parts;
}
private function addAccents(string &$trans, string $text): void
{
$trans .= $this->accents ? strtr($text, [
' ' => '',
'!' => '¡',
'"' => '″',
'#' => '♯',
'$' => '€',
'%' => '‰',
'&' => '⅋',
'\'' => '´',
'(' => '{',
')' => '}',
'*' => '',
'+' => '⁺',
',' => '،',
'-' => '',
'.' => '·',
'/' => '',
'0' => '⓪',
'1' => '①',
'2' => '②',
'3' => '③',
'4' => '④',
'5' => '⑤',
'6' => '⑥',
'7' => '⑦',
'8' => '⑧',
'9' => '⑨',
':' => '',
';' => '⁏',
'<' => '≤',
'=' => '≂',
'>' => '≥',
'?' => '¿',
'@' => '՞',
'A' => 'Å',
'B' => 'Ɓ',
'C' => 'Ç',
'D' => 'Ð',
'E' => 'É',
'F' => 'Ƒ',
'G' => 'Ĝ',
'H' => 'Ĥ',
'I' => 'Î',
'J' => 'Ĵ',
'K' => 'Ķ',
'L' => 'Ļ',
'M' => 'Ṁ',
'N' => 'Ñ',
'O' => 'Ö',
'P' => 'Þ',
'Q' => 'Ǫ',
'R' => 'Ŕ',
'S' => 'Š',
'T' => 'Ţ',
'U' => 'Û',
'V' => 'Ṽ',
'W' => 'Ŵ',
'X' => 'Ẋ',
'Y' => 'Ý',
'Z' => 'Ž',
'[' => '⁅',
'\\' => '',
']' => '⁆',
'^' => '˄',
'_' => '‿',
'`' => '',
'a' => 'å',
'b' => 'ƀ',
'c' => 'ç',
'd' => 'ð',
'e' => 'é',
'f' => 'ƒ',
'g' => 'ĝ',
'h' => 'ĥ',
'i' => 'î',
'j' => 'ĵ',
'k' => 'ķ',
'l' => 'ļ',
'm' => 'ɱ',
'n' => 'ñ',
'o' => 'ö',
'p' => 'þ',
'q' => 'ǫ',
'r' => 'ŕ',
's' => 'š',
't' => 'ţ',
'u' => 'û',
'v' => 'ṽ',
'w' => 'ŵ',
'x' => 'ẋ',
'y' => 'ý',
'z' => 'ž',
'{' => '(',
'|' => '¦',
'}' => ')',
'~' => '˞',
]) : $text;
}
private function expand(string &$trans, string $visibleText): void
{
if (1.0 >= $this->expansionFactor) {
return;
}
$visibleLength = $this->strlen($visibleText);
$missingLength = (int) ceil($visibleLength * $this->expansionFactor) - $visibleLength;
if ($this->brackets) {
$missingLength -= 2;
}
if (0 >= $missingLength) {
return;
}
$words = [];
$wordsCount = 0;
foreach (preg_split('/ +/', $visibleText, -1, \PREG_SPLIT_NO_EMPTY) as $word) {
$wordLength = $this->strlen($word);
if ($wordLength >= $missingLength) {
continue;
}
if (!isset($words[$wordLength])) {
$words[$wordLength] = 0;
}
++$words[$wordLength];
++$wordsCount;
}
if (!$words) {
$trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1);
return;
}
arsort($words, \SORT_NUMERIC);
$longestWordLength = max(array_keys($words));
while (true) {
$r = mt_rand(1, $wordsCount);
foreach ($words as $length => $count) {
$r -= $count;
if ($r <= 0) {
break;
}
}
$trans .= ' '.str_repeat(self::EXPANSION_CHARACTER, $length);
$missingLength -= $length + 1;
if (0 === $missingLength) {
return;
}
while ($longestWordLength >= $missingLength) {
$wordsCount -= $words[$longestWordLength];
unset($words[$longestWordLength]);
if (!$words) {
$trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1);
return;
}
$longestWordLength = max(array_keys($words));
}
}
}
private function addBrackets(string &$trans): void
{
if (!$this->brackets) {
return;
}
$trans = '['.$trans.']';
}
private function strlen(string $s): int
{
return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding);
}
}

View File

@@ -23,6 +23,16 @@ $translator->addResource('array', [
echo $translator->trans('Hello World!'); // outputs « Bonjour ! »
```
Sponsor
-------
The Translation component for Symfony 5.4/6.0 is [backed][1] by:
* [Crowdin][2], a cloud-based localization management software helping teams to go global and stay agile.
* [Lokalise][3], a continuous localization and translation management platform that integrates into your development workflow so you can ship localized products, faster.
Help Symfony by [sponsoring][4] its development!
Resources
---------
@@ -31,3 +41,8 @@ Resources
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://crowdin.com
[3]: https://lokalise.com
[4]: https://symfony.com/sponsor

View File

@@ -25,7 +25,7 @@ class TranslationReader implements TranslationReaderInterface
/**
* Loaders used for import.
*
* @var array
* @var array<string, LoaderInterface>
*/
private $loaders = [];
@@ -34,7 +34,7 @@ class TranslationReader implements TranslationReaderInterface
*
* @param string $format The format of the loader
*/
public function addLoader($format, LoaderInterface $loader)
public function addLoader(string $format, LoaderInterface $loader)
{
$this->loaders[$format] = $loader;
}
@@ -42,7 +42,7 @@ class TranslationReader implements TranslationReaderInterface
/**
* {@inheritdoc}
*/
public function read($directory, MessageCatalogue $catalogue)
public function read(string $directory, MessageCatalogue $catalogue)
{
if (!is_dir($directory)) {
return;

View File

@@ -22,8 +22,6 @@ interface TranslationReaderInterface
{
/**
* Reads translation messages from a directory to the catalogue.
*
* @param string $directory
*/
public function read($directory, MessageCatalogue $catalogue);
public function read(string $directory, MessageCatalogue $catalogue);
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
if (!\function_exists(t::class)) {
/**
* @author Nate Wiebe <nate@northern.co>
*/
function t(string $message, array $parameters = [], string $domain = null): TranslatableMessage
{
return new TranslatableMessage($message, $parameters, $domain);
}
}

View File

@@ -0,0 +1,147 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\Translation\Dumper\XliffFileDumper;
use Symfony\Component\Translation\Exception\IncompleteDsnException;
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Provider\Dsn;
use Symfony\Component\Translation\Provider\ProviderFactoryInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A test case to ease testing a translation provider factory.
*
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*
* @internal
*/
abstract class ProviderFactoryTestCase extends TestCase
{
protected $client;
protected $logger;
protected $defaultLocale;
protected $loader;
protected $xliffFileDumper;
abstract public function createFactory(): ProviderFactoryInterface;
/**
* @return iterable<array{0: bool, 1: string}>
*/
abstract public function supportsProvider(): iterable;
/**
* @return iterable<array{0: string, 1: string, 2: TransportInterface}>
*/
abstract public function createProvider(): iterable;
/**
* @return iterable<array{0: string, 1: string|null}>
*/
public function unsupportedSchemeProvider(): iterable
{
return [];
}
/**
* @return iterable<array{0: string, 1: string|null}>
*/
public function incompleteDsnProvider(): iterable
{
return [];
}
/**
* @dataProvider supportsProvider
*/
public function testSupports(bool $expected, string $dsn)
{
$factory = $this->createFactory();
$this->assertSame($expected, $factory->supports(new Dsn($dsn)));
}
/**
* @dataProvider createProvider
*/
public function testCreate(string $expected, string $dsn)
{
$factory = $this->createFactory();
$provider = $factory->create(new Dsn($dsn));
$this->assertSame($expected, (string) $provider);
}
/**
* @dataProvider unsupportedSchemeProvider
*/
public function testUnsupportedSchemeException(string $dsn, string $message = null)
{
$factory = $this->createFactory();
$dsn = new Dsn($dsn);
$this->expectException(UnsupportedSchemeException::class);
if (null !== $message) {
$this->expectExceptionMessage($message);
}
$factory->create($dsn);
}
/**
* @dataProvider incompleteDsnProvider
*/
public function testIncompleteDsnException(string $dsn, string $message = null)
{
$factory = $this->createFactory();
$dsn = new Dsn($dsn);
$this->expectException(IncompleteDsnException::class);
if (null !== $message) {
$this->expectExceptionMessage($message);
}
$factory->create($dsn);
}
protected function getClient(): HttpClientInterface
{
return $this->client ?? $this->client = new MockHttpClient();
}
protected function getLogger(): LoggerInterface
{
return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class);
}
protected function getDefaultLocale(): string
{
return $this->defaultLocale ?? $this->defaultLocale = 'en';
}
protected function getLoader(): LoaderInterface
{
return $this->loader ?? $this->loader = $this->createMock(LoaderInterface::class);
}
protected function getXliffFileDumper(): XliffFileDumper
{
return $this->xliffFileDumper ?? $this->xliffFileDumper = $this->createMock(XliffFileDumper::class);
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\Translation\Dumper\XliffFileDumper;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Provider\ProviderInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A test case to ease testing a translation provider.
*
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*
* @internal
*/
abstract class ProviderTestCase extends TestCase
{
protected $client;
protected $logger;
protected $defaultLocale;
protected $loader;
protected $xliffFileDumper;
abstract public function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface;
/**
* @return iterable<array{0: string, 1: ProviderInterface}>
*/
abstract public function toStringProvider(): iterable;
/**
* @dataProvider toStringProvider
*/
public function testToString(ProviderInterface $provider, string $expected)
{
$this->assertSame($expected, (string) $provider);
}
protected function getClient(): MockHttpClient
{
return $this->client ?? $this->client = new MockHttpClient();
}
protected function getLoader(): LoaderInterface
{
return $this->loader ?? $this->loader = $this->createMock(LoaderInterface::class);
}
protected function getLogger(): LoggerInterface
{
return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class);
}
protected function getDefaultLocale(): string
{
return $this->defaultLocale ?? $this->defaultLocale = 'en';
}
protected function getXliffFileDumper(): XliffFileDumper
{
return $this->xliffFileDumper ?? $this->xliffFileDumper = $this->createMock(XliffFileDumper::class);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Nate Wiebe <nate@northern.co>
*/
class TranslatableMessage implements TranslatableInterface
{
private $message;
private $parameters;
private $domain;
public function __construct(string $message, array $parameters = [], string $domain = null)
{
$this->message = $message;
$this->parameters = $parameters;
$this->domain = $domain;
}
public function __toString(): string
{
return $this->getMessage();
}
public function getMessage(): string
{
return $this->message;
}
public function getParameters(): array
{
return $this->parameters;
}
public function getDomain(): ?string
{
return $this->domain;
}
public function trans(TranslatorInterface $translator, string $locale = null): string
{
return $translator->trans($this->getMessage(), array_map(
static function ($parameter) use ($translator, $locale) {
return $parameter instanceof TranslatableInterface ? $parameter->trans($translator, $locale) : $parameter;
},
$this->getParameters()
), $this->getDomain(), $locale);
}
}

View File

@@ -15,21 +15,22 @@ use Symfony\Component\Config\ConfigCacheFactory;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
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\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
// Help opcache.preload discover always-needed symbols
class_exists(MessageCatalogue::class);
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface
class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
{
/**
* @var MessageCatalogueInterface[]
@@ -42,7 +43,7 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
private $locale;
/**
* @var array
* @var string[]
*/
private $fallbackLocales = [];
@@ -88,13 +89,9 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
/**
* @throws InvalidArgumentException If a locale contains invalid characters
*/
public function __construct(?string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = [])
public function __construct(string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = [])
{
if (null === $locale) {
@trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED);
}
$this->setLocale($locale, false);
$this->setLocale($locale);
if (null === $formatter) {
$formatter = new MessageFormatter();
@@ -117,7 +114,7 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
*
* @param string $format The name of the loader (@see addResource())
*/
public function addLoader($format, LoaderInterface $loader)
public function addLoader(string $format, LoaderInterface $loader)
{
$this->loaders[$format] = $loader;
}
@@ -127,21 +124,15 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
*
* @param string $format The name of the loader (@see addLoader())
* @param mixed $resource The resource name
* @param string $locale The locale
* @param string $domain The domain
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
public function addResource($format, $resource, $locale, $domain = null)
public function addResource(string $format, $resource, string $locale, string $domain = null)
{
if (null === $domain) {
$domain = 'messages';
}
if (null === $locale) {
@trigger_error(sprintf('Passing "null" to the third argument of the "%s" method has been deprecated since Symfony 4.4 and will throw an error in 5.0.', __METHOD__), \E_USER_DEPRECATED);
}
$this->assertValidLocale($locale);
$locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en';
@@ -157,12 +148,8 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function setLocale(string $locale)
{
if (null === $locale && (2 > \func_num_args() || func_get_arg(1))) {
@trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED);
}
$this->assertValidLocale($locale);
$this->locale = $locale;
}
@@ -178,6 +165,8 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
/**
* Sets the fallback locales.
*
* @param string[] $locales
*
* @throws InvalidArgumentException If a locale contains invalid characters
*/
public function setFallbackLocales(array $locales)
@@ -186,9 +175,6 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
$this->catalogues = [];
foreach ($locales as $locale) {
if (null === $locale) {
@trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED);
}
$this->assertValidLocale($locale);
}
@@ -198,11 +184,9 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
/**
* Gets the fallback locales.
*
* @internal since Symfony 4.2
*
* @return array The fallback locales
* @internal
*/
public function getFallbackLocales()
public function getFallbackLocales(): array
{
return $this->fallbackLocales;
}
@@ -210,9 +194,9 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
{
if ('' === $id = (string) $id) {
if (null === $id || '' === $id) {
return '';
}
@@ -231,7 +215,11 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
}
}
if ($this->hasIntlFormatter && $catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
$len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX);
if ($this->hasIntlFormatter
&& ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)
|| (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len)))
) {
return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters);
}
@@ -240,47 +228,8 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
/**
* {@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 ('' === $id = (string) $id) {
return '';
}
if (!$this->formatter instanceof ChoiceMessageFormatterInterface) {
throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter)));
}
if (null === $domain) {
$domain = 'messages';
}
$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, ['%count%' => $number] + $parameters);
}
return $this->formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters);
}
/**
* {@inheritdoc}
*/
public function getCatalogue($locale = null)
public function getCatalogue(string $locale = null)
{
if (!$locale) {
$locale = $this->getLocale();
@@ -295,20 +244,25 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
return $this->catalogues[$locale];
}
/**
* {@inheritdoc}
*/
public function getCatalogues(): array
{
return array_values($this->catalogues);
}
/**
* Gets the loaders.
*
* @return array LoaderInterface[]
* @return LoaderInterface[]
*/
protected function getLoaders()
{
return $this->loaders;
}
/**
* @param string $locale
*/
protected function loadCatalogue($locale)
protected function loadCatalogue(string $locale)
{
if (null === $this->cacheDir) {
$this->initializeCatalogue($locale);
@@ -317,10 +271,7 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran
}
}
/**
* @param string $locale
*/
protected function initializeCatalogue($locale)
protected function initializeCatalogue(string $locale)
{
$this->assertValidLocale($locale);
@@ -456,7 +407,7 @@ EOF
}
}
protected function computeFallbackLocales($locale)
protected function computeFallbackLocales(string $locale)
{
if (null === $this->parentLocales) {
$this->parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true);
@@ -502,13 +453,11 @@ EOF
/**
* Asserts that the locale is valid, throws an Exception if not.
*
* @param string $locale Locale to tests
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
protected function assertValidLocale($locale)
protected function assertValidLocale(string $locale)
{
if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', (string) $locale)) {
if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) {
throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
}
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Catalogue\AbstractOperation;
use Symfony\Component\Translation\Catalogue\TargetOperation;
final class TranslatorBag implements TranslatorBagInterface
{
/** @var MessageCatalogue[] */
private $catalogues = [];
public function addCatalogue(MessageCatalogue $catalogue): void
{
if (null !== $existingCatalogue = $this->getCatalogue($catalogue->getLocale())) {
$catalogue->addCatalogue($existingCatalogue);
}
$this->catalogues[$catalogue->getLocale()] = $catalogue;
}
public function addBag(TranslatorBagInterface $bag): void
{
foreach ($bag->getCatalogues() as $catalogue) {
$this->addCatalogue($catalogue);
}
}
/**
* {@inheritdoc}
*/
public function getCatalogue(string $locale = null): MessageCatalogueInterface
{
if (null === $locale || !isset($this->catalogues[$locale])) {
$this->catalogues[$locale] = new MessageCatalogue($locale);
}
return $this->catalogues[$locale];
}
/**
* {@inheritdoc}
*/
public function getCatalogues(): array
{
return array_values($this->catalogues);
}
public function diff(TranslatorBagInterface $diffBag): self
{
$diff = new self();
foreach ($this->catalogues as $locale => $catalogue) {
if (null === $diffCatalogue = $diffBag->getCatalogue($locale)) {
$diff->addCatalogue($catalogue);
continue;
}
$operation = new TargetOperation($diffCatalogue, $catalogue);
$operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::NEW_BATCH);
$newCatalogue = new MessageCatalogue($locale);
foreach ($operation->getDomains() as $domain) {
$newCatalogue->add($operation->getNewMessages($domain), $domain);
}
$diff->addCatalogue($newCatalogue);
}
return $diff;
}
public function intersect(TranslatorBagInterface $intersectBag): self
{
$diff = new self();
foreach ($this->catalogues as $locale => $catalogue) {
if (null === $intersectCatalogue = $intersectBag->getCatalogue($locale)) {
continue;
}
$operation = new TargetOperation($catalogue, $intersectCatalogue);
$operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::OBSOLETE_BATCH);
$obsoleteCatalogue = new MessageCatalogue($locale);
foreach ($operation->getDomains() as $domain) {
$obsoleteCatalogue->add(
array_diff($operation->getMessages($domain), $operation->getNewMessages($domain)),
$domain
);
}
$diff->addCatalogue($obsoleteCatalogue);
}
return $diff;
}
}

View File

@@ -16,6 +16,8 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException;
/**
* TranslatorBagInterface.
*
* @method MessageCatalogueInterface[] getCatalogues() Returns all catalogues of the instance
*
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
interface TranslatorBagInterface
@@ -29,5 +31,5 @@ interface TranslatorBagInterface
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
public function getCatalogue($locale = null);
public function getCatalogue(string $locale = null);
}

View File

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

View File

@@ -31,7 +31,7 @@ class ArrayConverter
*
* @param array $messages Linear messages array
*
* @return array Tree-like messages array
* @return array
*/
public static function expandToTree(array $messages)
{
@@ -84,7 +84,7 @@ class ArrayConverter
return $elem;
}
private static function cancelExpand(array &$tree, $prefix, array $node)
private static function cancelExpand(array &$tree, string $prefix, array $node)
{
$prefix .= '.';

View File

@@ -23,34 +23,19 @@ use Symfony\Component\Translation\MessageCatalogue;
*/
class TranslationWriter implements TranslationWriterInterface
{
/**
* @var array<string, DumperInterface>
*/
private $dumpers = [];
/**
* Adds a dumper to the writer.
*
* @param string $format The format of the dumper
*/
public function addDumper($format, DumperInterface $dumper)
public function addDumper(string $format, DumperInterface $dumper)
{
$this->dumpers[$format] = $dumper;
}
/**
* 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);
}
}
}
/**
* Obtains the list of supported formats.
*
@@ -69,7 +54,7 @@ class TranslationWriter implements TranslationWriterInterface
*
* @throws InvalidArgumentException
*/
public function write(MessageCatalogue $catalogue, $format, $options = [])
public function write(MessageCatalogue $catalogue, string $format, array $options = [])
{
if (!isset($this->dumpers[$format])) {
throw new InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format));

View File

@@ -29,5 +29,5 @@ interface TranslationWriterInterface
*
* @throws InvalidArgumentException
*/
public function write(MessageCatalogue $catalogue, $format, $options = []);
public function write(MessageCatalogue $catalogue, string $format, array $options = []);
}

View File

@@ -16,30 +16,35 @@
}
],
"require": {
"php": ">=7.1.3",
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "^1.16",
"symfony/translation-contracts": "^1.1.6|^2"
"symfony/translation-contracts": "^2.3"
},
"require-dev": {
"symfony/config": "^3.4|^4.0|^5.0",
"symfony/console": "^3.4|^4.0|^5.0",
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
"symfony/http-kernel": "^4.4",
"symfony/intl": "^3.4|^4.0|^5.0",
"symfony/service-contracts": "^1.1.2|^2",
"symfony/yaml": "^3.4|^4.0|^5.0",
"symfony/finder": "~2.8|~3.0|~4.0|^5.0",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/console": "^5.4|^6.0",
"symfony/dependency-injection": "^5.0|^6.0",
"symfony/http-client-contracts": "^1.1|^2.0|^3.0",
"symfony/http-kernel": "^5.0|^6.0",
"symfony/intl": "^4.4|^5.0|^6.0",
"symfony/polyfill-intl-icu": "^1.21",
"symfony/service-contracts": "^1.1.2|^2|^3",
"symfony/yaml": "^4.4|^5.0|^6.0",
"symfony/finder": "^4.4|^5.0|^6.0",
"psr/log": "^1|^2|^3"
},
"conflict": {
"symfony/config": "<3.4",
"symfony/dependency-injection": "<3.4",
"symfony/http-kernel": "<4.4",
"symfony/yaml": "<3.4"
"symfony/config": "<4.4",
"symfony/dependency-injection": "<5.0",
"symfony/http-kernel": "<5.0",
"symfony/twig-bundle": "<5.0",
"symfony/yaml": "<4.4",
"symfony/console": "<5.3"
},
"provide": {
"symfony/translation-implementation": "1.0|2.0"
"symfony/translation-implementation": "2.3"
},
"suggest": {
"symfony/config": "",
@@ -47,6 +52,7 @@
"psr/log-implementation": "To use logging capability in translator"
},
"autoload": {
"files": [ "Resources/functions.php" ],
"psr-4": { "Symfony\\Component\\Translation\\": "" },
"exclude-from-classmap": [
"/Tests/"