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;
+ }
+}