Skip to content

Commit

Permalink
[FrameworkBundle] Add completion feature on translation:update command
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenkhoo authored and fabpot committed Oct 29, 2021
1 parent ff70bd1 commit 2f301ae
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 29 deletions.
145 changes: 116 additions & 29 deletions src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand All @@ -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();

Expand All @@ -65,6 +72,7 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade
$this->defaultViewsPath = $defaultViewsPath;
$this->transPaths = $transPaths;
$this->codePaths = $codePaths;
$this->enabledLocales = $enabledLocales;
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -206,24 +206,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$io->title('Translation Messages Extractor and Dumper');
$io->comment(sprintf('Generating "<info>%s</info>" translation files for "<info>%s</info>"', $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);
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
}
}
Expand Up @@ -236,6 +236,7 @@
null, // twig.default_path
[], // Translator paths
[], // Twig paths
param('kernel.enabled_locales'),
])
->tag('console.command')

Expand Down
@@ -0,0 +1,150 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}

0 comments on commit 2f301ae

Please sign in to comment.