TranslationParserScript::getTranslations()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 0
dl 0
loc 21
rs 9.3142
c 0
b 0
f 0
1
<?php
2
namespace Charcoal\Translator\Script;
3
4
// From Pimple
5
use Pimple\Container;
6
7
// From PSR-7
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
11
// From 'charcoal-admin'
12
use Charcoal\Admin\AdminScript;
13
14
// From 'charcoal-translator'
15
use Charcoal\Translator\TranslatorAwareTrait;
16
17
/**
18
 * Find all strings to be translated in templates
19
 */
20
class TranslationParserScript extends AdminScript
21
{
22
    use TranslatorAwareTrait;
23
24
    /**
25
     * @var AppConfig
0 ignored issues
show
Bug introduced by
The type Charcoal\Translator\Script\AppConfig 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...
26
     */
27
    private $appConfig;
28
29
    /**
30
     * @var string
31
     */
32
    protected $fileType;
33
34
    /**
35
     * Output File.
36
     *
37
     * @var string
38
     */
39
    protected $output;
40
41
    /**
42
     * Paths to search.
43
     *
44
     * @var array
45
     */
46
    protected $paths;
47
48
    /**
49
     * Path to translations.
50
     *
51
     * @var string
52
     */
53
    protected $path;
54
55
    /**
56
     * Path for the CSV file to store.
57
     *
58
     * Noe: Full path with base path.
59
     *
60
     * @var string
61
     */
62
    protected $filePath;
63
64
    /**
65
     * @var array
66
     */
67
    protected $locales;
68
69
    /**
70
     * @param Container $container Pimple DI container.
71
     * @return void
72
     */
73
    public function setDependencies(Container $container)
74
    {
75
        $this->appConfig = $container['config'];
76
        $this->setTranslator($container['translator']);
77
        parent::setDependencies($container);
78
    }
79
80
    /**
81
     * Arguments that can be use in the script.
82
     * If no path is provided, all views path are parsed.
83
     * If you do precise a path, notice that you will loose
84
     * all modified translations that comes from other paths.
85
     *
86
     * Valid arguments:
87
     * - output : path-to-csv/
88
     * - domain : filename prefix
89
     * - recursive : level of recursiveness (how deep the glob checks for the strings)
90
     * - path : Path to get translation from a precise location (i.e: templates/emails/)
91
     * - type : file type (either mustache or php)
92
     *
93
     * @todo Support php file type.
94
     * @return array
95
     */
96
    public function defaultArguments()
97
    {
98
        $arguments = [
99
            'output' => [
100
                'prefix' => 'o',
101
                'longPrefix' => 'output',
102
                'description' => 'Output file path. Make sure the path exists in the translator paths definition. (Default: translation/)',
103
                'defaultValue' => 'translations/'
104
            ],
105
            'domain' => [
106
                'prefix' => 'd',
107
                'longPrefix' => 'domain',
108
                'description' => 'Doman for the csv file. Based on symfony/translator CsvLoader.',
109
                'defaultValue' => 'messages'
110
            ],
111
            'recursive' => [
112
                'prefix' => 'r',
113
                'longPrefix' => 'recursive-level',
114
                'description' => 'Max recursive level for the glob operation on folders.',
115
                'defaultValue' => 4
116
            ],
117
            'path' => [
118
                'longPrefix'   => 'path',
119
                'prefix'  => 'p',
120
                'description'  => 'Path relative to the project installation (ex: templates/*/*/)',
121
                'defaultValue' => false
122
            ],
123
            'type' => [
124
                'longPrefix'   => 'type',
125
                'prefix'  => 't',
126
                'description'  => 'File type (mustache || php)',
127
                'defaultValue' => 'mustache'
128
            ],
129
            'php_function' => [
130
                'longPrefix'   => 'php',
131
                'description'  => 'Php function to be parsed.',
132
                'defaultValue' => 'translate'
133
            ],
134
            'mustache_tag' => [
135
                'longPrefix'   => 'mustache',
136
                'description'  => 'Mustache function to be parsed.',
137
                'defaultValue' => '_t'
138
            ]
139
        ];
140
141
        $arguments = array_merge(parent::defaultArguments(), $arguments);
142
        return $arguments;
143
    }
144
145
    /**
146
     * @param RequestInterface  $request  A PSR-7 compatible Request instance.
147
     * @param ResponseInterface $response A PSR-7 compatible Response instance.
148
     * @return ResponseInterface
149
     */
150
    public function run(RequestInterface $request, ResponseInterface $response)
151
    {
152
        // Unused
153
        unset($request);
154
155
        // Parse arguments
156
        $this->displayInformations();
157
158
        // Get translations
159
        $translations = $this->parseTranslations($this->getTranslations());
160
161
        // Output to CSV file.
162
        $this->toCSV($translations);
163
164
        // Warn the user
165
        $base = $this->appConfig->get('base_path');
166
        $output = $this->output();
167
        $filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
168
169
        $this->climate()->backgroundGreen()->out(
170
            'Make sure to include <light_green>'.$filePath.'</light_green> in your <light_green>translator/paths</light_green> configurations.'
171
        );
172
173
        return $response;
174
    }
175
176
    /**
177
     * @param array $trans The translations array.
178
     * @return array
179
     */
180
    protected function parseTranslations(array $trans)
181
    {
182
        // Must be the first occurrence of the the key.
183
        foreach ($trans as $lang => &$value) {
184
            array_walk($value, function (&$val, $key) {
185
                // remove key template ident in translation value.
186
                if (preg_match('|^\[([^\]]*)\]|', $key, $translationContext)) {
187
                    $val = str_replace($translationContext[0], '', $val);
188
                }
189
190
                // remove key input type from translation value.
191
                if (preg_match('|:(?:\S*)$|', $key, $translationInputType)) {
192
                    $val = str_replace($translationInputType[0], '', $val);
193
                }
194
            });
195
        }
196
197
        return $trans;
198
    }
199
200
    /**
201
     * Give feedback about what's going on.
202
     * @return self Chainable.
203
     */
204
    protected function displayInformations()
205
    {
206
        $this->climate()->underline()->out(
207
            'Initializing translations parser script...'
208
        );
209
210
        $this->climate()->green()->out(
211
            'CSV file output: <white>'.$this->filePath().'</white>'
212
        );
213
214
        $this->climate()->green()->out(
215
            'CSV file names: <white>'.$this->domain().'.{locale}.csv</white>'
216
        );
217
218
        $this->climate()->green()->out(
219
            'Looping through <white>'.$this->maxRecursiveLevel().'</white> level of folders'
220
        );
221
222
        $this->climate()->green()->out(
223
            'File type parsed: <white>mustache</white>'
224
        );
225
226
        return $this;
227
    }
228
229
    /**
230
     * Complete filepath to the CSV location.
231
     * @return string Filepath to the csv.
232
     */
233
    protected function filePath()
234
    {
235
        if ($this->filePath) {
236
            return $this->filePath;
237
        }
238
239
        $base = $this->appConfig->get('base_path');
240
        $output = $this->output();
241
        $this->filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
242
        return $this->filePath;
243
    }
244
245
    /**
246
     * Available locales (languages)
247
     * @return array Locales.
248
     */
249
    protected function locales()
250
    {
251
        return $this->translator()->availableLocales();
252
    }
253
254
    /**
255
     * @return string Current locale.
256
     */
257
    protected function locale()
258
    {
259
        return $this->translator()->getLocale();
260
    }
261
262
    /**
263
     * @return string
264
     */
265
    public function output()
266
    {
267
        if ($this->output) {
268
            return $this->output;
269
        }
270
        $output = $this->argOrInput('output');
271
        $this->output = $output;
0 ignored issues
show
Documentation Bug introduced by
It seems like $output can also be of type true. However, the property $output is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
272
        return $this->output;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->output also could return the type true which is incompatible with the documented return type string.
Loading history...
273
    }
274
275
    /**
276
     * Domain which is the csv file name prefix
277
     * @return string domain.
278
     */
279
    public function domain()
280
    {
281
        return $this->argOrInput('domain');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->argOrInput('domain') also could return the type true which is incompatible with the documented return type string.
Loading history...
282
    }
283
284
    /**
285
     * Regex to match in files.
286
     *
287
     * @param  string $type File type (mustache|php).
288
     * @return string Regex string.
289
     */
290
    public function regEx($type)
291
    {
292
        switch ($type) {
293
            case 'php':
294
                $f = $this->phpFunction();
295
                $regex = '/->'.$f.'\(\s*\n*\r*(["\'])(?<text>(.|\n|\r|\n\r)*?)\s*\n*\r*\1\)/i';
296
                break;
297
298
            case 'mustache':
299
                $tag = $this->mustacheTag();
300
                $regex = '/({{|\[\[)\s*#\s*'.$tag.'\s*(}}|\]\])(?<text>(.|\n|\r|\n\r)*?)({{|\[\[)\s*\/\s*'.$tag.'\s*(}}|\]\])/i';
301
                break;
302
303
            default:
304
                $regex = '/({{|\[\[)\s*#\s*_t\s*(}}|\]\])(?<text>(.|\n|\r|\n\r)*?)({{|\[\[)\s*\/\s*_t\s*(}}|\]\])/i';
305
                break;
306
        }
307
308
        return $regex;
309
    }
310
311
    /**
312
     * Loop through all paths to get translations.
313
     * Also merge with already existing translations.
314
     * Translations associated with the locale.
315
     * [
316
     *    'fr' => [
317
     *        'string' => 'translation',
318
     *        'string' => 'translation',
319
     *        'string' => 'translation',
320
     *        'string' => 'translation'
321
     *    ],
322
     *    'en' => [
323
     *        'string' => 'translation',
324
     *        'string' => 'translation',
325
     *        'string' => 'translation',
326
     *        'string' => 'translation'
327
     *    ]
328
     * ]
329
     * @return array        Translations.
330
     */
331
    public function getTranslations()
332
    {
333
        $path = $this->path();
334
335
        if ($path) {
336
            $this->climate()->green()->out('Parsing files in <white>'.$path.'</white>');
337
            $translations = $this->getTranslationsFromPath($path, 'mustache');
338
            $translations = array_replace($translations, $this->getTranslationsFromPath($path, 'php'));
339
            return $translations;
340
        }
341
342
        $paths = $this->paths();
343
344
        $translations = [];
345
        foreach ($paths as $p) {
0 ignored issues
show
Bug introduced by
The expression $paths of type string is not traversable.
Loading history...
346
            $this->climate()->green()->out('Parsing files in <white>'.$p.'</white>');
347
            $translations = array_replace_recursive($translations, $this->getTranslationsFromPath($p, 'mustache'));
348
            $translations = array_replace_recursive($translations, $this->getTranslationsFromPath($p, 'php'));
349
        }
350
351
        return $translations;
352
    }
353
354
    /**
355
     * Get all translations in given path for the given file extension (mustache | php).
356
     * @param  string $path     The path.
357
     * @param  string $fileType The file extension|type.
358
     * @return array        Translations.
359
     */
360
    public function getTranslationsFromPath($path, $fileType)
361
    {
362
        // remove vendor/locomotivemtl/charcoal-app
363
        $base  = $this->appConfig->get('base_path');
364
        $glob  = $this->globRecursive($base.$path.'*.'.$fileType);
365
        $regex = $this->regEx($fileType);
366
367
        $translations = [];
368
369
        // Array index for the preg_match.
370
        $index = 'text';
371
        // if ($fileType == 'php') {
372
        //     $index = 'text';
373
        // }
374
375
        $k = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $k is dead and can be removed.
Loading history...
376
377
        $this->climate()->inline('.');
378
        // Loop files to get original text.
379
        foreach ($glob as $k => $file) {
380
            $k++;
381
            $this->climate()->inline('.');
382
            $text = file_get_contents($file);
383
384
            if (preg_match($regex, $text)) {
385
                preg_match_all($regex, $text, $array);
386
387
                $i = 0;
388
                $t = count($array[$index]);
389
                $locales = $this->locales();
390
391
                for (; $i < $t; $i++) {
392
                    $this->climate()->inline('.');
393
                    $orig = $array[$index][$i];
394
                    foreach ($locales as $lang) {
395
                        $this->climate()->inline('.');
396
                        $this->translator()->setLocale($lang);
397
                        // By calling translate, we make sure all existings translations are taking into consideration.
398
399
                        $translations[$lang][$orig] = stripslashes($this->translator()->translate($orig));
400
                    }
401
                }
402
            }
403
        }
404
        $this->climate()->out('.');
405
        $this->climate()->green()->out('Translations parsed from '.$path);
406
        return $translations;
407
    }
408
409
    /**
410
     * @todo  Added support for max depth.
411
     * @param string  $pattern The pattern to search.
412
     * @param integer $flags   The glob flags.
413
     * @return array
414
     * @see http://in.php.net/manual/en/function.glob.php#106595
415
     */
416
    public function globRecursive($pattern, $flags = 0)
417
    {
418
        // $max = $this->maxRecursiveLevel();
419
        $i = 1;
420
        $files = glob($pattern, $flags);
421
        foreach (glob(dirname($pattern).'/*', (GLOB_ONLYDIR | GLOB_NOSORT)) as $dir) {
422
            $files = array_merge($files, $this->globRecursive($dir.'/'.basename($pattern), $flags));
423
            $i++;
424
            // if ($i >= $max) {
425
            //     break;
426
            // }
427
        }
428
        return $files;
429
    }
430
431
    /**
432
     * Custom path
433
     * @return string
434
     */
435
    public function path()
436
    {
437
        if ($this->climate()->arguments->defined('path')) {
438
            $this->path = $this->climate()->arguments->get('path');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->climate()->arguments->get('path') can also be of type boolean. However, the property $path is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
439
        }
440
        return $this->path;
441
    }
442
443
    /**
444
     * @return string
445
     */
446
    public function paths()
447
    {
448
        if (!$this->paths) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->paths 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...
449
            $this->paths = $this->appConfig->get('translator.parser.view.paths') ?:
450
                $this->appConfig->get('view.paths');
451
452
            /** @todo Hardcoded; Change this! */
453
            $this->paths[] = 'src/';
454
        }
455
        return $this->paths;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->paths returns the type array which is incompatible with the documented return type string.
Loading history...
456
    }
