Passed
Push — main ( f6aab6...afae0a )
by Dimitri
07:52 queued 02:33
created

TranslationsFinder   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Test Coverage

Coverage 95.41%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 183
c 2
b 1
f 0
dl 0
loc 372
ccs 104
cts 109
cp 0.9541
rs 3.44
wmc 62

11 Methods

Rating   Name   Duplication   Size   Complexity  
A isIgnoredFile() 0 7 3
A writeIsVerbose() 0 4 2
A isSubDirectory() 0 3 1
C findTranslationsInFile() 0 47 13
A findLanguageKeysInFiles() 0 21 3
A buildMultiArray() 0 14 2
C process() 0 81 15
B replaceArraySyntax() 0 37 10
A arrayToTableRows() 0 17 4
B execute() 0 51 7
A templateFile() 0 16 2

How to fix   Complexity   

Complex Class

Complex classes like TranslationsFinder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TranslationsFinder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Cli\Commands\Translation;
13
14
use BlitzPHP\Cli\Console\Command;
15
use BlitzPHP\Utilities\Iterable\Arr;
16
use Locale;
17
use RecursiveDirectoryIterator;
18
use RecursiveIteratorIterator;
19
use SplFileInfo;
20
21
/**
22
 * @credit <a href="https://codeigniter.com">CodeIgniter 4 - \CodeIgniter\Commands\Translation\LocalizationFinder</a>
23
 */
