Skip to content

Commit 375a89a

Browse files
committed
feature #43676 [FrameworkBundle] Add completion feature on translation:update command (stephenkhoo)
This PR was squashed before being merged into the 5.4 branch. Discussion ---------- [FrameworkBundle] Add completion feature on translation:update command | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Part of #43594 | License | MIT | Doc PR | - Adding completion for translation:update. - [x] locale - [X] bundle - [X] --format - [X] --domain - [X] --sort Test for - [x] locale - [x] bundle - [X] --format - [X] --domain - [X] --sort Locale completion still under discussion in #43644 (review) Locale and bundle test still not complete Commits ------- 2f301ae [FrameworkBundle] Add completion feature on translation:update command
2 parents 3f0f0ce + 2f301ae commit 375a89a

File tree

3 files changed

+267
-29
lines changed

3 files changed

+267
-29
lines changed

src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php

+116-29
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\Bundle\FrameworkBundle\Command;
1313

1414
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Completion\CompletionInput;
16+
use Symfony\Component\Console\Completion\CompletionSuggestions;
1517
use Symfony\Component\Console\Exception\InvalidArgumentException;
1618
use Symfony\Component\Console\Input\InputArgument;
1719
use Symfony\Component\Console\Input\InputInterface;
@@ -41,6 +43,10 @@ class TranslationUpdateCommand extends Command
4143
private const ASC = 'asc';
4244
private const DESC = 'desc';
4345
private const SORT_ORDERS = [self::ASC, self::DESC];
46+
private const FORMATS = [
47+
'xlf12' => ['xlf', '1.2'],
48+
'xlf20' => ['xlf', '2.0'],
49+
];
4450

4551
protected static $defaultName = 'translation:extract|translation:update';
4652
protected static $defaultDescription = 'Extract missing translations keys from code to translation files.';
@@ -53,8 +59,9 @@ class TranslationUpdateCommand extends Command
5359
private $defaultViewsPath;
5460
private $transPaths;
5561
private $codePaths;
62+
private $enabledLocales;
5663

57-
public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [])
64+
public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = [])
5865
{
5966
parent::__construct();
6067

@@ -66,6 +73,7 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade
6673
$this->defaultViewsPath = $defaultViewsPath;
6774
$this->transPaths = $transPaths;
6875
$this->codePaths = $codePaths;
76+
$this->enabledLocales = $enabledLocales;
6977
}
7078

