LangReplacer   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 439
Duplicated Lines 6.15 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 27
loc 439
rs 3.2
c 0
b 0
f 0
wmc 65
lcom 1
cbo 2

22 Methods

Rating   Name   Duplication   Size   Complexity  
A findCyrillicKeys() 0 19 5
A replaceMainLangStrings() 0 11 2
A replaceModuleLangStrings() 0 10 2
A savePoMo() 0 15 4
B updateKeysInMoPoFiles() 0 29 7
B changeKeysInFile() 0 39 8
A getMainPaths() 0 19 1
A parseModule() 0 3 1
A parseMain() 0 12 3
B parseDir() 0 22 7
A translate() 0 9 3
A getDomainTranslations() 0 8 2
A find() 0 5 1
A isCyrillic() 0 3 2
A createReplacement() 0 3 3
A createExpression() 0 9 2
A getModuleLanguages() 5 14 3
A getDomainPoFilePath() 9 9 2
A createDomainMoFilePath() 8 8 2
A getTranslationDir() 5 8 2
A getModulePath() 0 3 1
A getAllModules() 0 11 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like LangReplacer 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 LangReplacer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace translator\classes;
4
5
use Gettext\Translation;
6
use Gettext\Translations;
7
8
class LangReplacer
9
{
10
11
    public static $domainTranslations = [];
12
13
    const LANG_FUNC = '(t?langf?)';
14
15
    const WORD = '([\s\S]+?)';
16
17
    const QUOTE = '[\'\"]{1}';
18
19
    const QUOTED_WORD = self::QUOTE . self::WORD . self::QUOTE;
20
21
    const QUOTED_DOMAIN = '(?:\s*,\s*' . self::QUOTE . self::WORD . self::QUOTE . ')?';
22
23
    const QUOTED_PARAMS = '(?:\s*,\s*' . self::WORD . ')?';
24
25
    const EXPRESSION = '/' . self::LANG_FUNC . '\(' . self::QUOTED_WORD . self::QUOTED_DOMAIN . '\)/';
26
27
28
    /*
29
     *******************************************************************************************************************
30
     * INTERFACE
31
     *
32
     * USAGE
33
     *  $module = 'aggregator';
34
     *  $allModules
35
     *  LangReplacer::replaceModuleLangStrings($module);
36
     *  LangReplacer::replaceMainLangStrings();
37
     *  LangReplacer::findCyrillicKeys($module)
38
     *******************************************************************************************************************
39
     */
40
41
    public static function findCyrillicKeys($module) {
42
        if ($module == 'main') {
43
            $langStrings = self::parseMain();
44
        } else {
45
            $langStrings = self::parseModule($module);
46
        }
47
        $found = [];
48
49
        foreach ($langStrings as $file => $found) {
50
            foreach (array_keys($found) as $key) {
51
52
                if (self::isCyrillic($key)) {
53
                    dump($file, $key);
54
                }
55
56
            }
57
        }
58
59
    }
60
61
    public static function replaceMainLangStrings() {
62
63
        $mainLangStrings = self::parseMain();
64
65
        $module = 'main';
66
        foreach ($mainLangStrings as $file => $data) {
67
            $translated = self::changeKeysInFile($file, $data);
68
            self::updateKeysInMoPoFiles($translated, $module);
69
        }
70
        self::savePoMo($module);
71
    }
72
73
    /**
74
     * Replace key to english translation
75
     * Add translation to other languages for this key
76
     * @param $module
77
     */
78
    public static function replaceModuleLangStrings($module) {
79
80
        $moduleLangStrings = self::parseModule($module);
81
82
        foreach ($moduleLangStrings as $file => $data) {
83
            $translated = self::changeKeysInFile($file, $data);
84
            self::updateKeysInMoPoFiles($translated, $module);
85
        }
86
87
    }
88
89
    private static function savePoMo($saveDomain) {
90
        foreach (self::$domainTranslations as $domain => $langs) {
91
            if ($domain == $saveDomain) {
92
93
                foreach ($langs as $lang => $translation) {
94
                    $dir = self::getDomainPoFilePath($domain, $lang);
95
                    dump(sprintf('Po file saving.. : %s', $dir));
96
                    $translation->toPoFile($dir);
97
                    $dir = self::createDomainMoFilePath($domain, $lang);
98
                    $translation->toMoFile($dir);
99
                    dump(sprintf('Mo file saving.. : %s', $dir));
100
                }
101
            }
102
        }
103
    }
104
105
    /**
106
     *
107
     * @param array $translated
108
     * @param string $module
109
     */
110
    private static function updateKeysInMoPoFiles($translated, $module) {
111
112
        $languages = self::getModuleLanguages($module);
113
        foreach ($languages as $language) {
114
            $translationsAllLocales[$language] = self::getDomainTranslations($module, $language);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$translationsAllLocales was never initialized. Although not strictly required by PHP, it is generally a good practice to add $translationsAllLocales = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
115
        }
116
117
        foreach ($translated as $changed) {
118
            foreach ($changed as $one) {
119
                foreach ($translationsAllLocales as $lang => $translationsOneLocale) {
120
                    /** @var Translation $oldTranslation */
121
                    $oldTranslation = $translationsOneLocale->find('', $one['from']);
122
123
                    if ($oldTranslation) {
124
                        $newTranslation = new Translation('', $one['to']);
125
                        $newTranslation->mergeWith($oldTranslation);
126
                        $translationsOneLocale[] = $newTranslation;
127
128
                        if (!$newTranslation->getTranslation()) {
129
                            $newTranslation->setTranslation($one['from']);
130
                        }
131
132
                    }
133
                }
134
            }
135
136
        }
137
138
    }
139
140
    /**
141
     * @param string $file
142
     * @param array $data
143
     * @return array
144
     */
145
    private static function changeKeysInFile($file, $data) {
146
        $translated = [];
147
        if (is_writable($file)) {
148
            $content = $newContent = file_get_contents($file);
149
            foreach ($data as $signature => $data) {
150
                $function = $data['function'];
151
                $word = $data['string'];
152
                $domain = $data['domain'];
153
                $params = $data['params'];
154
                $translation = self::translate($domain, $word, 'en_US');
155
                if ($translation && $translation !== $word) {
156
                    $newSignature = self::createReplacement($function, $translation, $domain, $params);
157
                    $translated[$domain][] = [
158
                                              'from'   => $word,
159
                                              'to'     => $translation,
160
                                              'domain' => $domain,
161
                                             ];
162
                    dump(sprintf('%s replaced to %s', $signature, $newSignature));
163
                    $newContent = str_replace($signature, $newSignature, $newContent);
164
                    //                    dump(sprintf('Changed %s to %s', $word, $translation));
165
                } else {
166
                    dump(sprintf("No default translation for: '%s' in domain %s", $word, $domain));
167
                }
168
169
            }
170
171
            if ($newContent && $newContent !== $content) {
172
                dump(sprintf('File %s saved: %s', $file, file_put_contents($file, $newContent) ? 'TRUE' : 'FALSE'));
173
            } else {
174
                //                dump("Warning: {$file} no content or no changes");
175
            }
176
        } else {
177
            dump("File: {$file} is not writable");
178
179
        }
180
181
        return $translated;
182
183
    }
184
185
    public static function getMainPaths() {
186
        return [
187
                APPPATH . 'core',
188
                APPPATH . 'errors',
189
                APPPATH . 'helpers',
190
                APPPATH . 'libraries',
191
                APPPATH . 'modules/shop/classes',
192
                APPPATH . 'modules/shop/helpers',
193
194
                PUBPATH . 'system/language/form_validation',
195
                PUBPATH . 'system/language/email_lang',
196
                PUBPATH . 'system/language/upload',
197
                PUBPATH . 'system/libraries',
198
                PUBPATH . 'templates/administrator/js/jquery-validate',
199
                PUBPATH . 'application/modules/shop/widgets',
200
                PUBPATH . 'application/modules/shop/models',
201
202
               ];
203
    }
204
205
    /*
206
     *******************************************************************************************************************
207
     * PARSE
208
     *******************************************************************************************************************
209
     */
210
211
    /**
212
     * Find lang calls in module files
213
     *
214
     * @param $module
215
     * @return array
216
     */
217
    public static function parseModule($module) {
218
        return self::parseDir(self::getModulePath($module));
219
    }
220
221
    /**
222
     * Find all main lang calls in main directories
223
     * @return array
224
     */
225
    public static function parseMain() {
226
227
        $results = [];
228
        foreach (self::getMainPaths() as $mainPath) {
229
230
            $add = self::parseDir($mainPath);
231
            if (is_array($add)) {
232
                $results = array_merge($results, $add);
233
            }
234
        }
235
        return $results;
236
    }
237
238
    /**
239
     * Search lang function call in all php|tpl files recursively
240
     * @param $dir
241
     * @return array
242
     */
243
    public static function parseDir($dir) {
244
        $dirIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
245
        $results = [];
246
        /** @var \SplFileInfo $item */
247
        foreach ($dirIterator as $item) {
248
            if ($item->isFile() && in_array($item->getExtension(), ['php', 'tpl']) && !strstr($item->getBasename(), 'jsLangs')) {
249
                $res = self::find(file_get_contents($item->getRealPath()));
250
                if (count($res[0])) {
251
                    foreach ($res[0] as $key => $signature) {
252
                        $results[$item->getRealPath()][$signature] = [
253
                                                                      'function' => $res[1][$key],
254
                                                                      'string'   => $res[2][$key],
255
                                                                      'domain'   => $res[3][$key],
256
                                                                      'params'   => $res[4][$key],
257
                                                                     ];
258
                    }
259
                }
260
            }
261
        }
262
263
        return $results;
264
    }
265
266
    /*
267
     *******************************************************************************************************************
268
     *  Translate
269
     *******************************************************************************************************************
270
     */
271
272
    /**
273
     * @param $domain
274
     * @param $word
275
     * @param string $lang
276
     * @return null|string
277
     */
278
    public static function translate($domain, $word, $lang = 'en_US') {
279
280
        $domain = $domain ?: 'main';
281
        $translations = self::getDomainTranslations($domain, $lang);
282
283
        $translation = $translations->find(null, $word);
284
285
        return $translation ? $translation->getTranslation() : null;
286
    }
287
288
    /**
289
     * @param $domain
290
     * @param string $locale
291
     * @return Translations
292
     */
293
    public static function getDomainTranslations($domain, $locale = 'en_US') {
294
295
        if (!isset(self::$domainTranslations[$domain][$locale])) {
296
            self::$domainTranslations[$domain][$locale] = Translations::fromPoFile(self::getDomainPoFilePath($domain, $locale));
297
298
        }
299
        return self::$domainTranslations[$domain][$locale];
300
    }
301
302
    /*
303
     *******************************************************************************************************************
304
     * REGEXP
305
     *******************************************************************************************************************
306
     */
307
308
    /**
309
     * Find all language calls in string
310
     * @param $fileContent
311
     * @param string $word
312
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be null|string[][]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
313
     */
314
    public static function find($fileContent, $word = '') {
315
        $expression = self::createExpression($word);
316
        preg_match_all($expression, $fileContent, $matches);
317
        return $matches;
318
    }
319
320
    public static function isCyrillic($word) {
321
        return preg_match('/[\p{Cyrillic}]/u', $word) || preg_match('/[А-Яа-яЁё]/u', $word);
322
    }
323
324
    /**
325
     * Create lang function call
326
     * @param $function
327
     * @param $word
328
     * @param $domain
329
     * @param $params
330
     * @return string
331
     */
332
    public static function createReplacement($function, $word, $domain, $params) {
333
        return sprintf("%s('%s'%s%s)", $function, $word, $domain ? sprintf(", '%s'", $domain) : '', $params ? sprintf(', %s', $params) : '');
334
    }
335
336
    /**
337
     * Create regular expression for lang call
338
     * @param $word
339
     * @return string
340
     */
341
    private static function createExpression($word = null) {
342
343
        $word = $word ? "($word)" : self::WORD;
344
        $quotedWord = self::QUOTE . $word . self::QUOTE;
345
        $expression = '/' . self::LANG_FUNC . '\(' . $quotedWord . self::QUOTED_DOMAIN . self::QUOTED_PARAMS . '\)/';
346
347
        return $expression;
348
349
    }
350
351
    /*
352
     *******************************************************************************************************************
353
     * PATHS
354
     *******************************************************************************************************************
355
     */
356
357
    /**
358
     * Get lang directories for module
359
     *
360
     * @param $module
361
     * @return array ['en_US', ...]
362
     */
363
    public static function getModuleLanguages($module) {
364
        $languages = [];
365
366 View Code Duplication
        if (in_array($module, ['shop', '', 'main'])) {
367
            $path = PUBPATH . APPPATH . 'language/main/*';
368
        } else {
369
            $path = self::getModulePath($module) . '/language/*';
370
        }
371
372
        foreach (glob($path) as $item) {
373
            $languages[] = substr($item, strrpos($item, '/') + 1);
374
        }
375
        return $languages;
376
    }
377
378
    /**
379
     * Get po file path
380
     * @param string $domain
381
     * @param string $locale
382
     * @return string
383
     */
384 View Code Duplication
    public static function getDomainPoFilePath($domain, $locale = 'en_US') {
385
        if (in_array($domain, ['shop', '', 'main'])) {
386
            $domain = 'main';
387
        }
388
389
        $dir = self::getTranslationDir($domain, $locale);
390
        return sprintf('%s%s.po', $dir, $domain);
391
392
    }
393
394
    /**
395
     * Create new mo file path
396
     * @param $domain
397
     * @param string $locale
398
     * @return string
399
     */
400 View Code Duplication
    public static function createDomainMoFilePath($domain, $locale = 'en_US') {
401
        if (in_array($domain, ['shop', '', 'main'])) {
402
            $domain = 'main';
403
        }
404
405
        $dir = self::getTranslationDir($domain, $locale);
406
        return sprintf('%s%s_%s.mo', $dir, $domain, time());
407
    }
408
409
    /**
410
     * Languages directory
411
     *
412
     * @param $domain
413
     * @param $locale
414
     * @return string
415
     */
416
    public static function getTranslationDir($domain, $locale) {
417 View Code Duplication
        if (in_array($domain, ['shop', '', 'main'])) {
418
            $transPath = PUBPATH . APPPATH . "language/main/{$locale}/LC_MESSAGES/";
419
        } else {
420
            $transPath = self::getModulePath($domain) . "/language/{$locale}/LC_MESSAGES/";
421
        }
422
        return $transPath;
423
    }
424
425
    /**
426
     * Full path to module
427
     * @param $moduleName
428
     * @return string
429
     */
430
    public static function getModulePath($moduleName) {
431
        return PUBPATH . APPPATH . 'modules/' . $moduleName;
432
    }
433
434
    public static function getAllModules() {
435
        $modules = [];
436
        $paths = PUBPATH . APPPATH . 'modules/*';
437
438
        foreach (glob($paths) as $path) {
439
440
            $modules[] = substr($path, strrpos($path, '/') + 1);
441
        }
442
443
        return $modules;
444
    }
445
446
}