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