Passed
Push — master ( f944d8...0dd60a )
by Thomas
15:52 queued 13:55
created

MultilingualTextCollector::mergeWithExisting()   D

Complexity

Conditions 18
Paths 183

Size

Total Lines 77
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 45
c 1
b 0
f 0
nc 183
nop 1
dl 0
loc 77
rs 4.175

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LeKoala\Multilingual;
4
5
use Exception;
6
use SilverStripe\Dev\Debug;
7
use SilverStripe\View\SSViewer;
8
use SilverStripe\Control\Director;
9
use SilverStripe\Core\Manifest\Module;
10
use SilverStripe\i18n\Messages\Reader;
11
use SilverStripe\i18n\Messages\Writer;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Manifest\ModuleLoader;
14
use SilverStripe\i18n\TextCollection\Parser;
15
use SilverStripe\i18n\TextCollection\i18nTextCollector;
16
use SilverStripe\Core\Path;
17
18
/**
19
 * Improved text collector
20
 */
21
class MultilingualTextCollector extends i18nTextCollector
22
{
23
    /**
24
     * @var boolean
25
     */
26
    protected $debug = false;
27
28
    /**
29
     * @var boolean
30
     */
31
    protected $clearUnused = false;
32
33
    /**
34
     * @var array<string>
35
     */
36
    protected $restrictToModules = [];
37
38
    /**
39
     * @var boolean
40
     */
41
    protected $mergeWithExisting = true;
42
43
    /**
44
     * @var boolean
45
     */
46
    protected $preventWrite = false;
47
48
    /**
49
     * @var boolean
50
     */
51
    protected $autoTranslate = false;
52
53
    /**
54
     * @var string
55
     */
56
    protected $autoTranslateLang = null;
57
58
    /**
59
     * @var string
60
     */
61
    protected $autoTranslateMode = 'all';
62
63
    /**
64
     * @param ?string $locale
65
     */
66
    public function __construct($locale = null)
67
    {
68
        parent::__construct($locale);
69
70
        // Somehow the injector is confused so we inject ourself
71
        $this->reader = Injector::inst()->create(Reader::class);
72
        $this->writer = Injector::inst()->create(Writer::class);
73
    }
74
75
    /**
76
     * This is the main method to build the master string tables with the
77
     * original strings. It will search for existent modules that use the
78
     * i18n feature, parse the _t() calls and write the resultant files
79
     * in the lang folder of each module.
80
     *
81
     * @param array<string> $restrictToModules
82
     * @param bool $mergeWithExisting Merge new master strings with existing
83
     * ones already defined in language files, rather than replacing them.
84
     * This can be useful for long-term maintenance of translations across
85
     * releases, because it allows "translation backports" to older releases
86
     * without removing strings these older releases still rely on.
87
     * @return array<string,mixed>|null $result
88
     */
89
    public function run($restrictToModules = null, $mergeWithExisting = false)
90
    {
91
        $entitiesByModule = $this->collect($restrictToModules, $mergeWithExisting);
92
        if (empty($entitiesByModule)) {
93
            Debug::message("No entities have been collected");
94
            return null;
95
        }
96
        if ($this->debug) {
97
            Debug::message("Debug mode is enabled and no files have been written");
98
            Debug::dump($entitiesByModule);
99
            return null;
100
        }
101
102
        $modules = $this->getModulesAndThemesExposed();
103
104
        // Write each module language file
105
        foreach ($entitiesByModule as $moduleName => $entities) {
106
            // Skip empty translations
107
            if (empty($entities)) {
108
                continue;
109
            }
110
111
            // Clean sorting prior to writing
112
            ksort($entities);
113
            $module = $modules[$moduleName];
114
            $this->write($module, $entities);
115
        }
116
117
        return $entitiesByModule;
118
    }
119
120
    protected function getModulesAndThemesExposed()
121
    {
122
        $refObject = new \ReflectionObject($this);
123
        $refMethod = $refObject->getMethod('getModulesAndThemes');
124
        $refMethod->setAccessible(true);
125
        return $refMethod->invoke($this);
126
    }
127
128
    protected function getModuleNameExposed($arg1, $arg2)
129
    {
130
        $refObject = new \ReflectionObject($this);
131
        $refMethod = $refObject->getMethod('getModuleName');
132
        $refMethod->setAccessible(true);
133
        return $refMethod->invoke($this, $arg1, $arg2);
134
    }
135
136
    /**
137
     * Extract all strings from modules and return these grouped by module name
138
     *
139
     * @param array<string> $restrictToModules
140
     * @param bool $mergeWithExisting
141
     * @return array<string,mixed>|null
142
     */
143
    public function collect($restrictToModules = null, $mergeWithExisting = null)
144
    {
145
        if ($mergeWithExisting === null) {
146
            $mergeWithExisting = $this->getMergeWithExisting();
147
        } else {
148
            $this->setMergeWithExisting($mergeWithExisting);
149
        }
150
        if ($restrictToModules === null) {
151
            $restrictToModules = $this->getRestrictToModules();
152
        } else {
153
            $this->setRestrictToModules($restrictToModules);
154
        }
155
156
        return parent::collect($restrictToModules, $mergeWithExisting);
157
    }
158
159
    /**
160
     * Collect all entities grouped by module
161
     *
162
     * @return array
163
     */
164
    protected function getEntitiesByModule()
165
    {
166
        $allModules = $this->getModulesAndThemesExposed();
167
        $modules = [];
168
        foreach ($this->restrictToModules as $m) {
169
            if (array_key_exists($m, $allModules)) {
170
                $modules[$m] = $allModules[$m];
171
            }
172
        }
173
174
        // A master string tables array (one mst per module)
175
        $entitiesByModule = [];
176
        foreach ($modules as $moduleName => $module) {
177
            // we store the master string tables
178
            $processedEntities = $this->processModule($module);
179
            $moduleName = $this->getModuleNameExposed($moduleName, $module);
180
            if (isset($entitiesByModule[$moduleName])) {
181
                $entitiesByModule[$moduleName] = array_merge_recursive(
182
                    $entitiesByModule[$moduleName],
183
                    $processedEntities
184
                );
185
            } else {
186
                $entitiesByModule[$moduleName] = $processedEntities;
187
            }
188
189
            // Extract all entities for "foreign" modules ('module' key in array form)
190
            // @see CMSMenu::provideI18nEntities for an example usage
191
            foreach ($entitiesByModule[$moduleName] as $fullName => $spec) {
192
                $specModuleName = $moduleName;
193
194
                // Rewrite spec if module is specified
195
                if (is_array($spec) && isset($spec['module'])) {
196
                    // Normalise name (in case non-composer name is specified)
197
                    $specModule = ModuleLoader::inst()->getManifest()->getModule($spec['module']);
198
                    if ($specModule) {
199
                        $specModuleName = $specModule->getName();
200
                    }
201
                    unset($spec['module']);
202
203
                    // If only element is default, simplify
204
                    if (count($spec ?? []) === 1 && !empty($spec['default'])) {
205
                        $spec = $spec['default'];
206
                    }
207
                }
208
209
                // Remove from source module
210
                if ($specModuleName !== $moduleName) {
211
                    unset($entitiesByModule[$moduleName][$fullName]);
212
                }
213
214
                // Write to target module
215
                if (!isset($entitiesByModule[$specModuleName])) {
216
                    $entitiesByModule[$specModuleName] = [];
217
                }
218
                $entitiesByModule[$specModuleName][$fullName] = $spec;
219
            }
220
        }
221
        return $entitiesByModule;
222
    }
223
224
    /**
225
     * Merge all entities with existing strings
226
     *
227
     * @param array<string,mixed> $entitiesByModule
228
     * @return array<string,mixed>|null
229
     */
230
    protected function mergeWithExisting($entitiesByModule)
231
    {
232
        $modules = $this->getModulesAndThemesExposed();
233
        // For each module do a simple merge of the default yml with these strings
234
        foreach ($entitiesByModule as $module => $messages) {
235
            $masterFile = Path::join($modules[$module]->getPath(), 'lang', $this->defaultLocale . '.yml');
236
237
            // YamlReader fails silently if path is not correct
238
            if (!is_file($masterFile)) {
239
                throw new Exception("File $masterFile does not exist. Please collect without merge first.");
240
            }
241
            $existingMessages = $this->getReader()->read($this->defaultLocale, $masterFile);
242
243
            // Merge
244
            if (!$existingMessages) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $existingMessages of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
245
                throw new Exception("No existing messages were found in $masterFile. Please collect without merge first.");
246
            }
247
248
            $newMessages = array_diff_key($messages, $existingMessages);
249
            $untranslatedMessages = [];
250
            foreach ($existingMessages as $k => $v) {
251
                $curr = $messages[$k] ?? null;
252
                if ($v == $curr) {
253
                    $untranslatedMessages[$k] = $v;
254
                }
255
            }
256
            $toTranslate = $this->autoTranslateMode == 'new' ? $newMessages : $untranslatedMessages;
257
258
            // attempt auto translation
259
            if ($this->autoTranslate) {
260
                if ($this->autoTranslateLang) {
261
                    EasyNmtHelper::$defaultLanguage = $this->autoTranslateLang;
0 ignored issues
show
Bug introduced by
The type LeKoala\Multilingual\EasyNmtHelper 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...
262
                }
263
                foreach ($toTranslate as $newMessageKey => $newMessageVal) {
264
                    try {
265
                        if (is_array($newMessageVal)) {
266
                            $result = [];
267
                            foreach ($newMessageVal as $newMessageValItem) {
268
                                $result[] = EasyNmtHelper::translate($newMessageValItem, $this->defaultLocale);
269
                            }
270
                        } else {
271
                            $result = EasyNmtHelper::translate($newMessageVal, $this->defaultLocale);
272
                        }
273
                        $messages[$newMessageKey] = $result;
274
                    } catch (Exception $ex) {
275
                        Debug::dump($ex->getMessage());
276
                    }
277
                }
278
            }
279
280
            if ($this->debug) {
281
                Debug::dump($existingMessages);
282
            }
283
            $entitiesByModule[$module] = array_merge(
284
                $messages,
285
                $existingMessages
286
            );
287
288
            // Clear unused
289
            if ($this->getClearUnused()) {
290
                $unusedEntities = array_diff(
291
                    array_keys($existingMessages),
292
                    array_keys($messages)
293
                );
294
                foreach ($unusedEntities as $unusedEntity) {
295
                    // Skip globals
296
                    if (strpos($unusedEntity, LangHelper::GLOBAL_ENTITY . '.') !== false) {
297
                        continue;
298
                    }
299
                    if ($this->debug) {
300
                        Debug::message("Removed $unusedEntity");
301
                    }
302
                    unset($entitiesByModule[$module][$unusedEntity]);
303
                }
304
            }
305
        }
306
        return $entitiesByModule;
307
    }
