Completed
Push — master ( 701418...4e7cc4 )
by
unknown
05:57
created

TranslationParserScript::paths()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
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
        ];
122
123
        $arguments = array_merge(parent::defaultArguments(), $arguments);
124
        return $arguments;
125
    }
126
127
    /**
128
     * @param RequestInterface  $request  A PSR-7 compatible Request instance.
129
     * @param ResponseInterface $response A PSR-7 compatible Response instance.
130
     * @return ResponseInterface
131
     */
132
    public function run(RequestInterface $request, ResponseInterface $response)
133
    {
134
        // Unused
135
        unset($request);
136
137
        // Parse arguments
138
        $this->climate()->arguments->parse();
139
        $this->displayInformations();
140
141
        // Get translations
142
        $translations = $this->getTranslations();
143
144
        // Output to CSV file.
145
        $this->toCSV($translations);
146
147
        // Warn the user
148
        $base = $this->appConfig->get('base_path');
149
        $output = $this->output();
150
        $filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
151
152
        $this->climate()->backgroundGreen()->out(
153
            'Make sure to include <light_green>' . $filePath . '</light_green> in your <light_green>translator/paths</light_green> configurations.'
154
        );
155
156
        return $response;
157
    }
158
159
    /**
160
     * Give feedback about what's going on.
161
     * @return self Chainable.
162
     */
163
    protected function displayInformations()
164
    {
165
        $this->climate()->underline()->out(
166
            'Initializing translations parser script...'
167
        );
168
169
170
        $this->climate()->green()->out(
171
            'CSV file output: <white>' . $this->filePath() . '</white>'
172
        );
173
174
        $this->climate()->green()->out(
175
            'CSV file names: <white>' . $this->domain().'.{locale}.csv</white>'
176
        );
177
178
        $this->climate()->green()->out(
179
            'Looping through <white>'.$this->maxRecursiveLevel().'</white> level of folders'
180
        );
181
182
        $this->climate()->green()->out(
183
            'File type parsed: <white>mustache</white>'
184
        );
185
186
        return $this;
187
    }
188
189
    /**
190
     * Complete filepath to the CSV location.
191
     * @return string Filepath to the csv.
192
     */
193
    protected function filePath()
194
    {
195
        if ($this->filePath) {
196
            return $this->filePath;
197
        }
198
199
        $base = $this->appConfig->get('base_path');
200
        $output = $this->output();
201
        $this->filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
202
        return $this->filePath;
203
    }
204
205
    /**
206
     * Available locales (languages)
207
     * @return array Locales.
208
     */
209
    protected function locales()
210
    {
211
        return $this->translator()->availableLocales();
212
    }
213
214
    /**
215
     * @return string Current locale.
216
     */
217
    protected function locale()
218
    {
219
        return $this->translator()->getLocale();
220
    }
221
222
    /**
223
     * @return string
224
     */
225
    public function output()
226
    {
227
        if ($this->output) {
228
            return $this->output;
229
        }
230
        $output = $this->argOrInput('output');
231
        $this->output = $output;
232
        return $this->output;
233
    }
234
235
    /**
236
     * Domain which is the csv file name prefix
237
     * @return string domain.
238
     */
239
    public function domain()
240
    {
241
        return $this->argOrInput('domain');
242
    }
243
244
    /**
245
     * Regex to match in files.
246
     * @param  string $type File type (mustache|php)
247
     * @return string       Regex string.
248
     */
249
    public function regEx($type)
250
    {
251
        switch ($type) {
252
            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...
253
                $regex = '/([^\d\wA-Za-z])_t\(\s*\n*\r*(["\'])(?<text>(.|\n|\r|\n\r)*?)\2\s*\n*\r*\)/i';
254
            break;
255
256
            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...
257
                $regex = '/{{\s*#\s*_t\s*}}((.|\n|\r|\n\r)*?){{\s*\/\s*_t\s*}}/i';
258
            break;
259
260
            default:
261
                $regex = '/{{\s*#\s*_t\s*}}((.|\n|\r|\n\r)*?){{\s*\/\s*_t\s*}}/i';
262
            break;
263
        }
264
265
        return $regex;
266
    }
267
268
    /**
269
     * Loop through all paths to get translations.
270
     * Also merge with already existing translations.
271
     * Translations associated with the locale.
272
     * [
273
     *    'fr' => [
274
     *        'string' => 'translation',
275
     *        'string' => 'translation',
276
     *        'string' => 'translation',
277
     *        'string' => 'translation'
278
     *    ],
279
     *    'en' => [
280
     *        'string' => 'translation',
281
     *        'string' => 'translation',
282
     *        'string' => 'translation',
283
     *        'string' => 'translation'
284
     *    ]
285
     * ]
286
     * @return array        Translations.
287
     */
288
    public function getTranslations()
289
    {
290
        $path = $this->path();
291
292
        if ($path) {
293
            $this->climate()->green()->out('Parsing files in <white>' . $path .'</white>');
294
            $translations = $this->getTranslationsFromPath($path, 'mustache');
295
            return $translations;
296
        }
297
        $paths = $this->paths();
298
299
        $translations = [];
300
        foreach ($paths as $p) {
0 ignored issues
show
Bug introduced by
The expression $paths of type string is not traversable.
Loading history...
301
            $this->climate()->green()->out('Parsing files in <white>' . $p .'</white>');
302
            $translations = array_merge_recursive($translations, $this->getTranslationsFromPath($p, 'mustache'));
303
        }
304
305
        return $translations;
306
    }