24
class TranslationsFinder extends Command
25
{
26
    protected $group       = 'Translation';
27
    protected $name        = 'translations:find';
28
    protected $description = 'Trouver et sauvegarder les phrases disponibles à traduire';
29
    protected $options     = [
30
        '--locale'   => 'Spécifier la locale (en, ru, etc.) pour enregistrer les fichiers',
31
        '--dir'      => 'Répertoire de recherche des traductions relatif à APP_PATH.',
32
        '--show-new' => 'N\'affiche que les nouvelles traductions dans le tableau. N\'écrit pas dans les fichiers.',
33
        '--verbose'  => 'Affiche des informations détaillées',
34
    ];
35
36
    /**
37
     * Indicateur pour afficher des informations détaillées
38
     */
39
    private bool $verbose = false;
40
41
    /**
42
     * Indicateur pour afficher uniquement les traductions, sans sauvegarde
43
     */
44
    private bool $showNew = false;
45
46
    private string $languagePath;
47
48
    /**
49
     * {@inheritDoc}
50
     */
51
    public function execute(array $params)
52
    {
53 2
        $this->verbose      = array_key_exists('verbose', $params);
54 2
        $this->showNew      = array_key_exists('show-new', $params);
55 2
        $optionLocale       = $params['locale'] ?? null;
56 2
        $optionDir          = $params['dir'] ?? null;
57 2
        $currentLocale      = Locale::getDefault();
58 2
        $currentDir         = APP_PATH;
59 2
        $this->languagePath = $currentDir . 'Translations';
60
61
        if (ENVIRONMENT === 'testing') {
0 ignored issues
show
Bug introduced by
The constant BlitzPHP\Cli\Commands\Translation\ENVIRONMENT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
62 2
            $currentDir         = ROOTPATH . 'Services' . DS;
63 2
            $this->languagePath = ROOTPATH . 'Translations';
64
        }
65
66
        if (is_string($optionLocale)) {
67
            if (! in_array($optionLocale, config('app.supported_locales'), true)) {
0 ignored issues
show
Bug introduced by
config('app.supported_locales') of type BlitzPHP\Config\Config|null is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

67
            if (! in_array($optionLocale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
68
                $this->error(
69
                    'Erreur: "' . $optionLocale . '" n\'est pas supporté. Les langues supportées sont: '
70
                    . implode(', ', config('app.supported_locales'))
0 ignored issues
show
Bug introduced by
config('app.supported_locales') of type BlitzPHP\Config\Config|null is incompatible with the type array expected by parameter $pieces of implode(). ( Ignorable by Annotation )

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

70
                    . implode(', ', /** @scrutinizer ignore-type */ config('app.supported_locales'))
Loading history...
71 2
                );
72
73 2
                return EXIT_USER_INPUT;
74
            }
75
76
            $currentLocale = $optionLocale;
77
        }
78
79
        if (is_string($optionDir)) {
80 2
            $tempCurrentDir = realpath($currentDir . $optionDir);
81
82
            if ($tempCurrentDir === false) {
83 2
                $this->error('Erreur: Le dossier doit se trouvé dans "' . $currentDir . '"');
84
85 2
                return EXIT_USER_INPUT;
86
            }
87
88
            if ($this->isSubDirectory($tempCurrentDir, $this->languagePath)) {
89
                $this->error('Erreur: Le dossier "' . $this->languagePath . '" est restreint à l\'analyse.');
90
91
                return EXIT_USER_INPUT;
92
            }
93
94
            $currentDir = $tempCurrentDir;
95
        }
96
97 2
        $this->process($currentDir, $currentLocale);
98
99 2
        $this->ok('Opérations terminées!');
100
101 2
        return EXIT_SUCCESS;
102
    }
103
104
    private function process(string $currentDir, string $currentLocale): void
105
    {
106 2
        $tableRows    = [];
107 2
        $countNewKeys = 0;
108
109 2
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($currentDir));
110 2
        $files    = iterator_to_array($iterator, true);
111 2
        ksort($files);
112
113
        [
114
            'foundLanguageKeys' => $foundLanguageKeys,
115
            'badLanguageKeys'   => $badLanguageKeys,
116
            'countFiles'        => $countFiles
117 2
        ] = $this->findLanguageKeysInFiles($files);
118
119 2
        ksort($foundLanguageKeys);
120
121 2
        $languageDiff        = [];
122 2
        $languageFoundGroups = array_unique(array_keys($foundLanguageKeys));
123
124
        foreach ($languageFoundGroups as $langFileName) {
125 2
            $languageStoredKeys = [];
126 2
            $languageFilePath   = $this->languagePath . DIRECTORY_SEPARATOR . $currentLocale . DIRECTORY_SEPARATOR . $langFileName . '.php';
127
128
            if (is_file($languageFilePath)) {
129
                // Charge les anciennes traductions
130
                $languageStoredKeys = require $languageFilePath;
131
            }
132
133 2
            $languageDiff = Arr::diffRecursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
134 2
            $countNewKeys += Arr::countRecursive($languageDiff);
135
136
            if ($this->showNew) {
137 2
                $tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows);
138
            } else {
139 2
                $newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
140
141
                if ($languageDiff !== []) {
142
                    if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) {
143 2
                        $this->writeIsVerbose('Fichier de traduction ' . $langFileName . ' (error write).', 'red');
144
                    } else {
145 2
                        $this->writeIsVerbose('Le fichier de traduction "' . $langFileName . '" a été modifié avec succès!', 'green');
146
                    }
147
                }
148
            }
149
        }
150
151
        if ($this->showNew && $tableRows !== []) {
152 2
            sort($tableRows);
153 2
            $table = [];
154
155
            foreach ($tableRows as $body) {
156 2
                $table[] = array_combine(['File', 'Key'], $body);
157
            }
158 2
            $this->table($table);
159
        }
160
161
        if (! $this->showNew && $countNewKeys > 0) {
162 2
            $this->writer->bgRed('Note: Vous devez utiliser votre outil de linting pour résoudre les problèmes liés aux normes de codage.');
163
        }
164
165 2
        $this->writeIsVerbose('Fichiers trouvés: ' . $countFiles);
166 2
        $this->writeIsVerbose('Nouvelles traductions trouvées: ' . $countNewKeys);
167 2
        $this->writeIsVerbose('Mauvaises traductions trouvées: ' . count($badLanguageKeys));
168
169
        if ($this->verbose && $badLanguageKeys !== []) {
170 2
            $tableBadRows = [];
171
172
            foreach ($badLanguageKeys as $value) {
173 2
                $tableBadRows[] = [$value[1], $value[0]];
174
            }
175
176 2
            usort($tableBadRows, static fn ($currentValue, $nextValue): int => strnatcmp((string) $currentValue[0], (string) $nextValue[0]));
177
178 2
            $table = [];
179
180
            foreach ($tableBadRows as $body) {
181 2
                $table[] = array_combine(['Bad Key', 'Filepath'], $body);
182
            }
183
184 2
            $this->table($table);
185
        }
186
    }
