diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index ee250f18f5dd..7da20f6394b1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -40,6 +42,10 @@ class TranslationUpdateCommand extends Command private const ASC = 'asc'; private const DESC = 'desc'; private const SORT_ORDERS = [self::ASC, self::DESC]; + private const FORMATS = [ + 'xlf12' => ['xlf', '1.2'], + 'xlf20' => ['xlf', '2.0'], + ]; protected static $defaultName = 'translation:update'; protected static $defaultDescription = 'Update the translation file'; @@ -52,8 +58,9 @@ class TranslationUpdateCommand extends Command private $defaultViewsPath; private $transPaths; private $codePaths; + private $enabledLocales; - public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = []) + public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = []) { parent::__construct(); @@ -65,6 +72,7 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade $this->defaultViewsPath = $defaultViewsPath; $this->transPaths = $transPaths; $this->codePaths = $codePaths; + $this->enabledLocales = $enabledLocales; } /** @@ -147,10 +155,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int 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'; + if (\in_array($format, array_keys(self::FORMATS), true)) { + [$format, $xliffVersion] = self::FORMATS[$format]; } // check format @@ -165,15 +171,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $kernel = $this->getApplication()->getKernel(); // Define Root Paths - $transPaths = $this->transPaths; - if ($this->defaultTransPath) { - $transPaths[] = $this->defaultTransPath; - } - $codePaths = $this->codePaths; - $codePaths[] = $kernel->getProjectDir().'/src'; - if ($this->defaultViewsPath) { - $codePaths[] = $this->defaultViewsPath; - } + $transPaths = $this->getRootTransPaths(); + $codePaths = $this->getRootCodePaths($kernel); + $currentName = 'default directory'; // Override with provided Bundle info @@ -206,24 +206,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->title('Translation Messages Extractor and Dumper'); $io->comment(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); - // load any messages from templates - $extractedCatalogue = new MessageCatalogue($input->getArgument('locale')); $io->comment('Parsing templates...'); - $this->extractor->setPrefix($input->getOption('prefix')); - foreach ($codePaths as $path) { - if (is_dir($path) || is_file($path)) { - $this->extractor->extract($path, $extractedCatalogue); - } - } + $extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $input->getOption('prefix')); - // load any existing messages from the translation files - $currentCatalogue = new MessageCatalogue($input->getArgument('locale')); $io->comment('Loading translation files...'); - foreach ($transPaths as $path) { - if (is_dir($path)) { - $this->reader->read($path, $currentCatalogue); - } - } + $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths); if (null !== $domain = $input->getOption('domain')) { $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); @@ -321,6 +308,60 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('locale')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + /** @var KernelInterface $kernel */ + $kernel = $this->getApplication()->getKernel(); + if ($input->mustSuggestArgumentValuesFor('bundle')) { + $bundles = []; + + foreach ($kernel->getBundles() as $bundle) { + $bundles[] = $bundle->getName(); + if ($bundle->getContainerExtension()) { + $bundles[] = $bundle->getContainerExtension()->getAlias(); + } + } + + $suggestions->suggestValues($bundles); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(array_merge( + $this->writer->getFormats(), + array_keys(self::FORMATS) + )); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domain') && $locale = $input->getArgument('locale')) { + $extractedCatalogue = $this->extractMessages($locale, $this->getRootCodePaths($kernel), $input->getOption('prefix')); + + $currentCatalogue = $this->loadCurrentMessages($locale, $this->getRootTransPaths()); + + // process catalogues + $operation = $input->getOption('clean') + ? new TargetOperation($currentCatalogue, $extractedCatalogue) + : new MergeOperation($currentCatalogue, $extractedCatalogue); + + $suggestions->suggestValues($operation->getDomains()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('sort')) { + $suggestions->suggestValues(self::SORT_ORDERS); + } + } + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue { $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); @@ -353,4 +394,50 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M return $filteredCatalogue; } + + private function extractMessages(string $locale, array $transPaths, string $prefix): MessageCatalogue + { + $extractedCatalogue = new MessageCatalogue($locale); + $this->extractor->setPrefix($prefix); + foreach ($transPaths as $path) { + if (is_dir($path) || is_file($path)) { + $this->extractor->extract($path, $extractedCatalogue); + } + } + + return $extractedCatalogue; + } + + private function loadCurrentMessages(string $locale, array $transPaths): MessageCatalogue + { + $currentCatalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + if (is_dir($path)) { + $this->reader->read($path, $currentCatalogue); + } + } + + return $currentCatalogue; + } + + private function getRootTransPaths(): array + { + $transPaths = $this->transPaths; + if ($this->defaultTransPath) { + $transPaths[] = $this->defaultTransPath; + } + + return $transPaths; + } + + private function getRootCodePaths(KernelInterface $kernel): array + { + $codePaths = $this->codePaths; + $codePaths[] = $kernel->getProjectDir().'/src'; + if ($this->defaultViewsPath) { + $codePaths[] = $this->defaultViewsPath; + } + + return $codePaths; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php index 81af31d7260a..e4db50c81536 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -236,6 +236,7 @@ null, // twig.default_path [], // Translator paths [], // Twig paths + param('kernel.enabled_locales'), ]) ->tag('console.command') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php new file mode 100644 index 000000000000..7e09a6ffad89 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php @@ -0,0 +1,150 @@ + + * + * 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 PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\ExtensionPresentBundle; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\Writer\TranslationWriter; + +class TranslationUpdateCommandCompletionTest extends TestCase +{ + private $fs; + private $translationDir; + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $tester = $this->createCommandCompletionTester(['messages' => ['foo' => 'foo']]); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + $bundle = new ExtensionPresentBundle(); + + yield 'locale' => [[''], ['en', 'fr']]; + yield 'bundle' => [['en', ''], [$bundle->getName(), $bundle->getContainerExtension()->getAlias()]]; + yield 'domain with locale' => [['en', '--domain=m'], ['messages']]; + yield 'domain without locale' => [['--domain=m'], []]; + yield 'format' => [['en', '--format='], ['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res', 'xlf12', 'xlf20']]; + yield 'sort' => [['en', '--sort='], ['asc', 'desc']]; + } + + protected function setUp(): void + { + $this->fs = new Filesystem(); + $this->translationDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true); + $this->fs->mkdir($this->translationDir.'/translations'); + $this->fs->mkdir($this->translationDir.'/templates'); + } + + protected function tearDown(): void + { + $this->fs->remove($this->translationDir); + } + + private function createCommandCompletionTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandCompletionTester + { + $translator = $this->createMock(Translator::class); + $translator + ->expects($this->any()) + ->method('getFallbackLocales') + ->willReturn(['en']); + + $extractor = $this->createMock(ExtractorInterface::class); + $extractor + ->expects($this->any()) + ->method('extract') + ->willReturnCallback( + function ($path, $catalogue) use ($extractedMessages) { + foreach ($extractedMessages as $domain => $messages) { + $catalogue->add($messages, $domain); + } + } + ); + + $loader = $this->createMock(TranslationReader::class); + $loader + ->expects($this->any()) + ->method('read') + ->willReturnCallback( + function ($path, $catalogue) use ($loadedMessages) { + $catalogue->add($loadedMessages); + } + ); + + $writer = $this->createMock(TranslationWriter::class); + $writer + ->expects($this->any()) + ->method('getFormats') + ->willReturn( + ['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res'] + ); + + if (null === $kernel) { + $returnValues = [ + ['foo', $this->getBundle($this->translationDir)], + ['test', $this->getBundle('test')], + ]; + $kernel = $this->createMock(KernelInterface::class); + $kernel + ->expects($this->any()) + ->method('getBundle') + ->willReturnMap($returnValues); + } + + $kernel + ->expects($this->any()) + ->method('getBundles') + ->willReturn([new ExtensionPresentBundle()]); + + $container = new Container(); + $kernel + ->expects($this->any()) + ->method('getContainer') + ->willReturn($container); + + $command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, ['en', 'fr']); + + $application = new Application($kernel); + $application->add($command); + + return new CommandCompletionTester($application->find('translation:update')); + } + + private function getBundle($path) + { + $bundle = $this->createMock(BundleInterface::class); + $bundle + ->expects($this->any()) + ->method('getPath') + ->willReturn($path) + ; + + return $bundle; + } +}