Added Translation Providers
Co-authored-by: Olivier Dolbeau <github@a.bbnt.me>
This commit is contained in:
parent
be384cf221
commit
6e55fa84b7
@ -44,6 +44,8 @@ FrameworkBundle
|
|||||||
* Deprecate the `KernelTestCase::$container` property, use `KernelTestCase::getContainer()` instead
|
* Deprecate the `KernelTestCase::$container` property, use `KernelTestCase::getContainer()` instead
|
||||||
* Rename the container parameter `profiler_listener.only_master_requests` to `profiler_listener.only_main_requests`
|
* Rename the container parameter `profiler_listener.only_master_requests` to `profiler_listener.only_main_requests`
|
||||||
* Deprecate registering workflow services as public
|
* Deprecate registering workflow services as public
|
||||||
|
* Deprecate option `--xliff-version` of the `translation:update` command, use e.g. `--format=xlf20` instead
|
||||||
|
* Deprecate option `--output-format` of the `translation:update` command, use e.g. `--format=xlf20` instead
|
||||||
|
|
||||||
HttpFoundation
|
HttpFoundation
|
||||||
--------------
|
--------------
|
||||||
|
@ -86,6 +86,8 @@ FrameworkBundle
|
|||||||
* Removed the `lock.RESOURCE_NAME` and `lock.RESOURCE_NAME.store` services and the `lock`, `LockInterface`, `lock.store` and `PersistingStoreInterface` aliases, use `lock.RESOURCE_NAME.factory`, `lock.factory` or `LockFactory` instead.
|
* Removed the `lock.RESOURCE_NAME` and `lock.RESOURCE_NAME.store` services and the `lock`, `LockInterface`, `lock.store` and `PersistingStoreInterface` aliases, use `lock.RESOURCE_NAME.factory`, `lock.factory` or `LockFactory` instead.
|
||||||
* Remove the `KernelTestCase::$container` property, use `KernelTestCase::getContainer()` instead
|
* Remove the `KernelTestCase::$container` property, use `KernelTestCase::getContainer()` instead
|
||||||
* Registered workflow services are now private
|
* Registered workflow services are now private
|
||||||
|
* Remove option `--xliff-version` of the `translation:update` command, use e.g. `--output-format=xlf20` instead
|
||||||
|
* Remove option `--output-format` of the `translation:update` command, use e.g. `--output-format=xlf20` instead
|
||||||
|
|
||||||
HttpFoundation
|
HttpFoundation
|
||||||
--------------
|
--------------
|
||||||
|
2
link
2
link
@ -41,7 +41,7 @@ if (!is_dir("$pathToProject/vendor/symfony")) {
|
|||||||
$sfPackages = array('symfony/symfony' => __DIR__);
|
$sfPackages = array('symfony/symfony' => __DIR__);
|
||||||
|
|
||||||
$filesystem = new Filesystem();
|
$filesystem = new Filesystem();
|
||||||
$braces = array('Bundle', 'Bridge', 'Component', 'Component/Security', 'Component/Mailer/Bridge', 'Component/Messenger/Bridge', 'Component/Notifier/Bridge', 'Contracts');
|
$braces = array('Bundle', 'Bridge', 'Component', 'Component/Security', 'Component/Mailer/Bridge', 'Component/Messenger/Bridge', 'Component/Notifier/Bridge', 'Contracts', 'Component/Translation/Bridge');
|
||||||
$directories = array_merge(...array_values(array_map(function ($part) {
|
$directories = array_merge(...array_values(array_map(function ($part) {
|
||||||
return glob(__DIR__.'/src/Symfony/'.$part.'/*', GLOB_ONLYDIR | GLOB_NOSORT);
|
return glob(__DIR__.'/src/Symfony/'.$part.'/*', GLOB_ONLYDIR | GLOB_NOSORT);
|
||||||
}, $braces)));
|
}, $braces)));
|
||||||
|
@ -20,6 +20,8 @@ CHANGELOG
|
|||||||
* Rename the container parameter `profiler_listener.only_master_requests` to `profiler_listener.only_main_requests`
|
* Rename the container parameter `profiler_listener.only_master_requests` to `profiler_listener.only_main_requests`
|
||||||
* Add service `fragment.uri_generator` to generate the URI of a fragment
|
* Add service `fragment.uri_generator` to generate the URI of a fragment
|
||||||
* Deprecate registering workflow services as public
|
* Deprecate registering workflow services as public
|
||||||
|
* Deprecate option `--xliff-version` of the `translation:update` command, use e.g. `--format=xlf20` instead
|
||||||
|
* Deprecate option `--output-format` of the `translation:update` command, use e.g. `--format=xlf20` instead
|
||||||
|
|
||||||
5.2.0
|
5.2.0
|
||||||
-----
|
-----
|
||||||
|
@ -77,12 +77,13 @@ class TranslationUpdateCommand extends Command
|
|||||||
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
|
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
|
||||||
new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
|
new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
|
||||||
new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'),
|
new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'),
|
||||||
new InputOption('output-format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf'),
|
new InputOption('output-format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format (deprecated)'),
|
||||||
|
new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'),
|
||||||
new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'),
|
new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'),
|
||||||
new InputOption('force', null, InputOption::VALUE_NONE, 'Should the update be done'),
|
new InputOption('force', null, InputOption::VALUE_NONE, 'Should the update be done'),
|
||||||
new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'),
|
new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'),
|
||||||
new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to update'),
|
new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to update'),
|
||||||
new InputOption('xliff-version', null, InputOption::VALUE_OPTIONAL, 'Override the default xliff version', '1.2'),
|
new InputOption('xliff-version', null, InputOption::VALUE_OPTIONAL, 'Override the default xliff version (deprecated)'),
|
||||||
new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically', 'asc'),
|
new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically', 'asc'),
|
||||||
new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
|
new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
|
||||||
])
|
])
|
||||||
@ -112,8 +113,8 @@ You can sort the output with the <comment>--sort</> flag:
|
|||||||
|
|
||||||
You can dump a tree-like structure using the yaml format with <comment>--as-tree</> flag:
|
You can dump a tree-like structure using the yaml format with <comment>--as-tree</> flag:
|
||||||
|
|
||||||
<info>php %command.full_name% --force --output-format=yaml --as-tree=3 en AcmeBundle</info>
|
<info>php %command.full_name% --force --format=yaml --as-tree=3 en AcmeBundle</info>
|
||||||
<info>php %command.full_name% --force --output-format=yaml --sort=asc --as-tree=3 fr</info>
|
<info>php %command.full_name% --force --format=yaml --sort=asc --as-tree=3 fr</info>
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
@ -135,13 +136,31 @@ EOF
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$format = $input->getOption('output-format') ?: $input->getOption('format');
|
||||||
|
$xliffVersion = $input->getOption('xliff-version') ?? '1.2';
|
||||||
|
|
||||||
|
if ($input->getOption('xliff-version')) {
|
||||||
|
trigger_deprecation('symfony/framework-bundle', '5.3', 'The "--xliff-version" option is deprecated, use "--format=xlf%d" instead.', 10 * $xliffVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input->getOption('output-format')) {
|
||||||
|
trigger_deprecation('symfony/framework-bundle', '5.3', 'The "--output-format" option is deprecated, use "--format=xlf%d" instead.', 10 * $xliffVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($format) {
|
||||||
|
case 'xlf20': $xliffVersion = '2.0';
|
||||||
|
// no break
|
||||||
|
case 'xlf12': $format = 'xlf';
|
||||||
|
}
|
||||||
|
|
||||||
// check format
|
// check format
|
||||||
$supportedFormats = $this->writer->getFormats();
|
$supportedFormats = $this->writer->getFormats();
|
||||||
if (!\in_array($input->getOption('output-format'), $supportedFormats, true)) {
|
if (!\in_array($format, $supportedFormats, true)) {
|
||||||
$errorIo->error(['Wrong output format', 'Supported formats are: '.implode(', ', $supportedFormats).'.']);
|
$errorIo->error(['Wrong output format', 'Supported formats are: '.implode(', ', $supportedFormats).', xlf12 and xlf20.']);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var KernelInterface $kernel */
|
/** @var KernelInterface $kernel */
|
||||||
$kernel = $this->getApplication()->getKernel();
|
$kernel = $this->getApplication()->getKernel();
|
||||||
|
|
||||||
@ -225,23 +244,7 @@ EOF
|
|||||||
|
|
||||||
$resultMessage = 'Translation files were successfully updated';
|
$resultMessage = 'Translation files were successfully updated';
|
||||||
|
|
||||||
// move new messages to intl domain when possible
|
$operation->moveMessagesToIntlDomainsIfPossible('new');
|
||||||
if (class_exists(\MessageFormatter::class)) {
|
|
||||||
foreach ($operation->getDomains() as $domain) {
|
|
||||||
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
|
||||||
$newMessages = $operation->getNewMessages($domain);
|
|
||||||
|
|
||||||
if ([] === $newMessages || ([] === $currentCatalogue->all($intlDomain) && [] !== $currentCatalogue->all($domain))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $operation->getResult();
|
|
||||||
$allIntlMessages = $result->all($intlDomain);
|
|
||||||
$currentMessages = array_diff_key($newMessages, $result->all($domain));
|
|
||||||
$result->replace($currentMessages, $domain);
|
|
||||||
$result->replace($allIntlMessages + $newMessages, $intlDomain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// show compiled list of messages
|
// show compiled list of messages
|
||||||
if (true === $input->getOption('dump-messages')) {
|
if (true === $input->getOption('dump-messages')) {
|
||||||
@ -284,8 +287,8 @@ EOF
|
|||||||
$extractedMessagesCount += $domainMessagesCount;
|
$extractedMessagesCount += $domainMessagesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('xlf' === $input->getOption('output-format')) {
|
if ('xlf' === $format) {
|
||||||
$io->comment(sprintf('Xliff output version is <info>%s</info>', $input->getOption('xliff-version')));
|
$io->comment(sprintf('Xliff output version is <info>%s</info>', $xliffVersion));
|
||||||
}
|
}
|
||||||
|
|
||||||
$resultMessage = sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was');
|
$resultMessage = sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was');
|
||||||
@ -306,7 +309,7 @@ EOF
|
|||||||
$bundleTransPath = end($transPaths);
|
$bundleTransPath = end($transPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->writer->write($operation->getResult(), $input->getOption('output-format'), ['path' => $bundleTransPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $input->getOption('xliff-version'), 'as_tree' => $input->getOption('as-tree'), 'inline' => $input->getOption('as-tree') ?? 0]);
|
$this->writer->write($operation->getResult(), $format, ['path' => $bundleTransPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $xliffVersion, 'as_tree' => $input->getOption('as-tree'), 'inline' => $input->getOption('as-tree') ?? 0]);
|
||||||
|
|
||||||
if (true === $input->getOption('dump-messages')) {
|
if (true === $input->getOption('dump-messages')) {
|
||||||
$resultMessage .= ' and translation files were updated';
|
$resultMessage .= ' and translation files were updated';
|
||||||
@ -335,11 +338,13 @@ EOF
|
|||||||
foreach ($catalogue->getResources() as $resource) {
|
foreach ($catalogue->getResources() as $resource) {
|
||||||
$filteredCatalogue->addResource($resource);
|
$filteredCatalogue->addResource($resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
|
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
|
||||||
foreach ($metadata as $k => $v) {
|
foreach ($metadata as $k => $v) {
|
||||||
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
|
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($metadata = $catalogue->getMetadata('', $domain)) {
|
if ($metadata = $catalogue->getMetadata('', $domain)) {
|
||||||
foreach ($metadata as $k => $v) {
|
foreach ($metadata as $k => $v) {
|
||||||
$filteredCatalogue->setMetadata($k, $v, $domain);
|
$filteredCatalogue->setMetadata($k, $v, $domain);
|
||||||
|
@ -85,6 +85,7 @@ class UnusedTagsPass implements CompilerPassInterface
|
|||||||
'translation.dumper',
|
'translation.dumper',
|
||||||
'translation.extractor',
|
'translation.extractor',
|
||||||
'translation.loader',
|
'translation.loader',
|
||||||
|
'translation.provider_factory',
|
||||||
'twig.extension',
|
'twig.extension',
|
||||||
'twig.loader',
|
'twig.loader',
|
||||||
'twig.runtime',
|
'twig.runtime',
|
||||||
|
@ -785,6 +785,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->fixXmlConfig('fallback')
|
->fixXmlConfig('fallback')
|
||||||
->fixXmlConfig('path')
|
->fixXmlConfig('path')
|
||||||
->fixXmlConfig('enabled_locale')
|
->fixXmlConfig('enabled_locale')
|
||||||
|
->fixXmlConfig('provider')
|
||||||
->children()
|
->children()
|
||||||
->arrayNode('fallbacks')
|
->arrayNode('fallbacks')
|
||||||
->info('Defaults to the value of "default_locale".')
|
->info('Defaults to the value of "default_locale".')
|
||||||
@ -822,6 +823,27 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
->arrayNode('providers')
|
||||||
|
->info('Translation providers you can read/write your translations from')
|
||||||
|
->useAttributeAsKey('name')
|
||||||
|
->prototype('array')
|
||||||
|
->fixXmlConfig('domain')
|
||||||
|
->fixXmlConfig('locale')
|
||||||
|
->children()
|
||||||
|
->scalarNode('dsn')->end()
|
||||||
|
->arrayNode('domains')
|
||||||
|
->prototype('scalar')->end()
|
||||||
|
->defaultValue([])
|
||||||
|
->end()
|
||||||
|
->arrayNode('locales')
|
||||||
|
->prototype('scalar')->end()
|
||||||
|
->defaultValue([])
|
||||||
|
->info('If not set, all locales listed under framework.translator.enabled_locales are used.')
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->defaultValue([])
|
||||||
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
@ -169,6 +169,7 @@ use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer;
|
|||||||
use Symfony\Component\Stopwatch\Stopwatch;
|
use Symfony\Component\Stopwatch\Stopwatch;
|
||||||
use Symfony\Component\String\LazyString;
|
use Symfony\Component\String\LazyString;
|
||||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||||
|
use Symfony\Component\Translation\Bridge\Loco\Provider\LocoProviderFactory;
|
||||||
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
||||||
use Symfony\Component\Translation\PseudoLocalizationTranslator;
|
use Symfony\Component\Translation\PseudoLocalizationTranslator;
|
||||||
use Symfony\Component\Translation\Translator;
|
use Symfony\Component\Translation\Translator;
|
||||||
@ -1222,11 +1223,14 @@ class FrameworkExtension extends Extension
|
|||||||
if (!$this->isConfigEnabled($container, $config)) {
|
if (!$this->isConfigEnabled($container, $config)) {
|
||||||
$container->removeDefinition('console.command.translation_debug');
|
$container->removeDefinition('console.command.translation_debug');
|
||||||
$container->removeDefinition('console.command.translation_update');
|
$container->removeDefinition('console.command.translation_update');
|
||||||
|
$container->removeDefinition('console.command.translation_pull');
|
||||||
|
$container->removeDefinition('console.command.translation_push');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$loader->load('translation.php');
|
$loader->load('translation.php');
|
||||||
|
$loader->load('translation_providers.php');
|
||||||
|
|
||||||
// Use the "real" translator instead of the identity default
|
// Use the "real" translator instead of the identity default
|
||||||
$container->setAlias('translator', 'translator.default')->setPublic(true);
|
$container->setAlias('translator', 'translator.default')->setPublic(true);
|
||||||
@ -1348,6 +1352,46 @@ class FrameworkExtension extends Extension
|
|||||||
$options,
|
$options,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$classToServices = [
|
||||||
|
LocoProviderFactory::class => 'translation.provider_factory.loco',
|
||||||
|
];
|
||||||
|
|
||||||
|
$parentPackages = ['symfony/framework-bundle', 'symfony/translation', 'symfony/http-client'];
|
||||||
|
|
||||||
|
foreach ($classToServices as $class => $service) {
|
||||||
|
$package = sprintf('symfony/%s-translation', substr($service, \strlen('translation.provider_factory.')));
|
||||||
|
|
||||||
|
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable($package, $class, $parentPackages)) {
|
||||||
|
$container->removeDefinition($service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$config['providers']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($config['providers'] as $name => $provider) {
|
||||||
|
if (!$config['enabled_locales'] && !$provider['locales']) {
|
||||||
|
throw new LogicException(sprintf('You must specify one of "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" in order to use translation providers.', $name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$container->getDefinition('console.command.translation_pull')
|
||||||
|
->replaceArgument(4, array_merge($transPaths, [$config['default_path']]))
|
||||||
|
->replaceArgument(5, $config['enabled_locales'])
|
||||||
|
;
|
||||||
|
|
||||||
|
$container->getDefinition('console.command.translation_push')
|
||||||
|
->replaceArgument(2, array_merge($transPaths, [$config['default_path']]))
|
||||||
|
->replaceArgument(3, $config['enabled_locales'])
|
||||||
|
;
|
||||||
|
|
||||||
|
$container->getDefinition('translation.provider_collection_factory')
|
||||||
|
->replaceArgument(1, $config['enabled_locales'])
|
||||||
|
;
|
||||||
|
|
||||||
|
$container->getDefinition('translation.provider_collection')->setArgument(0, $config['providers']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function registerValidationConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $propertyInfoEnabled)
|
private function registerValidationConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $propertyInfoEnabled)
|
||||||
|
@ -46,6 +46,8 @@ use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand;
|
|||||||
use Symfony\Component\Messenger\Command\FailedMessagesShowCommand;
|
use Symfony\Component\Messenger\Command\FailedMessagesShowCommand;
|
||||||
use Symfony\Component\Messenger\Command\SetupTransportsCommand;
|
use Symfony\Component\Messenger\Command\SetupTransportsCommand;
|
||||||
use Symfony\Component\Messenger\Command\StopWorkersCommand;
|
use Symfony\Component\Messenger\Command\StopWorkersCommand;
|
||||||
|
use Symfony\Component\Translation\Command\TranslationPullCommand;
|
||||||
|
use Symfony\Component\Translation\Command\TranslationPushCommand;
|
||||||
use Symfony\Component\Translation\Command\XliffLintCommand;
|
use Symfony\Component\Translation\Command\XliffLintCommand;
|
||||||
use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand;
|
use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand;
|
||||||
|
|
||||||
@ -232,6 +234,26 @@ return static function (ContainerConfigurator $container) {
|
|||||||
])
|
])
|
||||||
->tag('console.command')
|
->tag('console.command')
|
||||||
|
|
||||||
|
->set('console.command.translation_pull', TranslationPullCommand::class)
|
||||||
|
->args([
|
||||||
|
service('translation.provider_collection'),
|
||||||
|
service('translation.writer'),
|
||||||
|
service('translation.reader'),
|
||||||
|
param('kernel.default_locale'),
|
||||||
|
[], // Translator paths
|
||||||
|
[], // Enabled locales
|
||||||
|
])
|
||||||
|
->tag('console.command', ['command' => 'translation:pull'])
|
||||||
|
|
||||||
|
->set('console.command.translation_push', TranslationPushCommand::class)
|
||||||
|
->args([
|
||||||
|
service('translation.provider_collection'),
|
||||||
|
service('translation.reader'),
|
||||||
|
[], // Translator paths
|
||||||
|
[], // Enabled locales
|
||||||
|
])
|
||||||
|
->tag('console.command', ['command' => 'translation:push'])
|
||||||
|
|
||||||
->set('console.command.workflow_dump', WorkflowDumpCommand::class)
|
->set('console.command.workflow_dump', WorkflowDumpCommand::class)
|
||||||
->tag('console.command')
|
->tag('console.command')
|
||||||
|
|
||||||
|
@ -176,6 +176,7 @@
|
|||||||
<xsd:element name="path" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="path" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
||||||
<xsd:element name="enabled-locale" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="enabled-locale" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
||||||
<xsd:element name="pseudo-localization" type="pseudo_localization" minOccurs="0" maxOccurs="1" />
|
<xsd:element name="pseudo-localization" type="pseudo_localization" minOccurs="0" maxOccurs="1" />
|
||||||
|
<xsd:element name="provider" type="translation_provider" minOccurs="0" maxOccurs="unbounded" />
|
||||||
</xsd:sequence>
|
</xsd:sequence>
|
||||||
<xsd:attribute name="enabled" type="xsd:boolean" />
|
<xsd:attribute name="enabled" type="xsd:boolean" />
|
||||||
<xsd:attribute name="fallback" type="xsd:string" />
|
<xsd:attribute name="fallback" type="xsd:string" />
|
||||||
@ -195,6 +196,15 @@
|
|||||||
<xsd:attribute name="parse_html" type="xsd:boolean" />
|
<xsd:attribute name="parse_html" type="xsd:boolean" />
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="translation_provider">
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="domain" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
<xsd:element name="locale" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
<xsd:attribute name="dsn" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
<xsd:complexType name="validation">
|
<xsd:complexType name="validation">
|
||||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
<xsd:element name="static-method" type="xsd:string" />
|
<xsd:element name="static-method" type="xsd:string" />
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Bridge\Loco\Provider\LocoProviderFactory;
|
||||||
|
use Symfony\Component\Translation\Provider\NullProviderFactory;
|
||||||
|
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||||
|
use Symfony\Component\Translation\Provider\TranslationProviderCollectionFactory;
|
||||||
|
|
||||||
|
return static function (ContainerConfigurator $container) {
|
||||||
|
$container->services()
|
||||||
|
->set('translation.provider_collection', TranslationProviderCollection::class)
|
||||||
|
->factory([service('translation.provider_collection_factory'), 'fromConfig'])
|
||||||
|
->args([
|
||||||
|
[], // Providers
|
||||||
|
])
|
||||||
|
|
||||||
|
->set('translation.provider_collection_factory', TranslationProviderCollectionFactory::class)
|
||||||
|
->args([
|
||||||
|
tagged_iterator('translation.provider_factory'),
|
||||||
|
[], // Enabled locales
|
||||||
|
])
|
||||||
|
|
||||||
|
->set('translation.provider_factory.null', NullProviderFactory::class)
|
||||||
|
->tag('translation.provider_factory')
|
||||||
|
|
||||||
|
->set('translation.provider_factory.loco', LocoProviderFactory::class)
|
||||||
|
->args([
|
||||||
|
service('http_client'),
|
||||||
|
service('logger'),
|
||||||
|
param('kernel.default_locale'),
|
||||||
|
service('translation.loader.xliff'),
|
||||||
|
])
|
||||||
|
->tag('translation.provider_factory')
|
||||||
|
;
|
||||||
|
};
|
@ -418,6 +418,7 @@ class ConfigurationTest extends TestCase
|
|||||||
'parse_html' => false,
|
'parse_html' => false,
|
||||||
'localizable_html_attributes' => [],
|
'localizable_html_attributes' => [],
|
||||||
],
|
],
|
||||||
|
'providers' => [],
|
||||||
],
|
],
|
||||||
'validation' => [
|
'validation' => [
|
||||||
'enabled' => !class_exists(FullStack::class),
|
'enabled' => !class_exists(FullStack::class),
|
||||||
|
4
src/Symfony/Component/Translation/Bridge/Loco/.gitattributes
vendored
Normal file
4
src/Symfony/Component/Translation/Bridge/Loco/.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/Tests export-ignore
|
||||||
|
/phpunit.xml.dist export-ignore
|
||||||
|
/.gitattributes export-ignore
|
||||||
|
/.gitignore export-ignore
|
4
src/Symfony/Component/Translation/Bridge/Loco/.gitignore
vendored
Normal file
4
src/Symfony/Component/Translation/Bridge/Loco/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
vendor/
|
||||||
|
composer.lock
|
||||||
|
phpunit.xml
|
||||||
|
.phpunit.result.cache
|
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
5.3
|
||||||
|
---
|
||||||
|
|
||||||
|
* Create the bridge
|
19
src/Symfony/Component/Translation/Bridge/Loco/LICENSE
Normal file
19
src/Symfony/Component/Translation/Bridge/Loco/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2021 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
@ -0,0 +1,245 @@
|
|||||||
|
<?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\Bridge\Loco\Provider;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Translation\Exception\ProviderException;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* In Loco:
|
||||||
|
* * Tags refers to Symfony's translation domains
|
||||||
|
* * Assets refers to Symfony's translation keys
|
||||||
|
* * Translations refers to Symfony's translated messages
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class LocoProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
private $client;
|
||||||
|
private $loader;
|
||||||
|
private $logger;
|
||||||
|
private $defaultLocale;
|
||||||
|
private $endpoint;
|
||||||
|
|
||||||
|
public function __construct(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->loader = $loader;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->defaultLocale = $defaultLocale;
|
||||||
|
$this->endpoint = $endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return sprintf('%s://%s', LocoProviderFactory::SCHEME, $this->endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(TranslatorBagInterface $translatorBag): void
|
||||||
|
{
|
||||||
|
$catalogue = $translatorBag->getCatalogue($this->defaultLocale);
|
||||||
|
|
||||||
|
if (!$catalogue) {
|
||||||
|
$catalogue = $translatorBag->getCatalogues()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create keys on Loco
|
||||||
|
foreach ($catalogue->all() as $domain => $messages) {
|
||||||
|
$ids = [];
|
||||||
|
foreach ($messages as $id => $message) {
|
||||||
|
$ids[] = $id;
|
||||||
|
$this->createAsset($id);
|
||||||
|
}
|
||||||
|
if ($ids) {
|
||||||
|
$this->tagsAssets($ids, $domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push translations in all locales and tag them with domain
|
||||||
|
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||||
|
$locale = $catalogue->getLocale();
|
||||||
|
|
||||||
|
if (!\in_array($locale, $this->getLocales())) {
|
||||||
|
$this->createLocale($locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($catalogue->all() as $messages) {
|
||||||
|
foreach ($messages as $id => $message) {
|
||||||
|
$this->translateAsset($id, $message, $locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(array $domains, array $locales): TranslatorBag
|
||||||
|
{
|
||||||
|
$domains = $domains ?: ['*'];
|
||||||
|
$translatorBag = new TranslatorBag();
|
||||||
|
|
||||||
|
foreach ($locales as $locale) {
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
$response = $this->client->request('GET', sprintf('export/locale/%s.xlf?filter=%s&status=translated', $locale, $domain));
|
||||||
|
|
||||||
|
if (404 === $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Locale "%s" for domain "%s" does not exist in Loco.', $locale, $domain));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseContent = $response->getContent(false);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
throw new ProviderException('Unable to read the Loco response: '.$responseContent, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
$translatorBag->addCatalogue($this->loader->load($responseContent, $locale, $domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $translatorBag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(TranslatorBagInterface $translatorBag): void
|
||||||
|
{
|
||||||
|
$deletedIds = [];
|
||||||
|
|
||||||
|
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||||
|
foreach ($catalogue->all() as $messages) {
|
||||||
|
foreach ($messages as $id => $message) {
|
||||||
|
if (\in_array($id, $deletedIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->deleteAsset($id);
|
||||||
|
$deletedIds[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAsset(string $id): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', 'assets', [
|
||||||
|
'body' => [
|
||||||
|
'name' => $id,
|
||||||
|
'id' => $id,
|
||||||
|
'type' => 'text',
|
||||||
|
'default' => 'untranslated',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (409 === $response->getStatusCode()) {
|
||||||
|
$this->logger->info(sprintf('Translation key "%s" already exists in Loco.', $id), [
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
} elseif (201 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to add new translation key "%s" to Loco: (status code: "%s") "%s".', $id, $response->getStatusCode(), $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function translateAsset(string $id, string $message, string $locale): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', sprintf('translations/%s/%s', $id, $locale), [
|
||||||
|
'body' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to add translation message "%s" (for key: "%s" in locale "%s") to Loco: "%s".', $message, $id, $locale, $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tagsAssets(array $ids, string $tag): void
|
||||||
|
{
|
||||||
|
$idsAsString = implode(',', array_unique($ids));
|
||||||
|
|
||||||
|
if (!\in_array($tag, $this->getTags(), true)) {
|
||||||
|
$this->createTag($tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->client->request('POST', sprintf('tags/%s.json', $tag), [
|
||||||
|
'body' => $idsAsString,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to add tag "%s" on translation keys "%s" to Loco: "%s".', $tag, $idsAsString, $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTag(string $tag): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', 'tags.json', [
|
||||||
|
'body' => [
|
||||||
|
'name' => $tag,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (201 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to create tag "%s" on Loco: "%s".', $tag, $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTags(): array
|
||||||
|
{
|
||||||
|
$response = $this->client->request('GET', 'tags.json');
|
||||||
|
$content = $response->toArray(false);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
throw new ProviderException(sprintf('Unable to get tags on Loco: "%s".', $response->getContent(false)), $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createLocale(string $locale): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', 'locales', [
|
||||||
|
'body' => [
|
||||||
|
'code' => $locale,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (201 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to create locale "%s" on Loco: "%s".', $locale, $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLocales(): array
|
||||||
|
{
|
||||||
|
$response = $this->client->request('GET', 'locales');
|
||||||
|
$content = $response->toArray(false);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
throw new ProviderException(sprintf('Unable to get locales on Loco: "%s".', $response->getContent(false)), $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_reduce($content, function ($carry, $locale) {
|
||||||
|
$carry[] = $locale['code'];
|
||||||
|
|
||||||
|
return $carry;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteAsset(string $id): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('DELETE', sprintf('assets/%s.json', $id));
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to delete translation key "%s" to Loco: "%s".', $id, $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Bridge\Loco\Provider;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
|
||||||
|
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||||
|
use Symfony\Component\Translation\Provider\AbstractProviderFactory;
|
||||||
|
use Symfony\Component\Translation\Provider\Dsn;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class LocoProviderFactory extends AbstractProviderFactory
|
||||||
|
{
|
||||||
|
public const SCHEME = 'loco';
|
||||||
|
private const HOST = 'localise.biz/api/';
|
||||||
|
|
||||||
|
private $client;
|
||||||
|
private $logger;
|
||||||
|
private $defaultLocale;
|
||||||
|
private $loader;
|
||||||
|
|
||||||
|
public function __construct(HttpClientInterface $client, LoggerInterface $logger, string $defaultLocale, LoaderInterface $loader)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->defaultLocale = $defaultLocale;
|
||||||
|
$this->loader = $loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return LocoProvider
|
||||||
|
*/
|
||||||
|
public function create(Dsn $dsn): ProviderInterface
|
||||||
|
{
|
||||||
|
if (self::SCHEME !== $dsn->getScheme()) {
|
||||||
|
throw new UnsupportedSchemeException($dsn, self::SCHEME, $this->getSupportedSchemes());
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = sprintf('%s%s', 'default' === $dsn->getHost() ? self::HOST : $dsn->getHost(), $dsn->getPort() ? ':'.$dsn->getPort() : '');
|
||||||
|
$client = $this->client->withOptions([
|
||||||
|
'base_uri' => 'https://'.$endpoint,
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Loco '.$this->getUser($dsn),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new LocoProvider($client, $this->loader, $this->logger, $this->defaultLocale, $endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSupportedSchemes(): array
|
||||||
|
{
|
||||||
|
return [self::SCHEME];
|
||||||
|
}
|
||||||
|
}
|
25
src/Symfony/Component/Translation/Bridge/Loco/README.md
Normal file
25
src/Symfony/Component/Translation/Bridge/Loco/README.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
Loco Translation Provider
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Provides Loco integration for Symfony Translation.
|
||||||
|
|
||||||
|
DSN example
|
||||||
|
-----------
|
||||||
|
|
||||||
|
```
|
||||||
|
// .env file
|
||||||
|
LOCO_DSN=loco://API_KEY@default
|
||||||
|
```
|
||||||
|
|
||||||
|
where:
|
||||||
|
- `API_KEY` is your Loco project API key
|
||||||
|
|
||||||
|
[more information on Loco website](https://localise.biz/help/developers/api-keys)
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Bridge\Loco\Tests;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Bridge\Loco\Provider\LocoProviderFactory;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderFactoryInterface;
|
||||||
|
use Symfony\Component\Translation\Tests\ProviderFactoryTestCase;
|
||||||
|
|
||||||
|
class LocoProviderFactoryTest extends ProviderFactoryTestCase
|
||||||
|
{
|
||||||
|
public function supportsProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [true, 'loco://API_KEY@default'];
|
||||||
|
yield [false, 'somethingElse://API_KEY@default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unsupportedSchemeProvider(): iterable
|
||||||
|
{
|
||||||
|
yield ['somethingElse://API_KEY@default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
'loco://localise.biz/api/',
|
||||||
|
'loco://API_KEY@default',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function incompleteDsnProvider(): iterable
|
||||||
|
{
|
||||||
|
yield ['loco://default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createFactory(): ProviderFactoryInterface
|
||||||
|
{
|
||||||
|
return new LocoProviderFactory($this->getClient(), $this->getLogger(), $this->getDefaultLocale(), $this->getLoader());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,516 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Bridge\Loco\Tests;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\Translation\Bridge\Loco\Provider\LocoProvider;
|
||||||
|
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||||
|
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||||
|
use Symfony\Component\Translation\Loader\XliffFileLoader;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogue;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||||
|
use Symfony\Component\Translation\Tests\ProviderTestCase;
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
class LocoProviderTest extends ProviderTestCase
|
||||||
|
{
|
||||||
|
public function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface
|
||||||
|
{
|
||||||
|
return new LocoProvider($client, $loader, $logger, $defaultLocale, $endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCompleteWriteProcess()
|
||||||
|
{
|
||||||
|
$createAssetResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$createAssetResponse->expects($this->exactly(4))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(201);
|
||||||
|
|
||||||
|
$getLocalesResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$getLocalesResponse->expects($this->exactly(4))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(200);
|
||||||
|
$getLocalesResponse->expects($this->exactly(2))
|
||||||
|
->method('getContent')
|
||||||
|
->with(false)
|
||||||
|
->willReturn('[{"code":"en"}]');
|
||||||
|
|
||||||
|
$createLocaleResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$createLocaleResponse->expects($this->exactly(2))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(201);
|
||||||
|
|
||||||
|
$translateAssetResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$translateAssetResponse->expects($this->exactly(8))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(200);
|
||||||
|
|
||||||
|
$getTagsEmptyResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$getTagsEmptyResponse->expects($this->exactly(2))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(200);
|
||||||
|
$getTagsEmptyResponse->expects($this->once())
|
||||||
|
->method('getContent')
|
||||||
|
->with(false)
|
||||||
|
->willReturn('[]');
|
||||||
|
|
||||||
|
$getTagsNotEmptyResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$getTagsNotEmptyResponse->expects($this->exactly(2))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(200);
|
||||||
|
$getTagsNotEmptyResponse->expects($this->once())
|
||||||
|
->method('getContent')
|
||||||
|
->with(false)
|
||||||
|
->willReturn('["messages"]');
|
||||||
|
|
||||||
|
$createTagResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$createTagResponse->expects($this->exactly(4))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(201);
|
||||||
|
|
||||||
|
$tagAssetResponse = $this->createMock(ResponseInterface::class);
|
||||||
|
$tagAssetResponse->expects($this->exactly(4))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(200);
|
||||||
|
|
||||||
|
$expectedAuthHeader = 'Authorization: Loco API_KEY';
|
||||||
|
|
||||||
|
$responses = [
|
||||||
|
'createAsset1' => function (string $method, string $url, array $options = []) use ($createAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$expectedBody = http_build_query([
|
||||||
|
'name' => 'a',
|
||||||
|
'id' => 'a',
|
||||||
|
'type' => 'text',
|
||||||
|
'default' => 'untranslated',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals($expectedBody, $options['body']);
|
||||||
|
|
||||||
|
return $createAssetResponse;
|
||||||
|
},
|
||||||
|
'getTags1' => function (string $method, string $url, array $options = []) use ($getTagsEmptyResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('GET', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/tags.json', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
|
||||||
|
return $getTagsEmptyResponse;
|
||||||
|
},
|
||||||
|
'createTag1' => function (string $method, string $url, array $options = []) use ($createTagResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/tags.json', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals(http_build_query(['name' => 'messages']), $options['body']);
|
||||||
|
|
||||||
|
return $createTagResponse;
|
||||||
|
},
|
||||||
|
'tagAsset1' => function (string $method, string $url, array $options = []) use ($tagAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/tags/messages.json', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals('a', $options['body']);
|
||||||
|
|
||||||
|
return $tagAssetResponse;
|
||||||
|
},
|
||||||
|
'createAsset2' => function (string $method, string $url, array $options = []) use ($createAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$expectedBody = http_build_query([
|
||||||
|
'name' => 'post.num_comments',
|
||||||
|
'id' => 'post.num_comments',
|
||||||
|
'type' => 'text',
|
||||||
|
'default' => 'untranslated',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals($expectedBody, $options['body']);
|
||||||
|
|
||||||
|
return $createAssetResponse;
|
||||||
|
},
|
||||||
|
'getTags2' => function (string $method, string $url, array $options = []) use ($getTagsNotEmptyResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('GET', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/tags.json', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
|
||||||
|
return $getTagsNotEmptyResponse;
|
||||||
|
},
|
||||||
|
'createTag2' => function (string $method, string $url, array $options = []) use ($createTagResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/tags.json', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals(http_build_query(['name' => 'validators']), $options['body']);
|
||||||
|
|
||||||
|
return $createTagResponse;
|
||||||
|
},
|
||||||
|
'tagAsset2' => function (string $method, string $url, array $options = []) use ($tagAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/tags/validators.json', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals('post.num_comments', $options['body']);
|
||||||
|
|
||||||
|
return $tagAssetResponse;
|
||||||
|
},
|
||||||
|
|
||||||
|
'getLocales1' => function (string $method, string $url, array $options = []) use ($getLocalesResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('GET', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/locales', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
|
||||||
|
return $getLocalesResponse;
|
||||||
|
},
|
||||||
|
|
||||||
|
'translateAsset1' => function (string $method, string $url, array $options = []) use ($translateAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/translations/a/en', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals('trans_en_a', $options['body']);
|
||||||
|
|
||||||
|
return $translateAssetResponse;
|
||||||
|
},
|
||||||
|
'translateAsset2' => function (string $method, string $url, array $options = []) use ($translateAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/translations/post.num_comments/en', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals('{count, plural, one {# comment} other {# comments}}', $options['body']);
|
||||||
|
|
||||||
|
return $translateAssetResponse;
|
||||||
|
},
|
||||||
|
|
||||||
|
'getLocales2' => function (string $method, string $url, array $options = []) use ($getLocalesResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('GET', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/locales', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
|
||||||
|
return $getLocalesResponse;
|
||||||
|
},
|
||||||
|
|
||||||
|
'createLocale1' => function (string $method, string $url, array $options = []) use ($createLocaleResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/locales', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals('code=fr', $options['body']);
|
||||||
|
|
||||||
|
return $createLocaleResponse;
|
||||||
|
},
|
||||||
|
|
||||||
|
'translateAsset3' => function (string $method, string $url, array $options = []) use ($translateAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/translations/a/fr', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals('trans_fr_a', $options['body']);
|
||||||
|
|
||||||
|
return $translateAssetResponse;
|
||||||
|
},
|
||||||
|
'translateAsset4' => function (string $method, string $url, array $options = []) use ($translateAssetResponse, $expectedAuthHeader): ResponseInterface {
|
||||||
|
$this->assertEquals('POST', $method);
|
||||||
|
$this->assertEquals('https://localise.biz/api/translations/post.num_comments/fr', $url);
|
||||||
|
$this->assertEquals($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
|
||||||
|
$this->assertEquals('{count, plural, one {# commentaire} other {# commentaires}}', $options['body']);
|
||||||
|
|
||||||
|
return $translateAssetResponse;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
$translatorBag = new TranslatorBag();
|
||||||
|
$translatorBag->addCatalogue(new MessageCatalogue('en', [
|
||||||
|
'messages' => ['a' => 'trans_en_a'],
|
||||||
|
'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'],
|
||||||
|
]));
|
||||||
|
$translatorBag->addCatalogue(new MessageCatalogue('fr', [
|
||||||
|
'messages' => ['a' => 'trans_fr_a'],
|
||||||
|
'validators' => ['post.num_comments' => '{count, plural, one {# commentaire} other {# commentaires}}'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
|
||||||
|
'base_uri' => 'https://localise.biz/api/',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Loco API_KEY',
|
||||||
|
],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
|
||||||
|
$provider->write($translatorBag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getLocoResponsesForOneLocaleAndOneDomain
|
||||||
|
*/
|
||||||
|
public function testReadForOneLocaleAndOneDomain(string $locale, string $domain, string $responseContent, TranslatorBag $expectedTranslatorBag)
|
||||||
|
{
|
||||||
|
$response = $this->createMock(ResponseInterface::class);
|
||||||
|
$response->expects($this->once())
|
||||||
|
->method('getContent')
|
||||||
|
->willReturn($responseContent);
|
||||||
|
$response->expects($this->exactly(2))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(200);
|
||||||
|
|
||||||
|
$loader = $this->getLoader();
|
||||||
|
$loader->expects($this->once())
|
||||||
|
->method('load')
|
||||||
|
->willReturn($expectedTranslatorBag->getCatalogue($locale));
|
||||||
|
|
||||||
|
$locoProvider = $this->createProvider((new MockHttpClient($response))->withOptions([
|
||||||
|
'base_uri' => 'https://localise.biz/api/',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Loco API_KEY',
|
||||||
|
],
|
||||||
|
]), $loader, $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
|
||||||
|
$translatorBag = $locoProvider->read([$domain], [$locale]);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedTranslatorBag->getCatalogues(), $translatorBag->getCatalogues());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getLocoResponsesForManyLocalesAndManyDomains
|
||||||
|
*/
|
||||||
|
public function testReadForManyLocalesAndManyDomains(array $locales, array $domains, array $responseContents, array $expectedTranslatorBags)
|
||||||
|
{
|
||||||
|
foreach ($locales as $locale) {
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
$response = $this->createMock(ResponseInterface::class);
|
||||||
|
$response->expects($this->once())
|
||||||
|
->method('getContent')
|
||||||
|
->willReturn($responseContents[$domain][$locale]);
|
||||||
|
$response->expects($this->exactly(2))
|
||||||
|
->method('getStatusCode')
|
||||||
|
->willReturn(200);
|
||||||
|
|
||||||
|
$locoProvider = new LocoProvider((new MockHttpClient($response))->withOptions([
|
||||||
|
'base_uri' => 'https://localise.biz/api/',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Loco API_KEY',
|
||||||
|
],
|
||||||
|
]), new XliffFileLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
|
||||||
|
$translatorBag = $locoProvider->read([$domain], [$locale]);
|
||||||
|
// We don't want to assert equality of metadata here, due to the ArrayLoader usage.
|
||||||
|
$translatorBag->getCatalogue($locale)->deleteMetadata('foo', '');
|
||||||
|
|
||||||
|
$this->assertEquals($expectedTranslatorBags[$domain]->getCatalogue($locale), $translatorBag->getCatalogue($locale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toStringProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
new LocoProvider($this->getClient()->withOptions([
|
||||||
|
'base_uri' => 'https://localise.biz/api/',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Loco API_KEY',
|
||||||
|
],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'),
|
||||||
|
'loco://localise.biz/api/',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
new LocoProvider($this->getClient()->withOptions([
|
||||||
|
'base_uri' => 'https://example.com',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Loco API_KEY',
|
||||||
|
],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'example.com'),
|
||||||
|
'loco://example.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
new LocoProvider($this->getClient()->withOptions([
|
||||||
|
'base_uri' => 'https://example.com:99',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Loco API_KEY',
|
||||||
|
],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'example.com:99'),
|
||||||
|
'loco://example.com:99',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocoResponsesForOneLocaleAndOneDomain(): \Generator
|
||||||
|
{
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
|
||||||
|
$expectedTranslatorBagEn = new TranslatorBag();
|
||||||
|
$expectedTranslatorBagEn->addCatalogue($arrayLoader->load([
|
||||||
|
'index.hello' => 'Hello',
|
||||||
|
'index.greetings' => 'Welcome, {firstname}!',
|
||||||
|
], 'en', 'messages'));
|
||||||
|
|
||||||
|
yield ['en', 'messages', <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="https://localise.biz/user/symfony-translation-provider" source-language="en" datatype="database" tool-id="loco">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="loco" tool-name="Loco" tool-version="1.0.25 20201211-1" tool-company="Loco"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="loco:5fd89b853ee27904dd6c5f67" resname="index.hello" datatype="plaintext">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target state="translated">Hello</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="loco:5fd89b8542e5aa5cc27457e2" resname="index.greetings" datatype="plaintext" extradata="loco:format=icu">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target state="translated">Welcome, {firstname}!</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
$expectedTranslatorBagEn,
|
||||||
|
];
|
||||||
|
|
||||||
|
$expectedTranslatorBagFr = new TranslatorBag();
|
||||||
|
$expectedTranslatorBagFr->addCatalogue($arrayLoader->load([
|
||||||
|
'index.hello' => 'Bonjour',
|
||||||
|
'index.greetings' => 'Bienvenue, {firstname} !',
|
||||||
|
], 'fr', 'messages'));
|
||||||
|
|
||||||
|
yield ['fr', 'messages', <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="https://localise.biz/user/symfony-translation-provider" source-language="en" datatype="database" tool-id="loco">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="loco" tool-name="Loco" tool-version="1.0.25 20201211-1" tool-company="Loco"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="loco:5fd89b853ee27904dd6c5f67" resname="index.hello" datatype="plaintext">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target state="translated">Bonjour</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="loco:5fd89b8542e5aa5cc27457e2" resname="index.greetings" datatype="plaintext" extradata="loco:format=icu">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target state="translated">Bienvenue, {firstname} !</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
$expectedTranslatorBagFr,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocoResponsesForManyLocalesAndManyDomains(): \Generator
|
||||||
|
{
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
|
||||||
|
$expectedTranslatorBagMessages = new TranslatorBag();
|
||||||
|
$expectedTranslatorBagMessages->addCatalogue($arrayLoader->load([
|
||||||
|
'index.hello' => 'Hello',
|
||||||
|
'index.greetings' => 'Welcome, {firstname}!',
|
||||||
|
], 'en', 'messages'));
|
||||||
|
$expectedTranslatorBagMessages->addCatalogue($arrayLoader->load([
|
||||||
|
'index.hello' => 'Bonjour',
|
||||||
|
'index.greetings' => 'Bienvenue, {firstname} !',
|
||||||
|
], 'fr', 'messages'));
|
||||||
|
|
||||||
|
$expectedTranslatorBagValidators = new TranslatorBag();
|
||||||
|
$expectedTranslatorBagValidators->addCatalogue($arrayLoader->load([
|
||||||
|
'firstname.error' => 'Firstname must contains only letters.',
|
||||||
|
'lastname.error' => 'Lastname must contains only letters.',
|
||||||
|
], 'en', 'validators'));
|
||||||
|
$expectedTranslatorBagValidators->addCatalogue($arrayLoader->load([
|
||||||
|
'firstname.error' => 'Le prénom ne peut contenir que des lettres.',
|
||||||
|
'lastname.error' => 'Le nom de famille ne peut contenir que des lettres.',
|
||||||
|
], 'fr', 'validators'));
|
||||||
|
|
||||||
|
yield [
|
||||||
|
['en', 'fr'],
|
||||||
|
['messages', 'validators'],
|
||||||
|
[
|
||||||
|
'messages' => [
|
||||||
|
'en' => <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="https://localise.biz/user/symfony-translation-provider" source-language="en" datatype="database" tool-id="loco">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="loco" tool-name="Loco" tool-version="1.0.25 20201211-1" tool-company="Loco"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="loco:5fd89b853ee27904dd6c5f67" resname="index.hello" datatype="plaintext">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target state="translated">Hello</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="loco:5fd89b8542e5aa5cc27457e2" resname="index.greetings" datatype="plaintext" extradata="loco:format=icu">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target state="translated">Welcome, {firstname}!</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
'fr' => <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="https://localise.biz/user/symfony-translation-provider" source-language="en" datatype="database" tool-id="loco">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="loco" tool-name="Loco" tool-version="1.0.25 20201211-1" tool-company="Loco"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="loco:5fd89b853ee27904dd6c5f67" resname="index.hello" datatype="plaintext">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target state="translated">Bonjour</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="loco:5fd89b8542e5aa5cc27457e2" resname="index.greetings" datatype="plaintext" extradata="loco:format=icu">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target state="translated">Bienvenue, {firstname} !</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
],
|
||||||
|
'validators' => [
|
||||||
|
'en' => <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="https://localise.biz/user/symfony-translation-provider" source-language="en" datatype="database" tool-id="loco">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="loco" tool-name="Loco" tool-version="1.0.25 20201211-1" tool-company="Loco"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="loco:5fd89b853ee27904dd6c5f68" resname="firstname.error" datatype="plaintext">
|
||||||
|
<source>firstname.error</source>
|
||||||
|
<target state="translated">Firstname must contains only letters.</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="loco:5fd89b8542e5aa5cc27457e3" resname="lastname.error" datatype="plaintext" extradata="loco:format=icu">
|
||||||
|
<source>lastname.error</source>
|
||||||
|
<target state="translated">Lastname must contains only letters.</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
'fr' => <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="https://localise.biz/user/symfony-translation-provider" source-language="en" datatype="database" tool-id="loco">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="loco" tool-name="Loco" tool-version="1.0.25 20201211-1" tool-company="Loco"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="loco:5fd89b853ee27904dd6c5f68" resname="firstname.error" datatype="plaintext">
|
||||||
|
<source>firstname.error</source>
|
||||||
|
<target state="translated">Le prénom ne peut contenir que des lettres.</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="loco:5fd89b8542e5aa5cc27457e3" resname="lastname.error" datatype="plaintext" extradata="loco:format=icu">
|
||||||
|
<source>lastname.error</source>
|
||||||
|
<target state="translated">Le nom de famille ne peut contenir que des lettres.</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'messages' => $expectedTranslatorBagMessages,
|
||||||
|
'validators' => $expectedTranslatorBagValidators,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
30
src/Symfony/Component/Translation/Bridge/Loco/composer.json
Normal file
30
src/Symfony/Component/Translation/Bridge/Loco/composer.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/loco-translation",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Loco Translation Bridge",
|
||||||
|
"keywords": ["loco", "translation", "provider"],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mathieu Santostefano",
|
||||||
|
"homepage": "https://github.com/welcomattic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.2.5",
|
||||||
|
"symfony/http-client": "^5.3",
|
||||||
|
"symfony/translation": "^5.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Translation\\Bridge\\Loco\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev"
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Loco Translation Bridge Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Resources</directory>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
@ -1,6 +1,11 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
5.3
|
||||||
|
---
|
||||||
|
|
||||||
|
* Add `translation:pull` and `translation:push` commands to manage translations with third-party providers
|
||||||
|
|
||||||
5.2.0
|
5.2.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -26,6 +26,10 @@ use Symfony\Component\Translation\MessageCatalogueInterface;
|
|||||||
*/
|
*/
|
||||||
abstract class AbstractOperation implements OperationInterface
|
abstract class AbstractOperation implements OperationInterface
|
||||||
{
|
{
|
||||||
|
public const OBSOLETE_BATCH = 'obsolete';
|
||||||
|
public const NEW_BATCH = 'new';
|
||||||
|
public const ALL_BATCH = 'all';
|
||||||
|
|
||||||
protected $source;
|
protected $source;
|
||||||
protected $target;
|
protected $target;
|
||||||
protected $result;
|
protected $result;
|
||||||
@ -94,11 +98,11 @@ abstract class AbstractOperation implements OperationInterface
|
|||||||
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($this->messages[$domain]['all'])) {
|
if (!isset($this->messages[$domain][self::ALL_BATCH])) {
|
||||||
$this->processDomain($domain);
|
$this->processDomain($domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->messages[$domain]['all'];
|
return $this->messages[$domain][self::ALL_BATCH];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,11 +114,11 @@ abstract class AbstractOperation implements OperationInterface
|
|||||||
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($this->messages[$domain]['new'])) {
|
if (!isset($this->messages[$domain][self::NEW_BATCH])) {
|
||||||
$this->processDomain($domain);
|
$this->processDomain($domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->messages[$domain]['new'];
|
return $this->messages[$domain][self::NEW_BATCH];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,11 +130,11 @@ abstract class AbstractOperation implements OperationInterface
|
|||||||
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($this->messages[$domain]['obsolete'])) {
|
if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) {
|
||||||
$this->processDomain($domain);
|
$this->processDomain($domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->messages[$domain]['obsolete'];
|
return $this->messages[$domain][self::OBSOLETE_BATCH];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,6 +151,37 @@ abstract class AbstractOperation implements OperationInterface
|
|||||||
return $this->result;
|
return $this->result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param self::*_BATCH $batch
|
||||||
|
*/
|
||||||
|
public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void
|
||||||
|
{
|
||||||
|
// If MessageFormatter class does not exists, intl domains are not supported.
|
||||||
|
if (!class_exists(\MessageFormatter::class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->getDomains() as $domain) {
|
||||||
|
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
||||||
|
switch ($batch) {
|
||||||
|
case self::OBSOLETE_BATCH: $messages = $this->getObsoleteMessages($domain); break;
|
||||||
|
case self::NEW_BATCH: $messages = $this->getNewMessages($domain); break;
|
||||||
|
case self::ALL_BATCH: $messages = $this->getMessages($domain); break;
|
||||||
|
default: throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->getResult();
|
||||||
|
$allIntlMessages = $result->all($intlDomain);
|
||||||
|
$currentMessages = array_diff_key($messages, $result->all($domain));
|
||||||
|
$result->replace($currentMessages, $domain);
|
||||||
|
$result->replace($allIntlMessages + $messages, $intlDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs operation on source and target catalogues for the given domain and
|
* Performs operation on source and target catalogues for the given domain and
|
||||||
* stores the results.
|
* stores the results.
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\Translation\Catalogue\TargetOperation;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogue;
|
||||||
|
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||||
|
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
|
||||||
|
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class TranslationPullCommand extends Command
|
||||||
|
{
|
||||||
|
use TranslationTrait;
|
||||||
|
|
||||||
|
protected static $defaultName = 'translation:pull';
|
||||||
|
protected static $defaultDescription = 'Pull translations from a given provider.';
|
||||||
|
|
||||||
|
private $providerCollection;
|
||||||
|
private $writer;
|
||||||
|
private $reader;
|
||||||
|
private $defaultLocale;
|
||||||
|
private $transPaths;
|
||||||
|
private $enabledLocales;
|
||||||
|
|
||||||
|
public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = [])
|
||||||
|
{
|
||||||
|
$this->providerCollection = $providerCollection;
|
||||||
|
$this->writer = $writer;
|
||||||
|
$this->reader = $reader;
|
||||||
|
$this->defaultLocale = $defaultLocale;
|
||||||
|
$this->transPaths = $transPaths;
|
||||||
|
$this->enabledLocales = $enabledLocales;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$keys = $this->providerCollection->keys();
|
||||||
|
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
|
||||||
|
|
||||||
|
$this
|
||||||
|
->setDefinition([
|
||||||
|
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
|
||||||
|
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
|
||||||
|
new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
|
||||||
|
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
|
||||||
|
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
|
||||||
|
new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'),
|
||||||
|
])
|
||||||
|
->setHelp(<<<'EOF'
|
||||||
|
The <info>%command.name%</> command pulls translations from the given provider. Only
|
||||||
|
new translations are pulled, existing ones are not overwritten.
|
||||||
|
|
||||||
|
You can overwrite existing translations (and remove the missing ones on local side) by using the <comment>--force</> flag:
|
||||||
|
|
||||||
|
<info>php %command.full_name% --force provider</>
|
||||||
|
|
||||||
|
Full example:
|
||||||
|
|
||||||
|
<info>php %command.full_name% provider --force --domains=messages,validators --locales=en</>
|
||||||
|
|
||||||
|
This command pulls all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
|
||||||
|
Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case.
|
||||||
|
Local translations for others domains and locales are ignored.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$provider = $this->providerCollection->get($input->getArgument('provider'));
|
||||||
|
$force = $input->getOption('force');
|
||||||
|
$intlIcu = $input->getOption('intl-icu');
|
||||||
|
$locales = $input->getOption('locales') ?: $this->enabledLocales;
|
||||||
|
$domains = $input->getOption('domains');
|
||||||
|
$format = $input->getOption('format');
|
||||||
|
$xliffVersion = '1.2';
|
||||||
|
|
||||||
|
if ($intlIcu && !$force) {
|
||||||
|
$io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($format) {
|
||||||
|
case 'xlf20': $xliffVersion = '2.0';
|
||||||
|
// no break
|
||||||
|
case 'xlf12': $format = 'xlf';
|
||||||
|
}
|
||||||
|
|
||||||
|
$writeOptions = [
|
||||||
|
'path' => end($this->transPaths),
|
||||||
|
'xliff_version' => $xliffVersion,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$domains) {
|
||||||
|
$domains = $provider->getDomains();
|
||||||
|
}
|
||||||
|
|
||||||
|
$providerTranslations = $provider->read($domains, $locales);
|
||||||
|
|
||||||
|
if ($force) {
|
||||||
|
foreach ($providerTranslations->getCatalogues() as $catalogue) {
|
||||||
|
$operation = new TargetOperation((new MessageCatalogue($catalogue->getLocale())), $catalogue);
|
||||||
|
if ($intlIcu) {
|
||||||
|
$operation->moveMessagesToIntlDomainsIfPossible();
|
||||||
|
}
|
||||||
|
$this->writer->write($operation->getResult(), $format, $writeOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
|
||||||
|
|
||||||
|
// Append pulled translations to local ones.
|
||||||
|
$localTranslations->addBag($providerTranslations->diff($localTranslations));
|
||||||
|
|
||||||
|
foreach ($localTranslations->getCatalogues() as $catalogue) {
|
||||||
|
$this->writer->write($catalogue, $format, $writeOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||||
|
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class TranslationPushCommand extends Command
|
||||||
|
{
|
||||||
|
use TranslationTrait;
|
||||||
|
|
||||||
|
protected static $defaultName = 'translation:push';
|
||||||
|
protected static $defaultDescription = 'Push translations to a given provider.';
|
||||||
|
|
||||||
|
private $providers;
|
||||||
|
private $reader;
|
||||||
|
private $transPaths;
|
||||||
|
private $enabledLocales;
|
||||||
|
|
||||||
|
public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = [])
|
||||||
|
{
|
||||||
|
$this->providers = $providers;
|
||||||
|
$this->reader = $reader;
|
||||||
|
$this->transPaths = $transPaths;
|
||||||
|
$this->enabledLocales = $enabledLocales;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$keys = $this->providers->keys();
|
||||||
|
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
|
||||||
|
|
||||||
|
$this
|
||||||
|
->setDefinition([
|
||||||
|
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
|
||||||
|
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
|
||||||
|
new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
|
||||||
|
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
|
||||||
|
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
|
||||||
|
])
|
||||||
|
->setHelp(<<<'EOF'
|
||||||
|
The <info>%command.name%</> command pushes translations to the given provider. Only new
|
||||||
|
translations are pushed, existing ones are not overwritten.
|
||||||
|
|
||||||
|
You can overwrite existing translations by using the <comment>--force</> flag:
|
||||||
|
|
||||||
|
<info>php %command.full_name% --force provider</>
|
||||||
|
|
||||||
|
You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
|
||||||
|
|
||||||
|
<info>php %command.full_name% --delete-missing provider</>
|
||||||
|
|
||||||
|
Full example:
|
||||||
|
|
||||||
|
<info>php %command.full_name% provider --force --delete-missing --domains=messages,validators --locales=en</>
|
||||||
|
|
||||||
|
This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
|
||||||
|
Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
|
||||||
|
Provider translations for others domains and locales are ignored.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
if (!$this->enabledLocales) {
|
||||||
|
throw new InvalidArgumentException('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$provider = $this->providers->get($input->getArgument('provider'));
|
||||||
|
$domains = $input->getOption('domains');
|
||||||
|
$locales = $input->getOption('locales');
|
||||||
|
$force = $input->getOption('force');
|
||||||
|
$deleteMissing = $input->getOption('delete-missing');
|
||||||
|
|
||||||
|
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
|
||||||
|
|
||||||
|
if (!$domains) {
|
||||||
|
$domains = $this->getDomainsFromTranslatorBag($localTranslations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$deleteMissing && $force) {
|
||||||
|
$provider->write($localTranslations);
|
||||||
|
|
||||||
|
$io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$providerTranslations = $provider->read($domains, $locales);
|
||||||
|
|
||||||
|
if ($deleteMissing) {
|
||||||
|
$provider->delete($providerTranslations->diff($localTranslations));
|
||||||
|
|
||||||
|
$io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||||
|
|
||||||
|
// Read provider translations again, after missing translations deletion,
|
||||||
|
// to avoid push freshly deleted translations.
|
||||||
|
$providerTranslations = $provider->read($domains, $locales);
|
||||||
|
}
|
||||||
|
|
||||||
|
$translationsToWrite = $localTranslations->diff($providerTranslations);
|
||||||
|
|
||||||
|
if ($force) {
|
||||||
|
$translationsToWrite->addBag($localTranslations->intersect($providerTranslations));
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider->write($translationsToWrite);
|
||||||
|
|
||||||
|
$io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array
|
||||||
|
{
|
||||||
|
$domains = [];
|
||||||
|
|
||||||
|
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||||
|
$domains += $catalogue->getDomains();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique($domains);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\MessageCatalogue;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
trait TranslationTrait
|
||||||
|
{
|
||||||
|
private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag
|
||||||
|
{
|
||||||
|
$bag = new TranslatorBag();
|
||||||
|
|
||||||
|
foreach ($locales as $locale) {
|
||||||
|
$catalogue = new MessageCatalogue($locale);
|
||||||
|
foreach ($transPaths as $path) {
|
||||||
|
$this->reader->read($path, $catalogue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($domains) {
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
$catalogue = $this->filterCatalogue($catalogue, $domain);
|
||||||
|
$bag->addCatalogue($catalogue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$bag->addCatalogue($catalogue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
|
||||||
|
{
|
||||||
|
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
|
||||||
|
|
||||||
|
// extract intl-icu messages only
|
||||||
|
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
||||||
|
if ($intlMessages = $catalogue->all($intlDomain)) {
|
||||||
|
$filteredCatalogue->add($intlMessages, $intlDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract all messages and subtract intl-icu messages
|
||||||
|
if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
|
||||||
|
$filteredCatalogue->add($messages, $domain);
|
||||||
|
}
|
||||||
|
foreach ($catalogue->getResources() as $resource) {
|
||||||
|
$filteredCatalogue->addResource($resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
|
||||||
|
foreach ($metadata as $k => $v) {
|
||||||
|
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($metadata = $catalogue->getMetadata('', $domain)) {
|
||||||
|
foreach ($metadata as $k => $v) {
|
||||||
|
$filteredCatalogue->setMetadata($k, $v, $domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filteredCatalogue;
|
||||||
|
}
|
||||||
|
}
|
@ -79,6 +79,14 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
|
|||||||
return $this->translator->getCatalogue($locale);
|
return $this->translator->getCatalogue($locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getCatalogues(): array
|
||||||
|
{
|
||||||
|
return $this->translator->getCatalogues();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Exception;
|
||||||
|
|
||||||
|
class IncompleteDsnException extends InvalidArgumentException
|
||||||
|
{
|
||||||
|
public function __construct(string $message, string $dsn = null, ?\Throwable $previous = null)
|
||||||
|
{
|
||||||
|
if ($dsn) {
|
||||||
|
$message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($message, 0, $previous);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Exception;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
class ProviderException extends RuntimeException implements ProviderExceptionInterface
|
||||||
|
{
|
||||||
|
private $response;
|
||||||
|
private $debug;
|
||||||
|
|
||||||
|
public function __construct(string $message, ResponseInterface $response, int $code = 0, \Exception $previous = null)
|
||||||
|
{
|
||||||
|
$this->response = $response;
|
||||||
|
$this->debug .= $response->getInfo('debug') ?? '';
|
||||||
|
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponse(): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDebug(): string
|
||||||
|
{
|
||||||
|
return $this->debug;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
interface ProviderExceptionInterface extends ExceptionInterface
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface
|
||||||
|
*/
|
||||||
|
public function getDebug(): string;
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Exception;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Bridge;
|
||||||
|
use Symfony\Component\Translation\Provider\Dsn;
|
||||||
|
|
||||||
|
class UnsupportedSchemeException extends LogicException
|
||||||
|
{
|
||||||
|
private const SCHEME_TO_PACKAGE_MAP = [
|
||||||
|
'loco' => [
|
||||||
|
'class' => Bridge\Loco\Provider\LocoProviderFactory::class,
|
||||||
|
'package' => 'symfony/loco-translation',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(Dsn $dsn, string $name = null, array $supported = [])
|
||||||
|
{
|
||||||
|
$provider = $dsn->getScheme();
|
||||||
|
if (false !== $pos = strpos($provider, '+')) {
|
||||||
|
$provider = substr($provider, 0, $pos);
|
||||||
|
}
|
||||||
|
$package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null;
|
||||||
|
if ($package && !class_exists($package['class'])) {
|
||||||
|
parent::__construct(sprintf('Unable to synchronize translations via "%s" as the providers is not installed; try running "composer require %s".', $provider, $package['package']));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = sprintf('The "%s" scheme is not supported', $dsn->getScheme());
|
||||||
|
if ($name && $supported) {
|
||||||
|
$message .= sprintf('; supported schemes for translation providers "%s" are: "%s"', $name, implode('", "', $supported));
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($message.'.');
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,8 @@
|
|||||||
namespace Symfony\Component\Translation\Loader;
|
namespace Symfony\Component\Translation\Loader;
|
||||||
|
|
||||||
use Symfony\Component\Config\Resource\FileResource;
|
use Symfony\Component\Config\Resource\FileResource;
|
||||||
|
use Symfony\Component\Config\Util\Exception\InvalidXmlException;
|
||||||
|
use Symfony\Component\Config\Util\Exception\XmlParsingException;
|
||||||
use Symfony\Component\Config\Util\XmlUtils;
|
use Symfony\Component\Config\Util\XmlUtils;
|
||||||
use Symfony\Component\Translation\Exception\InvalidResourceException;
|
use Symfony\Component\Translation\Exception\InvalidResourceException;
|
||||||
use Symfony\Component\Translation\Exception\NotFoundResourceException;
|
use Symfony\Component\Translation\Exception\NotFoundResourceException;
|
||||||
@ -35,36 +37,47 @@ class XliffFileLoader implements LoaderInterface
|
|||||||
throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.');
|
throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stream_is_local($resource)) {
|
if (!$this->isXmlString($resource)) {
|
||||||
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
|
if (!stream_is_local($resource)) {
|
||||||
|
throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($resource)) {
|
||||||
|
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($resource)) {
|
||||||
|
throw new InvalidResourceException(sprintf('This is neither a file nor an XLIFF string "%s".', $resource));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file_exists($resource)) {
|
try {
|
||||||
throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
|
if ($this->isXmlString($resource)) {
|
||||||
|
$dom = XmlUtils::parse($resource);
|
||||||
|
} else {
|
||||||
|
$dom = XmlUtils::loadFile($resource);
|
||||||
|
}
|
||||||
|
} catch (\InvalidArgumentException | XmlParsingException | InvalidXmlException $e) {
|
||||||
|
throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors = XliffUtils::validateSchema($dom)) {
|
||||||
|
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
$catalogue = new MessageCatalogue($locale);
|
$catalogue = new MessageCatalogue($locale);
|
||||||
$this->extract($resource, $catalogue, $domain);
|
$this->extract($dom, $catalogue, $domain);
|
||||||
|
|
||||||
if (class_exists(FileResource::class)) {
|
if (is_file($resource) && class_exists(FileResource::class)) {
|
||||||
$catalogue->addResource(new FileResource($resource));
|
$catalogue->addResource(new FileResource($resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $catalogue;
|
return $catalogue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extract($resource, MessageCatalogue $catalogue, string $domain)
|
private function extract($dom, MessageCatalogue $catalogue, string $domain)
|
||||||
{
|
{
|
||||||
try {
|
|
||||||
$dom = XmlUtils::loadFile($resource);
|
|
||||||
} catch (\InvalidArgumentException $e) {
|
|
||||||
throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e);
|
|
||||||
}
|
|
||||||
|
|
||||||
$xliffVersion = XliffUtils::getVersionNumber($dom);
|
$xliffVersion = XliffUtils::getVersionNumber($dom);
|
||||||
if ($errors = XliffUtils::validateSchema($dom)) {
|
|
||||||
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('1.2' === $xliffVersion) {
|
if ('1.2' === $xliffVersion) {
|
||||||
$this->extractXliff1($dom, $catalogue, $domain);
|
$this->extractXliff1($dom, $catalogue, $domain);
|
||||||
@ -211,4 +224,9 @@ class XliffFileLoader implements LoaderInterface
|
|||||||
|
|
||||||
return $notes;
|
return $notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isXmlString(string $resource): bool
|
||||||
|
{
|
||||||
|
return 0 === strpos($resource, '<?xml');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,14 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
|
|||||||
return $this->translator->getCatalogue($locale);
|
return $this->translator->getCatalogue($locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getCatalogues(): array
|
||||||
|
{
|
||||||
|
return $this->translator->getCatalogues();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the fallback locales.
|
* Gets the fallback locales.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Exception\IncompleteDsnException;
|
||||||
|
|
||||||
|
abstract class AbstractProviderFactory implements ProviderFactoryInterface
|
||||||
|
{
|
||||||
|
public function supports(Dsn $dsn): bool
|
||||||
|
{
|
||||||
|
return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
abstract protected function getSupportedSchemes(): array;
|
||||||
|
|
||||||
|
protected function getUser(Dsn $dsn): string
|
||||||
|
{
|
||||||
|
if (null === $user = $dsn->getUser()) {
|
||||||
|
throw new IncompleteDsnException('User is not set.', $dsn->getOriginalDsn());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPassword(Dsn $dsn): string
|
||||||
|
{
|
||||||
|
if (null === $password = $dsn->getPassword()) {
|
||||||
|
throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
}
|
108
src/Symfony/Component/Translation/Provider/Dsn.php
Normal file
108
src/Symfony/Component/Translation/Provider/Dsn.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class Dsn
|
||||||
|
{
|
||||||
|
private $scheme;
|
||||||
|
private $host;
|
||||||
|
private $user;
|
||||||
|
private $password;
|
||||||
|
private $port;
|
||||||
|
private $options;
|
||||||
|
private $path;
|
||||||
|
private $dsn;
|
||||||
|
|
||||||
|
public function __construct(string $scheme, string $host, ?string $user = null, ?string $password = null, ?int $port = null, array $options = [], ?string $path = null)
|
||||||
|
{
|
||||||
|
$this->scheme = $scheme;
|
||||||
|
$this->host = $host;
|
||||||
|
$this->user = $user;
|
||||||
|
$this->password = $password;
|
||||||
|
$this->port = $port;
|
||||||
|
$this->options = $options;
|
||||||
|
$this->path = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromString(string $dsn): self
|
||||||
|
{
|
||||||
|
if (false === $parsedDsn = parse_url($dsn)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($parsedDsn['scheme'])) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($parsedDsn['host'])) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = isset($parsedDsn['user']) ? urldecode($parsedDsn['user']) : null;
|
||||||
|
$password = isset($parsedDsn['pass']) ? urldecode($parsedDsn['pass']) : null;
|
||||||
|
$port = $parsedDsn['port'] ?? null;
|
||||||
|
$path = $parsedDsn['path'] ?? null;
|
||||||
|
parse_str($parsedDsn['query'] ?? '', $query);
|
||||||
|
|
||||||
|
$dsnObject = new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query, $path);
|
||||||
|
$dsnObject->dsn = $dsn;
|
||||||
|
|
||||||
|
return $dsnObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScheme(): string
|
||||||
|
{
|
||||||
|
return $this->scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHost(): string
|
||||||
|
{
|
||||||
|
return $this->host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?string
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPort(int $default = null): ?int
|
||||||
|
{
|
||||||
|
return $this->port ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOption(string $key, $default = null)
|
||||||
|
{
|
||||||
|
return $this->options[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): ?string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOriginalDsn(): string
|
||||||
|
{
|
||||||
|
return $this->dsn;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
use Symfony\Component\Translation\TranslatorBagInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters domains and locales between the Translator config values and those specific to each provider.
|
||||||
|
*
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
class FilteringProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
private $provider;
|
||||||
|
private $locales;
|
||||||
|
private $domains;
|
||||||
|
|
||||||
|
public function __construct(ProviderInterface $provider, array $locales, array $domains = [])
|
||||||
|
{
|
||||||
|
$this->provider = $provider;
|
||||||
|
$this->locales = $locales;
|
||||||
|
$this->domains = $domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return (string) $this->provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function write(TranslatorBagInterface $translatorBag): void
|
||||||
|
{
|
||||||
|
$this->provider->write($translatorBag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(array $domains, array $locales): TranslatorBag
|
||||||
|
{
|
||||||
|
$domains = !$this->domains ? $domains : array_intersect($this->domains, $domains);
|
||||||
|
$locales = array_intersect($this->locales, $locales);
|
||||||
|
|
||||||
|
return $this->provider->read($domains, $locales);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(TranslatorBagInterface $translatorBag): void
|
||||||
|
{
|
||||||
|
$this->provider->delete($translatorBag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDomains(): array
|
||||||
|
{
|
||||||
|
return $this->domains;
|
||||||
|
}
|
||||||
|
}
|
41
src/Symfony/Component/Translation/Provider/NullProvider.php
Normal file
41
src/Symfony/Component/Translation/Provider/NullProvider.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
use Symfony\Component\Translation\TranslatorBagInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
class NullProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return NullProviderFactory::SCHEME.'://default';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(TranslatorBagInterface $translatorBag, bool $override = false): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(array $domains, array $locales): TranslatorBag
|
||||||
|
{
|
||||||
|
return new TranslatorBag();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(TranslatorBagInterface $translatorBag): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class NullProviderFactory extends AbstractProviderFactory
|
||||||
|
{
|
||||||
|
const SCHEME = 'null';
|
||||||
|
|
||||||
|
public function create(Dsn $dsn): ProviderInterface
|
||||||
|
{
|
||||||
|
if (self::SCHEME === $dsn->getScheme()) {
|
||||||
|
return new NullProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedSchemeException($dsn, self::SCHEME, $this->getSupportedSchemes());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSupportedSchemes(): array
|
||||||
|
{
|
||||||
|
return [self::SCHEME];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Exception\IncompleteDsnException;
|
||||||
|
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
|
||||||
|
|
||||||
|
interface ProviderFactoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws UnsupportedSchemeException
|
||||||
|
* @throws IncompleteDsnException
|
||||||
|
*/
|
||||||
|
public function create(Dsn $dsn): ProviderInterface;
|
||||||
|
|
||||||
|
public function supports(Dsn $dsn): bool;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
use Symfony\Component\Translation\TranslatorBagInterface;
|
||||||
|
|
||||||
|
interface ProviderInterface
|
||||||
|
{
|
||||||
|
public function __toString(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translations available in the TranslatorBag only must be created.
|
||||||
|
* Translations available in both the TranslatorBag and on the provider
|
||||||
|
* must be overwritten.
|
||||||
|
* Translations available on the provider only must be kept.
|
||||||
|
*/
|
||||||
|
public function write(TranslatorBagInterface $translatorBag): void;
|
||||||
|
|
||||||
|
public function read(array $domains, array $locales): TranslatorBag;
|
||||||
|
|
||||||
|
public function delete(TranslatorBagInterface $translatorBag): void;
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class TranslationProviderCollection
|
||||||
|
{
|
||||||
|
private $providers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, ProviderInterface> $providers
|
||||||
|
*/
|
||||||
|
public function __construct(iterable $providers)
|
||||||
|
{
|
||||||
|
$this->providers = [];
|
||||||
|
foreach ($providers as $name => $provider) {
|
||||||
|
$this->providers[$name] = $provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return '['.implode(',', array_keys($this->providers)).']';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $name): bool
|
||||||
|
{
|
||||||
|
return isset($this->providers[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $name): ProviderInterface
|
||||||
|
{
|
||||||
|
if (!$this->has($name)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->providers[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function keys(): array
|
||||||
|
{
|
||||||
|
return array_keys($this->providers);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Provider;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
class TranslationProviderCollectionFactory
|
||||||
|
{
|
||||||
|
private $factories;
|
||||||
|
private $enabledLocales;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ProviderFactoryInterface[] $factories
|
||||||
|
*/
|
||||||
|
public function __construct(iterable $factories, array $enabledLocales)
|
||||||
|
{
|
||||||
|
$this->factories = $factories;
|
||||||
|
$this->enabledLocales = $enabledLocales;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromConfig(array $config): TranslationProviderCollection
|
||||||
|
{
|
||||||
|
$providers = [];
|
||||||
|
foreach ($config as $name => $currentConfig) {
|
||||||
|
$providers[$name] = $this->fromDsnObject(
|
||||||
|
Dsn::fromString($currentConfig['dsn']),
|
||||||
|
!$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'],
|
||||||
|
!$currentConfig['domains'] ? [] : $currentConfig['domains']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TranslationProviderCollection($providers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromDsnObject(Dsn $dsn, array $locales, array $domains = []): ProviderInterface
|
||||||
|
{
|
||||||
|
foreach ($this->factories as $factory) {
|
||||||
|
if ($factory->supports($dsn)) {
|
||||||
|
return new FilteringProvider($factory->create($dsn), $locales, $domains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedSchemeException($dsn);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Tests\Command;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\CommonResponseTrait;
|
||||||
|
use Symfony\Component\Translation\Bridge\Loco\Provider\LocoProvider;
|
||||||
|
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||||
|
use Symfony\Component\Translation\Loader\XliffFileLoader;
|
||||||
|
use Symfony\Component\Translation\Provider\FilteringProvider;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||||
|
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*/
|
||||||
|
abstract class TranslationProviderTestCase extends TestCase
|
||||||
|
{
|
||||||
|
protected $fs;
|
||||||
|
protected $translationAppDir;
|
||||||
|
protected $files;
|
||||||
|
protected $defaultLocale;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->defaultLocale = \Locale::getDefault();
|
||||||
|
\Locale::setDefault('en');
|
||||||
|
$this->fs = new Filesystem();
|
||||||
|
$this->translationAppDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true);
|
||||||
|
$this->fs->mkdir($this->translationAppDir.'/translations');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
\Locale::setDefault($this->defaultLocale);
|
||||||
|
$this->fs->remove($this->translationAppDir);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getProviderCollection(ProviderInterface $provider, array $locales = ['en'], array $domains = ['messages']): TranslationProviderCollection
|
||||||
|
{
|
||||||
|
return new TranslationProviderCollection([
|
||||||
|
'loco' => new FilteringProvider($provider, $locales, $domains),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createFile(array $messages = ['note' => 'NOTE'], $targetLanguage = 'en', $fileNamePattern = 'messages.%locale%.xlf', string $xlfVersion = 'xlf12'): string
|
||||||
|
{
|
||||||
|
if ($xlfVersion === 'xlf12') {
|
||||||
|
$transUnits = '';
|
||||||
|
foreach ($messages as $key => $value) {
|
||||||
|
$transUnits .= <<<XLIFF
|
||||||
|
<trans-unit id="$key">
|
||||||
|
<source>$key</source>
|
||||||
|
<target>$value</target>
|
||||||
|
</trans-unit>
|
||||||
|
XLIFF;
|
||||||
|
}
|
||||||
|
$xliffContent = <<<XLIFF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="$targetLanguage" datatype="plaintext" original="file.ext">
|
||||||
|
<body>
|
||||||
|
$transUnits
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF;
|
||||||
|
} else {
|
||||||
|
$units = '';
|
||||||
|
foreach ($messages as $key => $value) {
|
||||||
|
$units .= <<<XLIFF
|
||||||
|
<unit id="$key">
|
||||||
|
<segment>
|
||||||
|
<source>$key</source>
|
||||||
|
<target>$value</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
XLIFF;
|
||||||
|
}
|
||||||
|
$xliffContent = <<<XLIFF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="$targetLanguage">
|
||||||
|
<file id="messages.$targetLanguage">
|
||||||
|
$units
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = sprintf('%s/%s', $this->translationAppDir.'/translations', str_replace('%locale%', $targetLanguage, $fileNamePattern));
|
||||||
|
file_put_contents($filename, $xliffContent);
|
||||||
|
|
||||||
|
$this->files[] = $filename;
|
||||||
|
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,356 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Tests\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Symfony\Component\Translation\Command\TranslationPullCommand;
|
||||||
|
use Symfony\Component\Translation\Dumper\XliffFileDumper;
|
||||||
|
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||||
|
use Symfony\Component\Translation\Loader\XliffFileLoader;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||||
|
use Symfony\Component\Translation\Reader\TranslationReader;
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
use Symfony\Component\Translation\Writer\TranslationWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*/
|
||||||
|
class TranslationPullCommandTest extends TranslationProviderTestCase
|
||||||
|
{
|
||||||
|
public function testPullNewXlf12Messages()
|
||||||
|
{
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
$filenameEn = $this->createFile();
|
||||||
|
$filenameFr = $this->createFile(['note' => 'NOTE'], 'fr');
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'new.foo' => 'newFoo',
|
||||||
|
], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'new.foo' => 'nouveauFoo',
|
||||||
|
], 'fr'));
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages']]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="symfony" tool-name="Symfony"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="994ixRL" resname="new.foo">
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>newFoo</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7bRlYkK" resname="note">
|
||||||
|
<source>note</source>
|
||||||
|
<target>NOTE</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameEn));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="symfony" tool-name="Symfony"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="994ixRL" resname="new.foo">
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>nouveauFoo</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7bRlYkK" resname="note">
|
||||||
|
<source>note</source>
|
||||||
|
<target>NOTE</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameFr));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPullNewXlf20Messages()
|
||||||
|
{
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
$filenameEn = $this->createFile(['note' => 'NOTE'], 'en', 'messages.%locale%.xlf', 'xlf20');
|
||||||
|
$filenameFr = $this->createFile(['note' => 'NOTE'], 'fr', 'messages.%locale%.xlf', 'xlf20');
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'new.foo' => 'newFoo',
|
||||||
|
], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'new.foo' => 'nouveauFoo',
|
||||||
|
], 'fr'));
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--format' => 'xlf20']);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
|
||||||
|
<file id="messages.en">
|
||||||
|
<unit id="994ixRL" name="new.foo">
|
||||||
|
<segment>
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>newFoo</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="7bRlYkK" name="note">
|
||||||
|
<segment>
|
||||||
|
<source>note</source>
|
||||||
|
<target>NOTE</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameEn));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="fr">
|
||||||
|
<file id="messages.fr">
|
||||||
|
<unit id="994ixRL" name="new.foo">
|
||||||
|
<segment>
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>nouveauFoo</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="7bRlYkK" name="note">
|
||||||
|
<segment>
|
||||||
|
<source>note</source>
|
||||||
|
<target>NOTE</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameFr));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPullForceMessages()
|
||||||
|
{
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
$filenameEn = $this->createFile();
|
||||||
|
$filenameFr = $this->createFile(['note' => 'NOTE'], 'fr');
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'UPDATED NOTE',
|
||||||
|
'new.foo' => 'newFoo',
|
||||||
|
], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE MISE À JOUR',
|
||||||
|
'new.foo' => 'nouveauFoo',
|
||||||
|
], 'fr'));
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--force' => true]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] Local translations has been updated from "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="symfony" tool-name="Symfony"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="7bRlYkK" resname="note">
|
||||||
|
<source>note</source>
|
||||||
|
<target>UPDATED NOTE</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="994ixRL" resname="new.foo">
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>newFoo</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameEn));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="symfony" tool-name="Symfony"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="7bRlYkK" resname="note">
|
||||||
|
<source>note</source>
|
||||||
|
<target>NOTE MISE À JOUR</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="994ixRL" resname="new.foo">
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>nouveauFoo</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameFr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires extension intl
|
||||||
|
*/
|
||||||
|
public function testPullForceIntlIcuMessages()
|
||||||
|
{
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
$filenameEn = $this->createFile(['note' => 'NOTE'], 'en', 'messages+intl-icu.%locale%.xlf');
|
||||||
|
$filenameFr = $this->createFile(['note' => 'NOTE'], 'fr', 'messages+intl-icu.%locale%.xlf');
|
||||||
|
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'UPDATED NOTE',
|
||||||
|
'new.foo' => 'newFoo',
|
||||||
|
], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE MISE À JOUR',
|
||||||
|
'new.foo' => 'nouveauFoo',
|
||||||
|
], 'fr'));
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--force' => true, '--intl-icu' => true]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] Local translations has been updated from "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="symfony" tool-name="Symfony"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="7bRlYkK" resname="note">
|
||||||
|
<source>note</source>
|
||||||
|
<target>UPDATED NOTE</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="994ixRL" resname="new.foo">
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>newFoo</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameEn));
|
||||||
|
$this->assertXmlStringEqualsXmlString(<<<XLIFF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="symfony" tool-name="Symfony"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="7bRlYkK" resname="note">
|
||||||
|
<source>note</source>
|
||||||
|
<target>NOTE MISE À JOUR</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="994ixRL" resname="new.foo">
|
||||||
|
<source>new.foo</source>
|
||||||
|
<target>nouveauFoo</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
, file_get_contents($filenameFr));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createCommandTester(ProviderInterface $provider, array $locales = ['en'], array $domains = ['messages']): CommandTester
|
||||||
|
{
|
||||||
|
$writer = new TranslationWriter();
|
||||||
|
$writer->addDumper('xlf', new XliffFileDumper());
|
||||||
|
|
||||||
|
$reader = new TranslationReader();
|
||||||
|
$reader->addLoader('xlf', new XliffFileLoader());
|
||||||
|
|
||||||
|
$command = new TranslationPullCommand(
|
||||||
|
$this->getProviderCollection($provider, $locales, $domains),
|
||||||
|
$writer,
|
||||||
|
$reader,
|
||||||
|
'en',
|
||||||
|
[$this->translationAppDir.'/translations']
|
||||||
|
);
|
||||||
|
$application = new Application();
|
||||||
|
$application->add($command);
|
||||||
|
|
||||||
|
return new CommandTester($application->find('translation:pull'));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,253 @@
|
|||||||
|
<?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\Bundle\FrameworkBundle\Tests\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Symfony\Component\Translation\Command\TranslationPushCommand;
|
||||||
|
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||||
|
use Symfony\Component\Translation\Loader\XliffFileLoader;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||||
|
use Symfony\Component\Translation\Reader\TranslationReader;
|
||||||
|
use Symfony\Component\Translation\Tests\Command\TranslationProviderTestCase;
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*/
|
||||||
|
class TranslationPushCommandTest extends TranslationProviderTestCase
|
||||||
|
{
|
||||||
|
public function testPushNewMessages()
|
||||||
|
{
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
$xliffLoader = new XliffFileLoader();
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
// Simulate existing messages on Provider
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr'));
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
// Create local files, with a new message
|
||||||
|
$filenameEn = $this->createFile([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'new.foo' => 'newFoo',
|
||||||
|
]);
|
||||||
|
$filenameFr = $this->createFile([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'new.foo' => 'nouveauFoo',
|
||||||
|
], 'fr');
|
||||||
|
$localTranslatorBag = new TranslatorBag();
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($filenameEn, 'en'));
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($filenameFr, 'fr'));
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($localTranslatorBag->diff($providerReadTranslatorBag));
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages']]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] New local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPushForceMessages()
|
||||||
|
{
|
||||||
|
$xliffLoader = new XliffFileLoader();
|
||||||
|
$filenameEn = $this->createFile([
|
||||||
|
'note' => 'NOTE UPDATED',
|
||||||
|
'new.foo' => 'newFoo',
|
||||||
|
]);
|
||||||
|
$filenameFr = $this->createFile([
|
||||||
|
'note' => 'NOTE MISE À JOUR',
|
||||||
|
'new.foo' => 'nouveauFoo',
|
||||||
|
], 'fr');
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
|
||||||
|
$localTranslatorBag = new TranslatorBag();
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($filenameEn, 'en'));
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($filenameFr, 'fr'));
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($localTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--force' => true]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] All local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteMissingMessages()
|
||||||
|
{
|
||||||
|
$xliffLoader = new XliffFileLoader();
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
// Simulate existing messages on Provider.
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'obsolete.foo' => 'obsoleteFoo',
|
||||||
|
], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'obsolete.foo' => 'obsolèteFoo',
|
||||||
|
], 'fr'));
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
$provider->expects($this->any())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
// Create local bag, with a missing message.
|
||||||
|
$localTranslatorBag = new TranslatorBag();
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(), 'en'));
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(['note' => 'NOTE'], 'fr'), 'fr'));
|
||||||
|
|
||||||
|
$missingTranslatorBag = $providerReadTranslatorBag->diff($localTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('delete')
|
||||||
|
->with($missingTranslatorBag);
|
||||||
|
|
||||||
|
// Read provider translations again, after missing translations deletion,
|
||||||
|
// to avoid push freshly deleted translations.
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr'));
|
||||||
|
|
||||||
|
$provider->expects($this->any())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($localTranslatorBag->diff($providerReadTranslatorBag));
|
||||||
|
|
||||||
|
$provider->expects($this->exactly(2))
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--delete-missing' => true]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] Missing translations on "null" has been deleted (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
$this->assertStringContainsString('[OK] New local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPushForceAndDeleteMissingMessages()
|
||||||
|
{
|
||||||
|
$xliffLoader = new XliffFileLoader();
|
||||||
|
$arrayLoader = new ArrayLoader();
|
||||||
|
$locales = ['en', 'fr'];
|
||||||
|
$domains = ['messages'];
|
||||||
|
|
||||||
|
// Simulate existing messages on Provider.
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'obsolete.foo' => 'obsoleteFoo',
|
||||||
|
], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load([
|
||||||
|
'note' => 'NOTE',
|
||||||
|
'obsolete.foo' => 'obsolèteFoo',
|
||||||
|
], 'fr'));
|
||||||
|
|
||||||
|
$provider = $this->createMock(ProviderInterface::class);
|
||||||
|
$provider->expects($this->any())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
// Create local bag, with a missing message, an updated one and a new one.
|
||||||
|
$localTranslatorBag = new TranslatorBag();
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(['note' => 'NOTE UPDATED', 'note2' => 'NOTE 2']), 'en'));
|
||||||
|
$localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(['note' => 'NOTE MISE À JOUR', 'note2' => 'NOTE 2'], 'fr'), 'fr'));
|
||||||
|
|
||||||
|
$missingTranslatorBag = $providerReadTranslatorBag->diff($localTranslatorBag);
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('delete')
|
||||||
|
->with($missingTranslatorBag);
|
||||||
|
|
||||||
|
// Read provider translations again, after missing translations deletion,
|
||||||
|
// to avoid push freshly deleted translations.
|
||||||
|
$providerReadTranslatorBag = new TranslatorBag();
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en'));
|
||||||
|
$providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr'));
|
||||||
|
|
||||||
|
$provider->expects($this->any())
|
||||||
|
->method('read')
|
||||||
|
->with($domains, $locales)
|
||||||
|
->willReturn($providerReadTranslatorBag);
|
||||||
|
|
||||||
|
$translationBagToWrite = $localTranslatorBag->diff($providerReadTranslatorBag);
|
||||||
|
$translationBagToWrite->addBag($localTranslatorBag->intersect($providerReadTranslatorBag));
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($translationBagToWrite);
|
||||||
|
|
||||||
|
$provider->expects($this->exactly(2))
|
||||||
|
->method('__toString')
|
||||||
|
->willReturn('null://default');
|
||||||
|
|
||||||
|
$tester = $this->createCommandTester($provider, $locales, $domains);
|
||||||
|
|
||||||
|
$tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--force' => true, '--delete-missing' => true]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('[OK] Missing translations on "null" has been deleted (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
$this->assertStringContainsString('[OK] All local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createCommandTester(ProviderInterface $provider, array $locales = ['en'], array $domains = ['messages']): CommandTester
|
||||||
|
{
|
||||||
|
$reader = new TranslationReader();
|
||||||
|
$reader->addLoader('xlf', new XliffFileLoader());
|
||||||
|
|
||||||
|
$command = new TranslationPushCommand(
|
||||||
|
$this->getProviderCollection($provider, $locales, $domains),
|
||||||
|
$reader,
|
||||||
|
[$this->translationAppDir.'/translations'],
|
||||||
|
$locales
|
||||||
|
);
|
||||||
|
$application = new Application();
|
||||||
|
$application->add($command);
|
||||||
|
|
||||||
|
return new CommandTester($application->find('translation:push'));
|
||||||
|
}
|
||||||
|
}
|
@ -72,7 +72,7 @@ class TranslationPathsPassTest extends TestCase
|
|||||||
->setArguments([new Reference('.service_locator.bar')])
|
->setArguments([new Reference('.service_locator.bar')])
|
||||||
;
|
;
|
||||||
|
|
||||||
$pass = new TranslatorPathsPass('translator', 'console.command.translation_debug', 'console.command.translation_update', 'argument_resolver.service');
|
$pass = new TranslatorPathsPass();
|
||||||
$pass->process($container);
|
$pass->process($container);
|
||||||
|
|
||||||
$expectedPaths = [
|
$expectedPaths = [
|
||||||
|
@ -19,7 +19,7 @@ use Symfony\Component\Translation\Loader\XliffFileLoader;
|
|||||||
|
|
||||||
class XliffFileLoaderTest extends TestCase
|
class XliffFileLoaderTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testLoad()
|
public function testLoadFile()
|
||||||
{
|
{
|
||||||
$loader = new XliffFileLoader();
|
$loader = new XliffFileLoader();
|
||||||
$resource = __DIR__.'/../fixtures/resources.xlf';
|
$resource = __DIR__.'/../fixtures/resources.xlf';
|
||||||
@ -31,6 +31,42 @@ class XliffFileLoaderTest extends TestCase
|
|||||||
$this->assertContainsOnly('string', $catalogue->all('domain1'));
|
$this->assertContainsOnly('string', $catalogue->all('domain1'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLoadRawXliff()
|
||||||
|
{
|
||||||
|
$loader = new XliffFileLoader();
|
||||||
|
$resource = <<<XLIFF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||||
|
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="1">
|
||||||
|
<source>foo</source>
|
||||||
|
<target>bar</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2">
|
||||||
|
<source>extra</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3">
|
||||||
|
<source>key</source>
|
||||||
|
<target></target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4">
|
||||||
|
<source>test</source>
|
||||||
|
<target>with</target>
|
||||||
|
<note>note</note>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF;
|
||||||
|
|
||||||
|
$catalogue = $loader->load($resource, 'en', 'domain1');
|
||||||
|
|
||||||
|
$this->assertEquals('en', $catalogue->getLocale());
|
||||||
|
$this->assertSame([], libxml_get_errors());
|
||||||
|
$this->assertContainsOnly('string', $catalogue->all('domain1'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testLoadWithInternalErrorsEnabled()
|
public function testLoadWithInternalErrorsEnabled()
|
||||||
{
|
{
|
||||||
$internalErrors = libxml_use_internal_errors(true);
|
$internalErrors = libxml_use_internal_errors(true);
|
||||||
|
108
src/Symfony/Component/Translation/Tests/Provider/DsnTest.php
Normal file
108
src/Symfony/Component/Translation/Tests/Provider/DsnTest.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Tests\Provider;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Translation\Provider\Dsn;
|
||||||
|
|
||||||
|
class DsnTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider fromStringProvider
|
||||||
|
*/
|
||||||
|
public function testFromString(string $string, Dsn $expectedDsn): void
|
||||||
|
{
|
||||||
|
$actualDsn = Dsn::fromString($string);
|
||||||
|
|
||||||
|
$this->assertSame($expectedDsn->getScheme(), $actualDsn->getScheme());
|
||||||
|
$this->assertSame($expectedDsn->getHost(), $actualDsn->getHost());
|
||||||
|
$this->assertSame($expectedDsn->getPort(), $actualDsn->getPort());
|
||||||
|
$this->assertSame($expectedDsn->getUser(), $actualDsn->getUser());
|
||||||
|
$this->assertSame($expectedDsn->getPassword(), $actualDsn->getPassword());
|
||||||
|
$this->assertSame($expectedDsn->getPath(), $actualDsn->getPath());
|
||||||
|
$this->assertSame($expectedDsn->getOption('from'), $actualDsn->getOption('from'));
|
||||||
|
|
||||||
|
$this->assertSame($string, $actualDsn->getOriginalDsn());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromStringProvider(): iterable
|
||||||
|
{
|
||||||
|
yield 'simple dsn' => [
|
||||||
|
'scheme://localhost',
|
||||||
|
new Dsn('scheme', 'localhost', null, null, null, [], null),
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'dsn with user and pass' => [
|
||||||
|
'scheme://u$er:pa$s@localhost',
|
||||||
|
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', null, [], null),
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'dsn with user and pass and custom port' => [
|
||||||
|
'scheme://u$er:pa$s@localhost:8000',
|
||||||
|
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', [], null),
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'dsn with user and pass, custom port and custom path' => [
|
||||||
|
'scheme://u$er:pa$s@localhost:8000/channel',
|
||||||
|
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', [], '/channel'),
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'dsn with user and pass, custom port, custom path and custom options' => [
|
||||||
|
'scheme://u$er:pa$s@localhost:8000/channel?from=FROM',
|
||||||
|
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', ['from' => 'FROM'], '/channel'),
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'dsn with user and pass that contains an urlencoded character' => [
|
||||||
|
'scheme://u$er:p%2Fa$s@localhost',
|
||||||
|
new Dsn('scheme', 'localhost', 'u$er', 'p/a$s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider invalidDsnProvider
|
||||||
|
*/
|
||||||
|
public function testInvalidDsn(string $dsn, string $exceptionMessage): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage($exceptionMessage);
|
||||||
|
Dsn::fromString($dsn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invalidDsnProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
'some://',
|
||||||
|
'The "some://" translation provider DSN is invalid.',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
'//loco',
|
||||||
|
'The "//loco" translation provider DSN must contain a scheme.',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
'file:///some/path',
|
||||||
|
'The "file:///some/path" translation provider DSN must contain a host (use "default" by default).',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetOption(): void
|
||||||
|
{
|
||||||
|
$options = ['with_value' => 'some value', 'nullable' => null];
|
||||||
|
$dsn = new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', $options, '/channel');
|
||||||
|
|
||||||
|
$this->assertSame('some value', $dsn->getOption('with_value'));
|
||||||
|
$this->assertSame('default', $dsn->getOption('nullable', 'default'));
|
||||||
|
$this->assertSame('default', $dsn->getOption('not_existent_property', 'default'));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Tests\Provider;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
|
||||||
|
use Symfony\Component\Translation\Provider\Dsn;
|
||||||
|
use Symfony\Component\Translation\Provider\NullProvider;
|
||||||
|
use Symfony\Component\Translation\Provider\NullProviderFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
class NullProviderFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testCreateThrowsUnsupportedSchemeException()
|
||||||
|
{
|
||||||
|
$this->expectException(UnsupportedSchemeException::class);
|
||||||
|
|
||||||
|
(new NullProviderFactory())->create(new Dsn('foo', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreate()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(NullProvider::class, (new NullProviderFactory())->create(new Dsn('null', '')));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\Translation\Dumper\XliffFileDumper;
|
||||||
|
use Symfony\Component\Translation\Exception\IncompleteDsnException;
|
||||||
|
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
|
||||||
|
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||||
|
use Symfony\Component\Translation\Provider\Dsn;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderFactoryInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test case to ease testing a translation provider factory.
|
||||||
|
*
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*/
|
||||||
|
abstract class ProviderFactoryTestCase extends TestCase
|
||||||
|
{
|
||||||
|
protected $client;
|
||||||
|
protected $logger;
|
||||||
|
protected $defaultLocale;
|
||||||
|
protected $loader;
|
||||||
|
protected $xliffFileDumper;
|
||||||
|
|
||||||
|
abstract public function createFactory(): ProviderFactoryInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<array{0: bool, 1: string}>
|
||||||
|
*/
|
||||||
|
abstract public function supportsProvider(): iterable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<array{0: string, 1: string, 2: TransportInterface}>
|
||||||
|
*/
|
||||||
|
abstract public function createProvider(): iterable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<array{0: string, 1: string|null}>
|
||||||
|
*/
|
||||||
|
public function unsupportedSchemeProvider(): iterable
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<array{0: string, 1: string|null}>
|
||||||
|
*/
|
||||||
|
public function incompleteDsnProvider(): iterable
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider supportsProvider
|
||||||
|
*/
|
||||||
|
public function testSupports(bool $expected, string $dsn)
|
||||||
|
{
|
||||||
|
$factory = $this->createFactory();
|
||||||
|
|
||||||
|
$this->assertSame($expected, $factory->supports(Dsn::fromString($dsn)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider unsupportedSchemeProvider
|
||||||
|
*/
|
||||||
|
public function testUnsupportedSchemeException(string $dsn, string $message = null)
|
||||||
|
{
|
||||||
|
$factory = $this->createFactory();
|
||||||
|
|
||||||
|
$dsn = Dsn::fromString($dsn);
|
||||||
|
|
||||||
|
$this->expectException(UnsupportedSchemeException::class);
|
||||||
|
if (null !== $message) {
|
||||||
|
$this->expectExceptionMessage($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$factory->create($dsn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider createProvider
|
||||||
|
*/
|
||||||
|
public function testCreate(string $expected, string $dsn)
|
||||||
|
{
|
||||||
|
$factory = $this->createFactory();
|
||||||
|
$provider = $factory->create(Dsn::fromString($dsn));
|
||||||
|
|
||||||
|
$this->assertSame($expected, (string) $provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider incompleteDsnProvider
|
||||||
|
*/
|
||||||
|
public function testIncompleteDsnException(string $dsn, string $message = null)
|
||||||
|
{
|
||||||
|
$factory = $this->createFactory();
|
||||||
|
|
||||||
|
$dsn = Dsn::fromString($dsn);
|
||||||
|
|
||||||
|
$this->expectException(IncompleteDsnException::class);
|
||||||
|
if (null !== $message) {
|
||||||
|
$this->expectExceptionMessage($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$factory->create($dsn);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClient(): HttpClientInterface
|
||||||
|
{
|
||||||
|
return $this->client ?? $this->client = new MockHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLogger(): LoggerInterface
|
||||||
|
{
|
||||||
|
return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultLocale(): string
|
||||||
|
{
|
||||||
|
return $this->defaultLocale ?? $this->defaultLocale = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLoader(): LoaderInterface
|
||||||
|
{
|
||||||
|
return $this->loader ?? $this->loader = $this->createMock(LoaderInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getXliffFileDumper(): XliffFileDumper
|
||||||
|
{
|
||||||
|
return $this->xliffFileDumper ?? $this->xliffFileDumper = $this->createMock(XliffFileDumper::class);
|
||||||
|
}
|
||||||
|
}
|
75
src/Symfony/Component/Translation/Tests/ProviderTestCase.php
Normal file
75
src/Symfony/Component/Translation/Tests/ProviderTestCase.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\Translation\Bridge\Loco\Provider\LocoProvider;
|
||||||
|
use Symfony\Component\Translation\Dumper\XliffFileDumper;
|
||||||
|
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test case to ease testing a translation provider.
|
||||||
|
*
|
||||||
|
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||||
|
*/
|
||||||
|
abstract class ProviderTestCase extends TestCase
|
||||||
|
{
|
||||||
|
protected $client;
|
||||||
|
protected $logger;
|
||||||
|
protected $defaultLocale;
|
||||||
|
protected $loader;
|
||||||
|
protected $xliffFileDumper;
|
||||||
|
|
||||||
|
abstract public function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<array{0: string, 1: ProviderInterface}>
|
||||||
|
*/
|
||||||
|
abstract public function toStringProvider(): iterable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider toStringProvider
|
||||||
|
*/
|
||||||
|
public function testToString(LocoProvider $provider, string $expected)
|
||||||
|
{
|
||||||
|
$this->assertSame($expected, (string) $provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClient(): MockHttpClient
|
||||||
|
{
|
||||||
|
return $this->client ?? $this->client = new MockHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLoader(): LoaderInterface
|
||||||
|
{
|
||||||
|
return $this->loader ?? $this->loader = $this->createMock(LoaderInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLogger(): LoggerInterface
|
||||||
|
{
|
||||||
|
return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultLocale(): string
|
||||||
|
{
|
||||||
|
return $this->defaultLocale ?? $this->defaultLocale = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getXliffFileDumper(): XliffFileDumper
|
||||||
|
{
|
||||||
|
return $this->xliffFileDumper ?? $this->xliffFileDumper = $this->createMock(XliffFileDumper::class);
|
||||||
|
}
|
||||||
|
}
|
100
src/Symfony/Component/Translation/Tests/TranslatorBagTest.php
Normal file
100
src/Symfony/Component/Translation/Tests/TranslatorBagTest.php
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogue;
|
||||||
|
use Symfony\Component\Translation\TranslatorBag;
|
||||||
|
|
||||||
|
class TranslatorBagTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testAll()
|
||||||
|
{
|
||||||
|
$catalogue = new MessageCatalogue('en', $messages = ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]);
|
||||||
|
|
||||||
|
$bag = new TranslatorBag();
|
||||||
|
$bag->addCatalogue($catalogue);
|
||||||
|
|
||||||
|
$this->assertEquals(['en' => $messages], $this->getAllMessagesFromTranslatorBag($bag));
|
||||||
|
|
||||||
|
$messages = ['domain1+intl-icu' => ['foo' => 'bar']] + $messages + [
|
||||||
|
'domain2+intl-icu' => ['bar' => 'foo'],
|
||||||
|
'domain3+intl-icu' => ['biz' => 'biz'],
|
||||||
|
];
|
||||||
|
$catalogue = new MessageCatalogue('en', $messages);
|
||||||
|
|
||||||
|
$bag = new TranslatorBag();
|
||||||
|
$bag->addCatalogue($catalogue);
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'en' => [
|
||||||
|
'domain1' => ['foo' => 'bar'],
|
||||||
|
'domain2' => ['bar' => 'foo'],
|
||||||
|
'domain3' => ['biz' => 'biz'],
|
||||||
|
],
|
||||||
|
], $this->getAllMessagesFromTranslatorBag($bag));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDiff()
|
||||||
|
{
|
||||||
|
$catalogueA = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo', 'bar' => 'bar'], 'domain2' => ['baz' => 'baz', 'qux' => 'qux']]);
|
||||||
|
|
||||||
|
$bagA = new TranslatorBag();
|
||||||
|
$bagA->addCatalogue($catalogueA);
|
||||||
|
|
||||||
|
$catalogueB = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['baz' => 'baz', 'corge' => 'corge']]);
|
||||||
|
|
||||||
|
$bagB = new TranslatorBag();
|
||||||
|
$bagB->addCatalogue($catalogueB);
|
||||||
|
|
||||||
|
$bagResult = $bagA->diff($bagB);
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'en' => [
|
||||||
|
'domain1' => ['bar' => 'bar'],
|
||||||
|
'domain2' => ['qux' => 'qux'],
|
||||||
|
],
|
||||||
|
], $this->getAllMessagesFromTranslatorBag($bagResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIntersect()
|
||||||
|
{
|
||||||
|
$catalogueA = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo', 'bar' => 'bar'], 'domain2' => ['baz' => 'baz', 'qux' => 'qux']]);
|
||||||
|
|
||||||
|
$bagA = new TranslatorBag();
|
||||||
|
$bagA->addCatalogue($catalogueA);
|
||||||
|
|
||||||
|
$catalogueB = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo', 'baz' => 'baz'], 'domain2' => ['baz' => 'baz', 'corge' => 'corge']]);
|
||||||
|
|
||||||
|
$bagB = new TranslatorBag();
|
||||||
|
$bagB->addCatalogue($catalogueB);
|
||||||
|
|
||||||
|
$bagResult = $bagA->intersect($bagB);
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'en' => [
|
||||||
|
'domain1' => ['bar' => 'bar'],
|
||||||
|
'domain2' => ['qux' => 'qux'],
|
||||||
|
],
|
||||||
|
], $this->getAllMessagesFromTranslatorBag($bagResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAllMessagesFromTranslatorBag(TranslatorBag $translatorBag): array
|
||||||
|
{
|
||||||
|
$allMessages = [];
|
||||||
|
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||||
|
$allMessages[$catalogue->getLocale()] = $catalogue->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $allMessages;
|
||||||
|
}
|
||||||
|
}
|
@ -243,6 +243,14 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
|
|||||||
return $this->catalogues[$locale];
|
return $this->catalogues[$locale];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getCatalogues(): array
|
||||||
|
{
|
||||||
|
return array_values($this->catalogues);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the loaders.
|
* Gets the loaders.
|
||||||
*
|
*
|
||||||
|
105
src/Symfony/Component/Translation/TranslatorBag.php
Normal file
105
src/Symfony/Component/Translation/TranslatorBag.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Catalogue\AbstractOperation;
|
||||||
|
use Symfony\Component\Translation\Catalogue\TargetOperation;
|
||||||
|
|
||||||
|
final class TranslatorBag implements TranslatorBagInterface
|
||||||
|
{
|
||||||
|
/** @var MessageCatalogue[] */
|
||||||
|
private $catalogues = [];
|
||||||
|
|
||||||
|
public function addCatalogue(MessageCatalogue $catalogue): void
|
||||||
|
{
|
||||||
|
if (null !== $existingCatalogue = $this->getCatalogue($catalogue->getLocale())) {
|
||||||
|
$catalogue->addCatalogue($existingCatalogue);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->catalogues[$catalogue->getLocale()] = $catalogue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addBag(TranslatorBagInterface $bag): void
|
||||||
|
{
|
||||||
|
foreach ($bag->getCatalogues() as $catalogue) {
|
||||||
|
$this->addCatalogue($catalogue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getCatalogue(string $locale = null)
|
||||||
|
{
|
||||||
|
if (null === $locale || !isset($this->catalogues[$locale])) {
|
||||||
|
$this->catalogues[$locale] = new MessageCatalogue($locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->catalogues[$locale];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getCatalogues(): array
|
||||||
|
{
|
||||||
|
return array_values($this->catalogues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function diff(TranslatorBagInterface $diffBag): self
|
||||||
|
{
|
||||||
|
$diff = new self();
|
||||||
|
|
||||||
|
foreach ($this->catalogues as $locale => $catalogue) {
|
||||||
|
if (null === $diffCatalogue = $diffBag->getCatalogue($locale)) {
|
||||||
|
$diff->addCatalogue($catalogue);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operation = new TargetOperation($diffCatalogue, $catalogue);
|
||||||
|
$operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::NEW_BATCH);
|
||||||
|
$newCatalogue = new MessageCatalogue($locale);
|
||||||
|
|
||||||
|
foreach ($operation->getDomains() as $domain) {
|
||||||
|
$newCatalogue->add($operation->getNewMessages($domain), $domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff->addCatalogue($newCatalogue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function intersect(TranslatorBagInterface $intersectBag): self
|
||||||
|
{
|
||||||
|
$diff = new self();
|
||||||
|
|
||||||
|
foreach ($this->catalogues as $locale => $catalogue) {
|
||||||
|
if (null === $intersectCatalogue = $intersectBag->getCatalogue($locale)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operation = new TargetOperation($catalogue, $intersectCatalogue);
|
||||||
|
$operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::OBSOLETE_BATCH);
|
||||||
|
$obsoleteCatalogue = new MessageCatalogue($locale);
|
||||||
|
|
||||||
|
foreach ($operation->getDomains() as $domain) {
|
||||||
|
$obsoleteCatalogue->add($operation->getObsoleteMessages($domain), $domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff->addCatalogue($obsoleteCatalogue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
|||||||
/**
|
/**
|
||||||
* TranslatorBagInterface.
|
* TranslatorBagInterface.
|
||||||
*
|
*
|
||||||
|
* @method MessageCatalogueInterface[] getCatalogues() Returns all catalogues of the instance
|
||||||
|
*
|
||||||
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
||||||
*/
|
*/
|
||||||
interface TranslatorBagInterface
|
interface TranslatorBagInterface
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"symfony/dependency-injection": "^5.0",
|
"symfony/dependency-injection": "^5.0",
|
||||||
"symfony/http-kernel": "^5.0",
|
"symfony/http-kernel": "^5.0",
|
||||||
"symfony/intl": "^4.4|^5.0",
|
"symfony/intl": "^4.4|^5.0",
|
||||||
|
"symfony/polyfill-intl-icu": "^1.21",
|
||||||
"symfony/service-contracts": "^1.1.2|^2",
|
"symfony/service-contracts": "^1.1.2|^2",
|
||||||
"symfony/yaml": "^4.4|^5.0",
|
"symfony/yaml": "^4.4|^5.0",
|
||||||
"symfony/finder": "^4.4|^5.0",
|
"symfony/finder": "^4.4|^5.0",
|
||||||
|
Reference in New Issue
Block a user