7179
/**
@@ -155,10 +163,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
155163
trigger_deprecation('symfony/framework-bundle', '5.3', 'The "--output-format" option is deprecated, use "--format=xlf%d" instead.', 10 * $xliffVersion);
156164
}
157165

158-
switch ($format) {
159-
case 'xlf20': $xliffVersion = '2.0';
160-
// no break
161-
case 'xlf12': $format = 'xlf';
166+
if (\in_array($format, array_keys(self::FORMATS), true)) {
167+
[$format, $xliffVersion] = self::FORMATS[$format];
162168
}
163169

164170
// check format
@@ -173,15 +179,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
173179
$kernel = $this->getApplication()->getKernel();
174180

175181
// Define Root Paths
176-
$transPaths = $this->transPaths;
177-
if ($this->defaultTransPath) {
178-
$transPaths[] = $this->defaultTransPath;
179-
}
180-
$codePaths = $this->codePaths;
181-
$codePaths[] = $kernel->getProjectDir().'/src';
182-
if ($this->defaultViewsPath) {
183-
$codePaths[] = $this->defaultViewsPath;
184-
}
182+
$transPaths = $this->getRootTransPaths();
183+
$codePaths = $this->getRootCodePaths($kernel);
184+
185185
$currentName = 'default directory';
186186

187187
// Override with provided Bundle info
@@ -214,24 +214,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
214214
$io->title('Translation Messages Extractor and Dumper');
215215
$io->comment(sprintf('Generating "<info>%s</info>" translation files for "<info>%s</info>"', $input->getArgument('locale'), $currentName));
216216

217-
// load any messages from templates
218-
$extractedCatalogue = new MessageCatalogue($input->getArgument('locale'));
219217
$io->comment('Parsing templates...');
220-
$this->extractor->setPrefix($input->getOption('prefix'));
221-
foreach ($codePaths as $path) {
222-
if (is_dir($path) || is_file($path)) {
223-
$this->extractor->extract($path, $extractedCatalogue);
224-
}
225-
}
218+
$extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $input->getOption('prefix'));
226219

227-
// load any existing messages from the translation files
228-
$currentCatalogue = new MessageCatalogue($input->getArgument('locale'));
229220
$io->comment('Loading translation files...');
230-
foreach ($transPaths as $path) {
231-
if (is_dir($path)) {
232-
$this->reader->read($path, $currentCatalogue);
233-
}
234-
}
221+
$currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths);
235222

236223
if (null !== $domain = $input->getOption('domain')) {
237224
$currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain);
@@ -329,6 +316,60 @@ protected function execute(InputInterface $input, OutputInterface $output): int
329316
return 0;
330317
}
331318

319+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
320+
{
321+
if ($input->mustSuggestArgumentValuesFor('locale')) {
322+
$suggestions->suggestValues($this->enabledLocales);
323+
324+
return;
325+
}
326+
327+
/** @var KernelInterface $kernel */
328+
$kernel = $this->getApplication()->getKernel();
329+
if ($input->mustSuggestArgumentValuesFor('bundle')) {
330+
$bundles = [];
331+
332+
foreach ($kernel->getBundles() as $bundle) {
333+
$bundles[] = $bundle->getName();
334+
if ($bundle->getContainerExtension()) {
335+
$bundles[] = $bundle->getContainerExtension()->getAlias();
336+
}
337+
}
338+
339+
$suggestions->suggestValues($bundles);
340+
341+
return;
342+
}
343+
344+
if ($input->mustSuggestOptionValuesFor('format')) {
345+
$suggestions->suggestValues(array_merge(
346+
$this->writer->getFormats(),
347+
array_keys(self::FORMATS)
348+
));
349+
350+
return;
351+
}
352+
353+
if ($input->mustSuggestOptionValuesFor('domain') && $locale = $input->getArgument('locale')) {
354+
$extractedCatalogue = $this->extractMessages($locale, $this->getRootCodePaths($kernel), $input->getOption('prefix'));
355+
356+
$currentCatalogue = $this->loadCurrentMessages($locale, $this->getRootTransPaths());
357+
358+
// process catalogues
359+
$operation = $input->getOption('clean')
360+
? new TargetOperation($currentCatalogue, $extractedCatalogue)
361+
: new MergeOperation($currentCatalogue, $extractedCatalogue);
362+
363+
$suggestions->suggestValues($operation->getDomains());
364+
365+
return;
366+
}
367+
368+
if ($input->mustSuggestOptionValuesFor('sort')) {
369+
$suggestions->suggestValues(self::SORT_ORDERS);
370+
}
371+
}
372+
332373
private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
333374
{
334375
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
@@ -361,4 +402,50 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M
361402

362403
return $filteredCatalogue;
363404
}
405+
406+
private function extractMessages(string $locale, array $transPaths, string $prefix): MessageCatalogue
407+
{
408+
$extractedCatalogue = new MessageCatalogue($locale);
409+
$this->extractor->setPrefix($prefix);
410+
foreach ($transPaths as $path) {
411+
if (is_dir($path) || is_file($path)) {
412+
$this->extractor->extract($path, $extractedCatalogue);
413+
}
414+
}
415+
416+
return $extractedCatalogue;
417+
}
418+
419+
private function loadCurrentMessages(string $locale, array $transPaths): MessageCatalogue
420+
{
421+
$currentCatalogue = new MessageCatalogue($locale);
422+
foreach ($transPaths as $path) {
423+
if (is_dir($path)) {
424+
$this->reader->read($path, $currentCatalogue);
425+
}
426+
}
427+
428+
return $currentCatalogue;
429+
}
430+
431+
private function getRootTransPaths(): array
432+
{
433+
$transPaths = $this->transPaths;
434+
if ($this->defaultTransPath) {
435+
$transPaths[] = $this->defaultTransPath;
436+
}
437+
438+
return $transPaths;
439+
}
440+
441+
private function getRootCodePaths(KernelInterface $kernel): array
442+
{
443+
$codePaths = $this->codePaths;
444+
$codePaths[] = $kernel->getProjectDir().'/src';
445+
if ($this->defaultViewsPath) {
446+
$codePaths[] = $this->defaultViewsPath;
447+
}
448+
449+
return $codePaths;
450+
}
364451
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