307
308
    /**
309
     * Get all translations in given path for the given file extension (mustache | php).
310
     * @param  string $path The path.
311
     * @param  string $fileType The file extension|type.
312
     * @return array        Translations.
313
     */
314
    public function getTranslationsFromPath($path, $fileType)
315
    {
316
317
        // remove vendor/locomotivemtl/charcoal-app
318
        $base = $this->appConfig->get('base_path');
319
        $glob = $this->globRecursive($base.$path.'*.'.$fileType);
320
        $regex = $this->regEx($fileType);
321
322
323
        $translations = [];
324
325
        // Array index for the preg_match.
326
        $index = 1;
327
328
        $this->climate()->inline('.');
329
        // Loop files to get original text.
330
        foreach ($glob as $k => $file) {
331
            $this->climate()->inline('.');
332
            $text = file_get_contents($file);
333
            if (preg_match($regex, $text)) {
334
                preg_match_all($regex, $text, $array);
335
336
                $i = 0;
337
                $t = count($array[$index]);
338
                $locales = $this->locales();
339
340
                for (; $i<$t; $i++) {
341
                    $this->climate()->inline('.');
342
                    $orig = $array[$index][$i];
343
                    foreach ($locales as $lang) {
344
                        $this->climate()->inline('.');
345
                        $this->translator()->setLocale($lang);
346
                        // By calling translate, we make sure all existings translations are taking into consideration.
347
                        $translations[$lang][$orig] = $this->translator()->translate($orig);
348
                    }
349
                }
350
            }
351
        }
352
353
        $this->climate()->out('.');
354
        $this->climate()->green()->out('Translations parsed from ' . $path);
355
        return $translations;
356
    }
357
358
    /**
359
     * @param string  $pattern The pattern to search.
360
     * @param integer $flags   The glob flags.
361
     * @return array
362
     * @see http://in.php.net/manual/en/function.glob.php#106595
363
     */
364
    public function globRecursive($pattern, $flags = 0)
365
    {
366
        $max = $this->maxRecursiveLevel();
367
        $i = 1;
368
        $files = glob($pattern, $flags);
369
        foreach (glob(dirname($pattern).'/*', (GLOB_ONLYDIR|GLOB_NOSORT)) as $dir) {
370
            $files = array_merge($files, $this->globRecursive($dir.'/'.basename($pattern), $flags));
371
            $i++;
372
            if ($i >= $max) {
373
                break;
374
            }
375
        }
376
        return $files;
377
    }
378
379
    /**
380
     * Custom path
381
     * @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...
382
     */
383
    public function path()
384
    {
385
        if ($this->climate()->arguments->defined('path')) {
386
            $this->path = $this->climate()->arguments->get('path');
387
        }
388
        return $this->path;
389
    }
390
391
    /**
392
     * @return string
393
     */
394
    public function paths()
395
    {
396
        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...
397
            $this->paths = $this->appConfig->get('view.paths');
398
        }
399
        return $this->paths;
400
    }
401
402
    /**
403
     * @return string
404
     */
405
    public function fileTypes()
406
    {
407
        if (!$this->fileTypes) {
408
            $this->fileTypes = [
409
                'php',
410
                'mustache'
411
            ];
412
        }
413
        return $this->fileTypes;
414
    }
415
416
    /**
417
     * @param array $translations The translations to save in CSV.
418
     * @return TranslateScript Chainable
419
     */
420
    public function toCSV(array $translations)
421
    {
422
        if (!count($translations)) {
423
            $this->climate()->error('
424
                There was no translations in the provided path ('. $this->path() .')
425
                with the given recursive level ('.$this->maxRecursiveLevel().')
426
            ');
427
            return $this;
428
        }
429
        $base = $this->appConfig->get('base_path');
430
        $output = $this->output();
431
        $domain = $this->domain();
432
433
        $separator = $this->separator();
434
        $enclosure = $this->enclosure();
435
436
        foreach ($translations as $lang => $trans) {
437
            // Create / open the handle
438
            $filePath = str_replace('/', DIRECTORY_SEPARATOR, $base.$output);
439
            $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...
440
441
            if (!file_exists($filePath)) {
442
                mkdir($filePath, 0755, true);
443
            }
444
            $file = fopen($base.$output.$domain.'.'.$lang.'.csv', 'w');
445
            if (!$file) {
446
                continue;
447
            }
448
449
            foreach ($trans as $key => $translation) {
450
                $data = [ $key, $translation ];
451
                fputcsv($file, $data, $separator, $enclosure);
452
            }
453
            fclose($file);
454
        }
455
456
457
        return $this;
458
    }
459
460
    /**
461
     * @return string
462
     */
463
    public function enclosure()
464
    {
465
        return '"';
466
    }
467
468
    /**
469
     * @return string
470
     */
471
    public function separator()
472
    {
473
        return ';';
474
    }
475
476
    /**
477
     * @return integer
478
     */
479
    public function maxRecursiveLevel()
480
    {
481
        if ($this->climate()->arguments->defined('recursive')) {
482
            return $this->climate()->arguments->get('recursive');
483
        }
484
        return 6;
485
    }
486
}
487