CompareCommand   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 141
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 83
c 1
b 0
f 0
dl 0
loc 141
rs 10
ccs 71
cts 71
cp 1
wmc 17

4 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 10 1
A __construct() 0 5 1
C execute() 0 84 12
A displayMessages() 0 10 3
1
<?php
2
3
namespace Incenteev\TranslationCheckerBundle\Command;
4
5
use Symfony\Component\Console\Command\Command;
6
use Symfony\Component\Console\Input\InputArgument;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Input\InputOption;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Translation\Catalogue\TargetOperation;
11
use Symfony\Component\Translation\MessageCatalogue;
12
use Symfony\Component\Translation\TranslatorBagInterface;
13
use Symfony\Component\Yaml\Yaml;
14
15
/**
16
 * @final
17
 */
18
class CompareCommand extends Command
19 18
{
20
    private TranslatorBagInterface $exposingTranslator;
21 18
22
    public function __construct(TranslatorBagInterface $exposingTranslator)
23 18
    {
24 18
        parent::__construct();
25
26 18
        $this->exposingTranslator = $exposingTranslator;
27
    }
28 18
29 18
    protected function configure(): void
30 18
    {
31 18
        $this->setName('incenteev:translation:compare')
32 18
            ->setDescription('Compares two translation catalogues to ensure they are in sync')
33 18
            ->addArgument('locale', InputArgument::REQUIRED, 'The locale being checked')
34 18
            ->addArgument('source', InputArgument::OPTIONAL, 'The source of the comparison', 'en')
35 18
            ->addOption('obsolete-only', null, InputOption::VALUE_NONE, 'Report only obsolete keys')
36 18
            ->addOption('domain', 'd', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The domains being compared')
37
            ->addOption('whitelist-file', 'w', InputOption::VALUE_REQUIRED, 'Path to a YAML whitelist file')
38
            ->setHelp(<<<EOF
39
The <info>%command.name%</info> command compares 2 translation catalogues to
40
ensure they are in sync. If there is missing keys or obsolete keys in the target
41
catalogue, the command will exit with an error code.
42
43
When running the command in verbose mode, the translation keys will also be displayed.
44
<info>php %command.full_name% fr --verbose</info>
45
46
The <info>--domain</info> option allows to restrict the domains being checked.
47
It can be specified several times to check several domains. If the option is not passed,
48
all domains will be compared.
49
50
The <info>--obsolete-only</info> option allows to check only obsolete keys, and ignore any
51
missing keys.
52
53
The <info>--whitelist-file</info> option allows to define a whitelist of keys which are
54
ignored from the comparison (they are never reported as missing or as obsolete). This
55 18
file must be a Yaml file where keys are domains, and values are an array of whitelisted
56
EOF
57 18
            );
58
    }
59 18
60 18
    protected function execute(InputInterface $input, OutputInterface $output): int
61
    {
62
        $sourceCatalogue = $this->exposingTranslator->getCatalogue($input->getArgument('source'));
63 18
        $comparedCatalogue = $this->exposingTranslator->getCatalogue($input->getArgument('locale'));
64
65 18
        // Change the locale of the catalogue as DiffOperation requires operating on a single locale
66
        $catalogue = new MessageCatalogue($sourceCatalogue->getLocale(), $comparedCatalogue->all());
67 18
68 18
        $operation = new TargetOperation($catalogue, $sourceCatalogue);
69 18
70 4
        $domains = $operation->getDomains();
71 4
        $restrictedDomains = $input->getOption('domain');
72
        if (!empty($restrictedDomains)) {
73
            $domains = array_intersect($domains, $restrictedDomains);
74 18
            $output->writeln(sprintf('<comment>Checking the domains %s</comment>', implode(', ', $domains)));
75
        }
76 18
77 18
        $checkMissing = !$input->getOption('obsolete-only');
78
79 18
        $whitelistFile = $input->getOption('whitelist-file');
80 5
        $whitelist = array();
81 1
82
        if (null !== $whitelistFile) {
83 1
            if (!file_exists($whitelistFile)) {
84
                $output->writeln(sprintf('<error>The whitelist file "%s" does not exist.</error>', $whitelistFile));
85
86 4
                return 1;
87
            }
88 4
89 1
            $whitelist = Yaml::parseFile($whitelistFile);
90
91 1
            if (!is_array($whitelist)) {
92
                $output->writeln(sprintf('<error>The whitelist file "%s" is invalid. It must be a Yaml file containing a map.</error>', $whitelistFile));
93
94
                return 1;
95 16
            }
96
            /** @var array<string, string[]> $whitelist */
97 16
        }
98 16
99 16
        $valid = true;
100 16
101
        foreach ($domains as $domain) {
102 16
            $missingMessages = $checkMissing ? $operation->getNewMessages($domain) : array();
103 2
            $obsoleteMessages = $operation->getObsoleteMessages($domain);
104 2
            $written = false;
105 2
106
            if (isset($whitelist[$domain])) {
107
                $domainWhitelist = array_flip($whitelist[$domain]);
108 16
                $missingMessages = array_diff_key($missingMessages, $domainWhitelist);
109 7
                $obsoleteMessages = array_diff_key($obsoleteMessages, $domainWhitelist);
110 7
            }
111 7
112
            if (!empty($missingMessages)) {
113 7
                $valid = false;
114
                $written = true;
115
                $output->writeln(sprintf('<comment>%s</comment> messages are missing in the <info>%s</info> domain', count($missingMessages), $domain));
116 16
117 6
                $this->displayMessages($output, $missingMessages);
118 6
            }
119 6
120
            if (!empty($obsoleteMessages)) {
121 6
                $valid = false;
122
                $written = true;
123
                $output->writeln(sprintf('<comment>%s</comment> messages are obsolete in the <info>%s</info> domain', count($obsoleteMessages), $domain));
124 16
125 10
                $this->displayMessages($output, $obsoleteMessages);
126
            }
127
128
            if ($written) {
129 16
                $output->writeln('');
130 6
            }
131 6
        }
132 6
133 6
        if ($valid) {
134
            $output->writeln(sprintf(
135
                '<info>The <comment>%s</comment> catalogue is in sync with the <comment>%s</comment> one.</info>',
136 6
                $input->getArgument('locale'),
137
                $input->getArgument('source')
138
            ));
139 10
140
            return 0;
141
        }
142 10
143
        return 1;
144 10
    }
145 8
146
    /**
147
     * @param array<string, string> $messages
148 2
     */
149 2
    private function displayMessages(OutputInterface $output, array $messages): void
150
    {
151 2
        if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
152 2
            return;
153
        }
154
155
        foreach ($messages as $key => $translation) {
156
            $output->writeln('    '.$key);
157
        }
158
        $output->writeln('');
159
    }
160
}
161