Skip to content

Commit 9112826

Browse files
authored
Merge pull request #403 from nicwortel/check-command
Introduce a translation:check-missing command
2 parents 50e54c5 + f6609f1 commit 9112826

File tree

3 files changed

+351
-0
lines changed

3 files changed

+351
-0
lines changed

Command/CheckMissingCommand.php

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Translation\Bundle\Command;
6+
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputArgument;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
use Symfony\Component\Console\Style\SymfonyStyle;
12+
use Symfony\Component\Finder\Finder;
13+
use Symfony\Component\Translation\MessageCatalogueInterface;
14+
use Translation\Bundle\Catalogue\CatalogueCounter;
15+
use Translation\Bundle\Catalogue\CatalogueFetcher;
16+
use Translation\Bundle\Model\Configuration;
17+
use Translation\Bundle\Service\ConfigurationManager;
18+
use Translation\Bundle\Service\Importer;
19+
20+
final class CheckMissingCommand extends Command
21+
{
22+
protected static $defaultName = 'translation:check-missing';
23+
24+
/**
25+
* @var ConfigurationManager
26+
*/
27+
private $configurationManager;
28+
29+
/**
30+
* @var CatalogueFetcher
31+
*/
32+
private $catalogueFetcher;
33+
34+
/**
35+
* @var Importer
36+
*/
37+
private $importer;
38+
39+
/**
40+
* @var CatalogueCounter
41+
*/
42+
private $catalogueCounter;
43+
44+
public function __construct(
45+
ConfigurationManager $configurationManager,
46+
CatalogueFetcher $catalogueFetcher,
47+
Importer $importer,
48+
CatalogueCounter $catalogueCounter
49+
) {
50+
parent::__construct();
51+
52+
$this->configurationManager = $configurationManager;
53+
$this->catalogueFetcher = $catalogueFetcher;
54+
$this->importer = $importer;
55+
$this->catalogueCounter = $catalogueCounter;
56+
}
57+
58+
protected function configure(): void
59+
{
60+
$this
61+
->setName(self::$defaultName)
62+
->setDescription('Check that all translations for a given locale are extracted.')
63+
->addArgument('locale', InputArgument::REQUIRED, 'The locale to check')
64+
->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default');
65+
}
66+
67+
protected function execute(InputInterface $input, OutputInterface $output): int
68+
{
69+
$config = $this->configurationManager->getConfiguration($input->getArgument('configuration'));
70+
71+
$locale = $input->getArgument('locale');
72+
73+
$catalogues = $this->catalogueFetcher->getCatalogues($config, [$locale]);
74+
$finder = $this->getConfiguredFinder($config);
75+
76+
$result = $this->importer->extractToCatalogues(
77+
$finder,
78+
$catalogues,
79+
[
80+
'blacklist_domains' => $config->getBlacklistDomains(),
81+
'whitelist_domains' => $config->getWhitelistDomains(),
82+
'project_root' => $config->getProjectRoot(),
83+
]
84+
);
85+
86+
$definedBefore = $this->catalogueCounter->getNumberOfDefinedMessages($catalogues[0]);
87+
$definedAfter = $this->catalogueCounter->getNumberOfDefinedMessages($result->getMessageCatalogues()[0]);
88+
89+
$newMessages = $definedAfter - $definedBefore;
90+
91+
$io = new SymfonyStyle($input, $output);
92+
93+
if ($newMessages > 0) {
94+
$io->error(\sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages));
95+
96+
return 1;
97+
}
98+
99+
$emptyTranslations = $this->countEmptyTranslations($result->getMessageCatalogues()[0]);
100+
101+
if ($emptyTranslations > 0) {
102+
$io->error(
103+
\sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations)
104+
);
105+
106+
return 1;
107+
}
108+
109+
$io->success('No new translation messages');
110+
111+
return 0;
112+
}
113+
114+
private function getConfiguredFinder(Configuration $config): Finder
115+
{
116+
$finder = new Finder();
117+
$finder->in($config->getDirs());
118+
119+
foreach ($config->getExcludedDirs() as $exclude) {
120+
$finder->notPath($exclude);
121+
}
122+
123+
foreach ($config->getExcludedNames() as $exclude) {
124+
$finder->notName($exclude);
125+
}
126+
127+
return $finder;
128+
}
129+
130+
private function countEmptyTranslations(MessageCatalogueInterface $catalogue): int
131+
{
132+
$total = 0;
133+
134+
foreach ($catalogue->getDomains() as $domain) {
135+
$emptyTranslations = \array_filter(
136+
$catalogue->all($domain),
137+
function (string $message): bool {
138+
return '' === $message;
139+
}
140+
);
141+
142+
$total += \count($emptyTranslations);
143+
}
144+
145+
return $total;
146+
}
147+
}

