Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4b07004

Browse files
committed
[Translator] Add lint:translations command
1 parent 1f90bc3 commit 4b07004

File tree

4 files changed

+283
-0
lines changed

4 files changed

+283
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
use Symfony\Component\String\LazyString;
166166
use Symfony\Component\String\Slugger\SluggerInterface;
167167
use Symfony\Component\Translation\Bridge as TranslationBridge;
168+
use Symfony\Component\Translation\Command\TranslationLintCommand as BaseTranslationLintCommand;
168169
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
169170
use Symfony\Component\Translation\Extractor\PhpAstExtractor;
170171
use Symfony\Component\Translation\LocaleSwitcher;
@@ -245,6 +246,10 @@ public function load(array $configs, ContainerBuilder $container): void
245246
$container->removeDefinition('console.command.yaml_lint');
246247
}
247248

249+
if (!class_exists(BaseTranslationLintCommand::class)) {
250+
$container->removeDefinition('console.command.translation_lint');
251+
}
252+
248253
if (!class_exists(DebugCommand::class)) {
249254
$container->removeDefinition('console.command.dotenv_debug');
250255
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
use Symfony\Component\Messenger\Command\StopWorkersCommand;
5555
use Symfony\Component\Scheduler\Command\DebugCommand as SchedulerDebugCommand;
5656
use Symfony\Component\Serializer\Command\DebugCommand as SerializerDebugCommand;
57+
use Symfony\Component\Translation\Command\TranslationLintCommand;
5758
use Symfony\Component\Translation\Command\TranslationPullCommand;
5859
use Symfony\Component\Translation\Command\TranslationPushCommand;
5960
use Symfony\Component\Translation\Command\XliffLintCommand;
@@ -317,6 +318,13 @@
317318
->set('console.command.yaml_lint', YamlLintCommand::class)
318319
->tag('console.command')
319320

321+
->set('console.command.translation_lint', TranslationLintCommand::class)
322+
->args([
323+
service('translator'),
324+
param('kernel.enabled_locales'),
325+
])
326+
->tag('console.command')
327+
320328
->set('console.command.form_debug', \Symfony\Component\Form\Command\DebugCommand::class)
321329
->args([
322330
service('form.registry'),
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Symfony package.
7+
*
8+
* (c) Fabien Potencier <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Symfony\Component\Translation\Command;
15+
16+
use Symfony\Component\Console\Attribute\AsCommand;
17+
use Symfony\Component\Console\Command\Command;
18+
use Symfony\Component\Console\Completion\CompletionInput;
19+
use Symfony\Component\Console\Completion\CompletionSuggestions;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Input\InputOption;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
use Symfony\Component\Console\Style\SymfonyStyle;
24+
use Symfony\Component\Translation\Exception\ExceptionInterface;
25+
use Symfony\Component\Translation\TranslatorBagInterface;
26+
use Symfony\Contracts\Translation\TranslatorInterface;
27+
28+
/**
29+
* Lint translations files syntax and outputs encountered errors.
30+
*
31+
* @author Hugo Alliaume <[email protected]>
32+
*/
33+
#[AsCommand(name: 'lint:translations', description: 'Lint translations files syntax and outputs encountered errors')]
34+
class TranslationLintCommand extends Command
35+
{
36+
private SymfonyStyle $io;
37+
38+
public function __construct(
39+
private TranslatorInterface&TranslatorBagInterface $translator,
40+
private array $enabledLocales = [],
41+
) {
42+
parent::__construct();
43+
}
44+
45+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
46+
{
47+
if ($input->mustSuggestOptionValuesFor('locales')) {
48+
$suggestions->suggestValues($this->enabledLocales);
49+
}
50+
}
51+
52+
protected function configure(): void
53+
{
54+
$this
55+
->setDefinition([
56+
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to lint.'),
57+
])
58+
->setHelp(<<<'EOF'
59+
The <info>%command.name%</> command lint translations.
60+
61+
<info>php %command.full_name%</>
62+
EOF
63+
);
64+
}
65+
66+
protected function initialize(InputInterface $input, OutputInterface $output): void
67+
{
68+
$this->io = new SymfonyStyle($input, $output);
69+
}
70+
71+
protected function execute(InputInterface $input, OutputInterface $output): int
72+
{
73+
$locales = $input->getOption('locales') ?: $this->enabledLocales;
74+
75+
/** @var array<string, array<string, array<string, \Throwable>> $errors */
76+
$errors = [];
77+
$domainsByLocales = [];
78+
79+
foreach ($locales as $locale) {
80+
$messageCatalogue = $this->translator->getCatalogue($locale);
81+
82+
foreach ($domainsByLocales[$locale] = $messageCatalogue->getDomains() as $domain) {
83+
foreach ($messageCatalogue->all($domain) as $id => $translation) {
84+
try {
85+
$this->translator->trans($id, [], $domain, $messageCatalogue->getLocale());
86+
} catch (ExceptionInterface $e) {
87+
$errors[$locale][$domain][$id] = $e;
88+
}
89+
}
90+
}
91+
}
92+
93+
if ([] === $domainsByLocales) {
94+
$this->io->error('No translation files were found.');
95+
96+
return Command::FAILURE;
97+
}
98+
99+
$this->io->table(
100+
['Locale', 'Domains', 'Valid?'],
101+
array_map(
102+
static fn (string $locale, array $domains) => [
103+
$locale,
104+
implode(', ', $domains),
105+
empty($errors[$locale]) ? '<info>Yes</>' : '<error>No</>',
106+
],
107+
array_keys($domainsByLocales),
108+
$domainsByLocales
109+
),
110+
);
111+
112+
if ([] !== $errors) {
113+
foreach ($errors as $locale => $domains) {
114+
foreach ($domains as $domain => $errors) {
115+
$this->io->section(sprintf('Errors for locale "%s" and domain "%s"', $locale, $domain));
116+
117+
foreach ($errors as $id => $error) {
118+
$this->io->text(sprintf('Translation key "%s" is invalid:', $id));
119+
$this->io->error($error->getMessage());
120+
}
121+
}
122+
}
123+
124+
return Command::FAILURE;
125+
}
126+
127+
$this->io->success('All translations are valid.');
128+
129+
return Command::SUCCESS;
130+
}
131+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Symfony package.
7+
*
8+
* (c) Fabien Potencier <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Symfony\Component\Translation\Tests\Command;
15+
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\Component\Console\Application;
18+
use Symfony\Component\Console\Command\Command;
19+
use Symfony\Component\Console\Tester\CommandTester;
20+
use Symfony\Component\Translation\Command\TranslationLintCommand;
21+
use Symfony\Component\Translation\Loader\ArrayLoader;
22+
use Symfony\Component\Translation\Translator;
23+
24+
final class TranslationLintCommandTest extends TestCase
25+
{
26+
public function testLintCorrectTranslations()
27+
{
28+
$translator = new Translator('en');
29+
$translator->addLoader('array', new ArrayLoader());
30+
$translator->addResource('array', ['hello' => 'Hello!'], 'en', 'messages');
31+
$translator->addResource('array', [
32+
'hello_name' => 'Hello {name}!',
33+
'num_of_apples' => <<<ICU
34+
{apples, plural,
35+
=0 {There are no apples}
36+
=1 {There is one apple...}
37+
other {There are # apples!}
38+
}
39+
ICU,
40+
], 'en', 'messages+intl-icu');
41+
$translator->addResource('array', ['hello' => 'Bonjour !'], 'fr', 'messages');
42+
$translator->addResource('array', [
43+
'hello_name' => 'Bonjour {name} !',
44+
'num_of_apples' => <<<ICU
45+
{apples, plural,
46+
=0 {Il n'y a pas de pommes}
47+
=1 {Il y a une pomme}
48+
other {Il y a # pommes !}
49+
}
50+
ICU,
51+
], 'fr', 'messages+intl-icu');
52+
53+
$command = $this->createCommand($translator, ['en', 'fr']);
54+
$commandTester = new CommandTester($command);
55+
56+
$commandTester->execute([], ['decorated' => false]);
57+
58+
$commandTester->assertCommandIsSuccessful();
59+
$this->assertStringContainsString('[OK] All translations are valid.', $commandTester->getDisplay(true));
60+
}
61+
62+
public function testLintMalformedIcuTranslations()
63+
{
64+
$translator = new Translator('en');
65+
$translator->addLoader('array', new ArrayLoader());
66+
$translator->addResource('array', ['hello' => 'Hello!'], 'en', 'messages');
67+
$translator->addResource('array', [
68+
'hello_name' => 'Hello {name}!',
69+
// Missing "other" case
70+
'num_of_apples' => <<<ICU
71+
{apples, plural,
72+
=0 {There are no apples}
73+
=1 {There is one apple...}
74+
}
75+
ICU,
76+
], 'en', 'messages+intl-icu');
77+
$translator->addResource('array', ['hello' => 'Bonjour !'], 'fr', 'messages');
78+
$translator->addResource('array', [
79+
// Missing "}"
80+
'hello_name' => 'Bonjour {name !',
81+
// Missing "=" at "0"
82+
'num_of_apples' => <<<ICU
83+
{apples, plural,
84+
0 {Il n'y a pas de pommes}
85+
=1 {Il y a une pomme
86+
other {Il y a # pommes !}
87+
}
88+
ICU,
89+
], 'fr', 'messages+intl-icu');
90+
91+
$command = $this->createCommand($translator, ['en', 'fr']);
92+
$commandTester = new CommandTester($command);
93+
94+
$this->assertSame(1, $commandTester->execute([], ['decorated' => false]));
95+
96+
$output = $commandTester->getDisplay(true);
97+
$this->assertStringContainsString(<<<EOF
98+
-------- ---------- --------
99+
Locale Domains Valid?
100+
-------- ---------- --------
101+
en messages No
102+
fr messages No
103+
-------- ---------- --------
104+
EOF, $output);
105+
$this->assertStringContainsString(<<<EOF
106+
Errors for locale "en" and domain "messages"
107+
--------------------------------------------
108+
109+
Translation key "num_of_apples" is invalid:
110+
111+
[ERROR] Invalid message format (error #65807): msgfmt_create: message formatter creation failed:
112+
U_DEFAULT_KEYWORD_MISSING
113+
EOF, $output);
114+
$this->assertStringContainsString(<<<EOF
115+
Errors for locale "fr" and domain "messages"
116+
--------------------------------------------
117+
118+
Translation key "hello_name" is invalid:
119+
120+
[ERROR] Invalid message format (error #65799): pattern syntax error (parse error at offset 9, after "Bonjour {", before
121+
or at "name !"): U_PATTERN_SYNTAX_ERROR
122+
123+
Translation key "num_of_apples" is invalid:
124+
125+
[ERROR] Invalid message format (error #65799): pattern syntax error (parse error at offset 106, after " other
126+
{", before or at "Il y a # pommes"): U_PATTERN_SYNTAX_ERROR
127+
EOF, $output);
128+
}
129+
130+
private function createCommand(Translator $translator, array $enabledLocales): Command
131+
{
132+
$command = new TranslationLintCommand($translator, $enabledLocales);
133+
134+
$application = new Application();
135+
$application->add($command);
136+
137+
return $command;
138+
}
139+
}

0 commit comments

Comments
 (0)