[Web] Update composer libraries

This commit is contained in:
FreddleSpl0it committed 2026-10-28 11:56:29 +01:00
1 parent 9cc8ac2908
commit bd8c784118
1297 files changed
+71585 -24460

No files matched your search

@@ -1,6 +1,43 @@
CHANGELOG
=========
7.4
---
* Make the extractor alias optional
* Deprecate `TranslatableMessage::__toString`
* Add `Symfony\Component\Translation\StaticMessage`
7.3
---
* Add `Translator::addGlobalParameter()` to allow defining global translation parameters
7.2
---
* Deprecate `ProviderFactoryTestCase`, extend `AbstractProviderFactoryTestCase` instead
The `testIncompleteDsnException()` test is no longer provided by default. If you make use of it by implementing the `incompleteDsnProvider()` data providers,
you now need to use the `IncompleteDsnTestTrait`.
* Make `ProviderFactoryTestCase` and `ProviderTestCase` compatible with PHPUnit 10+
* Add `lint:translations` command
* Deprecate passing an escape character to `CsvFileLoader::setCsvControl()`
* Make Xliff 2.0 attributes in segment element available as `segment-attributes`
metadata returned by `XliffFileLoader` and make `XliffFileDumper` write them to the file
7.1
---
* Mark class `DataCollectorTranslator` as `final`
7.0
---
* Remove `PhpStringTokenParser`
* Remove `PhpExtractor` in favor of `PhpAstExtractor`
6.4
---
@@ -30,9 +30,7 @@ abstract class AbstractOperation implements OperationInterface
public const NEW_BATCH = 'new';
public const ALL_BATCH = 'all';
protected $source;
protected $target;
protected $result;
protected MessageCatalogue $result;
/**
* This array stores 'all', 'new' and 'obsolete' messages for all valid domains.
@@ -55,21 +53,21 @@ abstract class AbstractOperation implements OperationInterface
*
* @var array The array that stores 'all', 'new' and 'obsolete' messages
*/
protected $messages;
protected array $messages;
private array $domains;
/**
* @throws LogicException
*/
public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target)
{
public function __construct(
protected MessageCatalogueInterface $source,
protected MessageCatalogueInterface $target,
) {
if ($source->getLocale() !== $target->getLocale()) {
throw new LogicException('Operated catalogues must belong to the same locale.');
}
$this->source = $source;
$this->target = $target;
$this->result = new MessageCatalogue($source->getLocale());
$this->messages = [];
}
@@ -96,8 +94,8 @@ abstract class AbstractOperation implements OperationInterface
public function getMessages(string $domain): array
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
if (!\in_array($domain, $this->getDomains(), true)) {
throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::ALL_BATCH])) {
@@ -109,8 +107,8 @@ abstract class AbstractOperation implements OperationInterface
public function getNewMessages(string $domain): array
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
if (!\in_array($domain, $this->getDomains(), true)) {
throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::NEW_BATCH])) {
@@ -122,8 +120,8 @@ abstract class AbstractOperation implements OperationInterface
public function getObsoleteMessages(string $domain): array
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
if (!\in_array($domain, $this->getDomains(), true)) {
throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) {
@@ -149,7 +147,7 @@ abstract class AbstractOperation implements OperationInterface
*/
public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void
{
// If MessageFormatter class does not exists, intl domains are not supported.
// If MessageFormatter class does not exist, intl domains are not supported.
if (!class_exists(\MessageFormatter::class)) {
return;
}
@@ -160,7 +158,7 @@ abstract class AbstractOperation implements OperationInterface
self::OBSOLETE_BATCH => $this->getObsoleteMessages($domain),
self::NEW_BATCH => $this->getNewMessages($domain),
self::ALL_BATCH => $this->getMessages($domain),
default => throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH)),
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))) {
@@ -172,6 +170,12 @@ abstract class AbstractOperation implements OperationInterface
$currentMessages = array_diff_key($messages, $result->all($domain));
$result->replace($currentMessages, $domain);
$result->replace($allIntlMessages + $messages, $intlDomain);
foreach ($result->getCatalogueMetadata('', $domain) ?? [] as $key => $value) {
if (null === $this->result->getCatalogueMetadata($key, $intlDomain)) {
$result->setCatalogueMetadata($key, $value, $intlDomain);
}
}
}
}
@@ -180,8 +184,6 @@ abstract class AbstractOperation implements OperationInterface
* stores the results.
*
* @param string $domain The domain which the operation will be performed for
*
* @return void
*/
abstract protected function processDomain(string $domain);
abstract protected function processDomain(string $domain): void;
}
@@ -24,10 +24,7 @@ use Symfony\Component\Translation\MessageCatalogueInterface;
*/
class MergeOperation extends AbstractOperation
{
/**
* @return void
*/
protected function processDomain(string $domain)
protected function processDomain(string $domain): void
{
$this->messages[$domain] = [
'all' => [],
@@ -25,10 +25,7 @@ use Symfony\Component\Translation\MessageCatalogueInterface;
*/
class TargetOperation extends AbstractOperation
{
/**
* @return void
*/
protected function processDomain(string $domain)
protected function processDomain(string $domain): void
{
$this->messages[$domain] = [
'all' => [],
@@ -31,18 +31,14 @@ interface CatalogueMetadataAwareInterface
/**
* Adds catalogue metadata to a message domain.
*
* @return void
*/
public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages');
public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages'): void;
/**
* Deletes catalogue metadata for the given key and domain.
*
* Passing an empty domain will delete all catalogue metadata. Passing an empty key will
* delete all metadata for the given domain.
*
* @return void
*/
public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages');
public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages'): void;
}
@@ -0,0 +1,129 @@
<?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\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Exception\ExceptionInterface;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Lint translations files syntax and outputs encountered errors.
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
#[AsCommand(name: 'lint:translations', description: 'Lint translations files syntax and outputs encountered errors')]
class TranslationLintCommand extends Command
{
private SymfonyStyle $io;
public function __construct(
private TranslatorInterface&TranslatorBagInterface $translator,
private array $enabledLocales = [],
) {
parent::__construct();
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('locale')) {
$suggestions->suggestValues($this->enabledLocales);
}
}
protected function configure(): void
{
$this
->setDefinition([
new InputOption('locale', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to lint.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command lint translations.
<info>php %command.full_name%</>
EOF
);
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$locales = $input->getOption('locale');
/** @var array<string, array<string, array<string, \Throwable>>> $errors */
$errors = [];
$domainsByLocales = [];
foreach ($locales as $locale) {
$messageCatalogue = $this->translator->getCatalogue($locale);
foreach ($domainsByLocales[$locale] = $messageCatalogue->getDomains() as $domain) {
foreach ($messageCatalogue->all($domain) as $id => $translation) {
try {
$this->translator->trans($id, [], $domain, $messageCatalogue->getLocale());
} catch (ExceptionInterface $e) {
$errors[$locale][$domain][$id] = $e;
}
}
}
}
if (!$domainsByLocales) {
$this->io->error('No translation files were found.');
return Command::SUCCESS;
}
$this->io->table(
['Locale', 'Domains', 'Valid?'],
array_map(
static fn (string $locale, array $domains) => [
$locale,
implode(', ', $domains),
!\array_key_exists($locale, $errors) ? '<info>Yes</>' : '<error>No</>',
],
array_keys($domainsByLocales),
$domainsByLocales
),
);
if ($errors) {
foreach ($errors as $locale => $domains) {
foreach ($domains as $domain => $domainsErrors) {
$this->io->section(\sprintf('Errors for locale "%s" and domain "%s"', $locale, $domain));
foreach ($domainsErrors as $id => $error) {
$this->io->text(\sprintf('Translation key "%s" is invalid:', $id));
$this->io->error($error->getMessage());
}
}
}
return Command::FAILURE;
}
$this->io->success('All translations are valid.');
return Command::SUCCESS;
}
}
@@ -34,22 +34,14 @@ final class TranslationPullCommand extends Command
{
use TranslationTrait;
private TranslationProviderCollection $providerCollection;
private TranslationWriterInterface $writer;
private TranslationReaderInterface $reader;
private string $defaultLocale;
private array $transPaths;
private array $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;
public function __construct(
private TranslationProviderCollection $providerCollection,
private TranslationWriterInterface $writer,
private TranslationReaderInterface $reader,
private string $defaultLocale,
private array $transPaths = [],
private array $enabledLocales = [],
) {
parent::__construct();
}
@@ -92,27 +84,28 @@ final class TranslationPullCommand extends Command
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'),
new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format.', 'xlf12'),
new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pulls translations from the given provider. Only
new translations are pulled, existing ones are not overwritten.
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:
You can overwrite existing translations (and remove the missing ones on local side) by using the <info>--force</> flag:
<info>php %command.full_name% --force provider</>
<info>php %command.full_name% --force provider</>
Full example:
Full example:
<info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</>
<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
This command pulls all translations associated with the <info>messages</> and <info>validators</> domains for the <info>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
)
;
}
@@ -163,7 +156,7 @@ EOF
$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)));
$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;
}
@@ -177,7 +170,7 @@ EOF
$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)));
$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;
}
@@ -34,18 +34,12 @@ final class TranslationPushCommand extends Command
{
use TranslationTrait;
private TranslationProviderCollection $providers;
private TranslationReaderInterface $reader;
private array $transPaths;
private array $enabledLocales;
public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = [])
{
$this->providers = $providers;
$this->reader = $reader;
$this->transPaths = $transPaths;
$this->enabledLocales = $enabledLocales;
public function __construct(
private TranslationProviderCollection $providers,
private TranslationReaderInterface $reader,
private array $transPaths = [],
private array $enabledLocales = [],
) {
parent::__construct();
}
@@ -60,7 +54,7 @@ final class TranslationPushCommand extends Command
if ($input->mustSuggestOptionValuesFor('domains')) {
$provider = $this->providers->get($input->getArgument('provider'));
if ($provider && method_exists($provider, 'getDomains')) {
if (method_exists($provider, 'getDomains')) {
$domains = $provider->getDomains();
$suggestions->suggestValues($domains);
}
@@ -83,29 +77,30 @@ final class TranslationPushCommand extends Command
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),
new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
new InputOption('locales', null, InputOption::VALUE_REQUIRED | 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.
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:
You can overwrite existing translations by using the <info>--force</> flag:
<info>php %command.full_name% --force provider</>
<info>php %command.full_name% --force provider</>
You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
You can delete provider translations which are not present locally by using the <info>--delete-missing</> flag:
<info>php %command.full_name% --delete-missing provider</>
<info>php %command.full_name% --delete-missing provider</>
Full example:
Full example:
<info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</>
<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
This command pushes all translations associated with the <info>messages</> and <info>validators</> domains for the <info>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
)
;
}
@@ -115,7 +110,7 @@ EOF
$provider = $this->providers->get($input->getArgument('provider'));
if (!$this->enabledLocales) {
throw new InvalidArgumentException(sprintf('You must define "framework.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME)));
throw new InvalidArgumentException(\sprintf('You must define "framework.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);
@@ -139,7 +134,7 @@ EOF
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)));
$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;
}
@@ -149,7 +144,7 @@ EOF
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)));
$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.
@@ -164,7 +159,7 @@ EOF
$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)));
$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;
}
@@ -174,9 +169,9 @@ EOF
$domains = [];
foreach ($translatorBag->getCatalogues() as $catalogue) {
$domains += $catalogue->getDomains();
$domains = array_merge($domains, $catalogue->getDomains());
}
return array_unique($domains);
return array_values(array_unique($domains));
}
}
@@ -39,43 +39,45 @@ class XliffLintCommand extends Command
private bool $displayCorrectFiles;
private ?\Closure $directoryIteratorProvider;
private ?\Closure $isReadableProvider;
private bool $requireStrictFileNames;
public function __construct(?string $name = null, ?callable $directoryIteratorProvider = null, ?callable $isReadableProvider = null, bool $requireStrictFileNames = true)
{
public function __construct(
?string $name = null,
?callable $directoryIteratorProvider = null,
?callable $isReadableProvider = null,
private bool $requireStrictFileNames = true,
) {
parent::__construct($name);
$this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...);
$this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...);
$this->requireStrictFileNames = $requireStrictFileNames;
}
/**
* @return void
*/
protected function configure()
protected function configure(): void
{
$this
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())))
->addOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())))
->setHelp(<<<EOF
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
You can validates XLIFF contents passed from STDIN:
You can validates XLIFF contents passed from STDIN:
<info>cat filename | php %command.full_name% -</info>
<info>cat filename | php %command.full_name% -</info>
You can also validate the syntax of a file:
You can also validate the syntax of a file:
<info>php %command.full_name% filename</info>
<info>php %command.full_name% filename</info>
Or of a whole directory:
Or of a whole directory:
<info>php %command.full_name% dirname</info>
<info>php %command.full_name% dirname --format=json</info>
<info>php %command.full_name% dirname</info>
EOF
The <info>--format</info> option specifies the format of the command output:
<info>php %command.full_name% dirname --format=json</info>
EOF
)
;
}
@@ -98,7 +100,7 @@ EOF
$filesInfo = [];
foreach ($filenames as $filename) {
if (!$this->isReadable($filename)) {
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
throw new RuntimeException(\sprintf('File or directory "%s" is not readable.', $filename));
}
foreach ($this->getFiles($filename) as $file) {
@@ -124,18 +126,18 @@ EOF
$document->loadXML($content);
if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) {
$normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/'));
$normalizedLocalePattern = \sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/'));
// strict file names require translation files to be named '____.locale.xlf'
// otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed
// also, the regexp matching must be case-insensitive, as defined for 'target-language' values
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
$expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
$expectedFilenamePattern = $this->requireStrictFileNames ? \sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : \sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
if (0 === preg_match($expectedFilenamePattern, basename($file))) {
$errors[] = [
'line' => -1,
'column' => -1,
'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
'message' => \sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
];
}
}
@@ -151,7 +153,7 @@ EOF
libxml_clear_errors();
libxml_use_internal_errors($internal);
return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors];
return ['file' => $file, 'valid' => !$errors, 'messages' => $errors];
}
private function display(SymfonyStyle $io, array $files): int
@@ -160,7 +162,7 @@ EOF
'txt' => $this->displayTxt($io, $files),
'json' => $this->displayJson($io, $files),
'github' => $this->displayTxt($io, $files, true),
default => throw new InvalidArgumentException(sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
default => throw new InvalidArgumentException(\sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
};
}
@@ -172,25 +174,25 @@ EOF
foreach ($filesInfo as $info) {
if ($info['valid'] && $this->displayCorrectFiles) {
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
$io->comment('<info>OK</info>'.($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
} elseif (!$info['valid']) {
++$erroredFiles;
$io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
$io->listing(array_map(function ($error) use ($info, $githubReporter) {
$io->text('<error> ERROR </error>'.($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
$io->listing(array_map(static function ($error) use ($info, $githubReporter) {
// general document errors have a '-1' line number
$line = -1 === $error['line'] ? null : $error['line'];
$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']);
return null === $line ? $error['message'] : \sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']);
}, $info['messages']));
}
}
if (0 === $erroredFiles) {
$io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles));
$io->success(\sprintf('All %d XLIFF files contain valid syntax.', $countFiles));
} else {
$io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
$io->warning(\sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
}
return min($erroredFiles, 1);
@@ -200,7 +202,7 @@ EOF
{
$errors = 0;
array_walk($filesInfo, function (&$v) use (&$errors) {
array_walk($filesInfo, static function (&$v) use (&$errors) {
$v['file'] = (string) $v['file'];
if (!$v['valid']) {
++$errors;
@@ -224,7 +226,7 @@ EOF
}
foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) {
if (!\in_array($file->getExtension(), ['xlf', 'xliff'], true)) {
continue;
}
@@ -237,7 +239,7 @@ EOF
*/
private function getDirectoryIterator(string $directory): iterable
{
$default = fn ($directory) => new \RecursiveIteratorIterator(
$default = static fn ($directory) => new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
@@ -251,7 +253,7 @@ EOF
private function isReadable(string $fileOrDirectory): bool
{
$default = fn ($fileOrDirectory) => is_readable($fileOrDirectory);
$default = static fn ($fileOrDirectory) => is_readable($fileOrDirectory);
if (null !== $this->isReadableProvider) {
return ($this->isReadableProvider)($fileOrDirectory, $default);
@@ -278,6 +280,7 @@ EOF
}
}
/** @return string[] */
private function getAvailableFormatOptions(): array
{
return ['txt', 'json', 'github'];
@@ -25,11 +25,9 @@ use Symfony\Component\VarDumper\Cloner\Data;
*/
class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface
{
private DataCollectorTranslator $translator;
public function __construct(DataCollectorTranslator $translator)
{
$this->translator = $translator;
public function __construct(
private DataCollectorTranslator $translator,
) {
}
public function lateCollect(): void
@@ -46,6 +44,7 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
{
$this->data['locale'] = $this->translator->getLocale();
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
$this->data['global_parameters'] = $this->translator->getGlobalParameters();
}
public function reset(): void
@@ -86,6 +85,14 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto
return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : [];
}
/**
* @internal
*/
public function getGlobalParameters(): Data|array
{
return $this->data['global_parameters'] ?? [];
}
public function getName(): string
{
return 'translation';
@@ -12,32 +12,31 @@
namespace Symfony\Component\Translation;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Contracts\Service\ResetInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @final since Symfony 7.1
*/
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface, ResetInterface
{
public const MESSAGE_DEFINED = 0;
public const MESSAGE_MISSING = 1;
public const MESSAGE_EQUALS_FALLBACK = 2;
private TranslatorInterface $translator;
private array $messages = [];
/**
* @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator)));
}
public function __construct(
private TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator,
) {
}
$this->translator = $translator;
public function reset(): void
{
$this->messages = [];
}
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
@@ -48,10 +47,7 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
return $trans;
}
/**
* @return void
*/
public function setLocale(string $locale)
public function setLocale(string $locale): void
{
$this->translator->setLocale($locale);
}
@@ -74,7 +70,7 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
if ($this->translator instanceof WarmableInterface) {
return (array) $this->translator->warmUp($cacheDir, $buildDir);
return $this->translator->warmUp($cacheDir, $buildDir);
}
return [];
@@ -92,10 +88,16 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
return [];
}
/**
* @return mixed
*/
public function __call(string $method, array $args)
public function getGlobalParameters(): array
{
if ($this->translator instanceof Translator || method_exists($this->translator, 'getGlobalParameters')) {
return $this->translator->getGlobalParameters();
}
return [];
}
public function __call(string $method, array $args): mixed
{
return $this->translator->{$method}(...$args);
}
@@ -37,7 +37,7 @@ class LoggingTranslatorPass implements CompilerPassInterface
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $translatorAlias));
throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $translatorAlias));
}
if (!$r->isSubclassOf(TranslatorInterface::class) || !$r->isSubclassOf(TranslatorBagInterface::class)) {
@@ -20,10 +20,7 @@ use Symfony\Component\DependencyInjection\Reference;
*/
class TranslationDumperPass implements CompilerPassInterface
{
/**
* @return void
*/
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translation.writer')) {
return;
@@ -13,7 +13,6 @@ namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
/**
@@ -21,10 +20,7 @@ use Symfony\Component\DependencyInjection\Reference;
*/
class TranslationExtractorPass implements CompilerPassInterface
{
/**
* @return void
*/
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translation.extractor')) {
return;
@@ -33,11 +29,7 @@ class TranslationExtractorPass implements CompilerPassInterface
$definition = $container->getDefinition('translation.extractor');
foreach ($container->findTaggedServiceIds('translation.extractor', true) as $id => $attributes) {
if (!isset($attributes[0]['alias'])) {
throw new RuntimeException(sprintf('The alias for the tag "translation.extractor" of service "%s" must be set.', $id));
}
$definition->addMethodCall('addExtractor', [$attributes[0]['alias'], new Reference($id)]);
$definition->addMethodCall('addExtractor', [$attributes[0]['alias'] ?? $id, new Reference($id)]);
}
}
}
@@ -18,10 +18,7 @@ use Symfony\Component\DependencyInjection\Reference;
class TranslatorPass implements CompilerPassInterface
{
/**
* @return void
*/
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translator.default')) {
return;
@@ -42,10 +42,7 @@ class TranslatorPathsPass extends AbstractRecursivePass
*/
private array $controllers = [];
/**
* @return void
*/
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translator')) {
return;
@@ -96,10 +93,10 @@ class TranslatorPathsPass extends AbstractRecursivePass
$class = $this->definitions[$i]->getClass();
if (ServiceLocator::class === $class) {
if (!isset($this->controllers[$this->currentId])) {
if (!isset($this->controllers[$this->currentId ?? ''])) {
continue;
}
foreach ($this->controllers[$this->currentId] as $class => $_) {
foreach ($this->controllers[$this->currentId ?? ''] as $class => $_) {
$this->paths[$class] = true;
}
} else {
@@ -28,7 +28,7 @@ class CsvFileDumper extends FileDumper
$handle = fopen('php://memory', 'r+');
foreach ($messages->all($domain) as $source => $target) {
fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure);
fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure, '\\');
}
rewind($handle);
@@ -40,10 +40,8 @@ class CsvFileDumper extends FileDumper
/**
* Sets the delimiter and escape character for CSV.
*
* @return void
*/
public function setCsvControl(string $delimiter = ';', string $enclosure = '"')
public function setCsvControl(string $delimiter = ';', string $enclosure = '"'): void
{
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
@@ -25,8 +25,6 @@ interface DumperInterface
* Dumps the message catalogue.
*
* @param array $options Options that are used by the dumper
*
* @return void
*/
public function dump(MessageCatalogue $messages, array $options = []);
public function dump(MessageCatalogue $messages, array $options = []): void;
}
@@ -27,25 +27,18 @@ abstract class FileDumper implements DumperInterface
{
/**
* A template for the relative paths to files.
*
* @var string
*/
protected $relativePathTemplate = '%domain%.%locale%.%extension%';
protected string $relativePathTemplate = '%domain%.%locale%.%extension%';
/**
* Sets the template for the relative paths to files.
*
* @return void
*/
public function setRelativePathTemplate(string $relativePathTemplate)
public function setRelativePathTemplate(string $relativePathTemplate): void
{
$this->relativePathTemplate = $relativePathTemplate;
}
/**
* @return void
*/
public function dump(MessageCatalogue $messages, array $options = [])
public function dump(MessageCatalogue $messages, array $options = []): void
{
if (!\array_key_exists('path', $options)) {
throw new InvalidArgumentException('The file dumper needs a path option.');
@@ -56,8 +49,8 @@ abstract class FileDumper implements DumperInterface
$fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale());
if (!file_exists($fullpath)) {
$directory = \dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory));
if (!file_exists($directory) && !@mkdir($directory, 0o777, true)) {
throw new RuntimeException(\sprintf('Unable to create directory "%s".', $directory));
}
}
@@ -20,7 +20,7 @@ use Symfony\Component\Translation\MessageCatalogue;
*/
class IcuResFileDumper extends FileDumper
{
protected $relativePathTemplate = '%domain%/%locale%.%extension%';
protected string $relativePathTemplate = '%domain%/%locale%.%extension%';
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
{
@@ -54,14 +54,11 @@ class MoFileDumper extends FileDumper
.$this->writeLong($offset[2] + $sourcesStart + $sourcesSize);
}
$output = implode('', array_map($this->writeLong(...), $header))
return implode('', array_map($this->writeLong(...), $header))
.$sourceOffsets
.$targetOffsets
.$sources
.$targets
;
return $output;
.$targets;
}
protected function getExtension(): string
@@ -30,6 +30,8 @@ class PoFileDumper extends FileDumper
$output .= "\n";
$newLine = false;
$isIntlDomain = str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX);
foreach ($messages->all($domain) as $source => $target) {
if ($newLine) {
$output .= "\n";
@@ -48,17 +50,18 @@ class PoFileDumper extends FileDumper
$output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':');
}
$sourceRules = $this->getStandardRules($source);
$targetRules = $this->getStandardRules($target);
if (2 == \count($sourceRules) && [] !== $targetRules) {
$output .= sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0]));
$output .= sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1]));
// in an ICU domain the pipe is an ordinary character, pluralization is expressed by the message itself
$sourceRules = $isIntlDomain ? [] : $this->getStandardRules($source);
$targetRules = $isIntlDomain ? [] : $this->getStandardRules($target);
if (2 == \count($sourceRules) && $targetRules) {
$output .= \sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0]));
$output .= \sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1]));
foreach ($targetRules as $i => $targetRule) {
$output .= sprintf('msgstr[%d] "%s"'."\n", $i, $this->escape($targetRule));
$output .= \sprintf('msgstr[%d] "%s"'."\n", $i, $this->escape($targetRule));
}
} else {
$output .= sprintf('msgid "%s"'."\n", $this->escape($source));
$output .= sprintf('msgstr "%s"'."\n", $this->escape($target));
$output .= \sprintf('msgid "%s"'."\n", $this->escape($source));
$output .= \sprintf('msgstr "%s"'."\n", $this->escape($target));
}
}
@@ -76,22 +79,22 @@ class PoFileDumper extends FileDumper
}
$intervalRegexp = <<<'EOF'
/^(?P<interval>
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
/^(?P<interval>
({\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>[\[\]])
)\s*(?P<message>.*?)$/xs
EOF;
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
)\s*(?P<message>.*?)$/xs
EOF;
$standardRules = [];
foreach ($parts as $part) {
@@ -100,9 +103,9 @@ EOF;
if (preg_match($intervalRegexp, $part)) {
// Explicit rule is not a standard rule.
return [];
} else {
$standardRules[] = $part;
}
$standardRules[] = $part;
}
return $standardRules;
@@ -123,7 +126,7 @@ EOF;
$output = null;
foreach ((array) $comments as $comment) {
$output .= sprintf('#%s %s'."\n", $prefix, $comment);
$output .= \sprintf('#%s %s'."\n", $prefix, $comment);
}
return $output;
@@ -46,7 +46,7 @@ class XliffFileDumper extends FileDumper
return $this->dumpXliff2($defaultLocale, $messages, $domain);
}
throw new InvalidArgumentException(sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion));
throw new InvalidArgumentException(\sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion));
}
protected function getExtension(): string
@@ -93,7 +93,7 @@ class XliffFileDumper extends FileDumper
foreach ($messages->all($domain) as $source => $target) {
$translation = $dom->createElement('trans-unit');
$translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._'));
$translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._'));
$translation->setAttribute('resname', $source);
$s = $translation->appendChild($dom->createElement('source'));
@@ -156,10 +156,11 @@ class XliffFileDumper extends FileDumper
}
if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) {
$xliff->setAttribute('xmlns:m', 'urn:oasis:names:tc:xliff:metadata:2.0');
$xliffMetadata = $xliffFile->appendChild($dom->createElement('m:metadata'));
$xliff->setAttribute('xmlns:mda', 'urn:oasis:names:tc:xliff:metadata:2.0');
$xliffMetadata = $xliffFile->appendChild($dom->createElement('mda:metadata'));
$xliffMetaGroup = $xliffMetadata->appendChild($dom->createElement('mda:metaGroup'));
foreach ($catalogueMetadata as $key => $value) {
$xliffMeta = $xliffMetadata->appendChild($dom->createElement('prop'));
$xliffMeta = $xliffMetaGroup->appendChild($dom->createElement('mda:meta'));
$xliffMeta->setAttribute('type', $key);
$xliffMeta->appendChild($dom->createTextNode($value));
}
@@ -167,7 +168,7 @@ class XliffFileDumper extends FileDumper
foreach ($messages->all($domain) as $source => $target) {
$translation = $dom->createElement('unit');
$translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._'));
$translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._'));
if (\strlen($source) <= 80) {
$translation->setAttribute('name', $source);
@@ -176,7 +177,7 @@ class XliffFileDumper extends FileDumper
$metadata = $messages->getMetadata($source, $domain);
// Add notes section
if ($this->hasMetadataArrayInfo('notes', $metadata)) {
if ($this->hasMetadataArrayInfo('notes', $metadata) && $metadata['notes']) {
$notesElement = $dom->createElement('notes');
foreach ($metadata['notes'] as $note) {
$n = $dom->createElement('note');
@@ -193,6 +194,12 @@ class XliffFileDumper extends FileDumper
$segment = $translation->appendChild($dom->createElement('segment'));
if ($this->hasMetadataArrayInfo('segment-attributes', $metadata)) {
foreach ($metadata['segment-attributes'] as $name => $value) {
$segment->setAttribute($name, $value);
}
}
$s = $segment->appendChild($dom->createElement('source'));
$s->appendChild($dom->createTextNode($source));
@@ -23,11 +23,9 @@ use Symfony\Component\Yaml\Yaml;
*/
class YamlFileDumper extends FileDumper
{
private string $extension;
public function __construct(string $extension = 'yml')
{
$this->extension = $extension;
public function __construct(
private string $extension = 'yml',
) {
}
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
@@ -16,7 +16,7 @@ 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;
$message = \sprintf('Invalid "%s" provider DSN: ', $dsn).$message;
}
parent::__construct($message, 0, $previous);
@@ -18,7 +18,7 @@ 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);
$message = \sprintf('The option "%s" is required but missing.', $option);
parent::__construct($message, $dsn, $previous);
}
@@ -18,12 +18,14 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
*/
class ProviderException extends RuntimeException implements ProviderExceptionInterface
{
private ResponseInterface $response;
private string $debug;
public function __construct(string $message, ResponseInterface $response, int $code = 0, ?\Exception $previous = null)
{
$this->response = $response;
public function __construct(
string $message,
private ResponseInterface $response,
int $code = 0,
?\Exception $previous = null,
) {
$this->debug = $response->getInfo('debug') ?? '';
parent::__construct($message, $code, $previous);
@@ -43,14 +43,14 @@ class UnsupportedSchemeException extends LogicException
}
$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']));
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());
$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));
$message .= \sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported));
}
parent::__construct($message.'.');
@@ -49,19 +49,13 @@ abstract class AbstractFileExtractor
protected function isFile(string $file): bool
{
if (!is_file($file)) {
throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file));
throw new InvalidArgumentException(\sprintf('The "%s" file does not exist.', $file));
}
return true;
}
/**
* @return bool
*/
abstract protected function canBeExtracted(string $file);
abstract protected function canBeExtracted(string $file): bool;
/**
* @return iterable
*/
abstract protected function extractFromDirectory(string|array $resource);
abstract protected function extractFromDirectory(string|array $resource): iterable;
}
@@ -29,28 +29,20 @@ class ChainExtractor implements ExtractorInterface
/**
* Adds a loader to the translation extractor.
*
* @return void
*/
public function addExtractor(string $format, ExtractorInterface $extractor)
public function addExtractor(string $format, ExtractorInterface $extractor): void
{
$this->extractors[$format] = $extractor;
}
/**
* @return void
*/
public function setPrefix(string $prefix)
public function setPrefix(string $prefix): void
{
foreach ($this->extractors as $extractor) {
$extractor->setPrefix($prefix);
}
}
/**
* @return void
*/
public function extract(string|iterable $directory, MessageCatalogue $catalogue)
public function extract(string|iterable $directory, MessageCatalogue $catalogue): void
{
foreach ($this->extractors as $extractor) {
$extractor->extract($directory, $catalogue);
@@ -36,7 +36,7 @@ final class PhpAstExtractor extends AbstractFileExtractor implements ExtractorIn
private string $prefix = '',
) {
if (!class_exists(ParserFactory::class)) {
throw new \LogicException(sprintf('You cannot use "%s" as the "nikic/php-parser" package is not installed. Try running "composer require nikic/php-parser".', static::class));
throw new \LogicException(\sprintf('You cannot use "%s" as the "nikic/php-parser" package is not installed. Try running "composer require nikic/php-parser".', static::class));
}
$this->parser = (new ParserFactory())->createForHostVersion();
@@ -51,7 +51,6 @@ final class PhpAstExtractor extends AbstractFileExtractor implements ExtractorIn
$nameResolver = new NodeVisitor\NameResolver();
$traverser->addVisitor($nameResolver);
/** @var AbstractVisitor&NodeVisitor $visitor */
foreach ($this->visitors as $visitor) {
$visitor->initialize($catalogue, $file, $this->prefix);
$traverser->addVisitor($visitor);
@@ -77,7 +76,7 @@ final class PhpAstExtractor extends AbstractFileExtractor implements ExtractorIn
protected function extractFromDirectory(array|string $resource): iterable|Finder
{
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));
throw new \LogicException(\sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class));
}
return (new Finder())->files()->name('*.php')->in($resource);
@@ -1,333 +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\Extractor;
trigger_deprecation('symfony/translation', '6.2', '"%s" is deprecated, use "%s" instead.', PhpExtractor::class, PhpAstExtractor::class);
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\MessageCatalogue;
/**
* PhpExtractor extracts translation messages from a PHP template.
*
* @author Michel Salib <michelsalib@hotmail.com>
*
* @deprecated since Symfony 6.2, use the PhpAstExtractor instead
*/
class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
{
public const MESSAGE_TOKEN = 300;
public const METHOD_ARGUMENTS_TOKEN = 1000;
public const DOMAIN_TOKEN = 1001;
/**
* Prefix for new found message.
*/
private string $prefix = '';
/**
* The sequence that captures translation messages.
*/
protected $sequences = [
[
'->',
'trans',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
'->',
'trans',
'(',
self::MESSAGE_TOKEN,
],
[
'new',
'TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
'new',
'TranslatableMessage',
'(',
self::MESSAGE_TOKEN,
],
[
'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,
],
];
/**
* @return void
*/
public function extract(string|iterable $resource, MessageCatalogue $catalog)
{
$files = $this->extractFiles($resource);
foreach ($files as $file) {
$this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file);
gc_mem_caches();
}
}
/**
* @return void
*/
public function setPrefix(string $prefix)
{
$this->prefix = $prefix;
}
/**
* Normalizes a token.
*/
protected function normalizeToken(mixed $token): ?string
{
if (isset($token[1]) && 'b"' !== $token) {
return $token[1];
}
return $token;
}
/**
* Seeks to a non-whitespace token.
*/
private function seekToNextRelevantToken(\Iterator $tokenIterator): void
{
for (; $tokenIterator->valid(); $tokenIterator->next()) {
$t = $tokenIterator->current();
if (\T_WHITESPACE !== $t[0]) {
break;
}
}
}
private function skipMethodArgument(\Iterator $tokenIterator): void
{
$openBraces = 0;
for (; $tokenIterator->valid(); $tokenIterator->next()) {
$t = $tokenIterator->current();
if ('[' === $t[0] || '(' === $t[0]) {
++$openBraces;
}
if (']' === $t[0] || ')' === $t[0]) {
--$openBraces;
}
if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) {
break;
}
}
}
/**
* Extracts the message from the iterator while the tokens
* match allowed message tokens.
*/
private function getValue(\Iterator $tokenIterator): string
{
$message = '';
$docToken = '';
$docPart = '';
for (; $tokenIterator->valid(); $tokenIterator->next()) {
$t = $tokenIterator->current();
if ('.' === $t) {
// Concatenate with next token
continue;
}
if (!isset($t[1])) {
break;
}
switch ($t[0]) {
case \T_START_HEREDOC:
$docToken = $t[1];
break;
case \T_ENCAPSED_AND_WHITESPACE:
case \T_CONSTANT_ENCAPSED_STRING:
if ('' === $docToken) {
$message .= PhpStringTokenParser::parse($t[1]);
} else {
$docPart = $t[1];
}
break;
case \T_END_HEREDOC:
if ($indentation = strspn($t[1], ' ')) {
$docPartWithLineBreaks = $docPart;
$docPart = '';
foreach (preg_split('~(\r\n|\n|\r)~', $docPartWithLineBreaks, -1, \PREG_SPLIT_DELIM_CAPTURE) as $str) {
if (\in_array($str, ["\r\n", "\n", "\r"], true)) {
$docPart .= $str;
} else {
$docPart .= substr($str, $indentation);
}
}
}
$message .= PhpStringTokenParser::parseDocString($docToken, $docPart);
$docToken = '';
$docPart = '';
break;
case \T_WHITESPACE:
break;
default:
break 2;
}
}
return $message;
}
/**
* Extracts trans message from PHP tokens.
*
* @return void
*/
protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename)
{
$tokenIterator = new \ArrayIterator($tokens);
for ($key = 0; $key < $tokenIterator->count(); ++$key) {
foreach ($this->sequences as $sequence) {
$message = '';
$domain = 'messages';
$tokenIterator->seek($key);
foreach ($sequence as $sequenceKey => $item) {
$this->seekToNextRelevantToken($tokenIterator);
if ($this->normalizeToken($tokenIterator->current()) === $item) {
$tokenIterator->next();
continue;
} elseif (self::MESSAGE_TOKEN === $item) {
$message = $this->getValue($tokenIterator);
if (\count($sequence) === ($sequenceKey + 1)) {
break;
}
} elseif (self::METHOD_ARGUMENTS_TOKEN === $item) {
$this->skipMethodArgument($tokenIterator);
} elseif (self::DOMAIN_TOKEN === $item) {
$domainToken = $this->getValue($tokenIterator);
if ('' !== $domainToken) {
$domain = $domainToken;
}
break;
} else {
break;
}
}
if ($message) {
$catalog->set($message, $this->prefix.$message, $domain);
$metadata = $catalog->getMetadata($message, $domain) ?? [];
$normalizedFilename = preg_replace('{[\\\\/]+}', '/', $filename);
$metadata['sources'][] = $normalizedFilename.':'.$tokens[$key][2];
$catalog->setMetadata($message, $metadata, $domain);
break;
}
}
}
}
/**
* @throws \InvalidArgumentException
*/
protected function canBeExtracted(string $file): bool
{
return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION);
}
protected function extractFromDirectory(string|array $directory): iterable
{
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);
}
}
@@ -1,141 +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\Extractor;
trigger_deprecation('symfony/translation', '6.2', '"%s" is deprecated.', PhpStringTokenParser::class);
/*
* The following is derived from code at http://github.com/nikic/PHP-Parser
*
* Copyright (c) 2011 by Nikita Popov
*
* Some rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* * The names of the contributors may not be used to endorse or
* promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @deprecated since Symfony 6.2
*/
class PhpStringTokenParser
{
protected static $replacements = [
'\\' => '\\',
'$' => '$',
'n' => "\n",
'r' => "\r",
't' => "\t",
'f' => "\f",
'v' => "\v",
'e' => "\x1B",
];
/**
* Parses a string token.
*
* @param string $str String token content
*/
public static function parse(string $str): string
{
$bLength = 0;
if ('b' === $str[0]) {
$bLength = 1;
}
if ('\'' === $str[$bLength]) {
return str_replace(
['\\\\', '\\\''],
['\\', '\''],
substr($str, $bLength + 1, -1)
);
} else {
return self::parseEscapeSequences(substr($str, $bLength + 1, -1), '"');
}
}
/**
* Parses escape sequences in strings (all string types apart from single quoted).
*
* @param string $str String without quotes
* @param string|null $quote Quote type
*/
public static function parseEscapeSequences(string $str, ?string $quote = null): string
{
if (null !== $quote) {
$str = str_replace('\\'.$quote, $quote, $str);
}
return preg_replace_callback(
'~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3})~',
[__CLASS__, 'parseCallback'],
$str
);
}
private static function parseCallback(array $matches): string
{
$str = $matches[1];
if (isset(self::$replacements[$str])) {
return self::$replacements[$str];
} elseif ('x' === $str[0] || 'X' === $str[0]) {
return \chr(hexdec($str));
} else {
return \chr(octdec($str));
}
}
/**
* Parses a constant doc string.
*
* @param string $startToken Doc string start token content (<<<SMTHG)
* @param string $str String token content
*/
public static function parseDocString(string $startToken, string $str): string
{
// strip last newline (thanks tokenizer for sticking it into the string!)
$str = preg_replace('~(\r\n|\n|\r)$~', '', $str);
// nowdoc string
if (str_contains($startToken, '\'')) {
return $str;
}
return self::parseEscapeSequences($str, null);
}
}
@@ -22,7 +22,7 @@ use PhpParser\NodeVisitor;
final class ConstraintVisitor extends AbstractVisitor implements NodeVisitor
{
public function __construct(
private readonly array $constraintClassNames = []
private readonly array $constraintClassNames = [],
) {
}
@@ -78,7 +78,6 @@ final class ConstraintVisitor extends AbstractVisitor implements NodeVisitor
$messages = [];
$options = $arg->value;
/** @var Node\Expr\ArrayItem $item */
foreach ($options->items as $item) {
if (!$item->key instanceof Node\Scalar\String_) {
continue;
@@ -39,12 +39,12 @@ final class TransMethodVisitor extends AbstractVisitor implements NodeVisitor
return null;
}
$name = (string) $node->name;
$name = $node->name instanceof Node\Name ? $node->name->getLast() : (string) $node->name;
if ('trans' === $name || 't' === $name) {
$firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node);
if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'message')) {
if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'id')) {
return null;
}
@@ -13,6 +13,7 @@ namespace Symfony\Component\Translation\Extractor\Visitor;
use PhpParser\Node;
use PhpParser\NodeVisitor;
use Symfony\Component\Translation\TranslatableMessage;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
@@ -39,7 +40,9 @@ final class TranslatableMessageVisitor extends AbstractVisitor implements NodeVi
return null;
}
if (!\in_array('TranslatableMessage', $className->getParts(), true)) {
// the name resolver gives a fully qualified name; templates without a "use"
// statement resolve to the global namespace, which is accepted as well
if (!\in_array($className->toString(), [TranslatableMessage::class, 'TranslatableMessage'], true)) {
return null;
}
@@ -37,7 +37,7 @@ class IntlFormatter implements IntlFormatterInterface
try {
$this->cache[$locale][$message] = $formatter = new \MessageFormatter($locale, $message);
} catch (\IntlException $e) {
throw new InvalidArgumentException(sprintf('Invalid message format (error #%d): ', intl_get_error_code()).intl_get_error_message(), 0, $e);
throw new InvalidArgumentException(\sprintf('Invalid message format (error #%d): ', intl_get_error_code()).intl_get_error_message(), 0, $e);
}
}
@@ -49,7 +49,7 @@ class IntlFormatter implements IntlFormatterInterface
}
if (false === $message = $formatter->format($parameters)) {
throw new InvalidArgumentException(sprintf('Unable to format message (error #%s): ', $formatter->getErrorCode()).$formatter->getErrorMessage());
throw new InvalidArgumentException(\sprintf('Unable to format message (error #%s): ', $formatter->getErrorCode()).$formatter->getErrorMessage());
}
return $message;
@@ -22,29 +22,28 @@ class CsvFileLoader extends FileLoader
{
private string $delimiter = ';';
private string $enclosure = '"';
private string $escape = '\\';
/**
* @deprecated since Symfony 7.2, to be removed in 8.0
*/
private string $escape = '';
protected function loadResource(string $resource): array
{
$messages = [];
try {
$file = new \SplFileObject($resource, 'rb');
} catch (\RuntimeException $e) {
throw new NotFoundResourceException(sprintf('Error opening file "%s".', $resource), 0, $e);
if (!$file = @fopen($resource, 'r')) {
throw new NotFoundResourceException(\sprintf('Error opening file "%s".', $resource));
}
$file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY);
$file->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
foreach ($file as $data) {
if (false === $data) {
continue;
}
if (!str_starts_with($data[0], '#') && isset($data[1]) && 2 === \count($data)) {
$messages[$data[0]] = $data[1];
try {
while (false !== $data = fgetcsv($file, null, $this->delimiter, $this->enclosure, $this->escape)) {
// empty lines are read as [null]
if (isset($data[1]) && 2 === \count($data) && !str_starts_with($data[0], '#')) {
$messages[$data[0]] = $data[1];
}
}
} finally {
fclose($file);
}
return $messages;
@@ -52,13 +51,15 @@ class CsvFileLoader extends FileLoader
/**
* Sets the delimiter, enclosure, and escape character for CSV.
*
* @return void
*/
public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = '\\')
public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = ''): void
{
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
if ('' !== $escape) {
trigger_deprecation('symfony/translation', '7.2', 'The "escape" parameter of the "%s" method is deprecated. It will be removed in 8.0.', __METHOD__);
}
$this->escape = $escape;
}
}
@@ -24,11 +24,11 @@ abstract class FileLoader extends ArrayLoader
public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $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));
throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource));
}
$messages = $this->loadResource($resource);
@@ -38,7 +38,7 @@ abstract class FileLoader extends ArrayLoader
// not an array
if (!\is_array($messages)) {
throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource));
throw new InvalidResourceException(\sprintf('Unable to load file "%s".', $resource));
}
$catalogue = parent::load($messages, $locale, $domain);
@@ -26,11 +26,11 @@ class IcuDatFileLoader extends IcuResFileLoader
public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue
{
if (!stream_is_local($resource.'.dat')) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
throw new InvalidResourceException(\sprintf('This is not a local file "%s".', $resource));
}
if (!file_exists($resource.'.dat')) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource));
}
try {
@@ -40,7 +40,7 @@ class IcuDatFileLoader extends IcuResFileLoader
}
if (!$rb) {
throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource));
throw new InvalidResourceException(\sprintf('Cannot load resource "%s".', $resource));
} elseif (intl_is_failure($rb->getErrorCode())) {
throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
}
@@ -26,11 +26,11 @@ class IcuResFileLoader implements LoaderInterface
public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue
{
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
throw new InvalidResourceException(\sprintf('This is not a local file "%s".', $resource));
}
if (!is_dir($resource)) {
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource));
}
try {
@@ -40,7 +40,7 @@ class IcuResFileLoader implements LoaderInterface
}
if (!$rb) {
throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource));
throw new InvalidResourceException(\sprintf('Cannot load resource "%s".', $resource));
} elseif (intl_is_failure($rb->getErrorCode())) {
throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
}
@@ -55,6 +55,7 @@ class PoFileLoader extends FileLoader
* - No support for comments spanning multiple lines.
* - Translator and extracted comments are treated as being the same type.
* - Message IDs are allowed to have other encodings as just US-ASCII.
* - Contexts (msgctxt) are parsed but discarded.
*
* Items with an empty id are ignored.
*/
@@ -65,6 +66,7 @@ class PoFileLoader extends FileLoader
$defaults = [
'ids' => [],
'translated' => null,
'context' => null,
];
$messages = [];
@@ -76,23 +78,28 @@ class PoFileLoader extends FileLoader
if ('' === $line) {
// Whitespace indicated current item is done
if (!\in_array('fuzzy', $flags)) {
$this->addMessage($messages, $item);
}
$item = $defaults;
$flags = [];
$this->saveItem($messages, $item, $flags, $defaults);
} elseif (str_starts_with($line, '#,')) {
// flags belong to the next entry, so the previous one ends here
if (null !== $item['translated']) {
$this->saveItem($messages, $item, $flags, $defaults);
}
$flags = array_map('trim', explode(',', substr($line, 2)));
} elseif (str_starts_with($line, 'msgctxt "')) {
if (null !== $item['translated']) {
$this->saveItem($messages, $item, $flags, $defaults);
}
$item['context'] = substr($line, 9, -1);
} elseif (str_starts_with($line, 'msgid "')) {
// We start a new msg so save previous
// TODO: this fails when comments or contexts are added
$this->addMessage($messages, $item);
$item = $defaults;
if ($item['ids']) {
$this->saveItem($messages, $item, $flags, $defaults);
}
$item['ids']['singular'] = substr($line, 7, -1);
} elseif (str_starts_with($line, 'msgstr "')) {
$item['translated'] = substr($line, 8, -1);
} elseif ('"' === $line[0]) {
$continues = isset($item['translated']) ? 'translated' : 'ids';
$continues = isset($item['translated']) ? 'translated' : ($item['ids'] ? 'ids' : 'context');
if (\is_array($item[$continues])) {
end($item[$continues]);
@@ -108,14 +115,21 @@ class PoFileLoader extends FileLoader
}
}
// save last item
if (!\in_array('fuzzy', $flags)) {
$this->addMessage($messages, $item);
}
$this->saveItem($messages, $item, $flags, $defaults);
fclose($stream);
return $messages;
}
private function saveItem(array &$messages, array &$item, array &$flags, array $defaults): void
{
if (!\in_array('fuzzy', $flags, true)) {
$this->addMessage($messages, $item);
}
$item = $defaults;
$flags = [];
}
/**
* Save a translation item to the messages.
*
@@ -32,17 +32,17 @@ class QtFileLoader implements LoaderInterface
}
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $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));
throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource));
}
try {
$dom = XmlUtils::loadFile($resource);
} catch (\InvalidArgumentException $e) {
throw new InvalidResourceException(sprintf('Unable to load "%s".', $resource), $e->getCode(), $e);
throw new InvalidResourceException(\sprintf('Unable to load "%s".', $resource), $e->getCode(), $e);
}
$internalErrors = libxml_use_internal_errors(true);
@@ -57,7 +57,7 @@ class QtFileLoader implements LoaderInterface
foreach ($translations as $translation) {
$translationValue = (string) $translation->getElementsByTagName('translation')->item(0)->nodeValue;
if (!empty($translationValue)) {
if ($translationValue) {
$catalogue->set(
(string) $translation->getElementsByTagName('source')->item(0)->nodeValue,
$translationValue,
@@ -36,15 +36,15 @@ class XliffFileLoader implements LoaderInterface
if (!$this->isXmlString($resource)) {
if (!stream_is_local($resource)) {
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $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));
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));
throw new InvalidResourceException(\sprintf('This is neither a file nor an XLIFF string "%s".', $resource));
}
}
@@ -55,11 +55,11 @@ class XliffFileLoader implements LoaderInterface
$dom = XmlUtils::loadFile($resource);
}
} catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) {
throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $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));
throw new InvalidResourceException(\sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors));
}
$catalogue = new MessageCatalogue($locale);
@@ -112,16 +112,20 @@ class XliffFileLoader implements LoaderInterface
continue;
}
if (isset($translation->target) && 'needs-translation' === (string) $translation->target->attributes()['state']) {
$source = (string) (isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source);
if (isset($translation->target)
&& 'needs-translation' === (string) $translation->target->attributes()['state']
&& \in_array((string) $translation->target, [$source, (string) $translation->source], true)
) {
continue;
}
$source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source;
// If the xlf file has another encoding specified, try to convert it because
// simple_xml will always return utf-8 encoded values
$target = $this->utf8ToCharset((string) ($translation->target ?? $translation->source), $encoding);
$catalogue->set((string) $source, $target, $domain);
$catalogue->set($source, $target, $domain);
$metadata = [
'source' => (string) $translation->source,
@@ -144,7 +148,7 @@ class XliffFileLoader implements LoaderInterface
$metadata['id'] = (string) $attributes['id'];
}
$catalogue->setMetadata((string) $source, $metadata, $domain);
$catalogue->setMetadata($source, $metadata, $domain);
}
}
}
@@ -154,6 +158,12 @@ class XliffFileLoader implements LoaderInterface
$xml = simplexml_import_dom($dom);
$encoding = $dom->encoding ? strtoupper($dom->encoding) : null;
$xml->registerXPathNamespace('mda', 'urn:oasis:names:tc:xliff:metadata:2.0');
foreach ($xml->xpath('//mda:meta') as $meta) {
$catalogue->setCatalogueMetadata($meta->attributes()['type'] ?? '', (string) $meta, $domain);
}
$xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0');
foreach ($xml->xpath('//xliff:unit') as $unit) {
@@ -168,6 +178,13 @@ class XliffFileLoader implements LoaderInterface
$catalogue->set((string) $source, $target, $domain);
$metadata = [];
if ($segment->attributes()) {
$metadata['segment-attributes'] = [];
foreach ($segment->attributes() as $key => $value) {
$metadata['segment-attributes'][$key] = (string) $value;
}
}
if (isset($segment->target) && $segment->target->attributes()) {
$metadata['target-attributes'] = [];
foreach ($segment->target->attributes() as $key => $value) {
@@ -197,7 +214,7 @@ class XliffFileLoader implements LoaderInterface
*/
private function utf8ToCharset(string $content, ?string $encoding = null): string
{
if ('UTF-8' !== $encoding && !empty($encoding)) {
if ('UTF-8' !== $encoding && $encoding) {
return mb_convert_encoding($content, $encoding, 'UTF-8');
}
@@ -29,7 +29,7 @@ class YamlFileLoader extends FileLoader
protected function loadResource(string $resource): array
{
if (!isset($this->yamlParser)) {
if (!class_exists(\Symfony\Component\Yaml\Parser::class)) {
if (!class_exists(YamlParser::class)) {
throw new LogicException('Loading translations from the YAML format requires the Symfony Yaml component.');
}
@@ -39,11 +39,11 @@ class YamlFileLoader extends FileLoader
try {
$messages = $this->yamlParser->parseFile($resource, Yaml::PARSE_CONSTANT);
} catch (ParseException $e) {
throw new InvalidResourceException(sprintf('The file "%s" does not contain valid YAML: ', $resource).$e->getMessage(), 0, $e);
throw new InvalidResourceException(\sprintf('The file "%s" does not contain valid YAML: ', $resource).$e->getMessage(), 0, $e);
}
if (null !== $messages && !\is_array($messages)) {
throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource));
throw new InvalidResourceException(\sprintf('Unable to load file "%s".', $resource));
}
return $messages ?: [];
@@ -34,9 +34,14 @@ class LocaleSwitcher implements LocaleAwareInterface
public function setLocale(string $locale): void
{
if (class_exists(\Locale::class)) {
\Locale::setDefault($locale);
// Silently ignore if the intl extension is not loaded
try {
if (class_exists(\Locale::class, false)) {
\Locale::setDefault($locale);
}
} catch (\Exception) {
}
$this->locale = $locale;
$this->requestContext?->setParameter('_locale', $locale);
@@ -12,7 +12,6 @@
namespace Symfony\Component\Translation;
use Psr\Log\LoggerInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -21,20 +20,10 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
{
private TranslatorInterface $translator;
private LoggerInterface $logger;
/**
* @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator The translator must implement TranslatorBagInterface
*/
public function __construct(TranslatorInterface $translator, LoggerInterface $logger)
{
if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator)));
}
$this->translator = $translator;
$this->logger = $logger;
public function __construct(
private TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator,
private LoggerInterface $logger,
) {
}
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
@@ -45,10 +34,7 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
return $trans;
}
/**
* @return void
*/
public function setLocale(string $locale)
public function setLocale(string $locale): void
{
$prev = $this->translator->getLocale();
$this->translator->setLocale($locale);
@@ -56,7 +42,7 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
return;
}
$this->logger->debug(sprintf('The locale of the translator has changed from "%s" to "%s".', $prev, $locale));
$this->logger->debug(\sprintf('The locale of the translator has changed from "%s" to "%s".', $prev, $locale));
}
public function getLocale(): string
@@ -86,10 +72,7 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
return [];
}
/**
* @return mixed
*/
public function __call(string $method, array $args)
public function __call(string $method, array $args): mixed
{
return $this->translator->{$method}(...$args);
}
@@ -19,21 +19,19 @@ use Symfony\Component\Translation\Exception\LogicException;
*/
class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface, CatalogueMetadataAwareInterface
{
private array $messages = [];
private array $metadata = [];
private array $catalogueMetadata = [];
private array $resources = [];
private string $locale;
private ?MessageCatalogueInterface $fallbackCatalogue = null;
private ?self $parent = null;
/**
* @param array $messages An array of messages classified by domain
*/
public function __construct(string $locale, array $messages = [])
{
$this->locale = $locale;
$this->messages = $messages;
public function __construct(
private string $locale,
private array $messages = [],
) {
}
public function getLocale(): string
@@ -80,10 +78,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
return $allMessages;
}
/**
* @return void
*/
public function set(string $id, string $translation, string $domain = 'messages')
public function set(string $id, string $translation, string $domain = 'messages'): void
{
$this->add([$id => $translation], $domain);
}
@@ -123,20 +118,14 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
return $id;
}
/**
* @return void
*/
public function replace(array $messages, string $domain = 'messages')
public function replace(array $messages, string $domain = 'messages'): void
{
unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]);
$this->add($messages, $domain);
}
/**
* @return void
*/
public function add(array $messages, string $domain = 'messages')
public function add(array $messages, string $domain = 'messages'): void
{
$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) {
@@ -149,13 +138,10 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
}
}
/**
* @return void
*/
public function addCatalogue(MessageCatalogueInterface $catalogue)
public function addCatalogue(MessageCatalogueInterface $catalogue): void
{
if ($catalogue->getLocale() !== $this->locale) {
throw new LogicException(sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s".', $catalogue->getLocale(), $this->locale));
throw new LogicException(\sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s".', $catalogue->getLocale(), $this->locale));
}
foreach ($catalogue->all() as $domain => $messages) {
@@ -181,23 +167,20 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
}
}
/**
* @return void
*/
public function addFallbackCatalogue(MessageCatalogueInterface $catalogue)
public function addFallbackCatalogue(MessageCatalogueInterface $catalogue): void
{
// detect circular references
$c = $catalogue;
while ($c = $c->getFallbackCatalogue()) {
if ($c->getLocale() === $this->getLocale()) {
throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
throw new LogicException(\sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
}
}
$c = $this;
do {
if ($c->getLocale() === $catalogue->getLocale()) {
throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
throw new LogicException(\sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
}
foreach ($catalogue->getResources() as $resource) {
@@ -223,10 +206,7 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
return array_values($this->resources);
}
/**
* @return void
*/
public function addResource(ResourceInterface $resource)
public function addResource(ResourceInterface $resource): void
{
$this->resources[$resource->__toString()] = $resource;
}
@@ -237,6 +217,16 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
return $this->metadata;
}
if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX])) {
if ('' === $key) {
return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX];
}
if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key])) {
return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key];
}
}
if (isset($this->metadata[$domain])) {
if ('' == $key) {
return $this->metadata[$domain];
@@ -250,18 +240,12 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
return null;
}
/**
* @return void
*/
public function setMetadata(string $key, mixed $value, string $domain = 'messages')
public function setMetadata(string $key, mixed $value, string $domain = 'messages'): void
{
$this->metadata[$domain][$key] = $value;
}
/**
* @return void
*/
public function deleteMetadata(string $key = '', string $domain = 'messages')
public function deleteMetadata(string $key = '', string $domain = 'messages'): void
{
if ('' == $domain) {
$this->metadata = [];
@@ -291,18 +275,12 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
return null;
}
/**
* @return void
*/
public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages')
public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages'): void
{
$this->catalogueMetadata[$domain][$key] = $value;
}
/**
* @return void
*/
public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages')
public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages'): void
{
if (!$domain) {
$this->catalogueMetadata = [];
@@ -45,10 +45,8 @@ interface MessageCatalogueInterface
* @param string $id The message id
* @param string $translation The messages translation
* @param string $domain The domain name
*
* @return void
*/
public function set(string $id, string $translation, string $domain = 'messages');
public function set(string $id, string $translation, string $domain = 'messages'): void;
/**
* Checks if a message has a translation.
@@ -79,39 +77,31 @@ interface MessageCatalogueInterface
*
* @param array $messages An array of translations
* @param string $domain The domain name
*
* @return void
*/
public function replace(array $messages, string $domain = 'messages');
public function replace(array $messages, string $domain = 'messages'): void;
/**
* Adds translations for a given domain.
*
* @param array $messages An array of translations
* @param string $domain The domain name
*
* @return void
*/
public function add(array $messages, string $domain = 'messages');
public function add(array $messages, string $domain = 'messages'): void;
/**
* Merges translations from the given Catalogue into the current one.
*
* The two catalogues must have the same locale.
*
* @return void
*/
public function addCatalogue(self $catalogue);
public function addCatalogue(self $catalogue): void;
/**
* Merges translations from the given Catalogue into the current one
* only when the translation does not exist.
*
* This is used to provide default translations when they do not exist for the current locale.
*
* @return void
*/
public function addFallbackCatalogue(self $catalogue);
public function addFallbackCatalogue(self $catalogue): void;
/**
* Gets the fallback catalogue.
@@ -127,8 +117,6 @@ interface MessageCatalogueInterface
/**
* Adds a resource for this collection.
*
* @return void
*/
public function addResource(ResourceInterface $resource);
public function addResource(ResourceInterface $resource): void;
}
@@ -31,18 +31,14 @@ interface MetadataAwareInterface
/**
* Adds metadata to a message domain.
*
* @return void
*/
public function setMetadata(string $key, mixed $value, string $domain = 'messages');
public function setMetadata(string $key, mixed $value, string $domain = 'messages'): void;
/**
* 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.
*
* @return void
*/
public function deleteMetadata(string $key = '', string $domain = 'messages');
public function deleteMetadata(string $key = '', string $domain = 'messages'): void;
}
@@ -21,15 +21,11 @@ use Symfony\Component\Translation\TranslatorBagInterface;
*/
class FilteringProvider implements ProviderInterface
{
private ProviderInterface $provider;
private array $locales;
private array $domains;
public function __construct(ProviderInterface $provider, array $locales, array $domains = [])
{
$this->provider = $provider;
$this->locales = $locales;
$this->domains = $domains;
public function __construct(
private ProviderInterface $provider,
private array $locales,
private array $domains = [],
) {
}
public function __toString(): string
@@ -44,7 +44,7 @@ final class TranslationProviderCollection
public function get(string $name): ProviderInterface
{
if (!$this->has($name)) {
throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this));
throw new InvalidArgumentException(\sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this));
}
return $this->providers[$name];
@@ -18,16 +18,13 @@ use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
*/
class TranslationProviderCollectionFactory
{
private iterable $factories;
private array $enabledLocales;
/**
* @param iterable<mixed, ProviderFactoryInterface> $factories
*/
public function __construct(iterable $factories, array $enabledLocales)
{
$this->factories = $factories;
$this->enabledLocales = $enabledLocales;
public function __construct(
private iterable $factories,
private array $enabledLocales,
) {
}
public function fromConfig(array $config): TranslationProviderCollection
@@ -11,16 +11,16 @@
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Exception\LogicException;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* This translator should only be used in a development environment.
*/
final class PseudoLocalizationTranslator implements TranslatorInterface
final class PseudoLocalizationTranslator implements TranslatorInterface, TranslatorBagInterface
{
private const EXPANSION_CHARACTER = '~';
private TranslatorInterface $translator;
private bool $accents;
private float $expansionFactor;
private bool $brackets;
@@ -55,7 +55,7 @@ final class PseudoLocalizationTranslator implements TranslatorInterface
* * 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
* 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 or 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:
@@ -64,8 +64,10 @@ final class PseudoLocalizationTranslator implements TranslatorInterface
* 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 = [])
{
public function __construct(
private TranslatorInterface $translator,
array $options = [],
) {
$this->translator = $translator;
$this->accents = $options['accents'] ?? true;
@@ -114,6 +116,24 @@ final class PseudoLocalizationTranslator implements TranslatorInterface
return $this->translator->getLocale();
}
public function getCatalogue(?string $locale = null): MessageCatalogueInterface
{
if (!$this->translator instanceof TranslatorBagInterface) {
throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class));
}
return $this->translator->getCatalogue($locale);
}
public function getCatalogues(): array
{
if (!$this->translator instanceof TranslatorBagInterface) {
throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class));
}
return $this->translator->getCatalogues();
}
private function getParts(string $originalTrans): array
{
if (!$this->parseHTML) {
@@ -146,7 +166,6 @@ final class PseudoLocalizationTranslator implements TranslatorInterface
$parts[] = [false, false, '<'.$childNode->tagName];
/** @var \DOMAttr $attribute */
foreach ($childNode->attributes as $attribute) {
$parts[] = [false, false, ' '.$attribute->nodeName.'="'];
@@ -164,7 +183,7 @@ final class PseudoLocalizationTranslator implements TranslatorInterface
$parts[] = [false, false, '>'];
$parts = array_merge($parts, $this->parseNode($childNode, $parts));
$parts = array_merge($parts, $this->parseNode($childNode));
$parts[] = [false, false, '</'.$childNode->tagName.'>'];
}
@@ -363,3 +382,5 @@ final class PseudoLocalizationTranslator implements TranslatorInterface
return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding);
}
}
// @php-cs-fixer-ignore random_api_migration As logic is coupled with mt_srand() in tests
+4 -4
View File
@@ -6,8 +6,8 @@ The Translation component provides tools to internationalize your application.
Getting Started
---------------
```
$ composer require symfony/translation
```bash
composer require symfony/translation
```
```php
@@ -26,7 +26,7 @@ echo $translator->trans('Hello World!'); // outputs « Bonjour ! »
Sponsor
-------
Help Symfony by [sponsoring][1] its development!
Help Symfony by [sponsoring][3] its development!
Resources
---------
@@ -37,4 +37,4 @@ Resources
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/sponsor
[3]: https://symfony.com/sponsor
@@ -33,18 +33,13 @@ class TranslationReader implements TranslationReaderInterface
* Adds a loader to the translation extractor.
*
* @param string $format The format of the loader
*
* @return void
*/
public function addLoader(string $format, LoaderInterface $loader)
public function addLoader(string $format, LoaderInterface $loader): void
{
$this->loaders[$format] = $loader;
}
/**
* @return void
*/
public function read(string $directory, MessageCatalogue $catalogue)
public function read(string $directory, MessageCatalogue $catalogue): void
{
if (!is_dir($directory)) {
return;
@@ -22,8 +22,6 @@ interface TranslationReaderInterface
{
/**
* Reads translation messages from a directory to the catalogue.
*
* @return void
*/
public function read(string $directory, MessageCatalogue $catalogue);
public function read(string $directory, MessageCatalogue $catalogue): void;
}
@@ -15,27 +15,27 @@ if ('cli' !== \PHP_SAPI) {
$usageInstructions = <<<END
Usage instructions
-------------------------------------------------------------------------------
Usage instructions
-------------------------------------------------------------------------------
$ cd symfony-code-root-directory/
$ cd symfony-code-root-directory/
# show the translation status of all locales
$ php translation-status.php
# show the translation status of all locales
$ php translation-status.php
# only show the translation status of incomplete or erroneous locales
$ php translation-status.php --incomplete
# only show the translation status of incomplete or erroneous locales
$ php translation-status.php --incomplete
# show the translation status of all locales, all their missing translations and mismatches between trans-unit id and source
$ php translation-status.php -v
# show the translation status of all locales, all their missing translations and mismatches between trans-unit id and source
$ php translation-status.php -v
# show the status of a single locale
$ php translation-status.php fr
# show the status of a single locale
$ php translation-status.php fr
# show the status of a single locale, missing translations and mismatches between trans-unit id and source
$ php translation-status.php fr -v
# show the status of a single locale, missing translations and mismatches between trans-unit id and source
$ php translation-status.php fr -v
END;
END;
$config = [
// if TRUE, the full list of missing translations is displayed
@@ -87,8 +87,8 @@ foreach ($config['original_files'] as $originalFilePath) {
$translationFilePaths = findTranslationFiles($originalFilePath, $config['locale_to_analyze']);
$translationStatus = calculateTranslationStatus($originalFilePath, $translationFilePaths);
$totalMissingTranslations += array_sum(array_map(fn ($translation) => count($translation['missingKeys']), array_values($translationStatus)));
$totalTranslationMismatches += array_sum(array_map(fn ($translation) => count($translation['mismatches']), array_values($translationStatus)));
$totalMissingTranslations += array_sum(array_map(static fn ($translation) => count($translation['missingKeys']), array_values($translationStatus)));
$totalTranslationMismatches += array_sum(array_map(static fn ($translation) => count($translation['mismatches']), array_values($translationStatus)));
printTranslationStatus($originalFilePath, $translationStatus, $config['verbose_output'], $config['include_completed_languages']);
}
@@ -162,11 +162,11 @@ function extractLocaleFromFilePath($filePath)
function extractTranslationKeys($filePath): array
{
$translationKeys = [];
$contents = new \SimpleXMLElement(file_get_contents($filePath));
$contents = new SimpleXMLElement(file_get_contents($filePath));
foreach ($contents->file->body->{'trans-unit'} as $translationKey) {
$translationId = (string) $translationKey['id'];
$translationKey = (string) $translationKey->source;
$translationKey = (string) ($translationKey['resname'] ?? $translationKey->source);
$translationKeys[$translationId] = $translationKey;
}
@@ -218,7 +218,7 @@ function printTable($translations, $verboseOutput, bool $includeCompletedLanguag
if ($translation['translated'] > $translation['total']) {
textColorRed();
} elseif (count($translation['mismatches']) > 0) {
} elseif ($translation['mismatches']) {
textColorRed();
} elseif ($translation['is_completed']) {
textColorGreen();
@@ -235,7 +235,7 @@ function printTable($translations, $verboseOutput, bool $includeCompletedLanguag
textColorNormal();
$shouldBeClosed = false;
if (true === $verboseOutput && count($translation['missingKeys']) > 0) {
if ($verboseOutput && $translation['missingKeys']) {
echo '| Missing Translations:'.\PHP_EOL;
foreach ($translation['missingKeys'] as $id => $content) {
@@ -243,7 +243,7 @@ function printTable($translations, $verboseOutput, bool $includeCompletedLanguag
}
$shouldBeClosed = true;
}
if (true === $verboseOutput && count($translation['mismatches']) > 0) {
if ($verboseOutput && $translation['mismatches']) {
echo '| Mismatches between trans-unit id and source:'.\PHP_EOL;
foreach ($translation['mismatches'] as $id => $content) {
@@ -18,29 +18,37 @@
"en_CM": "en_001",
"en_CX": "en_001",
"en_CY": "en_001",
"en_CZ": "en_150",
"en_DE": "en_150",
"en_DG": "en_001",
"en_DK": "en_150",
"en_DM": "en_001",
"en_EE": "en_150",
"en_ER": "en_001",
"en_ES": "en_150",
"en_FI": "en_150",
"en_FJ": "en_001",
"en_FK": "en_001",
"en_FM": "en_001",
"en_FR": "en_150",
"en_GB": "en_001",
"en_GD": "en_001",
"en_GE": "en_150",
"en_GG": "en_001",
"en_GH": "en_001",
"en_GI": "en_001",
"en_GM": "en_001",
"en_GS": "en_001",
"en_GY": "en_001",
"en_HK": "en_001",
"en_HU": "en_150",
"en_ID": "en_001",
"en_IE": "en_001",
"en_IL": "en_001",
"en_IM": "en_001",
"en_IN": "en_001",
"en_IO": "en_001",
"en_IT": "en_150",
"en_JE": "en_001",
"en_JM": "en_001",
"en_KE": "en_001",
@@ -50,6 +58,8 @@
"en_LC": "en_001",
"en_LR": "en_001",
"en_LS": "en_001",
"en_LT": "en_150",
"en_LV": "en_150",
"en_MG": "en_001",
"en_MO": "en_001",
"en_MS": "en_001",
@@ -62,13 +72,17 @@
"en_NF": "en_001",
"en_NG": "en_001",
"en_NL": "en_150",
"en_NO": "en_150",
"en_NR": "en_001",
"en_NU": "en_001",
"en_NZ": "en_001",
"en_PG": "en_001",
"en_PK": "en_001",
"en_PL": "en_150",
"en_PN": "en_001",
"en_PT": "en_150",
"en_PW": "en_001",
"en_RO": "en_150",
"en_RW": "en_001",
"en_SB": "en_001",
"en_SC": "en_001",
@@ -77,6 +91,7 @@
"en_SG": "en_001",
"en_SH": "en_001",
"en_SI": "en_150",
"en_SK": "en_150",
"en_SL": "en_001",
"en_SS": "en_001",
"en_SX": "en_001",
@@ -87,6 +102,7 @@
"en_TT": "en_001",
"en_TV": "en_001",
"en_TZ": "en_001",
"en_UA": "en_150",
"en_UG": "en_001",
"en_VC": "en_001",
"en_VG": "en_001",
@@ -119,6 +135,7 @@
"es_VE": "es_419",
"ff_Adlm": "root",
"hi_Latn": "en_IN",
"kk_Arab": "root",
"ks_Deva": "root",
"nb": "no",
"nn": "no",
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class StaticMessage implements TranslatableInterface
{
public function __construct(
private string $message,
) {
}
public function getMessage(): string
{
return $this->message;
}
public function trans(TranslatorInterface $translator, ?string $locale = null): string
{
return $this->getMessage();
}
}
@@ -0,0 +1,79 @@
<?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\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
use Symfony\Component\Translation\Provider\Dsn;
use Symfony\Component\Translation\Provider\ProviderFactoryInterface;
abstract class AbstractProviderFactoryTestCase extends TestCase
{
abstract public function createFactory(): ProviderFactoryInterface;
/**
* @return iterable<array{0: bool, 1: string}>
*/
abstract public static function supportsProvider(): iterable;
/**
* @return iterable<array{0: string, 1: string}>
*/
abstract public static function createProvider(): iterable;
/**
* @return iterable<array{0: string, 1?: string|null}>
*/
abstract public static function unsupportedSchemeProvider(): iterable;
/**
* @dataProvider supportsProvider
*/
#[DataProvider('supportsProvider')]
public function testSupports(bool $expected, string $dsn)
{
$factory = $this->createFactory();
$this->assertSame($expected, $factory->supports(new Dsn($dsn)));
}
/**
* @dataProvider createProvider
*/
#[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
*/
#[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);
}
}
@@ -0,0 +1,42 @@
<?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\Attributes\DataProvider;
use Symfony\Component\Translation\Exception\IncompleteDsnException;
use Symfony\Component\Translation\Provider\Dsn;
trait IncompleteDsnTestTrait
{
/**
* @return iterable<array{0: string, 1?: string|null}>
*/
abstract public static function incompleteDsnProvider(): iterable;
/**
* @dataProvider incompleteDsnProvider
*/
#[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);
}
}
@@ -12,15 +12,10 @@
namespace Symfony\Component\Translation\Test;
use PHPUnit\Framework\MockObject\MockObject;
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\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -28,9 +23,13 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
* A test case to ease testing a translation provider factory.
*
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*
* @deprecated since Symfony 7.2, use AbstractProviderFactoryTestCase instead
*/
abstract class ProviderFactoryTestCase extends TestCase
abstract class ProviderFactoryTestCase extends AbstractProviderFactoryTestCase
{
use IncompleteDsnTestTrait;
protected HttpClientInterface $client;
protected LoggerInterface|MockObject $logger;
protected string $defaultLocale;
@@ -38,20 +37,8 @@ abstract class ProviderFactoryTestCase extends TestCase
protected XliffFileDumper|MockObject $xliffFileDumper;
protected TranslatorBagInterface|MockObject $translatorBag;
abstract public function createFactory(): ProviderFactoryInterface;
/**
* @return iterable<array{0: bool, 1: string}>
*/
abstract public static function supportsProvider(): iterable;
/**
* @return iterable<array{0: string, 1: string}>
*/
abstract public static function createProvider(): iterable;
/**
* @return iterable<array{0: string, 1: string|null}>
* @return iterable<array{0: string, 1?: string|null}>
*/
public static function unsupportedSchemeProvider(): iterable
{
@@ -59,68 +46,13 @@ abstract class ProviderFactoryTestCase extends TestCase
}
/**
* @return iterable<array{0: string, 1: string|null}>
* @return iterable<array{0: string, 1?: string|null}>
*/
public static 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 ??= new MockHttpClient();
@@ -11,13 +11,17 @@
namespace Symfony\Component\Translation\Test;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\Translation\Dumper\XliffFileDumper;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Provider\ProviderInterface;
use Symfony\Component\Translation\TranslatorBag;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -45,6 +49,7 @@ abstract class ProviderTestCase extends TestCase
/**
* @dataProvider toStringProvider
*/
#[DataProvider('toStringProvider')]
public function testToString(ProviderInterface $provider, string $expected)
{
$this->assertSame($expected, (string) $provider);
@@ -57,12 +62,12 @@ abstract class ProviderTestCase extends TestCase
protected function getLoader(): LoaderInterface
{
return $this->loader ??= $this->createMock(LoaderInterface::class);
return $this->loader ??= new ArrayLoader();
}
protected function getLogger(): LoggerInterface
{
return $this->logger ??= $this->createMock(LoggerInterface::class);
return $this->logger ??= new NullLogger();
}
protected function getDefaultLocale(): string
@@ -72,11 +77,11 @@ abstract class ProviderTestCase extends TestCase
protected function getXliffFileDumper(): XliffFileDumper
{
return $this->xliffFileDumper ??= $this->createMock(XliffFileDumper::class);
return $this->xliffFileDumper ??= new XliffFileDumper();
}
protected function getTranslatorBag(): TranslatorBagInterface
{
return $this->translatorBag ??= $this->createMock(TranslatorBagInterface::class);
return $this->translatorBag ??= new TranslatorBag();
}
}
@@ -19,19 +19,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class TranslatableMessage implements TranslatableInterface
{
private string $message;
private array $parameters;
private ?string $domain;
public function __construct(string $message, array $parameters = [], ?string $domain = null)
{
$this->message = $message;
$this->parameters = $parameters;
$this->domain = $domain;
public function __construct(
private string $message,
private array $parameters = [],
private ?string $domain = null,
) {
}
/**
* @deprecated since Symfony 7.4
*/
public function __toString(): string
{
trigger_deprecation('symfony/translation', '7.4', 'Method "%s()" is deprecated.', __METHOD__);
return $this->getMessage();
}
@@ -52,9 +53,13 @@ class TranslatableMessage implements TranslatableInterface
public function trans(TranslatorInterface $translator, ?string $locale = null): string
{
return $translator->trans($this->getMessage(), array_map(
static fn ($parameter) => $parameter instanceof TranslatableInterface ? $parameter->trans($translator, $locale) : $parameter,
$this->getParameters()
), $this->getDomain(), $locale);
$parameters = $this->getParameters();
foreach ($parameters as $k => $v) {
if ($v instanceof TranslatableInterface) {
$parameters[$k] = $v->trans($translator, $locale);
}
}
return $translator->trans($this->getMessage(), $parameters, $this->getDomain(), $locale);
}
}
+72 -63
View File
@@ -36,7 +36,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
/**
* @var MessageCatalogueInterface[]
*/
protected $catalogues = [];
protected array $catalogues = [];
private string $locale;
@@ -54,36 +54,39 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
private MessageFormatterInterface $formatter;
private ?string $cacheDir;
private bool $debug;
private array $cacheVary;
private ?ConfigCacheFactoryInterface $configCacheFactory;
private array $parentLocales;
private bool $hasIntlFormatter;
/**
* @var array<string, string|int|float|TranslatableInterface>
*/
private array $globalParameters = [];
/**
* @var array<string, string|int|float>
*/
private array $globalTranslatedParameters = [];
/**
* @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,
private ?string $cacheDir = null,
private bool $debug = false,
private array $cacheVary = [],
) {
$this->setLocale($locale);
$this->formatter = $formatter ??= new MessageFormatter();
$this->cacheDir = $cacheDir;
$this->debug = $debug;
$this->cacheVary = $cacheVary;
$this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface;
}
/**
* @return void
*/
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory): void
{
$this->configCacheFactory = $configCacheFactory;
}
@@ -92,10 +95,8 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
* Adds a Loader.
*
* @param string $format The name of the loader (@see addResource())
*
* @return void
*/
public function addLoader(string $format, LoaderInterface $loader)
public function addLoader(string $format, LoaderInterface $loader): void
{
$this->loaders[$format] = $loader;
}
@@ -106,11 +107,9 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
* @param string $format The name of the loader (@see addLoader())
* @param mixed $resource The resource name
*
* @return void
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
public function addResource(string $format, mixed $resource, string $locale, ?string $domain = null)
public function addResource(string $format, mixed $resource, string $locale, ?string $domain = null): void
{
$domain ??= 'messages';
@@ -119,17 +118,14 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
$this->resources[$locale][] = [$format, $resource, $domain];
if (\in_array($locale, $this->fallbackLocales)) {
if (\in_array($locale, $this->fallbackLocales, true)) {
$this->catalogues = [];
} else {
unset($this->catalogues[$locale]);
}
}
/**
* @return void
*/
public function setLocale(string $locale)
public function setLocale(string $locale): void
{
$this->assertValidLocale($locale);
$this->locale = $locale;
@@ -145,11 +141,9 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
*
* @param string[] $locales
*
* @return void
*
* @throws InvalidArgumentException If a locale contains invalid characters
*/
public function setFallbackLocales(array $locales)
public function setFallbackLocales(array $locales): void
{
// needed as the fallback locales are linked to the already loaded catalogues
$this->catalogues = [];
@@ -171,6 +165,17 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
return $this->fallbackLocales;
}
public function addGlobalParameter(string $id, string|int|float|TranslatableInterface $value): void
{
$this->globalParameters[$id] = $value;
$this->globalTranslatedParameters = [];
}
public function getGlobalParameters(): array
{
return $this->globalParameters;
}
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
if (null === $id || '' === $id) {
@@ -190,7 +195,24 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
}
}
$parameters = array_map(fn ($parameter) => $parameter instanceof TranslatableInterface ? $parameter->trans($this, $locale) : $parameter, $parameters);
foreach ($parameters as $key => $value) {
if ($value instanceof TranslatableInterface) {
$parameters[$key] = $value->trans($this, $locale);
}
}
if (null === $globalParameters = &$this->globalTranslatedParameters[$locale]) {
$globalParameters = $this->globalParameters;
foreach ($globalParameters as $key => $value) {
if ($value instanceof TranslatableInterface) {
$globalParameters[$key] = $value->trans($this, $locale);
}
}
}
if ($globalParameters) {
$parameters += $globalParameters;
}
$len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX);
if ($this->hasIntlFormatter
@@ -233,10 +255,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
return $this->loaders;
}
/**
* @return void
*/
protected function loadCatalogue(string $locale)
protected function loadCatalogue(string $locale): void
{
if (null === $this->cacheDir) {
$this->initializeCatalogue($locale);
@@ -245,10 +264,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
}
}
/**
* @return void
*/
protected function initializeCatalogue(string $locale)
protected function initializeCatalogue(string $locale): void
{
$this->assertValidLocale($locale);
@@ -290,18 +306,17 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
$this->initializeCatalogue($locale);
$fallbackContent = $this->getFallbackContent($this->catalogues[$locale]);
$content = sprintf(<<<EOF
<?php
$content = \sprintf(<<<EOF
<?php
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogue;
\$catalogue = new MessageCatalogue('%s', %s);
\$catalogue = new MessageCatalogue('%s', %s);
%s
return \$catalogue;
%s
return \$catalogue;
EOF
,
EOF,
$locale,
var_export($this->getAllMessages($this->catalogues[$locale]), true),
$fallbackContent
@@ -321,12 +336,11 @@ EOF
$fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback));
$currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current));
$fallbackContent .= sprintf(<<<'EOF'
$catalogue%s = new MessageCatalogue('%s', %s);
$catalogue%s->addFallbackCatalogue($catalogue%s);
$fallbackContent .= \sprintf(<<<'EOF'
$catalogue%s = new MessageCatalogue('%s', %s);
$catalogue%s->addFallbackCatalogue($catalogue%s);
EOF
,
EOF,
$fallbackSuffix,
$fallback,
var_export($this->getAllMessages($fallbackCatalogue), true),
@@ -342,7 +356,7 @@ EOF
private function getCatalogueCachePath(string $locale): string
{
return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php';
return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('xxh128', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php';
}
/**
@@ -356,10 +370,10 @@ EOF
foreach ($this->resources[$locale] as $resource) {
if (!isset($this->loaders[$resource[0]])) {
if (\is_string($resource[1])) {
throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1]));
throw new RuntimeException(\sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1]));
}
throw new RuntimeException(sprintf('No loader is registered for the "%s" format.', $resource[0]));
throw new RuntimeException(\sprintf('No loader is registered for the "%s" format.', $resource[0]));
}
$this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2]));
}
@@ -384,10 +398,7 @@ EOF
}
}
/**
* @return array
*/
protected function computeFallbackLocales(string $locale)
protected function computeFallbackLocales(string $locale): array
{
$this->parentLocales ??= json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true);
@@ -431,14 +442,12 @@ EOF
/**
* Asserts that the locale is valid, throws an Exception if not.
*
* @return void
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
protected function assertValidLocale(string $locale)
protected function assertValidLocale(string $locale): void
{
if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) {
throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
throw new InvalidArgumentException(\sprintf('Invalid "%s" locale.', $locale));
}
}
@@ -27,7 +27,7 @@ class ArrayConverter
{
/**
* Converts linear messages array to tree-like array.
* For example this array('foo.bar' => 'value') will be converted to ['foo' => ['bar' => 'value']].
* For example: ['foo.bar' => 'value'] will be converted to ['foo' => ['bar' => 'value']].
*
* @param array $messages Linear messages array
*/
@@ -31,7 +31,6 @@ class XliffUtils
*/
public static function getVersionNumber(\DOMDocument $dom): string
{
/** @var \DOMNode $xliff */
foreach ($dom->getElementsByTagName('xliff') as $xliff) {
$version = $xliff->attributes->getNamedItem('version');
if ($version) {
@@ -41,7 +40,7 @@ class XliffUtils
$namespace = $xliff->attributes->getNamedItem('xmlns');
if ($namespace) {
if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) {
throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s".', $namespace));
throw new InvalidArgumentException(\sprintf('Not a valid XLIFF namespace "%s".', $namespace));
}
return substr($namespace, 34);
@@ -83,6 +82,24 @@ class XliffUtils
return [];
}
public static function getErrorsAsString(array $xmlErrors): string
{
$errorsAsString = '';
foreach ($xmlErrors as $error) {
$errorsAsString .= \sprintf("[%s %s] %s (in %s - line %d, column %d)\n",
\LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR',
$error['code'],
$error['message'],
$error['file'],
$error['line'],
$error['column']
);
}
return $errorsAsString;
}
private static function shouldEnableEntityLoader(): bool
{
static $dom, $schema;
@@ -96,7 +113,7 @@ class XliffUtils
});
$schema = '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:include schemaLocation="file:///'.str_replace('\\', '/', $tmpfile).'" />
<xsd:include schemaLocation="'.self::getFileUrl($tmpfile).'" />
</xsd:schema>';
file_put_contents($tmpfile, '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
@@ -108,22 +125,17 @@ class XliffUtils
return !@$dom->schemaValidateSource($schema);
}
public static function getErrorsAsString(array $xmlErrors): string
private static function getFileUrl(string $path): string
{
$errorsAsString = '';
foreach ($xmlErrors as $error) {
$errorsAsString .= sprintf("[%s %s] %s (in %s - line %d, column %d)\n",
\LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR',
$error['code'],
$error['message'],
$error['file'],
$error['line'],
$error['column']
);
if ('\\' === \DIRECTORY_SEPARATOR) {
$parts = explode('/', str_replace('\\', '/', $path));
$drive = array_shift($parts).'/';
} else {
$parts = explode('/', $path);
$drive = '';
}
return $errorsAsString;
return 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
}
private static function getSchema(string $xliffVersion): string
@@ -135,7 +147,7 @@ class XliffUtils
$schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-2.0.xsd');
$xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd';
} else {
throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion));
throw new InvalidArgumentException(\sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion));
}
return self::fixXmlLocation($schemaSource, $xmlUri);
@@ -146,23 +158,27 @@ class XliffUtils
*/
private static function fixXmlLocation(string $schemaSource, string $xmlUri): string
{
$newPath = str_replace('\\', '/', __DIR__).'/../Resources/schemas/xml.xsd';
$parts = explode('/', $newPath);
$locationstart = 'file:///';
if (0 === stripos($newPath, 'phar://')) {
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
if ($tmpfile) {
copy($newPath, $tmpfile);
$parts = explode('/', str_replace('\\', '/', $tmpfile));
static $newPath;
if (null === $newPath) {
$path = __DIR__.'/../Resources/schemas/xml.xsd';
if (0 !== stripos($path, 'phar://')) {
$newPath = self::getFileUrl($path);
} elseif ($tmpfile = tempnam(sys_get_temp_dir(), 'symfony')) {
copy($path, $tmpfile);
register_shutdown_function(static function () use ($tmpfile) {
@unlink($tmpfile);
});
$newPath = self::getFileUrl($tmpfile);
} else {
$parts = explode('/', '\\' === \DIRECTORY_SEPARATOR ? str_replace('\\', '/', $path) : $path);
array_shift($parts);
$locationstart = 'phar:///';
$drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$newPath = 'phar:///'.$drive.implode('/', array_map('rawurlencode', $parts));
}
}
$drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts));
return str_replace($xmlUri, $newPath, $schemaSource);
}
@@ -30,10 +30,8 @@ class TranslationWriter implements TranslationWriterInterface
/**
* Adds a dumper to the writer.
*
* @return void
*/
public function addDumper(string $format, DumperInterface $dumper)
public function addDumper(string $format, DumperInterface $dumper): void
{
$this->dumpers[$format] = $dumper;
}
@@ -52,21 +50,19 @@ class TranslationWriter implements TranslationWriterInterface
* @param string $format The format to use to dump the messages
* @param array $options Options that are passed to the dumper
*
* @return void
*
* @throws InvalidArgumentException
*/
public function write(MessageCatalogue $catalogue, string $format, array $options = [])
public function write(MessageCatalogue $catalogue, string $format, array $options = []): void
{
if (!isset($this->dumpers[$format])) {
throw new InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format));
throw new InvalidArgumentException(\sprintf('There is no dumper associated with format "%s".', $format));
}
// get the right dumper
$dumper = $this->dumpers[$format];
if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0777, true) && !is_dir($options['path'])) {
throw new RuntimeException(sprintf('Translation Writer was not able to create directory "%s".', $options['path']));
if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0o777, true) && !is_dir($options['path'])) {
throw new RuntimeException(\sprintf('Translation Writer was not able to create directory "%s".', $options['path']));
}
// save
@@ -27,9 +27,7 @@ interface TranslationWriterInterface
* @param string $format The format to use to dump the messages
* @param array $options Options that are passed to the dumper
*
* @return void
*
* @throws InvalidArgumentException
*/
public function write(MessageCatalogue $catalogue, string $format, array $options = []);
public function write(MessageCatalogue $catalogue, string $format, array $options = []): void;
}
+19 -18
View File
@@ -16,35 +16,36 @@
}
],
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^2.5|^3.0"
"symfony/translation-contracts": "^2.5.3|^3.3",
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"nikic/php-parser": "^4.18|^5.0",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"nikic/php-parser": "^5.0",
"symfony/config": "^6.4|^7.0|^8.0",
"symfony/console": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^2.5|^3.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0",
"symfony/intl": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/intl": "^6.4|^7.0|^8.0",
"symfony/polyfill-intl-icu": "^1.21",
"symfony/routing": "^5.4|^6.0|^7.0",
"symfony/routing": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^5.4|^6.0|^7.0",
"symfony/finder": "^5.4|^6.0|^7.0",
"symfony/yaml": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3"
},
"conflict": {
"symfony/config": "<5.4",
"symfony/dependency-injection": "<5.4",
"nikic/php-parser": "<5.0",
"symfony/config": "<6.4",
"symfony/dependency-injection": "<6.4",
"symfony/http-client-contracts": "<2.5",
"symfony/http-kernel": "<5.4",
"symfony/http-kernel": "<6.4",
"symfony/service-contracts": "<2.5",
"symfony/twig-bundle": "<5.4",
"symfony/yaml": "<5.4",
"symfony/console": "<5.4"
"symfony/twig-bundle": "<6.4",
"symfony/yaml": "<6.4",
"symfony/console": "<6.4"
},
"provide": {
"symfony/translation-implementation": "2.3|3.0"