diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index e9a7186a4165e..19ce70b6be0ef 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.2 --- + * Add support for `--sort` option when extracting translations with `translation:extract` command and `--force` option * Add support for setting `headers` with `Symfony\Bundle\FrameworkBundle\Controller\TemplateController` * Add `--resolve-env-vars` option to `lint:container` command * Derivate `kernel.secret` from the decryption secret when its env var is not defined diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index 2f61c81d678bb..04f812146d167 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -76,7 +76,7 @@ protected function configure(): void new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'), new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'), new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'), - new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically (only works with --dump-messages)', 'asc'), + new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically'), 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'), ]) ->setHelp(<<<'EOF' @@ -100,7 +100,7 @@ protected function configure(): void You can sort the output with the --sort flag: php %command.full_name% --dump-messages --sort=asc en AcmeBundle - php %command.full_name% --dump-messages --sort=desc fr + php %command.full_name% --force --sort=desc fr You can dump a tree-like structure using the yaml format with --as-tree flag: @@ -207,6 +207,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $operation->moveMessagesToIntlDomainsIfPossible('new'); + if ($sort = $input->getOption('sort')) { + $sort = strtolower($sort); + if (!\in_array($sort, self::SORT_ORDERS, true)) { + $errorIo->error(['Wrong sort order', 'Supported formats are: '.implode(', ', self::SORT_ORDERS).'.']); + + return 1; + } + } + // show compiled list of messages if (true === $input->getOption('dump-messages')) { $extractedMessagesCount = 0; @@ -223,19 +232,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $domainMessagesCount = \count($list); - if ($sort = $input->getOption('sort')) { - $sort = strtolower($sort); - if (!\in_array($sort, self::SORT_ORDERS, true)) { - $errorIo->error(['Wrong sort order', 'Supported formats are: '.implode(', ', self::SORT_ORDERS).'.']); - - return 1; - } - - if (self::DESC === $sort) { - rsort($list); - } else { - sort($list); - } + if (self::DESC === $sort) { + rsort($list); + } else { + sort($list); } $io->section(\sprintf('Messages extracted for domain "%s" (%d message%s)', $domain, $domainMessagesCount, $domainMessagesCount > 1 ? 's' : '')); @@ -266,7 +266,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $bundleTransPath = end($transPaths); } - $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]); + $operationResult = $operation->getResult(); + if ($sort) { + $operationResult = $this->sortCatalogue($operationResult, $sort); + } + + $this->writer->write($operationResult, $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')) { $resultMessage .= ' and translation files were updated'; @@ -365,6 +370,54 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M return $filteredCatalogue; } + private function sortCatalogue(MessageCatalogue $catalogue, string $sort): MessageCatalogue + { + $sortedCatalogue = new MessageCatalogue($catalogue->getLocale()); + + foreach ($catalogue->getDomains() as $domain) { + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + if (self::DESC === $sort) { + krsort($intlMessages); + } elseif (self::ASC === $sort) { + ksort($intlMessages); + } + + $sortedCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + if (self::DESC === $sort) { + krsort($messages); + } elseif (self::ASC === $sort) { + ksort($messages); + } + + $sortedCatalogue->add($messages, $domain); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $sortedCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $sortedCatalogue->setMetadata($k, $v, $domain); + } + } + } + + foreach ($catalogue->getResources() as $resource) { + $sortedCatalogue->addResource($resource); + } + + return $sortedCatalogue; + } + private function extractMessages(string $locale, array $transPaths, string $prefix): MessageCatalogue { $extractedCatalogue = new MessageCatalogue($locale); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php index def589f58a646..2304e75addbef 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Reader\TranslationReader; use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Writer\TranslationWriter; @@ -103,11 +104,25 @@ public function testDumpMessagesForSpecificDomain() public function testWriteMessages() { - $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']], writerMessages: ['foo', 'test', 'bar']); $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--force' => true]); $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } + public function testWriteSortMessages() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']], writerMessages: ['bar', 'foo', 'test']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--force' => true, '--sort' => 'asc']); + $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); + } + + public function testWriteReverseSortedMessages() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']], writerMessages: ['test', 'foo', 'bar']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--force' => true, '--sort' => 'desc']); + $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); + } + public function testWriteMessagesInRootDirectory() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); @@ -175,7 +190,7 @@ protected function tearDown(): void $this->fs->remove($this->translationDir); } - private function createCommandTester($extractedMessages = [], $loadedMessages = [], ?KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandTester + private function createCommandTester($extractedMessages = [], $loadedMessages = [], ?KernelInterface $kernel = null, array $transPaths = [], array $codePaths = [], ?array $writerMessages = null): CommandTester { $translator = $this->createMock(Translator::class); $translator @@ -212,6 +227,16 @@ function ($path, $catalogue) use ($loadedMessages) { ->willReturn( ['xlf', 'yml', 'yaml'] ); + if (null !== $writerMessages) { + $writer + ->expects($this->any()) + ->method('write') + ->willReturnCallback( + function (MessageCatalogue $catalogue) use ($writerMessages) { + $this->assertSame($writerMessages, array_keys($catalogue->all()['messages'])); + } + ); + } if (null === $kernel) { $returnValues = [