diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php
new file mode 100644
index 00000000..e0162f19
--- /dev/null
+++ b/Command/CheckMissingCommand.php
@@ -0,0 +1,147 @@
+configurationManager = $configurationManager;
+ $this->catalogueFetcher = $catalogueFetcher;
+ $this->importer = $importer;
+ $this->catalogueCounter = $catalogueCounter;
+ }
+
+ protected function configure(): void
+ {
+ $this
+ ->setName(self::$defaultName)
+ ->setDescription('Check that all translations for a given locale are extracted.')
+ ->addArgument('locale', InputArgument::REQUIRED, 'The locale to check')
+ ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $config = $this->configurationManager->getConfiguration($input->getArgument('configuration'));
+
+ $locale = $input->getArgument('locale');
+
+ $catalogues = $this->catalogueFetcher->getCatalogues($config, [$locale]);
+ $finder = $this->getConfiguredFinder($config);
+
+ $result = $this->importer->extractToCatalogues(
+ $finder,
+ $catalogues,
+ [
+ 'blacklist_domains' => $config->getBlacklistDomains(),
+ 'whitelist_domains' => $config->getWhitelistDomains(),
+ 'project_root' => $config->getProjectRoot(),
+ ]
+ );
+
+ $definedBefore = $this->catalogueCounter->getNumberOfDefinedMessages($catalogues[0]);
+ $definedAfter = $this->catalogueCounter->getNumberOfDefinedMessages($result->getMessageCatalogues()[0]);
+
+ $newMessages = $definedAfter - $definedBefore;
+
+ $io = new SymfonyStyle($input, $output);
+
+ if ($newMessages > 0) {
+ $io->error(\sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages));
+
+ return 1;
+ }
+
+ $emptyTranslations = $this->countEmptyTranslations($result->getMessageCatalogues()[0]);
+
+ if ($emptyTranslations > 0) {
+ $io->error(
+ \sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations)
+ );
+
+ return 1;
+ }
+
+ $io->success('No new translation messages');
+
+ return 0;
+ }
+
+ private function getConfiguredFinder(Configuration $config): Finder
+ {
+ $finder = new Finder();
+ $finder->in($config->getDirs());
+
+ foreach ($config->getExcludedDirs() as $exclude) {
+ $finder->notPath($exclude);
+ }
+
+ foreach ($config->getExcludedNames() as $exclude) {
+ $finder->notName($exclude);
+ }
+
+ return $finder;
+ }
+
+ private function countEmptyTranslations(MessageCatalogueInterface $catalogue): int
+ {
+ $total = 0;
+
+ foreach ($catalogue->getDomains() as $domain) {
+ $emptyTranslations = \array_filter(
+ $catalogue->all($domain),
+ function (string $message): bool {
+ return '' === $message;
+ }
+ );
+
+ $total += \count($emptyTranslations);
+ }
+
+ return $total;
+ }
+}
diff --git a/Resources/config/console.yaml b/Resources/config/console.yaml
index f9eeb2a9..e2daeaf4 100644
--- a/Resources/config/console.yaml
+++ b/Resources/config/console.yaml
@@ -1,4 +1,14 @@
services:
+ Translation\Bundle\Command\CheckMissingCommand:
+ public: true
+ arguments:
+ - '@Translation\Bundle\Service\ConfigurationManager'
+ - '@Translation\Bundle\Catalogue\CatalogueFetcher'
+ - '@Translation\Bundle\Service\Importer'
+ - '@Translation\Bundle\Catalogue\CatalogueCounter'
+ tags:
+ - { name: console.command, command: translation:check-missing }
+
Translation\Bundle\Command\DeleteObsoleteCommand:
public: true
arguments:
diff --git a/Tests/Functional/Command/CheckMissingCommandTest.php b/Tests/Functional/Command/CheckMissingCommandTest.php
new file mode 100644
index 00000000..f64dfbc9
--- /dev/null
+++ b/Tests/Functional/Command/CheckMissingCommandTest.php
@@ -0,0 +1,194 @@
+kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml');
+ $this->bootKernel();
+ $this->application = new Application($this->kernel);
+
+ \file_put_contents(
+ __DIR__.'/../app/Resources/translations/messages.sv.xlf',
+ <<<'XML'
+
+
+
+
+
+ translated.heading
+ My translated heading
+
+
+
+
+ translated.paragraph0
+ My translated paragraph0
+
+
+
+
+ foobar.html.twig:9
+
+
+ translated.paragraph1
+ My translated paragraph1
+
+
+
+
+ not.in.source
+ This is not in the source code
+
+
+
+
+XML
+ );
+ }
+
+ public function testReportsMissingTranslations(): void
+ {
+ $commandTester = new CommandTester($this->application->find('translation:check-missing'));
+
+ $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']);
+
+ $this->assertStringContainsString(
+ '4 new message(s) have been found, run bin/console translation:extract',
+ $commandTester->getDisplay()
+ );
+ $this->assertGreaterThan(0, $commandTester->getStatusCode());
+ }
+
+ public function testReportsEmptyTranslationMessages(): void
+ {
+ // run translation:extract first, so all translations are extracted
+ (new CommandTester($this->application->find('translation:extract')))->execute(['locale' => 'sv']);
+
+ $commandTester = new CommandTester($this->application->find('translation:check-missing'));
+
+ $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']);
+
+ $this->assertStringContainsString(
+ '4 messages have empty translations, please provide translations',
+ $commandTester->getDisplay()
+ );
+ $this->assertGreaterThan(0, $commandTester->getStatusCode());
+ }
+
+ public function testReportsNoNewTranslationMessages(): void
+ {
+ \file_put_contents(
+ __DIR__.'/../app/Resources/translations/messages.sv.xlf',
+ <<<'XML'
+
+
+
+
+
+ Resources/views/translated.html.twig:5
+ new
+
+
+ translated.title
+ My translated title
+
+
+
+
+ Resources/views/translated.html.twig:8
+
+
+ translated.heading
+ My translated heading
+
+
+
+
+ Resources/views/translated.html.twig:9
+
+
+ translated.paragraph0
+ My translated paragraph0
+
+
+
+
+ Resources/views/translated.html.twig:9
+
+
+ translated.paragraph1
+ My translated paragraph1
+
+
+
+
+ Resources/views/translated.html.twig:11
+ new
+
+
+ translated.paragraph2
+ My translated paragraph2
+
+
+
+
+ Resources/views/translated.html.twig:12
+ Resources/views/translated.html.twig:12
+ new
+
+
+ localized.email
+ My localized email
+
+
+
+
+ Resources/views/translated.html.twig:14
+ new
+
+
+ translated.attribute
+ My translated attribute
+
+
+
+
+ obsolete
+
+
+ not.in.source
+ This is not in the source code
+
+
+
+
+XML
+ );
+
+ $commandTester = new CommandTester($this->application->find('translation:check-missing'));
+
+ $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']);
+
+ $this->assertStringContainsString(
+ 'No new translation messages',
+ $commandTester->getDisplay()
+ );
+ $this->assertSame(0, $commandTester->getStatusCode());
+ }
+}