Passed
Branch master (532093)
by Chauncey
17:20
created

TranslationParserScript::displayInformations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 0
dl 0
loc 23
rs 9.0856
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->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
     * Give feedback about what's going on.
178
     * @return self Chainable.
179
     */
180
    protected function displayInformations()
181
    {
182
        $this->climate()->underline()->out(
183
            'Initializing translations parser script...'
184
        );
185
186
        $this->climate()->green()->out(
187
            'CSV file output: <white>'.$this->filePath().'</white>'
188
        );
189
190
        $this->climate()->green()->out(
191
            'CSV file names: <white>'.$this->domain().'.{locale}.csv</white>'
192
        );
193
194
        $this->climate()->green()->out(
195
            'Looping through <white>'.$this->maxRecursiveLevel().'</white> level of folders'
196
        );
197
198
        $this->climate()->green()->out(
199
            'File type parsed: <white>mustache</white>'
200
        );
201
202
        return $this;
203
    }
204
205
    /**
206
     * Complete filepath to the CSV location.
207
     * @return string Filepath to the csv.
208
     */
209
    protected function filePath()
210
    {
211
        if ($this->filePath) {
212
            return $this->filePath;
213
        }
214
215
        $base = $this->appConfig->get('base_path');
216
        $output = $this->output();
217
        $this->filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
218
        return $this->filePath;
219
    }
220
221
    /**
222
     * Available locales (languages)
223
     * @return array Locales.
224
     */
225
    protected function locales()
226
    {
227
        return $this->translator()->availableLocales();
228
    }
229
230
    /**
231
     * @return string Current locale.
232
     */
233
    protected function locale()
234
    {
235
        return $this->translator()->getLocale();
236
    }
237
238
    /**
239
     * @return string
240
     */
241
    public function output()
242
    {
243
        if ($this->output) {
244
            return $this->output;
245
        }
246
        $output = $this->argOrInput('output');
247
        $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...
248
        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...
249
    }
250
251
    /**
252
     * Domain which is the csv file name prefix
253
     * @return string domain.
254
     */
255
    public function domain()
256
    {
257
        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...
258
    }
259
260
    /**
261
     * Regex to match in files.
262
     *
263
     * @param  string $type File type (mustache|php).
264
     * @return string       Regex string.
265
     */
266
    public function regEx($type)
267
    {
268
        switch ($type) {
269
            case 'php':
270
                $f = $this->phpFunction();
271
                $regex = '/->'.$f.'\(\s*\n*\r*(["\'])(?<text>(.|\n|\r|\n\r)*?)\s*\n*\r*\1\)/i';
272
                break;
273
274
            case 'mustache':
275
                $tag = $this->mustacheTag();
276
                $regex = '/({{|\[\[)\s*#\s*'.$tag.'\s*(}}|\]\])(?<text>(.|\n|\r|\n\r)*?)({{|\[\[)\s*\/\s*'.$tag.'\s*(}}|\]\])/i';
277
                break;
278
279
            default:
280
                $regex = '/({{|\[\[)\s*#\s*_t\s*(}}|\]\])(?<text>(.|\n|\r|\n\r)*?)({{|\[\[)\s*\/\s*_t\s*(}}|\]\])/i';
281
                break;
282
        }
283
284
        return $regex;
285
    }
286
287
    /**
288
     * Loop through all paths to get translations.
289
     * Also merge with already existing translations.
290
     * Translations associated with the locale.
291
     * [
292
     *    'fr' => [
293
     *        'string' => 'translation',
294
     *        'string' => 'translation',
295
     *        'string' => 'translation',
296
     *        'string' => 'translation'
297
     *    ],
298
     *    'en' => [
299
     *        'string' => 'translation',
300
     *        'string' => 'translation',
301
     *        'string' => 'translation',
302
     *        'string' => 'translation'
303
     *    ]
304
     * ]
305
     * @return array        Translations.
306
     */
307
    public function getTranslations()
308
    {
309
        $path = $this->path();
310
311
        if ($path) {
312
            $this->climate()->green()->out('Parsing files in <white>'.$path.'</white>');
313
            $translations = $this->getTranslationsFromPath($path, 'mustache');
314
            $translations = array_replace($translations, $this->getTranslationsFromPath($path, 'php'));
315
            return $translations;
316
        }
317
318
        $paths = $this->paths();
319
320
        $translations = [];
321
        foreach ($paths as $p) {
0 ignored issues
show
Bug introduced by
The expression $paths of type string is not traversable.
Loading history...
322
            $this->climate()->green()->out('Parsing files in <white>'.$p.'</white>');
323
            $translations = array_replace_recursive($translations, $this->getTranslationsFromPath($p, 'mustache'));
324
            $translations = array_replace_recursive($translations, $this->getTranslationsFromPath($p, 'php'));
325
        }
326
327
        return $translations;
328
    }
329
330
    /**
331
     * Get all translations in given path for the given file extension (mustache | php).
332
     * @param  string $path     The path.
333
     * @param  string $fileType The file extension|type.
334
     * @return array        Translations.
335
     */
336
    public function getTranslationsFromPath($path, $fileType)
