Completed
Push — master ( d260a2...c2a5df )
by
unknown
02:40
created

TranslationParserScript::defaultArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 48
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

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

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
262
                $f = $this->phpFunction();
263
                $regex = '/->'.$f.'\(\s*\n*\r*(["\'])(?<text>(.|\n|\r|\n\r)*?)\s*\n*\r*\1\)/i';
264
            break;
265
266
            case 'mustache' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
267
                $tag = $this->mustacheTag();
268
                $regex = '/{{\s*#\s*'.$tag.'\s*}}(?<text>(.|\n|\r|\n\r)*?){{\s*\/\s*'.$tag.'\s*}}/i';
269
            break;
270
271
            default:
272
                $regex = '/{{\s*#\s*_t\s*}}(?<text>(.|\n|\r|\n\r)*?){{\s*\/\s*_t\s*}}/i';
273
            break;
274
        }
275
276
        return $regex;
277
    }
278
279
    /**
280
     * Loop through all paths to get translations.
281
     * Also merge with already existing translations.
282
     * Translations associated with the locale.
283
     * [
284
     *    'fr' => [
285
     *        'string' => 'translation',
286
     *        'string' => 'translation',
287
     *        'string' => 'translation',
288
     *        'string' => 'translation'
289
     *    ],
290
     *    'en' => [
291
     *        'string' => 'translation',
292
     *        'string' => 'translation',
293
     *        'string' => 'translation',
294
     *        'string' => 'translation'
295
     *    ]
296
     * ]
297
     * @return array        Translations.
298
     */
299
    public function getTranslations()
300
    {
301
        $path = $this->path();
302
303
        if ($path) {
304
            $this->climate()->green()->out('Parsing files in <white>' . $path .'</white>');
305
            $translations = $this->getTranslationsFromPath($path, 'mustache');
306
            $translations = array_merge($translations, $this->getTranslationsFromPath($path, 'php'));
307
            return $translations;
308
        }
309
310
        $paths = $this->paths();
311
312
        $translations = [];
313
        foreach ($paths as $p) {
314
            $this->climate()->green()->out('Parsing files in <white>' . $p .'</white>');
315
            $translations = array_merge_recursive($translations, $this->getTranslationsFromPath($p, 'mustache'));
316
            $translations = array_merge_recursive($translations, $this->getTranslationsFromPath($p, 'php'));
317
        }
318
319
        return $translations;
320
    }
321
322
    /**
323
     * Get all translations in given path for the given file extension (mustache | php).
324
     * @param  string $path The path.
325
     * @param  string $fileType The file extension|type.
326
     * @return array        Translations.
327
     */
328
    public function getTranslationsFromPath($path, $fileType)
329
    {
330
        // remove vendor/locomotivemtl/charcoal-app
331
        $base = $this->appConfig->get('base_path');
332
        $glob = $this->globRecursive($base.$path.'*.'.$fileType);
333
        $regex = $this->regEx($fileType);
334
335
        $translations = [];
336
337
        // Array index for the preg_match.
338
        $index = 'text';
339
        // if ($fileType == 'php') {
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
340
        //     $index = 'text';
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
341
        // }
342
343
        $k = 0;
344
345
        $this->climate()->inline('.');
346
        // Loop files to get original text.
347
        foreach ($glob as $k => $file) {
348
            $k++;
349
            $this->climate()->inline('.');
350
            $text = file_get_contents($file);
351
352
            if (preg_match($regex, $text)) {
353
                preg_match_all($regex, $text, $array);
354
355
356
                $i = 0;
357
                $t = count($array[$index]);
358
                $locales = $this->locales();
359
360
                for (; $i<$t; $i++) {
361
                    $this->climate()->inline('.');
362
                    $orig = $array[$index][$i];
363
                    foreach ($locales as $lang) {
364
                        $this->climate()->inline('.');
365
                        $this->translator()->setLocale($lang);
366
                        // By calling translate, we make sure all existings translations are taking into consideration.
367
368
                        $translations[$lang][$orig] = stripslashes($this->translator()->translate($orig));
369
                    }
370
                }
371
            }
372
        }
373
        $this->climate()->out('.');
374
        $this->climate()->green()->out('Translations parsed from ' . $path);
375
        return $translations;
376
    }
