Completed
Push — master ( 1abf17...425208 )
by Mathieu
06:14
created

TranslateScript::globRecursive()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
rs 9.4285
cc 3
eloc 10
nc 3
nop 2
1
<?php
2
3
namespace Charcoal\Admin\Script\Translation;
4
5
// PSR-7 (http messaging) dependencies
6
use \Psr\Http\Message\RequestInterface;
7
use \Psr\Http\Message\ResponseInterface;
8
9
// Module `charcoal-core` dependencies
10
use \Charcoal\Model\ModelFactory;
11
12
// Intra-module (`charcoal-admin`) dependencies
13
use \Charcoal\Admin\AdminScript;
14
15
/**
16
 * Find all strings to be translated in mustache or php files
17
 */
18
class TranslateScript extends AdminScript
19
{
20
    /**
21
     * @var string $fileType
22
     */
23
    protected $fileType;
24
25
    /**
26
     * @var string $output
27
     */
28
    protected $output;
29
30
    /**
31
     * @var string $path
32
     */
33
    protected $path;
34
35
    /**
36
     * @var array $locales
37
     */
38
    protected $locales;
39
40
    /**
41
     * Valid arguments:
42
     * - path : path/to/files
43
     * - type : mustache | php
44
     *
45
     * @return array
46
     */
47
    public function defaultArguments()
48
    {
49
        $arguments = [
50
            'path' => [
51
                'longPrefix'   => 'path',
52
                'description'  => 'Path relative to the project installation (ex: templates/*/*/)',
53
                'defaultValue' => ''
54
            ],
55
            'type' => [
56
                'longPrefix'   => 'type',
57
                'description'  => 'File type (mustache || php)',
58
                'defaultValue' => ''
59
            ],
60
            'output' => [
61
                'longPrefix'   => 'output',
62
                'description'  => 'Output file path',
63
                'defaultValue' => ''
64
            ]
65
        ];
66
67
        $arguments = array_merge(parent::defaultArguments(), $arguments);
68
        return $arguments;
69
    }
70
71
    /**
72
     * @param RequestInterface  $request  A PSR-7 compatible Request instance.
73
     * @param ResponseInterface $response A PSR-7 compatible Response instance.
74
     * @return ResponseInterface
75
     */
76
    public function run(RequestInterface $request, ResponseInterface $response)
77
    {
78
        // Unused
79
        unset($request);
80
81
        $climate = $this->climate();
82
83
        $climate->underline()->out(
84
            'TRANSLATIONS'
85
        );
86
87
        $path = $this->path();
88
        $type = $this->fileType();
89
90
        switch ($type) {
91
            case 'mustache':
92
                $regex = '/{{\s*#\s*_t\s*}}((.|\n|\r|\n\r)*?){{\s*\/\s*_t\s*}}/i';
93
                $file = '*.mustache';
94
                $index = 1;
95
                break;
96
            case 'php':
97
                $regex = '/([^\d\wA-Za-z])_t\(\s*\n*\r*(["\'])(?<text>(.|\n|\r|\n\r)*?)\2\s*\n*\r*\)/i';
98
                $index = 'text';
99
                $file = '*.php';
100
                break;
101
            default:
102
                $regex = '/{{\s*#\s*_t\s*}}((.|\n|\r|\n\r)*?){{\s*\/\s*_t\s*}}/i';
103
                $file = '*.mustache';
104
                $index = 1;
105
                break;
106
        }
107
108
        // remove vendor/locomotivemtl/charcoal-app
109
        $base = $this->base();
110
        $glob = $this->globRecursive($base.$path.$file);
111
112
113
        $input = $climate->confirm(
114
            'Save to CSV?'
115
        );
116
117
        $translations = [];
118
        $toCSV = $input->confirmed();
119
120
        // Check out existing translations
121
        if ($toCSV) {
122
            $output = $this->file();
123
            if (file_exists($base.$output)) {
124
                // loop all
125
                $translations = $this->fromCSV();
126
            }
127
        }
128
129
        // Loop files to get original text.
130
        foreach ($glob as $k => $f) {
131
            $text = file_get_contents($f);
132
            if (preg_match($regex, $text)) {
133
                preg_match_all($regex, $text, $array);
134
135
                $i = 0;
136
                $t = count($array[$index]);
137
138
                for (; $i<$t; $i++) {
139
                    $orig = $array[$index][$i];
140
                    if (!isset($translations[$orig])) {
141
                        $translations[$orig] = [
142
                            'translation' => '',
143
                            'context' => $f
144
                        ];
145
                    }
146
                }
147
            }
148
        }
149
150
        if ($toCSV) {
151
            $this->toCSV($translations);
152
        }
153
154
        return $response;
155
    }
156
157
    /**
158
     * @param string  $pattern The pattern to search.
159
     * @param integer $flags   The glob flags.
160
     * @return array
161
     * @see http://in.php.net/manual/en/function.glob.php#106595
162
     */
163
    public function globRecursive($pattern, $flags = 0)
164
    {
165
        $max = $this->maxRecursiveLevel();
166
        $i = 1;
167
        $files = glob($pattern, $flags);
168
        foreach (glob(dirname($pattern).'/*', (GLOB_ONLYDIR|GLOB_NOSORT)) as $dir) {
169
            $files = array_merge($files, $this->globRecursive($dir.'/'.basename($pattern), $flags));
170
            $i++;
171
            if ($i >= $max) {
172
                break;
173
            }
174
        }
175
        return $files;
176
    }
177
178
    /**
179
     * BASE URL
180
     * Realpath
181
     * @return string
182
     */
183
    public function base()
184
    {
185
        return realpath($this->app()->config()->get('base_path').'../../../').'/';
0 ignored issues
show
Bug introduced by
The method app() does not seem to exist on object<Charcoal\Admin\Sc...lation\TranslateScript>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
186
    }
187
188
    /**
189
     * ARGUMENTS
190
     * @return TranslateScript Chainable
191
     */
192
    public function getPath()
193
    {
194
        $path = $this->argOrInput('path');
195
        $this->path = $path;
196
        return $this;
197
    }
198
199
    /**
200
     * @return string
201
     */
202
    public function path()
203
    {
204
        if (!$this->path) {
205
            $this->getPath();
206
        }
207
        return $this->path;
208
    }
209
210
    /**
211
     * @return TranslateScript Chainable
212
     */
213
    public function getFileType()
214
    {
215
        $type = $this->argOrInput('type');
216
        $this->fileType = $type;
217
        return $this;
218
    }
219
220
    /**
221
     * @return string
222
     */
223
    public function fileType()
224
    {
225
        if (!$this->fileType) {
226
            $this->getFileType();
227
        }
228
        return $this->fileType;
229
230
    }
231
232
    /**
233
     * @return string
234
     */
235
    public function file()
236
    {
237
        if ($this->output) {
238
            return $this->output;
239
        }
240
        $locales = $this->locales();
241
        $this->output = $locales['file'];
242
        return $this->output;
243
    }
244
245
    /**
246
     * Returns associative array
247
     * 'original text' => [ 'translation' => 'translation text', 'context' => 'filename' ]
248
     *
249
     * @return array
250
     */
251
    public function fromCSV()
252
    {
253
        $output = $this->file();
254
        $base = $this->base();
255
        $file = fopen($base.$output, 'r');
256
257
        if (!$file) {
258
            return [];
259
        }
260
261
        $results = [];
262
        $row = 0;
263
        while (($data = fgetcsv($file, 0, ',')) !== false) {
264
            $row++;
265
            // Skip column names
266
            if ($row == 1) {
267
                continue;
268
            }
269
            // data[0] = ORIGINAL
270
            // data[1] = TRANSLATION
271
            // data[2] = CONTEXT
272
            $translation = $this->translateCSV($data);
273
            if (!empty($translation)) {
274
                $results[$translation[0]] = $translation[1];
275
            }
276
        }
277
278
        return $results;
279
    }
280
281
    /**
282
     * @param array $translations The translations to save in CSV.
283
     * @return TranslateScript Chainable
284
     */
285
    public function toCSV(array $translations)
286
    {
287
        $base = $this->base();
288
        $output = $this->file();
289
290
        $separator = $this->separator();
291
        $enclosure = $this->enclosure();
292
        $columns = $this->columns();
293
294
        // Create / open the handle
295
        $dirname = dirname($base.$output);
296
        if (!is_dir($dirname)) {
297
            mkdir($dirname, 0755, true);
298
        }
299
        $file = fopen($base.$output, 'w');
300
        if (!$file) {
301
            // Wtf happened?
302
            return $this;
303
        }
304
        fputcsv($file, $columns, $separator, $enclosure);
305
306
        foreach ($translations as $orig => $translation) {
307
            $data = [ $orig, $translation['translation'], $translation['context'] ];
308
            fputcsv($file, $data, $separator, $enclosure);
309
        }
310
311
        fclose($file);
312
313
        return $this;
314
    }
315
316
    /**
317
     * @param array $data The translation data.
318
     * @return array
319
     * @todo multiple langs
320
     * data[0] = ORIGINAL
321
     * data[1] = TRANSLATION
322
     * data[2] = CONTEXT
323
     */
324
    public function translateCSV(array $data)
325
    {
326
        if (count($data) < 3) {
327
            return [];
328
        }
329
330
        $output = [
331
            $data[0],
332
            [
333
                'translation' => $data[1],
334
                'context' => $data[2]
335
            ]
336
        ];
337
338
        return $output;
339
    }
340
341
    /**
342
     * @todo make this optional
343
     * @return string lang ident
344
     */
345
    public function origLanguage()
346
    {
347
        return 'fr';
348
    }
349
350
    /**
351
     * Get opposite languages from DATABASE
352
     *
353
     * @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...
354
     */
355
    public function oppositeLanguages()
356
    {
357
        $cfg = $this->app()->config();
0 ignored issues
show
Unused Code introduced by
$cfg 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...
Bug introduced by
The method app() does not seem to exist on object<Charcoal\Admin\Sc...lation\TranslateScript>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
358
        $locales = $this->locales();
359
        $languages = $locales['languages'];
360
361
        $opposite = [];
362
        $orig = $this->origLanguage();
363
364
        foreach ($languages as $ident => $opts) {
365
            if ($ident != $orig) {
366
                $opposite[] = $ident;
367
            }
368
        }
369
        return $opposite;
370
    }
371
372
    /**
373
     * Locales set in config.json
374
     * Expects languages | file | default_language
375
     *
376
     * @return array
377
     */
378
    public function locales()
379
    {
380
        if ($this->locales) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->locales 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...
381
            return $this->locales;
382
        }
383
384
        $cfg = $this->app()->config();
0 ignored issues
show
Bug introduced by
The method app() does not seem to exist on object<Charcoal\Admin\Sc...lation\TranslateScript>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
385
        $locales = isset($cfg['locales']) ? $cfg['locales'] : [];
386
        $languages = isset($locales['languages']) ? $locales['languages'] : [];
387
        $file = isset($locales['file']) ? $locales['file'] : $this->argOrInput('output');
388
        // Default to FR
389
        $default = isset($locales['default_language']) ? $locales['default_language'] : 'fr';
390
391
        $this->locales = [
392
            'languages' => $languages,
393
            'file' => $file,
394
            'default_language' => $default
395
        ];
396
        return $this->locales;
397
    }
398
399
    /**
400
     * Columns of CSV file
401
     * This is already built to take multiple languages
402
     *
403
     * @return array
404
     */
405
    public function columns()
406
    {
407
        $orig = $this->origLanguage();
408
        $opposites = $this->oppositeLanguages();
409
410
        $columns = [ $orig ];
411
412
        foreach ($opposites as $lang) {
413
            $columns[] = $lang;
414
        }
415
416
        // Add context.
417
        $columns[] = 'context';
418
419
        return $columns;
420
    }
421
422
    /**
423
     * @return string
424
     */
425
    public function enclosure()
426
    {
427
        return '"';
428
    }
429
430
    /**
431
     * @return string
432
     */
433
    public function separator()
434
    {
435
        return ',';
436
    }
437
438
    /**
439
     * @return integer
440
     */
441
    public function maxRecursiveLevel()
442
    {
443
        return 4;
444
    }
445
}
446