308
309
    /**
310
     * @param Module $module
311
     * @return array<string,mixed>|null
312
     */
313
    public function collectFromTheme(Module $module)
314
    {
315
        $themeDir = $this->getThemeDir();
316
        $themeFolder = Director::baseFolder() . '/' . $themeDir . '/Templates';
317
318
        $files = $this->getFilesRecursive($themeFolder, [], 'ss');
319
320
        $entities = [];
321
        foreach ($files as $file) {
322
            $fileContent = file_get_contents($file);
323
            if (!$fileContent) {
324
                continue;
325
            }
326
            $fileEntities = $this->collectFromTemplate($fileContent, $file, $module);
327
            if ($fileEntities) {
328
                $entities = array_merge($entities, $fileEntities);
329
            }
330
        }
331
332
        return $entities;
333
    }
334
335
    /**
336
     * Extracts translatables from .ss templates (Self referencing)
337
     *
338
     * @param string $content The text content of a parsed template-file
339
     * @param string $fileName The name of a template file when method is used in self-referencing mode
340
     * @param Module $module Module being collected
341
     * @param array<mixed> $parsedFiles
342
     * @return array<string,mixed>|null $entities An array of entities representing the extracted template function calls
343
     */
344
    public function collectFromTemplate($content, $fileName, Module $module, &$parsedFiles = [])