187
188
    /**
189
     * @param SplFileInfo|string $file
190
     *
191
     * @return array<string, array>
192
     */
193
    private function findTranslationsInFile($file): array
194
    {
195 2
        $foundLanguageKeys = [];
196 2
        $badLanguageKeys   = [];
197
198
        if (is_string($file) && is_file($file)) {
199 2
            $file = new SplFileInfo($file);
200
        }
201
202 2
        $fileContent = file_get_contents($file->getRealPath());
203 2
        preg_match_all('/lang\(\'([._a-z0-9\-]+)\'\)/ui', $fileContent, $matches);
204
205
        if ($matches[1] === []) {
206 2
            return compact('foundLanguageKeys', 'badLanguageKeys');
207
        }
208
209
        foreach ($matches[1] as $phraseKey) {
210 2
            $phraseKeys = explode('.', $phraseKey);
211
212
            // Le code langue n'a pas de nom de fichier ou de code langue.
213
            if (count($phraseKeys) < 2) {
214 2
                $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
215
216 2
                continue;
217
            }
218
219 2
            $languageFileName   = array_shift($phraseKeys);
220
            $isEmptyNestedArray = ($languageFileName !== '' && $phraseKeys[0] === '')
221
                || ($languageFileName === '' && $phraseKeys[0] !== '')
222 2
                || ($languageFileName === '' && $phraseKeys[0] === '');
223
224
            if ($isEmptyNestedArray) {
225 2
                $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
226
227 2
                continue;
228
            }
229
230
            if (count($phraseKeys) === 1) {
231 2
                $foundLanguageKeys[$languageFileName][$phraseKeys[0]] = $phraseKey;
232
            } else {
233 2
                $childKeys = $this->buildMultiArray($phraseKeys, $phraseKey);
234
235 2
                $foundLanguageKeys[$languageFileName] = array_replace_recursive($foundLanguageKeys[$languageFileName] ?? [], $childKeys);
236
            }
237
        }
238
239 2
        return compact('foundLanguageKeys', 'badLanguageKeys');
240
    }
241
242
    private function isIgnoredFile(SplFileInfo $file): bool
243
    {
244
        if ($file->isDir() || $this->isSubDirectory($file->getRealPath(), $this->languagePath)) {
245 2
            return true;
246
        }
247
248 2
        return $file->getExtension() !== 'php';
249
    }
250
251
    private function templateFile(array $language = []): string
252
    {
253
        if ($language !== []) {
254 2
            $languageArrayString = var_export($language, true);
255
256
            $code = <<<PHP
257
                <?php
258
259
                return {$languageArrayString};
260
261
                PHP;
262
263 2
            return $this->replaceArraySyntax($code);
264
        }
265
266
        return <<<'PHP'
267
            <?php
268
269
            return [];
270
271
            PHP;
272
    }
273
274
    private function replaceArraySyntax(string $code): string
