Skip to content

Commit b606d68

Browse files
stephenkhoofabpot
authored andcommitted
[FrameworkBundle] Add completion feature on translation:update command
1 parent 0d4336b commit b606d68

File tree

3 files changed

+267
-29
lines changed

3 files changed

+267
-29
lines changed

Command/TranslationUpdateCommand.php

Lines changed: 116 additions & 29 deletions
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;
@@ -40,6 +42,10 @@ class TranslationUpdateCommand extends Command
4042
private const ASC = 'asc';
4143
private const DESC = 'desc';
4244
private const SORT_ORDERS = [self::ASC, self::DESC];
45+
private const FORMATS = [
46+
'xlf12' => ['xlf', '1.2'],
47+
'xlf20' => ['xlf', '2.0'],
48+
];
4349

4450
protected static $defaultName = 'translation:update';
4551
protected static $defaultDescription = 'Update the translation file';
@@ -52,8 +58,9 @@ class TranslationUpdateCommand extends Command
5258
private $defaultViewsPath;
5359
private $transPaths;
5460
private $codePaths;
61+
private $enabledLocales;
5562

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

@@ -65,6 +72,7 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade
6572
$this->defaultViewsPath = $defaultViewsPath;
6673
$this->transPaths = $transPaths;
6774
$this->codePaths = $codePaths;
75+
$this->enabledLocales = $enabledLocales;
6876
}
6977

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

150-
switch ($format) {
151-
case 'xlf20': $xliffVersion = '2.0';
152-
// no break
153-
case 'xlf12': $format = 'xlf';
158+
if (\in_array($format, array_keys(self::FORMATS), true)) {
159+
[$format, $xliffVersion] = self::FORMATS[$format];
154160
}
155161

156162
// check format
@@ -165,15 +171,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
165171
$kernel = $this->getApplication()->getKernel();
166172

167173
// Define Root Paths
168-
$transPaths = $this->transPaths;
169-
if ($this->defaultTransPath) {
170-
$transPaths[] = $this->defaultTransPath;
171-
}
172-
$codePaths = $this->codePaths;
173-
$codePaths[] = $kernel->getProjectDir().'/src';
174-
if ($this->defaultViewsPath) {
175-
$codePaths[] = $this->defaultViewsPath;
176-
}
174+
$transPaths = $this->getRootTransPaths();
175+
$codePaths = $this->getRootCodePaths($kernel);
176+
177177
$currentName = 'default directory';
178178

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

209-
// load any messages from templates
210-
$extractedCatalogue = new MessageCatalogue($input->getArgument('locale'));
211209
$io->comment('Parsing templates...');
212-
$this->extractor->setPrefix($input->getOption('prefix'));
213-
foreach ($codePaths as $path) {
214-
if (is_dir($path) || is_file($path)) {
215-
$this->extractor->extract($path, $extractedCatalogue);
216-
}
217-
}
210+
$extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $input->getOption('prefix'));
218211

219-
// load any existing messages from the translation files
220-
$currentCatalogue = new MessageCatalogue($input->getArgument('locale'));
221212
$io->comment('Loading translation files...');
222-
foreach ($transPaths as $path) {
223-
if (is_dir($path)) {
224-
$this->reader->read($path, $currentCatalogue);
225-
}
226-
}
213+
$currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths);
227214

228215
if (null !== $domain = $input->getOption('domain')) {
229216
$currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain);
@@ -321,6 +308,60 @@ protected function execute(InputInterface $input, OutputInterface $output): int
321308
return 0;
322309
}
323310

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

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

Resources/config/console.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
null, // twig.default_path
237237
[], // Translator paths
238238
[], // Twig paths
239+
param('kernel.enabled_locales'),
239240
])
240241
->tag('console.command')
241242

Lines changed: 150 additions & 0 deletions
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 <[email protected]>
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)