mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-30 13:13:24 +00:00
[Web] Update composer libraries
This commit is contained in:
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
|
||||
---
|
||||
|
||||
|
||||
+21
-19
@@ -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' => [],
|
||||
|
||||
+2
-6
@@ -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;
|
||||
}
|
||||
+129
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
-32
@@ -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;
|
||||
}
|
||||
|
||||
+28
-33
@@ -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'];
|
||||
|
||||
+12
-5
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)) {
|
||||
|
||||
+1
-4
@@ -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;
|
||||
|
||||
Vendored
+2
-10
@@ -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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -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;
|
||||
|
||||
+3
-6
@@ -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
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+6
-4
@@ -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);
|
||||
|
||||
+3
-3
@@ -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.'.');
|
||||
|
||||
+3
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
-141
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+4
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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];
|
||||
|
||||
Vendored
+4
-7
@@ -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
|
||||
|
||||
+28
-7
@@ -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
@@ -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;
|
||||
|
||||
+1
-3
@@ -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;
|
||||
}
|
||||
+21
-21
@@ -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();
|
||||
}
|
||||
}
|
||||
+79
@@ -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);
|
||||
}
|
||||
}
|
||||
+7
-75
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
+45
-29
@@ -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
|
||||
|
||||
+1
-3
@@ -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
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user