457
458
    /**
459
     * @return string
460
     */
461
    public function fileTypes()
462
    {
463
        if (!$this->fileTypes) {
464
            $this->fileTypes = [
0 ignored issues
show
Bug Best Practice introduced by
The property fileTypes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
465
                'php',
466
                'mustache'
467
            ];
468
        }
469
        return $this->fileTypes;
470
    }
471
472
    /**
473
     * @param array $translations The translations to save in CSV.
474
     * @return TranslateScript Chainable
0 ignored issues
show
Bug introduced by
The type Charcoal\Translator\Script\TranslateScript 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...
475
     */
476
    public function toCSV(array $translations)
477
    {
478
        if (!count($translations)) {
479
            $this->climate()->error('
480
                There was no translations in the provided path ('.$this->path().')
481
                with the given recursive level ('.$this->maxRecursiveLevel().')
482
            ');
483
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Charcoal\Translator\Script\TranslationParserScript which is incompatible with the documented return type Charcoal\Translator\Script\TranslateScript.
Loading history...
484
        }
485
        $base = $this->appConfig->get('base_path');
486
        $output = $this->output();
487
        $domain = $this->domain();
488
489
        $separator = $this->separator();
490
        $enclosure = $this->enclosure();
491
492
        foreach ($translations as $lang => $trans) {
493
            // Create / open the handle
494
            $filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
495
            $dirname = dirname($filePath);
0 ignored issues
show
Unused Code introduced by
The assignment to $dirname is dead and can be removed.
Loading history...
496
497
            if (!file_exists($filePath)) {
498
                mkdir($filePath, 0755, true);
499
            }
500
            $file = fopen($base.$output.$domain.'.'.$lang.'.csv', 'w');
501
            if (!$file) {
502
                continue;
503
            }
504
505
            foreach ($trans as $key => $translation) {
506
                $data = [ $key, $translation ];
507
                fputcsv($file, $data, $separator, $enclosure);
508
            }
509
            fclose($file);
510
        }
511
512
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Charcoal\Translator\Script\TranslationParserScript which is incompatible with the documented return type Charcoal\Translator\Script\TranslateScript.
Loading history...
513
    }
514
515
    /**
516
     * @return string
517
     */
518
    public function enclosure()
519
    {
520
        return '"';
521
    }
522
523
    /**
524
     * @return string
525
     */
526
    public function separator()
527
    {
528
        return ';';
529
    }
530
531
    /**
532
     * @return integer
533
     */
534
    public function maxRecursiveLevel()
535
    {
536
        if ($this->climate()->arguments->defined('recursive')) {
537
            return $this->climate()->arguments->get('recursive');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->climate()-...ments->get('recursive') also could return the type string|boolean which is incompatible with the documented return type integer.
Loading history...
538
        }
539
        return 10;
540
    }
541
542
    /**
543
     * @return string Php function
544
     */
545
    private function phpFunction()
546
    {
547
        if ($this->climate()->arguments->defined('php_function')) {
548
            return $this->climate()->arguments->get('php_function');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->climate()-...ts->get('php_function') also could return the type boolean which is incompatible with the documented return type string.
Loading history...
549
        }
550
551
        return 'translate';
552
    }
553
554
    /**
555
     * @return string Mustache tag
556
     */
557
    private function mustacheTag()
558
    {
559
        if ($this->climate()->arguments->defined('mustache_tag')) {
560
            return $this->climate()->arguments->get('mustache_tag');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->climate()-...ts->get('mustache_tag') also could return the type boolean which is incompatible with the documented return type string.
Loading history...
561
        }
562
        return '_t';
563
    }
564
}
565