345
    {
346
        // Get namespace either from $fileName or $module fallback
347
        $namespace = $fileName ? basename($fileName) : $module->getName();
348
349
        // use parser to extract <%t style translatable entities
350
        $entities = Parser::getTranslatables($content, $this->getWarnOnEmptyDefault());
351
352
        // use the old method of getting _t() style translatable entities is forbidden
353
        if (preg_match_all('/(_t\([^\)]*?\))/ms', $content, $matches)) {
354
            throw new Exception("Old _t calls in $fileName are not allowed in templates. Please use <%t instead.");
355
        }
356
357
        foreach ($entities as $entity => $spec) {
358
            unset($entities[$entity]);
359
            $entities[$this->normalizeEntity($entity, $namespace)] = $spec;
360
        }
361
        ksort($entities);
362
363
        return $entities;
364
    }
365
366
    /**
367
     * Get current theme dir (regardless of current theme set)
368
     * This will work in admin for instance
369
     *
370
     * @return string
371
     */
372
    public function getThemeDir()
373
    {
374
        $themes = SSViewer::config()->themes;
375
        if (!$themes) {
376
            $themes = SSViewer::get_themes();
377
        }
378
        if ($themes) {
379
            do {
380
                $mainTheme = array_shift($themes);
381
            } while (strpos($mainTheme, '$') === 0);
382
383
            return 'themes/' . $mainTheme;
384
        }
385
        return project();
386
    }
