8000 [FrameworkBundle] Add completion feature on translation:update command by stephenkhoo · Pull Request #43676 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[FrameworkBundle] Add completion feature on translation:update command #43676

New issue

8000 Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 116 additions & 29 deletions src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
Original file line number Diff line number Diff line change
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
null, // twig.default_path
[], // Translator paths
[], // Twig paths
param('kernel.enabled_locales'),
])
->tag('console.command')

Expand Down
Original file line number Diff line number Diff line change
@@ -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\ 67E6 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