Resources/config/console.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
services:
2+
Translation\Bundle\Command\CheckMissingCommand:
3+
public: true
4+
arguments:
5+
- '@Translation\Bundle\Service\ConfigurationManager'
6+
- '@Translation\Bundle\Catalogue\CatalogueFetcher'
7+
- '@Translation\Bundle\Service\Importer'
8+
- '@Translation\Bundle\Catalogue\CatalogueCounter'
9+
tags:
10+
- { name: console.command, command: translation:check-missing }
11+
212
Translation\Bundle\Command\DeleteObsoleteCommand:
313
public: true
414
arguments:
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Translation\Bundle\Tests\Functional\Command;
6+
7+
use Symfony\Bundle\FrameworkBundle\Console\Application;
8+
use Symfony\Component\Console\Tester\CommandTester;
9+
use Translation\Bundle\Tests\Functional\BaseTestCase;
10+
11+
class CheckMissingCommandTest extends BaseTestCase
12+
{
13+
/**
14+
* @var Application
15+
*/
16+
private $application;
17+
18+
protected function setUp(): void
19+
{
20+
parent::setUp();
21+
22+
$this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml');
23+
$this->bootKernel();
24+
$this->application = new Application($this->kernel);
25+
26+
\file_put_contents(
27+
__DIR__.'/../app/Resources/translations/messages.sv.xlf',
28+
<<<'XML'
29+
<?xml version="1.0" encoding="utf-8"?>
30+
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="sv">
31+
<file id="messages.sv">
32+
<unit id="xx1">
33+
<segment>
34+
<source>translated.heading</source>
35+
<target>My translated heading</target>
36+
</segment>
37+
</unit>
38+
<unit id="xx2">
39+
<segment>
40+
<source>translated.paragraph0</source>
41+
<target>My translated paragraph0</target>
42+
</segment>
43+
</unit>
44+
<unit id="xx3">
45+
<notes>
46+
<note category="file-source" priority="1">foobar.html.twig:9</note>
47+
</notes>
48+
<segment>
49+
<source>translated.paragraph1</source>
50+
<target>My translated paragraph1</target>
51+
</segment>
52+
</unit>
53+
<unit id="xx4">
54+
<segment>
55+
<source>not.in.source</source>
56+
<target>This is not in the source code</target>
57+
</segment>
58+
</unit>
59+
</file>
60+
</xliff>
61+
XML
62+
);
63+
}
64+
65+
public function testReportsMissingTranslations(): void
66+
{
67+
$commandTester = new CommandTester($this->application->find('translation:check-missing'));
68+
69+
$commandTester->execute(['locale' => 'sv', 'configuration' => 'app']);
70+
71+
$this->assertStringContainsString(
72+
'4 new message(s) have been found, run bin/console translation:extract',
73+
$commandTester->getDisplay()
74+
);
75+
$this->assertGreaterThan(0, $commandTester->getStatusCode());
76+
}
77+
78+
public function testReportsEmptyTranslationMessages(): void
79+
{
80+
// run translation:extract first, so all translations are extracted
81+
(new CommandTester($this->application->find('translation:extract')))->execute(['locale' => 'sv']);
82+
83+
$commandTester = new CommandTester($this->application->find('translation:check-missing'));
84+
85+
$commandTester->execute(['locale' => 'sv', 'configuration' => 'app']);
86+
87+
$this->assertStringContainsString(
88+
'4 messages have empty translations, please provide translations',
89+
$commandTester->getDisplay()
90+
);
91+
$this->assertGreaterThan(0, $commandTester->getStatusCode());
92+
}
93+
94+
public function testReportsNoNewTranslationMessages(): void
95+
{
96+
\file_put_contents(
97+
__DIR__.'/../app/Resources/translations/messages.sv.xlf',
98+
<<<'XML'
99+
<?xml version="1.0" encoding="utf-8"?>
100+
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="sv">
101+
<file id="messages.sv">
102+
<unit id="gwCXP88" name="translated.title">
103+
<notes>
104+
<note category="file-source" priority="1">Resources/views/translated.html.twig:5</note>
105+
<note category="state" priority="1">new</note>
106+
</notes>
107+
<segment>
108+
<source>translated.title</source>
109+
<target>My translated title</target>
110+
</segment>
111+
</unit>
112+
<unit id="MVOZYWq" name="translated.heading">
113+
<notes>
114+
<note category="file-source" priority="1">Resources/views/translated.html.twig:8</note>
115+
</notes>
116+
<segment>
117+
<source>translated.heading</source>
118+
<target>My translated heading</target>
119+
</segment>
120+
</unit>
121+
<unit id="bJFCP77" name="translated.paragraph0">
122+
<notes>
123+
<note category="file-source" priority="1">Resources/views/translated.html.twig:9</note>
124+
</notes>
125+
<segment>
126+
<source>translated.paragraph0</source>
127+
<target>My translated paragraph0</target>
128+
</segment>
129+
</unit>
130+
<unit id="1QAmWwr" name="translated.paragraph1">
131+
<notes>
132+
<note category="file-source" priority="1">Resources/views/translated.html.twig:9</note>
133+
</notes>
134+
<segment>
135+
<source>translated.paragraph1</source>
136+
<target>My translated paragraph1</target>
137+
</segment>
138+
</unit>
139+
<unit id="7AdXS54" name="translated.paragraph2">
140+
<notes>
141+
<note category="file-source" priority="1">Resources/views/translated.html.twig:11</note>
142+
<note category="state" priority="1">new</note>
143+
</notes>
144+
<segment>
145+
<source>translated.paragraph2</source>
146+
<target>My translated paragraph2</target>
147+
</segment>
148+
</unit>
149+
<unit id="WvnvT8X" name="localized.email">
150+
<notes>
151+
<note category="file-source" priority="1">Resources/views/translated.html.twig:12</note>
152+
<note category="file-source" priority="1">Resources/views/translated.html.twig:12</note>
153+
<note category="state" priority="1">new</note>
154+
</notes>
155+
<segment>
156+
<source>localized.email</source>
157+
<target>My localized email</target>
158+
</segment>
159+
</unit>
160+
<unit id="ETjQiEP" name="translated.attribute">
161+
<notes>
162+
<note category="file-source" priority="1">Resources/views/translated.html.twig:14</note>
163+
<note category="state" priority="1">new</note>
164+
</notes>
165+
<segment>
166+
<source>translated.attribute</source>
167+
<target>My translated attribute</target>
168+
</segment>
169+
</unit>
170+
<unit id="GO15Lkx" name="not.in.source">
171+
<notes>
172+
<note category="state" priority="1">obsolete</note>
173+
</notes>
174+
<segment>
175+
<source>not.in.source</source>
176+
<target>This is not in the source code</target>
177+
</segment>
178+
</unit>
179+
</file>
180+
</xliff>
181+
XML
182+
);
183+
184+
$commandTester = new CommandTester($this->application->find('translation:check-missing'));
185+
186+
$commandTester->execute(['locale' => 'sv', 'configuration' => 'app']);
187+
188+
$this->assertStringContainsString(
189+
'No new translation messages',
190+
$commandTester->getDisplay()
191+
);
192+
$this->assertSame(0, $commandTester->getStatusCode());
193+
}
194+
}

0 commit comments

Comments
 (0)