diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index f6fb34737cd56..6f74b3ce36aa6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG * Deprecate `session.sid_length` and `session.sid_bits_per_character` config options * Add the ability to use an existing service as a lock/semaphore resource * Add support for configuring multiple serializer instances via the configuration + * Add a `translation:update-xliff-sources` command to use de default locale target in the `source` tag of XLIFF files 7.1 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 17c779276c379..ab7ea7974ef67 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1420,6 +1420,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container->removeDefinition('console.command.translation_pull'); $container->removeDefinition('console.command.translation_push'); $container->removeDefinition('console.command.translation_lint'); + $container->removeDefinition('console.command.translation_xliff_update_sources'); return; } @@ -1497,6 +1498,12 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container->getDefinition('console.command.translation_extract')->replaceArgument(6, $transPaths); } + if ($container->hasDefinition('console.command.translation_xliff_update_sources')) { + $container->getDefinition('console.command.translation_xliff_update_sources') + ->replaceArgument(3, array_merge($transPaths, [$config['default_path']])) + ; + } + if (null === $defaultDir) { // allow null } elseif ($container->fileExists($defaultDir)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php index 9df82e20e2c28..4649e3c3cbaee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -58,6 +58,7 @@ use Symfony\Component\Translation\Command\TranslationPullCommand; use Symfony\Component\Translation\Command\TranslationPushCommand; use Symfony\Component\Translation\Command\XliffLintCommand; +use Symfony\Component\Translation\Command\XliffUpdateSourcesCommand; use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand; return static function (ContainerConfigurator $container) { @@ -306,6 +307,16 @@ ]) ->tag('console.command', ['command' => 'translation:push']) + ->set('console.command.translation_xliff_update_sources', XliffUpdateSourcesCommand::class) + ->args([ + service('translation.writer'), + service('translation.reader'), + param('kernel.default_locale'), + [], // Translator paths + param('kernel.enabled_locales'), + ]) + ->tag('console.command') + ->set('console.command.workflow_dump', WorkflowDumpCommand::class) ->args([ tagged_locator('workflow', 'name'), diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index 0d9382a216ff5..cd78202b604a0 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Deprecate passing an escape character to `CsvFileLoader::setCsvControl()` * Make Xliff 2.0 attributes in segment element available as `segment-attributes` metadata returned by `XliffFileLoader` and make `XliffFileDumper` write them to the file + * `XliffFileDumper` now uses the `source` metadata to create the `source` tag if available 7.1 --- diff --git a/src/Symfony/Component/Translation/Command/XliffUpdateSourcesCommand.php b/src/Symfony/Component/Translation/Command/XliffUpdateSourcesCommand.php new file mode 100644 index 0000000000000..90bc27cb81f32 --- /dev/null +++ b/src/Symfony/Component/Translation/Command/XliffUpdateSourcesCommand.php @@ -0,0 +1,203 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\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\MessageCatalogue; +use Symfony\Component\Translation\MetadataAwareInterface; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; + +/** + * @author Nicolas Rigaud + */ +#[AsCommand(name: 'translation:update-xliff-sources', description: 'Update source tags with default locale targets in XLIFF files.')] +class XliffUpdateSourcesCommand extends Command +{ + use TranslationTrait; + + private const FORMATS = [ + 'xlf12' => ['xlf', '1.2'], + 'xlf20' => ['xlf', '2.0'], + ]; + + public function __construct( + private readonly TranslationWriterInterface $writer, + private readonly TranslationReaderInterface $reader, + private readonly string $defaultLocale, + private readonly array $transPaths = [], + private readonly array $enabledLocales = [], + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDefinition([ + new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths to look for translations.'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format.', 'xlf12'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domain to update.'), + new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locale to update.', $this->enabledLocales), + ]) + ->setHelp(<<%command.name% command updates the source tags in XLIFF files with the default translation locale if available. + +You can specify directories to update in the command arguments: + + php %command.full_name% path/to/dir path/to/another/dir + +To restrict the updates to one or more locales, including the default locale itself, use the --locales option: + + php %command.full_name% --locales en --locales fr + +You can specify one or more domains to target with the --domains option. By default, all available domains for the targeted locales are used. + + php %command.full_name% --domains messages + +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $format = $input->getOption('format'); + + if (!\array_key_exists($format, self::FORMATS)) { + $io->error(\sprintf('Unknown format "%s". Available formats are: %s.', $format, implode(', ', array_map(fn ($f) => '"'.$f.'"', array_keys(self::FORMATS))))); + + return self::INVALID; + } + + [$format, $xliffVersion] = self::FORMATS[$format]; + + $locales = $input->getOption('locales'); + + if (!$locales) { + $io->error('No locales provided in --locales options and no defaults provided to the command.'); + + return self::INVALID; + } + + $transPaths = $input->getArgument('paths') ?: $this->transPaths; + + if (!$transPaths) { + $io->error('No paths specified in arguments, and no default paths provided to the command.'); + + return self::INVALID; + } + + $domains = $input->getOption('domains'); + + $io->title('XLIFF Source Tag Updater'); + + foreach ($transPaths as $transPath) { + $io->comment(\sprintf('Updating XLIFF files in %s...', $transPath)); + + $translatorBag = $this->readLocalTranslations(array_unique(array_merge($locales, [$this->defaultLocale])), $domains, [$transPath]); + + $defaultLocaleCatalogue = $translatorBag->getCatalogue($this->defaultLocale); + + if (!$defaultLocaleCatalogue instanceof MetadataAwareInterface) { + $io->error(\sprintf('The default locale catalogue must implement "%s" to be used by this command.', MetadataAwareInterface::class)); + + return self::FAILURE; + } + + foreach ($locales as $locale) { + $currentCatalogue = $translatorBag->getCatalogue($locale); + + if (!$currentCatalogue instanceof MessageCatalogue) { + $io->warning(\sprintf('The catalogue for locale "%s" must be an instance of "%s" to be used by this command.', $locale, MessageCatalogue::class)); + + continue; + } + + if (!\count($currentCatalogue->getDomains())) { + $io->warning(\sprintf('No messages found for locale "%s".', $locale)); + + continue; + } + + $updateSourceCount = 0; + + foreach ($currentCatalogue->getDomains() as $domain) { + // Update source metadata with default locale target for each message in result catalogue + foreach ($currentCatalogue->all($domain) as $key => $value) { + if (!$defaultLocaleCatalogue->has($key, $domain)) { + continue; + } + + $resultMetadata = $currentCatalogue->getMetadata($key, $domain); + $defaultTranslation = $defaultLocaleCatalogue->get($key, $domain); + if (!isset($resultMetadata['source']) || $resultMetadata['source'] !== $defaultTranslation) { + ++$updateSourceCount; + $resultMetadata['source'] = $defaultTranslation; + $currentCatalogue->setMetadata($key, $resultMetadata, $domain); + } + } + } + + $this->writer->write($currentCatalogue, $format, ['path' => $transPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $xliffVersion]); + + if (0 === $updateSourceCount) { + $message = \sprintf('All source tags are already up-to-date for locale "%s".', $locale); + } else { + $message = \sprintf('Updated %d source tag%s for locale "%s".', $updateSourceCount, $updateSourceCount > 1 ? 's' : '', $locale); + } + + $io->info($message); + } + } + + $io->success('Operation succeeded.'); + + return self::SUCCESS; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('locales')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(array_keys(self::FORMATS)); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domains') && $locales = $input->getOption('locales')) { + $suggestedDomains = []; + $translatorBag = $this->readLocalTranslations($locales, [], $input->getArgument('paths') ?: $this->transPaths); + foreach ($translatorBag->getCatalogues() as $catalogue) { + array_push($suggestedDomains, ...$catalogue->getDomains()); + } + if ($suggestedDomains) { + $suggestions->suggestValues(array_unique($suggestedDomains)); + } + } + } +} diff --git a/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php b/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php index b41394c193789..f2c17c0d4aa43 100644 --- a/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php @@ -96,14 +96,15 @@ private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ? $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); $translation->setAttribute('resname', $source); + $metadata = $messages->getMetadata($source, $domain); + $s = $translation->appendChild($dom->createElement('source')); - $s->appendChild($dom->createTextNode($source)); + $s->appendChild($dom->createTextNode($metadata['source'] ?? $source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); $targetElement = $dom->createElement('target'); - $metadata = $messages->getMetadata($source, $domain); if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { foreach ($metadata['target-attributes'] as $name => $value) { $targetElement->setAttribute($name, $value); @@ -141,7 +142,6 @@ private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ? { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; - $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); $xliff->setAttribute('version', '2.0'); @@ -200,7 +200,7 @@ private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ? } $s = $segment->appendChild($dom->createElement('source')); - $s->appendChild($dom->createTextNode($source)); + $s->appendChild($dom->createTextNode($metadata['source'] ?? $source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); diff --git a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php index e76245dac49fa..0ca239748820a 100644 --- a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php @@ -172,6 +172,9 @@ private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, s $catalogue->set((string) $source, $target, $domain); $metadata = []; + + $metadata['source'] = (string) $segment->source; + if ($segment->attributes()) { $metadata['segment-attributes'] = []; foreach ($segment->attributes() as $key => $value) { diff --git a/src/Symfony/Component/Translation/Tests/Command/XliffUpdateSourcesCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/XliffUpdateSourcesCommandTest.php new file mode 100644 index 0000000000000..a6e619a4c3455 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Command/XliffUpdateSourcesCommandTest.php @@ -0,0 +1,331 @@ + + * + * 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 Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandCompletionTester; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Translation\Command\XliffUpdateSourcesCommand; +use Symfony\Component\Translation\Dumper\XliffFileDumper; +use Symfony\Component\Translation\Loader\XliffFileLoader; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriter; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; + +class XliffUpdateSourcesCommandTest extends TestCase +{ + private string $translationAppDir; + + private Filesystem $fs; + + protected function setUp(): void + { + $this->fs = new Filesystem(); + $this->translationAppDir = \sprintf('%s/translation-xliff-update-source-test', sys_get_temp_dir()); + $this->fs->mkdir(\sprintf('%s/translations', $this->translationAppDir)); + } + + protected function tearDown(): void + { + $this->fs->remove($this->translationAppDir); + } + + public function testSourceTagsAreUpdatedInXliff1() + { + $originalEnContent = $this->createXliff1('en', 'foo', 'foo', 'bar-en'); + $enFile = $this->createFile($originalEnContent, 'messages.en.xlf'); + + $originalFrContent = $this->createXliff1('fr', 'foo', 'foo', 'bar-fr'); + $frFile = $this->createFile($originalFrContent, 'messages.fr.xlf'); + + $tester = new CommandTester($this->createCommand(enabledLocales: ['en', 'fr'])); + $tester->execute([]); + + $tester->assertCommandIsSuccessful(); + + // All locales should be updated + $expectedEnContent = $this->createXliff1('en', 'foo', 'bar-en', 'bar-en'); + $this->assertStringEqualsFile($enFile, $expectedEnContent); + + $expectedFrContent = $this->createXliff1('fr', 'foo', 'bar-en', 'bar-fr'); + $this->assertStringEqualsFile($frFile, $expectedFrContent); + } + + public function testSourceTagsAreUpdatedInXliff2() + { + $originalEnContent = $this->createXliff2('en', 'foo', 'foo', 'bar-en'); + $enFile = $this->createFile($originalEnContent, 'messages.en.xlf'); + + $originalFrContent = $this->createXliff2('fr', 'foo', 'foo', 'bar-fr'); + $frFile = $this->createFile($originalFrContent, 'messages.fr.xlf'); + + $tester = new CommandTester($this->createCommand(enabledLocales: ['en', 'fr'])); + $tester->execute(['--format' => 'xlf20']); + + $tester->assertCommandIsSuccessful(); + + // All locales should be updated + $expectedEnContent = $this->createXliff2('en', 'foo', 'bar-en', 'bar-en'); + $this->assertStringEqualsFile($enFile, $expectedEnContent); + + $expectedFrContent = $this->createXliff2('fr', 'foo', 'bar-en', 'bar-fr'); + $this->assertStringEqualsFile($frFile, $expectedFrContent); + } + + public function testFilesAreUpdatedOnlyForSpecifiedLocales() + { + $originalEnContent = $this->createXliff1('en', 'foo', 'foo', 'bar-en'); + $enFile = $this->createFile($originalEnContent, 'messages.en.xlf'); + + $originalFrContent = $this->createXliff1('fr', 'foo', 'foo', 'bar-fr'); + $frFile = $this->createFile($originalFrContent, 'messages.fr.xlf'); + + $tester = new CommandTester($this->createCommand()); + $tester->execute(['--locales' => ['fr']]); + + $tester->assertCommandIsSuccessful(); + + // Locale fr should be updated + $expectedFrContent = $this->createXliff1('fr', 'foo', 'bar-en', 'bar-fr'); + $this->assertStringEqualsFile($frFile, $expectedFrContent); + + // Default locale shouldn't be updated + $this->assertStringEqualsFile($enFile, $originalEnContent); + } + + public function testFilesAreUpdatedInOtherTranslationPaths() + { + $otherPath = \sprintf('%s/other', $this->translationAppDir); + $this->fs->mkdir($otherPath); + + $originalContent = $this->createXliff1('en', 'foo', 'foo', 'bar-en'); + $enFileInOtherDir = $this->createFile($originalContent, 'messages.en.xlf', 'other'); + $enFileInDefaultDir = $this->createFile($originalContent, 'messages.en.xlf'); + + $tester = new CommandTester($this->createCommand([$otherPath])); + $tester->execute(['--locales' => ['en']]); + + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString(\sprintf('Updating XLIFF files in %s...', $otherPath), $tester->getDisplay()); + + $expectedContent = $this->createXliff1('en', 'foo', 'bar-en', 'bar-en'); + + // other/messages.en.xlf should be updated + $this->assertStringEqualsFile($enFileInOtherDir, $expectedContent); + + // translations/messages.en.xlf should be updated + $this->assertStringContainsString(\sprintf('Updating XLIFF files in %s/translations...', $this->translationAppDir), $tester->getDisplay()); + $this->assertStringEqualsFile($enFileInDefaultDir, $expectedContent); + } + + public function testFilesAreUpdatedOnlyForPathsArgument() + { + $fooDir = \sprintf('%s/foo', $this->translationAppDir); + $barDir = \sprintf('%s/bar', $this->translationAppDir); + + $this->fs->mkdir([$fooDir, $barDir]); + + $originalContent = $this->createXliff1('en', 'foo', 'foo', 'bar-en'); + $fileInFooDir = $this->createFile($originalContent, 'messages.en.xlf', 'foo'); + $fileInDefaultDir = $this->createFile($originalContent, 'messages.en.xlf'); + + $tester = new CommandTester($this->createCommand()); + $tester->execute(['paths' => [$fooDir], '--locales' => ['en']]); + + $tester->assertCommandIsSuccessful(); + + // foo/messages.en.xlf should be updated + $updatedContent = $this->createXliff1('en', 'foo', 'bar-en', 'bar-en'); + $this->assertStringContainsString(\sprintf('Updating XLIFF files in %s...', $fooDir), $tester->getDisplay()); + $this->assertStringEqualsFile($fileInFooDir, $updatedContent); + + // translations/messages.en.xlf shouldn't be updated + $this->assertStringNotContainsString(\sprintf('Updating XLIFF files in %s/translations...', $this->translationAppDir), $tester->getDisplay()); + $this->assertStringEqualsFile($fileInDefaultDir, $originalContent); + } + + public function testCommandFailsIfNoTranslationPathIsAvailable() + { + $command = new XliffUpdateSourcesCommand( + $this->createMock(TranslationWriterInterface::class), + $this->createMock(TranslationReaderInterface::class), + defaultLocale: 'en', + transPaths: [] + ); + + $tester = new CommandTester($command); + + $tester->execute(['--locales' => ['en']]); + + $this->assertEquals(Command::INVALID, $tester->getStatusCode()); + $this->assertStringContainsString('No paths specified in arguments, and no default paths provided to the command.', $tester->getDisplay()); + } + + public function testNoLocalesOptionsFailsIfNoEnabledLocalesAreAvailable() + { + $command = new XliffUpdateSourcesCommand( + $this->createMock(TranslationWriterInterface::class), + $this->createMock(TranslationReaderInterface::class), + defaultLocale: 'en', + transPaths: ['some/path'], + enabledLocales: [] + ); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertStringContainsString('No locales provided in --locales options and no defaults provided to the command.', $tester->getDisplay()); + $this->assertEquals(Command::INVALID, $tester->getStatusCode()); + } + + public function testProcessAllDomainsByDefault() + { + $originalContent = $this->createXliff1('en', 'foo', 'foo', 'bar-en'); + $messagesEnFile = $this->createFile($originalContent, 'messages.en.xlf'); + $othersEnFile = $this->createFile($originalContent, 'others.en.xlf'); + + $tester = new CommandTester($this->createCommand()); + $tester->execute(['--locales' => ['en']]); + + $tester->assertCommandIsSuccessful(); + + // All files should be updated + $expected = $this->createXliff1('en', 'foo', 'bar-en', 'bar-en'); + $this->assertStringEqualsFile($messagesEnFile, $expected); + $this->assertStringEqualsFile($othersEnFile, $expected); + } + + public function testOnlyProcessDomainsSpecifiedInOptions() + { + $originalContent = $this->createXliff1('en', 'foo', 'foo', 'bar-en'); + $messagesEnFile = $this->createFile($originalContent, 'messages.en.xlf'); + $othersEnFile = $this->createFile($originalContent, 'others.en.xlf'); + + $tester = new CommandTester($this->createCommand()); + $tester->execute(['--locales' => ['en'], '--domains' => ['others']]); + + $tester->assertCommandIsSuccessful(); + + // messages.en.xlf shouldn't be updated + $this->assertStringEqualsFile($messagesEnFile, $originalContent); + + // others.en.xlf should be updated + $expectedContent = $this->createXliff1('en', 'foo', 'bar-en', 'bar-en'); + $this->assertStringEqualsFile($othersEnFile, $expectedContent); + } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $domainsByLocale = [ + 'en' => ['messages', 'others'], + 'fr' => ['messages'], + 'it' => ['validators'], + ]; + + foreach ($domainsByLocale as $locale => $domains) { + foreach ($domains as $domain) { + $this->createFile($this->createXliff1($locale, 'foo', 'foo', 'bar'), \sprintf('%s.%s.xlf', $domain, $locale)); + } + } + + $application = new Application(); + $application->add($this->createCommand(enabledLocales: array_keys($domainsByLocale))); + + $tester = new CommandCompletionTester($application->get('translation:update-xliff-sources')); + $suggestions = $tester->complete($input); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public static function provideCompletionSuggestions(): \Generator + { + yield '--locales' => [ + ['--locales'], + ['en', 'fr', 'it'], + ]; + + yield '--domains' => [ + ['--locales', 'fr', '--locales', 'it', '--domains'], + ['messages', 'validators'], + ]; + } + + private function createCommand(array $transPaths = [], array $enabledLocales = []): Command + { + $application = new Application(); + + $reader = new TranslationReader(); + $reader->addLoader('xlf', new XliffFileLoader()); + $writer = new TranslationWriter(); + $writer->addDumper('xlf', new XliffFileDumper()); + $transPaths[] = \sprintf('%s/translations', $this->translationAppDir); + + $command = new XliffUpdateSourcesCommand($writer, $reader, 'en', $transPaths, $enabledLocales); + $application->add($command); + + return $command; + } + + private function createXliff1(string $locale, string $resname, string $source, string $target): string + { + return << + + +
+ +
+ + + {$source} + {$target} + + +
+
+ +XLIFF; + } + + private function createXliff2(string $locale, string $name, string $source, string $target, string $domain = 'messages'): string + { + return << + + + + + {$source} + {$target} + + + + + +XLIFF; + } + + private function createFile(string $content, string $filename, string $directory = 'translations'): string + { + $filename = \sprintf('%s/%s/%s', $this->translationAppDir, $directory, $filename); + file_put_contents($filename, $content); + + return $filename; + } +} diff --git a/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php index 73e6681716def..47fc56c4440f0 100644 --- a/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php +++ b/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php @@ -165,4 +165,20 @@ public function testFormatCatalogueXliff2WithSegmentAttributes() $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) ); } + + public function testFormatCatalogueWithSourceMetadata() + { + $catalogue = new MessageCatalogue('en_US'); + $catalogue->add([ + 'foo' => 'bar', + ]); + $catalogue->setMetadata('foo', ['source' => 'foo_source']); + + $dumper = new XliffFileDumper('xliff'); + + $this->assertStringEqualsFile( + __DIR__.'/../Fixtures/resources-source-meta.xliff', + $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR']) + ); + } } diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-source-meta.xliff b/src/Symfony/Component/Translation/Tests/Fixtures/resources-source-meta.xliff new file mode 100644 index 0000000000000..8ac4f7ef4435c --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-source-meta.xliff @@ -0,0 +1,14 @@ + + + +
+ +
+ + + foo_source + bar + + +
+
diff --git a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php index af20f9e7a5e1e..483747efa551f 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php @@ -254,8 +254,15 @@ public function testLoadVersion2() $this->assertCount(3, $domains['domain1']); $this->assertContainsOnly('string', $catalogue->all('domain1')); + $metadata = $catalogue->getMetadata('bar', 'domain1'); + // target attributes - $this->assertEquals(['target-attributes' => ['order' => 1]], $catalogue->getMetadata('bar', 'domain1')); + $this->assertArrayHasKey('target-attributes', $metadata); + $this->assertEquals(['order' => 1], $metadata['target-attributes']); + + // source metadata + $this->assertArrayHasKey('source', $metadata); + $this->assertEquals('bar', $metadata['source']); } public function testLoadVersion2WithNoteMeta()