377
378
    /**
379
     * @param string  $pattern The pattern to search.
380
     * @param integer $flags   The glob flags.
381
     * @return array
382
     * @see http://in.php.net/manual/en/function.glob.php#106595
383
     */
384
    public function globRecursive($pattern, $flags = 0)
385
    {
386
        $max = $this->maxRecursiveLevel();
0 ignored issues
show
Unused Code introduced by
$max is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
387
        $i = 1;
388
        $files = glob($pattern, $flags);
389
        foreach (glob(dirname($pattern).'/*', (GLOB_ONLYDIR|GLOB_NOSORT)) as $dir) {
390
            $files = array_merge($files, $this->globRecursive($dir.'/'.basename($pattern), $flags));
391
            $i++;
392
            // if ($i >= $max) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
393
            //     break;
394
            // }
395
        }
396
        return $files;
397
    }
398
399
    /**
400
     * Custom path
401
     * @return [type] [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
402
     */
403
    public function path()
404
    {
405
        if ($this->climate()->arguments->defined('path')) {
406
            $this->path = $this->climate()->arguments->get('path');
407
        }
408
        return $this->path;
409
    }
410
411
    /**
412
     * @return string
413
     */
414
    public function paths()
415
    {
416
        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...
417
            $this->paths = $this->appConfig->get('view.paths');
418
419
            // Hardcoded
420
            // @todo change this
421
            $this->paths[] = 'src/';
422
        }
423
        return $this->paths;
424
    }
425
426
    /**
427
     * @return string
428
     */
429
    public function fileTypes()
430
    {
431
        if (!$this->fileTypes) {
432
            $this->fileTypes = [
433
                'php',
434
                'mustache'
435
            ];
436
        }
437
        return $this->fileTypes;
438
    }
439
440
    /**
441
     * @param array $translations The translations to save in CSV.
442
     * @return TranslateScript Chainable
443
     */
444
    public function toCSV(array $translations)
445
    {
446
        if (!count($translations)) {
447
            $this->climate()->error('
448
                There was no translations in the provided path ('. $this->path() .')
449
                with the given recursive level ('.$this->maxRecursiveLevel().')
450
            ');
451
            return $this;
452
        }
453
        $base = $this->appConfig->get('base_path');
454
        $output = $this->output();
455
        $domain = $this->domain();
456
457
        $separator = $this->separator();
458
        $enclosure = $this->enclosure();
459
460
        foreach ($translations as $lang => $trans) {
461
            // Create / open the handle
462
            $filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
463
            $dirname = dirname($filePath);
0 ignored issues
show
Unused Code introduced by
$dirname is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
464
465
            if (!file_exists($filePath)) {
466
                mkdir($filePath, 0755, true);
467
            }
468
            $file = fopen($base.$output.$domain.'.'.$lang.'.csv', 'w');
469
            if (!$file) {
470
                continue;
471
            }
472
473
            foreach ($trans as $key => $translation) {
474
                $data = [ $key, $translation ];
475
                fputcsv($file, $data, $separator, $enclosure);
476
            }
477
            fclose($file);
478
        }
479
480
481
        return $this;
482
    }
483
484
    /**
485
     * @return string
486
     */
487
    public function enclosure()
488
    {
489
        return '"';
490
    }
491
492
    /**
493
     * @return string
494
     */
495
    public function separator()
496
    {
497
        return ';';
498
    }
499
500
    /**
501
     * @return integer
502
     */
503
    public function maxRecursiveLevel()
504
    {
505
        if ($this->climate()->arguments->defined('recursive')) {
506
            return $this->climate()->arguments->get('recursive');
507
        }
508
        return 10;
509
    }
510
511
    /**
512
     * @return string Php function
513
     */
514
    private function phpFunction()
515
    {
516
        if ($this->climate()->arguments->defined('php_function')) {
517
            return $this->climate()->arguments->get('php_function');
518
        }
519
520
        return 'translate';
521
    }
522
523
    /**
524
     * @return string Mustache tag
525
     */
526
    private function mustacheTag()
527
    {
528
        if ($this->climate()->arguments->defined('mustache_tag')) {
529
            return $this->climate()->arguments->get('mustache_tag');
530
        }
531
        return '_t';
532
    }
533
}
534