Skip to content

Introduce a translation:check-missing command #403

New issue

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 6 commits into from
Feb 21, 2020
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
147 changes: 147 additions & 0 deletions Command/CheckMissingCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

declare(strict_types=1);

namespace Translation\Bundle\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Translation\Bundle\Catalogue\CatalogueCounter;
use Translation\Bundle\Catalogue\CatalogueFetcher;
use Translation\Bundle\Model\Configuration;
use Translation\Bundle\Service\ConfigurationManager;
use Translation\Bundle\Service\Importer;

final class CheckMissingCommand extends Command
{
protected static $defaultName = 'translation:check-missing';

/**
* @var ConfigurationManager
*/
private $configurationManager;

/**
* @var CatalogueFetcher
*/
private $catalogueFetcher;

/**
* @var Importer
*/
private $importer;

/**
* @var CatalogueCounter
*/
private $catalogueCounter;

public function __construct(
ConfigurationManager $configurationManager,
CatalogueFetcher $catalogueFetcher,
Importer $importer,
CatalogueCounter $catalogueCounter
) {
parent::__construct();

$this->configurationManager = $configurationManager;
$this->catalogueFetcher = $catalogueFetcher;
$this->importer = $importer;
$this->catalogueCounter = $catalogueCounter;
}

protected function configure(): void
{
$this
->setName(self::$defaultName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not needed when defining the static property.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you are right, the Symfony documentation example also doesn't call setName() anymore.

However, I copied this from the other commands in this bundle which also call ->setName(self::$defaultName). So perhaps this should be fixed for all commands at once in a separate PR.

->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;
}
}
10 changes: 10 additions & 0 deletions Resources/config/console.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
194 changes: 194 additions & 0 deletions Tests/Functional/Command/CheckMissingCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?php

declare(strict_types=1);

namespace Translation\Bundle\Tests\Functional\Command;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Translation\Bundle\Tests\Functional\BaseTestCase;

class CheckMissingCommandTest extends BaseTestCase
{
/**
* @var Application
*/
private $application;

protected function setUp(): void
{
parent::setUp();

$this->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'
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="sv">
<file id="messages.sv">
<unit id="xx1">
<segment>
<source>translated.heading</source>
<target>My translated heading</target>
</segment>
</unit>
<unit id="xx2">
<segment>
<source>translated.paragraph0</source>
<target>My translated paragraph0</target>
</segment>
</unit>
<unit id="xx3">
<notes>
<note category="file-source" priority="1">foobar.html.twig:9</note>
</notes>
<segment>
<source>translated.paragraph1</source>
<target>My translated paragraph1</target>
</segment>
</unit>
<unit id="xx4">
<segment>
<source>not.in.source</source>
<target>This is not in the source code</target>
</segment>
</unit>
</file>
</xliff>
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'
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="sv">
<file id="messages.sv">
<unit id="gwCXP88" name="translated.title">
<notes>
<note category="file-source" priority="1">Resources/views/translated.html.twig:5</note>
<note category="state" priority="1">new</note>
</notes>
<segment>
<source>translated.title</source>
<target>My translated title</target>
</segment>
</unit>
<unit id="MVOZYWq" name="translated.heading">
<notes>
<note category="file-source" priority="1">Resources/views/translated.html.twig:8</note>
</notes>
<segment>
<source>translated.heading</source>
<target>My translated heading</target>
</segment>
</unit>
<unit id="bJFCP77" name="translated.paragraph0">
<notes>
<note category="file-source" priority="1">Resources/views/translated.html.twig:9</note>
</notes>
<segment>
<source>translated.paragraph0</source>
<target>My translated paragraph0</target>
</segment>
</unit>
<unit id="1QAmWwr" name="translated.paragraph1">
<notes>
<note category="file-source" priority="1">Resources/views/translated.html.twig:9</note>
</notes>
<segment>
<source>translated.paragraph1</source>
<target>My translated paragraph1</target>
</segment>
</unit>
<unit id="7AdXS54" name="translated.paragraph2">
<notes>
<note category="file-source" priority="1">Resources/views/translated.html.twig:11</note>
<note category="state" priority="1">new</note>
</notes>
<segment>
<source>translated.paragraph2</source>
<target>My translated paragraph2</target>
</segment>
</unit>
<unit id="WvnvT8X" name="localized.email">
<notes>
<note category="file-source" priority="1">Resources/views/translated.html.twig:12</note>
<note category="file-source" priority="1">Resources/views/translated.html.twig:12</note>
<note category="state" priority="1">new</note>
</notes>
<segment>
<source>localized.email</source>
<target>My localized email</target>
</segment>
</unit>
<unit id="ETjQiEP" name="translated.attribute">
<notes>
<note category="file-source" priority="1">Resources/views/translated.html.twig:14</note>
<note category="state" priority="1">new</note>
</notes>
<segment>
<source>translated.attribute</source>
<target>My translated attribute</target>
</segment>
</unit>
<unit id="GO15Lkx" name="not.in.source">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<source>not.in.source</source>
<target>This is not in the source code</target>
</segment>
</unit>
</file>
</xliff>
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());
}
}