Issues (15)

CheckTranslations/CheckTranslationsCommand.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace Efabrica\TranslationsAutomatization\Command\CheckTranslations;
4
5
use Efabrica\TranslationsAutomatization\Command\CheckDictionaries\CheckDictionariesConfig;
6
use Efabrica\TranslationsAutomatization\Exception\InvalidConfigInstanceReturnedException;
7
use InvalidArgumentException;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\InputArgument;
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Input\InputOption;
12
use Symfony\Component\Console\Output\OutputInterface;
13
14
class CheckTranslationsCommand extends Command
15
{
16
    private $translationFindConfig;
17
18
    public function __construct(?string $name = null)
19
    {
20
        parent::__construct($name);
21
        $this->translationFindConfig = require __DIR__ . '/Config.php';
22
    }
23
24
    protected function configure()
25
    {
26
        $this->setName('check:translations')
27
            ->setDescription('Compare all translation keys with dictionaries(from files or api) for languages(default en_US)')
28
            ->addArgument('config', InputArgument::REQUIRED, 'Path to config file. Instance of ' . CheckDictionariesConfig::class . ' have to be returned')
29
            ->addOption('params', null, InputOption::VALUE_REQUIRED, 'Params for config in format --params="a=b&c=d"')
30
            ->addOption('include', null, InputOption::VALUE_REQUIRED, 'Params for translationFindConfig in format json --include="{"CLASS_ARGPOS_METHODS": {"Module": { "2": ["addResource"] }}}"')
31
            ->addOption('exclude', null, InputOption::VALUE_REQUIRED, 'Params for translationFindConfig in format json --exclude="{"CLASS_ARGPOS_METHODS": {"Module": { "2": ["addResource"] }}}"');
32
        // example exclude: --exclude='{"ARGPOS_CLASSES":{"0":["Efabrica\\WebComponent\\Core\\Menu\\MenuItem"]},"CLASS_ARGPOS_METHODS":{"Module":{"2":["addResource"]}}}'
33
        // example include: --include='{"CLASS_ARGPOS_METHODS":{"ALL":{"0":["trans"]}}}'
34
    }
35
36
    protected function execute(InputInterface $input, OutputInterface $output)
37
    {
38
        if (!is_file($input->getArgument('config'))) {
39
            throw new InvalidArgumentException('File "' . $input->getArgument('config') . '" does not exist');
40
        }
41
        parse_str($input->getOption('params'), $params);
42
        extract($params);
43
44
        $checkDictionariesConfig = require $input->getArgument('config');
45
        if ($checkDictionariesConfig instanceof InvalidConfigInstanceReturnedException) {
46
            throw $checkDictionariesConfig;
47
        }
48
        if (!$checkDictionariesConfig instanceof CheckDictionariesConfig) {
49
            throw new InvalidConfigInstanceReturnedException('"' . (is_object($checkDictionariesConfig) ? get_class($checkDictionariesConfig) : $checkDictionariesConfig) . '" is not instance of ' . CheckDictionariesConfig::class);
50
        }
51
52
        $output->writeln('');
53
        $output->writeln('Loading dictionaries...');
54
55
        $dictionaries = $checkDictionariesConfig->load();
56
        $onlyOneLang = (count($dictionaries) === 1);
57
        $errors = [];
58
        $dirs = ['./app', './src'];
59
60
        $exclude = json_decode($input->getOption('exclude') ?? '', true) ?? [];
61
        $include = json_decode($input->getOption('include') ?? '', true) ?? [];
62
        $this->processTranslationFindConfig($exclude, $include);
63
        $results = (new CodeAnalyzer($dirs, $this->translationFindConfig))->analyzeDirectories();
64
        foreach ($results as $call) {
65
            $key = $call['key'];
66
            if ($key === 'dynamic_value' || !is_string($key)) {
67
                continue;
68
            }
69
            if ($dictionaries === []) {
70
                $errors[] = 'No dictionaries found.';
71
                break;
72
            }
73
            foreach ($dictionaries as $lang => $dictionary) {
74
                $langText = !$onlyOneLang ? ' for language "' . $lang . '"' : '';
75
                if (!isset($dictionary[$key])) {
76
                    $errors[] = sprintf(
77
                        'Missing translation for key "%s" ' . $langText . 'in file: %s:%s call: "%s"',
78
                        $key,
79
                        $call['file'],
80
                        $call['line'],
81
                        $call['call']
82
                    );
83
                } else {
84
                    // find plural bad key
85
                    $dictionaryTranslate = $dictionary[$key];
86
                    $pluralKey = $call['arg'] ?? null;
87
                    $pluralKeyInFile = $pluralKey ? '%' . $pluralKey . '%' : null;
88
                    if ($pluralKey && strpos($dictionaryTranslate, $pluralKeyInFile) === false) {
0 ignored issues
show
It seems like $pluralKeyInFile can also be of type null; however, parameter $needle of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
                    if ($pluralKey && strpos($dictionaryTranslate, /** @scrutinizer ignore-type */ $pluralKeyInFile) === false) {
Loading history...
89
                        $errors[] = sprintf(
90
                            'Translation key "%s" ' . $langText . 'in file: %s:%s call: "%s" has bad plural key: %s for translation: "%s"',
91
                            $key,
92
                            $call['file'],
93
                            $call['line'],
94
                            $call['call'],
95
                            $pluralKeyInFile,
96
                            $dictionaryTranslate
97
                        );
98
                    }
99
                    if ($pluralKey === null && preg_match('/.*%.+%.*/', $dictionaryTranslate) === false) {
100
                        $errors[] = sprintf(
101
                            'Translation key "%s" ' . $langText . 'in file: %s:%s call: "%s" has missing plural key for translation: "%s"',
102
                            $key,
103
                            $call['file'],
104
                            $call['line'],
105
                            $call['call'],
106
                            $dictionaryTranslate
107
                        );
108
                    }
109
                }
110
            }
111
        }
112
        $output->writeln('', OutputInterface::VERBOSITY_VERY_VERBOSE);
113
        foreach (array_unique($errors) as $error) {
114
            $output->writeln($error, OutputInterface::VERBOSITY_VERY_VERBOSE);
115
        }
116
117
        $output->writeln('');
118
        $output->writeln('<comment>' . count($errors) . ' errors found</comment>');
119
        return count($errors);
120
    }
121
122
    private function processTranslationFindConfig(array $exclude, array $include): void
123
    {
124
        $this->translationFindConfig = array_merge_recursive($this->translationFindConfig, $include);
125
        foreach ($exclude as $key => $value) {
126
            $this->removeValueFromConfig($this->translationFindConfig, $key, $value);
127
        }
128
    }
129
130
    private function removeValueFromConfig(array &$config, $key, $value): void
131
    {
132
        if (isset($config[$key])) {
133
            if (is_array($config[$key]) && is_array($value)) {
134
                foreach ($value as $subKey => $subValue) {
135
                    $this->removeValueFromConfig($config[$key], $subKey, $subValue);
136
                }
137
            } elseif (($configKey = array_search($value, $config, true)) !== false) {
138
                unset($config[$configKey]);
139
            }
140
        }
141
    }
142
}
143