337
    {
338
        // remove vendor/locomotivemtl/charcoal-app
339
        $base  = $this->appConfig->get('base_path');
340
        $glob  = $this->globRecursive($base.$path.'*.'.$fileType);
341
        $regex = $this->regEx($fileType);
342
343
        $translations = [];
344
345
        // Array index for the preg_match.
346
        $index = 'text';
347
        // if ($fileType == 'php') {
348
        //     $index = 'text';
349
        // }
350
351
        $k = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $k is dead and can be removed.
Loading history...
352
353
        $this->climate()->inline('.');
354
        // Loop files to get original text.
355
        foreach ($glob as $k => $file) {
356
            $k++;
357
            $this->climate()->inline('.');
358
            $text = file_get_contents($file);
359
360
            if (preg_match($regex, $text)) {
361
                preg_match_all($regex, $text, $array);
362
363
                $i = 0;
364
                $t = count($array[$index]);
365
                $locales = $this->locales();
366
367
                for (; $i<$t; $i++) {
368
                    $this->climate()->inline('.');
369
                    $orig = $array[$index][$i];
370
                    foreach ($locales as $lang) {
371
                        $this->climate()->inline('.');
372
                        $this->translator()->setLocale($lang);
373
                        // By calling translate, we make sure all existings translations are taking into consideration.
374
375
                        $translations[$lang][$orig] = stripslashes($this->translator()->translate($orig));
376
                    }
377
                }
378
            }
379
        }
380
        $this->climate()->out('.');
381
        $this->climate()->green()->out('Translations parsed from '.$path);
382
        return $translations;
383
    }
384
385
    /**
386
     * @todo  Added support for max depth.
387
     * @param string  $pattern The pattern to search.
388
     * @param integer $flags   The glob flags.
389
     * @return array
390
     * @see http://in.php.net/manual/en/function.glob.php#106595
391
     */
392
    public function globRecursive($pattern, $flags = 0)
393
    {
394
        // $max = $this->maxRecursiveLevel();
395
        $i = 1;
396
        $files = glob($pattern, $flags);
397
        foreach (glob(dirname($pattern).'/*', (GLOB_ONLYDIR|GLOB_NOSORT)) as $dir) {
398
            $files = array_merge($files, $this->globRecursive($dir.'/'.basename($pattern), $flags));
399
            $i++;
400
            // if ($i >= $max) {
401
            //     break;
402
            // }
403
        }
404
        return $files;
405
    }
406
407
    /**
408
     * Custom path
409
     * @return string
410
     */
411
    public function path()
412
    {
413
        if ($this->climate()->arguments->defined('path')) {
414
            $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...
415
        }
416
        return $this->path;
417
    }
418
419
    /**
420
     * @return string
421
     */
422
    public function paths()
423
    {
424
        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...
425
            $this->paths = $this->appConfig->get('view.paths');
426
427
            /** @todo Hardcoded; Change this! */
428
            $this->paths[] = 'src/';
429
        }
430
        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...
431
    }
432
433
    /**
434
     * @return string
435
     */
436
    public function fileTypes()
437
    {
438
        if (!$this->fileTypes) {
439
            $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...
440
                'php',
441
                'mustache'
442
            ];
443
        }
444
        return $this->fileTypes;
445
    }
446
447
    /**
448
     * @param array $translations The translations to save in CSV.
449
     * @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...
450
     */
451
    public function toCSV(array $translations)
452
    {
453
        if (!count($translations)) {
454
            $this->climate()->error('
455
                There was no translations in the provided path ('.$this->path().')
456
                with the given recursive level ('.$this->maxRecursiveLevel().')
457
            ');
458
            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...
459
        }
460
        $base = $this->appConfig->get('base_path');
461
        $output = $this->output();
462
        $domain = $this->domain();
463
464
        $separator = $this->separator();
465
        $enclosure = $this->enclosure();
466
467
        foreach ($translations as $lang => $trans) {
468
            // Create / open the handle
469
            $filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
470
            $dirname = dirname($filePath);
0 ignored issues
show
Unused Code introduced by
The assignment to $dirname is dead and can be removed.
Loading history...
471
472
            if (!file_exists($filePath)) {
473
                mkdir($filePath, 0755, true);
474
            }
475
            $file = fopen($base.$output.$domain.'.'.$lang.'.csv', 'w');
476
            if (!$file) {
477
                continue;
478
            }
479
480
            foreach ($trans as $key => $translation) {
481
                $data = [ $key, $translation ];
482
                fputcsv($file, $data, $separator, $enclosure);
483
            }
484
            fclose($file);
485
        }
486
487
        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...
488
    }
489
490
    /**
491
     * @return string
492
     */
493
    public function enclosure()
494
    {
495
        return '"';
496
    }
497
498
    /**
499
     * @return string
500
     */
501
    public function separator()
502
    {
503
        return ';';
504
    }
505
506
    /**
507
     * @return integer
508
     */
509
    public function maxRecursiveLevel()
510
    {
511
        if ($this->climate()->arguments->defined('recursive')) {
512
            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...
513
        }
514
        return 10;
515
    }
516
517
    /**
518
     * @return string Php function
519
     */
520
    private function phpFunction()
521
    {
522
        if ($this->climate()->arguments->defined('php_function')) {
523
            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...
524
        }
525
526
        return 'translate';
527
    }
528
529
    /**
530
     * @return string Mustache tag
531
     */
532
    private function mustacheTag()
533
    {
534
        if ($this->climate()->arguments->defined('mustache_tag')) {
535
            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...
536
        }
537
        return '_t';
538
    }
539
}
540