275
    {
276 2
        $tokens    = token_get_all($code);
277 2
        $newTokens = $tokens;
278
279
        foreach ($tokens as $i => $token) {
280
            if (is_array($token)) {
281 2
                [$tokenId, $tokenValue] = $token;
282
283
                // Remplace "array (" par "["
284
                if (
285
                    $tokenId === T_ARRAY
286
                    && $tokens[$i + 1][0] === T_WHITESPACE
287
                    && $tokens[$i + 2] === '('
288
                ) {
289 2
                    $newTokens[$i][1]     = '[';
290 2
                    $newTokens[$i + 1][1] = '';
291 2
                    $newTokens[$i + 2]    = '';
292
                }
293
294
                // Remplace les indentations
295
                if ($tokenId === T_WHITESPACE && preg_match('/\n([ ]+)/u', $tokenValue, $matches)) {
296 2
                    $newTokens[$i][1] = "\n{$matches[1]}{$matches[1]}";
297
                }
298
            } // Remplace ")"
299
            elseif ($token === ')') {
300 2
                $newTokens[$i] = ']';
301
            }
302
        }
303
304 2
        $output = '';
305
306
        foreach ($newTokens as $token) {
307 2
            $output .= $token[1] ?? $token;
308
        }
309
310 2
        return $output;
311
    }
312
313
    /**
314
     * Crée un tableau multidimensionnel à partir d'autres clés
315
     */
316
    private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): array
317
    {
318 2
        $newArray  = [];
319 2
        $lastIndex = array_pop($fromKeys);
320 2
        $current   = &$newArray;
321
322
        foreach ($fromKeys as $value) {
323 2
            $current[$value] = [];
324 2
            $current         = &$current[$value];
325
        }
326
327 2
        $current[$lastIndex] = $lastArrayValue;
328
329 2
        return $newArray;
330
    }
331
332
    /**
333
     * Convertit les tableaux multidimensionnels en lignes de table CLI spécifiques (tableau plat)
334
     */
335
    private function arrayToTableRows(string $langFileName, array $array): array
336
    {
337 2
        $rows = [];
338
339
        foreach ($array as $value) {
340
            if (is_array($value)) {
341 2
                $rows = array_merge($rows, $this->arrayToTableRows($langFileName, $value));
342
343 2
                continue;
344
            }
345
346
            if (is_string($value)) {
347 2
                $rows[] = [$langFileName, $value];
348
            }
349
        }
350
351 2
        return $rows;
352
    }
353
354
    /**
355
     * Affiche les détails dans la console si l'indicateur est défini
356
     */
357
    private function writeIsVerbose(string $text = '', ?string $foreground = null, ?string $background = null): void
358
    {
359
        if ($this->verbose) {
360 2
            $this->write($this->color->line($text, ['fg' => $foreground, 'bg' => $background]));
361
        }
362
    }
363
364
    private function isSubDirectory(string $directory, string $rootDirectory): bool
365
    {
366 2
        return 0 === strncmp($directory, $rootDirectory, strlen($directory));
367
    }
368
369
    /**
370
     * @param list<SplFileInfo> $files
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Cli\Commands\Translation\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
371
     *
372
     * @return         array<string, array|int>
373
     * @phpstan-return array{'foundLanguageKeys': array<string, array<string, string>>, 'badLanguageKeys': array<int, array<int, string>>, 'countFiles': int}
374
     */
375
    private function findLanguageKeysInFiles(array $files): array
376
    {
377 2
        $foundLanguageKeys = [];
378 2
        $badLanguageKeys   = [];
379 2
        $countFiles        = 0;
380
381
        foreach ($files as $file) {
382
            if ($this->isIgnoredFile($file)) {
383 2
                continue;
384
            }
385
386 2
            $this->writeIsVerbose('Ficher trouvé: ' . mb_substr($file->getRealPath(), mb_strlen(APP_PATH)));
387 2
            $countFiles++;
388
389 2
            $findInFile = $this->findTranslationsInFile($file);
390
391 2
            $foundLanguageKeys = array_replace_recursive($findInFile['foundLanguageKeys'], $foundLanguageKeys);
392 2
            $badLanguageKeys   = array_merge($findInFile['badLanguageKeys'], $badLanguageKeys);
393
        }
394
395 2
        return compact('foundLanguageKeys', 'badLanguageKeys', 'countFiles');
396
    }
397
}
398