387
388
    /**
389
     * @return boolean
390
     */
391
    public function isAdminTheme()
392
    {
393
        $themes = SSViewer::get_themes();
394
        if (empty($themes)) {
395
            return false;
396
        }
397
        $theme = $themes[0];
398
        return strpos($theme, 'silverstripe/admin') === 0;
399
    }
400
401
    /**
402
     * Get the value of clearUnused
403
     *
404
     * @return boolean
405
     */
406
    public function getClearUnused()
407
    {
408
        return $this->clearUnused;
409
    }
410
411
    /**
412
     * Set the value of clearUnused
413
     *
414
     * @param boolean $clearUnused
415
     *
416
     * @return self
417
     */
418
    public function setClearUnused($clearUnused)
419
    {
420
        $this->clearUnused = $clearUnused;
421
        return $this;
422
    }
423
424
    /**
425
     * Get the value of restrictToModules
426
     *
427
     * @return array<string>
428
     */
429
    public function getRestrictToModules()
430
    {
431
        return $this->restrictToModules;
432
    }
433
434
    /**
435
     * Set the value of restrictToModules
436
     *
437
     * @param array<string> $restrictToModules
438
     *
439
     * @return self
440
     */
441
    public function setRestrictToModules($restrictToModules)
442
    {
443
        $this->restrictToModules = $restrictToModules;
444
        return $this;
445
    }
446
447
    /**
448
     * Get the value of mergeWithExisting
449
     *
450
     * @return boolean
451
     */
452
    public function getMergeWithExisting()
453
    {
454
        return $this->mergeWithExisting;
455
    }
456
457
    /**
458
     * Set the value of mergeWithExisting
459
     *
460
     * @param boolean $mergeWithExisting
461
     *
462
     * @return self
463
     */
464
    public function setMergeWithExisting($mergeWithExisting)
465
    {
466
        $this->mergeWithExisting = $mergeWithExisting;
467
        return $this;
468
    }
469
470
    /**
471
     * Get the value of debug
472
     *
473
     * @return boolean
474
     */
475
    public function getDebug()
476
    {
477
        return $this->debug;
478
    }
479
480
    /**
481
     * Set the value of debug
482
     *
483
     * @param boolean $debug
484
     *
485
     * @return self
486
     */
487
    public function setDebug($debug)
488
    {
489
        $this->debug = $debug;
490
        return $this;
491
    }
492
493
    /**
494
     * Get the value of autoTranslate
495
     * @return boolean
496
     */
497
    public function getAutoTranslate()
498
    {
499
        return $this->autoTranslate;
500
    }
501
502
    /**
503
     * Set the value of autoTranslate
504
     *
505
     * @param boolean $autoTranslate
506
     * @return self
507
     */
508
    public function setAutoTranslate($autoTranslate, $lang = null, $mode = 'new')
509
    {
510
        $this->autoTranslate = $autoTranslate;
511
        $this->autoTranslateLang = $lang;
512
        $this->autoTranslateMode = $mode;
513
        return $this;
514
    }
515
}
516