+1
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@
234234
null, // twig.default_path
235235
[], // Translator paths
236236
[], // Twig paths
237+
param('kernel.enabled_locales'),
237238
])
238239
->tag('console.command')
239240

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\Command;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand;
16+
use Symfony\Bundle\FrameworkBundle\Console\Application;
17+
use Symfony\Component\Console\Tester\CommandCompletionTester;
18+
use Symfony\Component\DependencyInjection\Container;
19+
use Symfony\Component\Filesystem\Filesystem;
20+
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
21+
use Symfony\Component\HttpKernel\KernelInterface;
22+
use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\ExtensionPresentBundle;
23+
use Symfony\Component\Translation\Extractor\ExtractorInterface;
24+
use Symfony\Component\Translation\Reader\TranslationReader;
25+
use Symfony\Component\Translation\Translator;
26+
use Symfony\Component\Translation\Writer\TranslationWriter;
27+
28+
class TranslationUpdateCommandCompletionTest extends TestCase
29+
{
30+
private $fs;
31+
private $translationDir;
32+
33+
/**
34+
* @dataProvider provideCompletionSuggestions
35+
*/
36+
public function testComplete(array $input, array $expectedSuggestions)
37+
{
38+
$tester = $this->createCommandCompletionTester(['messages' => ['foo' => 'foo']]);
39+
40+
$suggestions = $tester->complete($input);
41+
42+
$this->assertSame($expectedSuggestions, $suggestions);
43+
}
44+
45+
public function provideCompletionSuggestions()
46+
{
47+
$bundle = new ExtensionPresentBundle();
48+
49+
yield 'locale' => [[''], ['en', 'fr']];
50+
yield 'bundle' => [['en', ''], [$bundle->getName(), $bundle->getContainerExtension()->getAlias()]];
51+
yield 'domain with locale' => [['en', '--domain=m'], ['messages']];
52+
yield 'domain without locale' => [['--domain=m'], []];
53+
yield 'format' => [['en', '--format='], ['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res', 'xlf12', 'xlf20']];
54+
yield 'sort' => [['en', '--sort='], ['asc', 'desc']];
55+
}
56+
57+
protected function setUp(): void
58+
{
59+
$this->fs = new Filesystem();
60+
$this->translationDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true);
61+
$this->fs->mkdir($this->translationDir.'/translations');
62+
$this->fs->mkdir($this->translationDir.'/templates');
63+
}
64+
65+
protected function tearDown(): void
66+
{
67+
$this->fs->remove($this->translationDir);
68+
}
69+
70+
private function createCommandCompletionTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandCompletionTester
71+
{
72+
$translator = $this->createMock(Translator::class);
73+
$translator
74+
->expects($this->any())
75+
->method('getFallbackLocales')
76+
->willReturn(['en']);
77+
78+
$extractor = $this->createMock(ExtractorInterface::class);
79+
$extractor
80+
->expects($this->any())
81+
->method('extract')
82+
->willReturnCallback(
83+
function ($path, $catalogue) use ($extractedMessages) {
84+
foreach ($extractedMessages as $domain => $messages) {
85+
$catalogue->add($messages, $domain);
86+
}
87+
}
88+
);
89+
90+
$loader = $this->createMock(TranslationReader::class);
91+
$loader
92+
->expects($this->any())
93+
->method('read')
94+
->willReturnCallback(
95+
function ($path, $catalogue) use ($loadedMessages) {
96+
$catalogue->add($loadedMessages);
97+
}
98+
);
99+
100+
$writer = $this->createMock(TranslationWriter::class);
101+
$writer
102+
->expects($this->any())
103+
->method('getFormats')
104+
->willReturn(
105+
['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res']
106+
);
107+
108+
if (null === $kernel) {
109+
$returnValues = [
110+
['foo', $this->getBundle($this->translationDir)],
111+
['test', $this->getBundle('test')],
112+
];
113+
$kernel = $this->createMock(KernelInterface::class);
114+
$kernel
115+
->expects($this->any())
116+
->method('getBundle')
117+
->willReturnMap($returnValues);
118+
}
119+
120+
$kernel
121+
->expects($this->any())
122+
->method('getBundles')
123+
->willReturn([new ExtensionPresentBundle()]);
124+
125+
$container = new Container();
126+
$kernel
127+
->expects($this->any())
128+
->method('getContainer')
129+
->willReturn($container);
130+
131+
$command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, ['en', 'fr']);
132+
133+
$application = new Application($kernel);
134+
$application->add($command);
135+
136+
return new CommandCompletionTester($application->find('translation:update'));
137+
}
138+
139+
private function getBundle($path)
140+
{
141+
$bundle = $this->createMock(BundleInterface::class);
142+
$bundle
143+
->expects($this->any())
144+
->method('getPath')
145+
->willReturn($path)
146+
;
147+
148+
return $bundle;
149+
}
150+
}

0 commit comments

Comments
 (0)