Completed
Push — 2.1 ( c952e8...98ed49 )
by Carsten
10:00
created

MessageController::saveMessagesToDb()   D

Complexity

Conditions 17
Paths 384

Size

Total Lines 75
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 0
Metric Value
dl 0
loc 75
rs 4
c 0
b 0
f 0
ccs 0
cts 61
cp 0
cc 17
eloc 51
nc 384
nop 7
crap 306

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\controllers;
9
10
use Yii;
11
use yii\console\Controller;
12
use yii\console\Exception;
13
use yii\helpers\Console;
14
use yii\helpers\FileHelper;
15
use yii\helpers\VarDumper;
16
use yii\i18n\GettextPoFile;
17
18
/**
19
 * Extracts messages to be translated from source files.
20
 *
21
 * The extracted messages can be saved the following depending on `format`
22
 * setting in config file:
23
 *
24
 * - PHP message source files.
25
 * - ".po" files.
26
 * - Database.
27
 *
28
 * Usage:
29
 * 1. Create a configuration file using the 'message/config' command:
30
 *    yii message/config /path/to/myapp/messages/config.php
31
 * 2. Edit the created config file, adjusting it for your web application needs.
32
 * 3. Run the 'message/extract' command, using created config:
33
 *    yii message /path/to/myapp/messages/config.php
34
 *
35
 * @author Qiang Xue <[email protected]>
36
 * @since 2.0
37
 */
38
class MessageController extends Controller
39
{
40
    /**
41
     * @var string controller default action ID.
42
     */
43
    public $defaultAction = 'extract';
44
    /**
45
     * @var string required, root directory of all source files.
46
     */
47
    public $sourcePath = '@yii';
48
    /**
49
     * @var string required, root directory containing message translations.
50
     */
51
    public $messagePath = '@yii/messages';
52
    /**
53
     * @var array required, list of language codes that the extracted messages
54
     * should be translated to. For example, ['zh-CN', 'de'].
55
     */
56
    public $languages = [];
57
    /**
58
     * @var string the name of the function for translating messages.
59
     * Defaults to 'Yii::t'. This is used as a mark to find the messages to be
60
     * translated. You may use a string for single function name or an array for
61
     * multiple function names.
62
     */
63
    public $translator = 'Yii::t';
64
    /**
65
     * @var boolean whether to sort messages by keys when merging new messages
66
     * with the existing ones. Defaults to false, which means the new (untranslated)
67
     * messages will be separated from the old (translated) ones.
68
     */
69
    public $sort = false;
70
    /**
71
     * @var boolean whether the message file should be overwritten with the merged messages
72
     */
73
    public $overwrite = true;
74
    /**
75
     * @var boolean whether to remove messages that no longer appear in the source code.
76
     * Defaults to false, which means these messages will NOT be removed.
77
     */
78
    public $removeUnused = false;
79
    /**
80
     * @var boolean whether to mark messages that no longer appear in the source code.
81
     * Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks.
82
     */
83
    public $markUnused = true;
84
    /**
85
     * @var array list of patterns that specify which files/directories should NOT be processed.
86
     * If empty or not set, all files/directories will be processed.
87
     * A path matches a pattern if it contains the pattern string at its end. For example,
88
     * '/a/b' will match all files and directories ending with '/a/b';
89
     * the '*.svn' will match all files and directories whose name ends with '.svn'.
90
     * and the '.svn' will match all files and directories named exactly '.svn'.
91
     * Note, the '/' characters in a pattern matches both '/' and '\'.
92
     * See helpers/FileHelper::findFiles() description for more details on pattern matching rules.
93
     */
94
    public $except = [
95
        '.svn',
96
        '.git',
97
        '.gitignore',
98
        '.gitkeep',
99
        '.hgignore',
100
        '.hgkeep',
101
        '/messages',
102
        '/BaseYii.php', // contains examples about Yii:t()
103
    ];
104
    /**
105
     * @var array list of patterns that specify which files (not directories) should be processed.
106
     * If empty or not set, all files will be processed.
107
     * Please refer to "except" for details about the patterns.
108
     * If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
109
     */
110
    public $only = ['*.php'];
111
    /**
112
     * @var string generated file format. Can be "php", "db" or "po".
113
     */
114
    public $format = 'php';
115
    /**
116
     * @var string connection component ID for "db" format.
117
     */
118
    public $db = 'db';
119
    /**
120
     * @var string custom name for source message table for "db" format.
121
     */
122
    public $sourceMessageTable = '{{%source_message}}';
123
    /**
124
     * @var string custom name for translation message table for "db" format.
125
     */
126
    public $messageTable = '{{%message}}';
127
    /**
128
     * @var string name of the file that will be used for translations for "po" format.
129
     */
130
    public $catalog = 'messages';
131
    /**
132
     * @var array message categories to ignore. For example, 'yii', 'app*', 'widgets/menu', etc.
133
     * @see isCategoryIgnored
134
     */
135
    public $ignoreCategories = [];
136
137
138
    /**
139
     * @inheritdoc
140
     */
141 22
    public function options($actionID)
142
    {
143 22
        return array_merge(parent::options($actionID), [
144 22
            'sourcePath',
145 22
            'messagePath',
146 22
            'languages',
147 22
            'translator',
148 22
            'sort',
149 22
            'overwrite',
150 22
            'removeUnused',
151 22
            'markUnused',
152 22
            'except',
153 22
            'only',
154 22
            'format',
155 22
            'db',
156 22
            'sourceMessageTable',
157 22
            'messageTable',
158 22
            'catalog',
159 22
            'ignoreCategories',
160 22
        ]);
161
    }
162
163
    /**
164
     * @inheritdoc
165
     * @since 2.0.8
166
     */
167
    public function optionAliases()
168
    {
169
        return array_merge(parent::optionAliases(), [
170
            'c' => 'catalog',
171
            'e' => 'except',
172
            'f' => 'format',
173
            'i' => 'ignoreCategories',
174
            'l' => 'languages',
175
            'u' => 'markUnused',
176
            'p' => 'messagePath',
177
            'o' => 'only',
178
            'w' => 'overwrite',
179
            'S' => 'sort',
180
            't' => 'translator',
181
            'm' => 'sourceMessageTable',
182
            's' => 'sourcePath',
183
            'r' => 'removeUnused',
184
        ]);
185
    }
186
187
    /**
188
     * Creates a configuration file for the "extract" command using command line options specified
189
     *
190
     * The generated configuration file contains parameters required
191
     * for source code messages extraction.
192
     * You may use this configuration file with the "extract" command.
193
     *
194
     * @param string $filePath output file name or alias.
195
     * @return integer CLI exit code
196
     * @throws Exception on failure.
197
     */
198 2
    public function actionConfig($filePath)
199
    {
200 2
        $filePath = Yii::getAlias($filePath);
201 2
        if (file_exists($filePath)) {
202
            if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm("File '{$...wish to overwrite it?") of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
203
                return self::EXIT_CODE_NORMAL;
204
            }
205
        }
206
207 2
        $array = VarDumper::export($this->getOptionValues($this->action->id));
208
        $content = <<<EOD
209
<?php
210
/**
211 2
 * Configuration file for 'yii {$this->id}/{$this->defaultAction}' command.
212
 *
213 2
 * This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
214
 * It contains parameters for source code messages extraction.
215
 * You may modify this file to suit your needs.
216
 *
217 2
 * You can use 'yii {$this->id}/{$this->action->id}-template' command to create
218
 * template configuration file with detaild description for each parameter.
219
 */
220 2
return $array;
221
222 2
EOD;
223
224 2
        if (file_put_contents($filePath, $content) !== false) {
225 2
            $this->stdout("Configuration file created: '{$filePath}'.\n\n", Console::FG_GREEN);
226 2
            return self::EXIT_CODE_NORMAL;
227
        } else {
228
            $this->stdout("Configuration file was NOT created: '{$filePath}'.\n\n", Console::FG_RED);
229
            return self::EXIT_CODE_ERROR;
230
        }
231
    }
232
233
    /**
234
     * Creates a configuration file template for the "extract" command.
235
     *
236
     * The created configuration file contains detailed instructions on
237
     * how to customize it to fit for your needs. After customization,
238
     * you may use this configuration file with the "extract" command.
239
     *
240
     * @param string $filePath output file name or alias.
241
     * @return integer CLI exit code
242
     * @throws Exception on failure.
243
     */
244
    public function actionConfigTemplate($filePath)
245
    {
246
        $filePath = Yii::getAlias($filePath);
247
        if (file_exists($filePath)) {
248
            if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm("File '{$...wish to overwrite it?") of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
249
                return self::EXIT_CODE_NORMAL;
250
            }
251
        }
252
        if (copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath)) {
253
            $this->stdout("Configuration file template created at '{$filePath}'.\n\n", Console::FG_GREEN);
254
            return self::EXIT_CODE_NORMAL;
255
        } else {
256
            $this->stdout("Configuration file template was NOT created at '{$filePath}'.\n\n", Console::FG_RED);
257
            return self::EXIT_CODE_ERROR;
258
        }
259
    }
260
261
    /**
262
     * Extracts messages to be translated from source code.
263
     *
264
     * This command will search through source code files and extract
265
     * messages that need to be translated in different languages.
266
     *
267
     * @param string $configFile the path or alias of the configuration file.
268
     * You may use the "yii message/config" command to generate
269
     * this file and then customize it for your needs.
270
     * @throws Exception on failure.
271
     */
272 20
    public function actionExtract($configFile = null)
273
    {
274 20
        $configFileContent = [];
275 20
        if ($configFile !== null) {
276 20
            $configFile = Yii::getAlias($configFile);
277 20
            if (!is_file($configFile)) {
278 2
                throw new Exception("The configuration file does not exist: $configFile");
279
            } else {
280 18
                $configFileContent = require($configFile);
281
            }
282 18
        }
283
284 18
        $config = array_merge(
285 18
            $this->getOptionValues($this->action->id),
286 18
            $configFileContent,
287 18
            $this->getPassedOptionValues()
288 18
        );
289 18
        $config['sourcePath'] = Yii::getAlias($config['sourcePath']);
290 18
        $config['messagePath'] = Yii::getAlias($config['messagePath']);
291
292 18
        if (!isset($config['sourcePath'], $config['languages'])) {
293
            throw new Exception('The configuration file must specify "sourcePath" and "languages".');
294
        }
295 18
        if (!is_dir($config['sourcePath'])) {
296
            throw new Exception("The source path {$config['sourcePath']} is not a valid directory.");
297
        }
298 18
        if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'pot', 'db'])) {
299
            throw new Exception('Format should be either "php", "po", "pot" or "db".');
300
        }
301 18
        if (in_array($config['format'], ['php', 'po', 'pot'])) {
302 18
            if (!isset($config['messagePath'])) {
303
                throw new Exception('The configuration file must specify "messagePath".');
304 18
            } elseif (!is_dir($config['messagePath'])) {
305
                throw new Exception("The message path {$config['messagePath']} is not a valid directory.");
306
            }
307 18
        }
308 18
        if (empty($config['languages'])) {
309
            throw new Exception('Languages cannot be empty.');
310
        }
311
312 18
        $files = FileHelper::findFiles(realpath($config['sourcePath']), $config);
313
314 18
        $messages = [];
315 18
        foreach ($files as $file) {
316 18
            $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator'], $config['ignoreCategories']));
317 18
        }
318 18
        if (in_array($config['format'], ['php', 'po'])) {
319 18
            foreach ($config['languages'] as $language) {
320 18
                $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language;
321 18
                if (!is_dir($dir)) {
322 12
                    @mkdir($dir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
323 12
                }
324 18
                if ($config['format'] === 'po') {
325 9
                    $catalog = isset($config['catalog']) ? $config['catalog'] : 'messages';
326 9
                    $this->saveMessagesToPO($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $catalog, $config['markUnused']);
327 9
                } else {
328 9
                    $this->saveMessagesToPHP($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $config['markUnused']);
329
                }
330 18
            }
331 18
        } elseif ($config['format'] === 'db') {
332
            $db = \Yii::$app->get(isset($config['db']) ? $config['db'] : 'db');
333
            if (!$db instanceof \yii\db\Connection) {
334
                throw new Exception('The "db" option must refer to a valid database application component.');
335
            }
336
            $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}';
337
            $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}';
338
            $this->saveMessagesToDb(
339
                $messages,
340
                $db,
341
                $sourceMessageTable,
342
                $messageTable,
343
                $config['removeUnused'],
344
                $config['languages'],
345
                $config['markUnused']
346
            );
347
        } elseif ($config['format'] === 'pot') {
348
            $catalog = isset($config['catalog']) ? $config['catalog'] : 'messages';
349
            $this->saveMessagesToPOT($messages, $config['messagePath'], $catalog);
350
        }
351 18
    }
352
353
    /**
354
     * Saves messages to database
355
     *
356
     * @param array $messages
357
     * @param \yii\db\Connection $db
358
     * @param string $sourceMessageTable
359
     * @param string $messageTable
360
     * @param boolean $removeUnused
361
     * @param array $languages
362
     * @param boolean $markUnused
363
     */
364
    protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages, $markUnused)
365
    {
366
        $q = new \yii\db\Query;
367
        $current = [];
368
369
        foreach ($q->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db) as $row) {
370
            $current[$row['category']][$row['id']] = $row['message'];
371
        }
372
373
        $new = [];
374
        $obsolete = [];
375
376
        foreach ($messages as $category => $msgs) {
377
            $msgs = array_unique($msgs);
378
379
            if (isset($current[$category])) {
380
                $new[$category] = array_diff($msgs, $current[$category]);
381
                $obsolete += array_diff($current[$category], $msgs);
382
            } else {
383
                $new[$category] = $msgs;
384
            }
385
        }
386
387
        foreach (array_diff(array_keys($current), array_keys($messages)) as $category) {
388
            $obsolete += $current[$category];
389
        }
390
391
        if (!$removeUnused) {
392
            foreach ($obsolete as $pk => $m) {
393
                if (mb_substr($m, 0, 2) === '@@' && mb_substr($m, -2) === '@@') {
394
                    unset($obsolete[$pk]);
395
                }
396
            }
397
        }
398
399
        $obsolete = array_keys($obsolete);
400
        $this->stdout('Inserting new messages...');
401
        $savedFlag = false;
402
403
        foreach ($new as $category => $msgs) {
404
            foreach ($msgs as $m) {
405
                $savedFlag = true;
406
                $lastPk = $db->schema->insert($sourceMessageTable, ['category' => $category, 'message' => $m]);
407
                foreach ($languages as $language) {
408
                    $db->createCommand()
409
                       ->insert($messageTable, ['id' => $lastPk['id'], 'language' => $language])
410
                       ->execute();
411
                }
412
            }
413
        }
414
415
        $this->stdout($savedFlag ? "saved.\n" : "Nothing new...skipped.\n");
416
        $this->stdout($removeUnused ? 'Deleting obsoleted messages...' : 'Updating obsoleted messages...');
417
418
        if (empty($obsolete)) {
419
            $this->stdout("Nothing obsoleted...skipped.\n");
420
        } else {
421
            if ($removeUnused) {
422
                $db->createCommand()
423
                   ->delete($sourceMessageTable, ['in', 'id', $obsolete])
424
                   ->execute();
425
                $this->stdout("deleted.\n");
426
            } elseif ($markUnused) {
427
                $db->createCommand()
428
                   ->update(
429
                       $sourceMessageTable,
430
                       ['message' => new \yii\db\Expression("CONCAT('@@',message,'@@')")],
431
                       ['in', 'id', $obsolete]
432
                   )->execute();
433
                $this->stdout("updated.\n");
434
            } else {
435
                $this->stdout("kept untouched.\n");
436
            }
437
        }
438
    }
439
440
    /**
441
     * Extracts messages from a file
442
     *
443
     * @param string $fileName name of the file to extract messages from
444
     * @param string $translator name of the function used to translate messages
445
     * @param array $ignoreCategories message categories to ignore.
446
     * This parameter is available since version 2.0.4.
447
     * @return array
448
     */
449 18
    protected function extractMessages($fileName, $translator, $ignoreCategories = [])
450
    {
451 18
        $coloredFileName = Console::ansiFormat($fileName, [Console::FG_CYAN]);
452 18
        $this->stdout("Extracting messages from $coloredFileName...\n");
453
454 18
        $subject = file_get_contents($fileName);
455 18
        $messages = [];
456 18
        foreach ((array) $translator as $currentTranslator) {
457 18
            $translatorTokens = token_get_all('<?php ' . $currentTranslator);
458 18
            array_shift($translatorTokens);
459 18
            $tokens = token_get_all($subject);
460 18
            $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($tokens, $translatorTokens, $ignoreCategories));
461 18
        }
462
463 18
        $this->stdout("\n");
464
465 18
        return $messages;
466
    }
467
468
    /**
469
     * Extracts messages from a parsed PHP tokens list.
470
     * @param array $tokens tokens to be processed.
471
     * @param array $translatorTokens translator tokens.
472
     * @param array $ignoreCategories message categories to ignore.
473
     * @return array messages.
474
     */
475 18
    private function extractMessagesFromTokens(array $tokens, array $translatorTokens, array $ignoreCategories)
476
    {
477 18
        $messages = [];
478 18
        $translatorTokensCount = count($translatorTokens);
479 18
        $matchedTokensCount = 0;
480 18
        $buffer = [];
481 18
        $pendingParenthesisCount = 0;
482
483 18
        foreach ($tokens as $token) {
484
            // finding out translator call
485 18
            if ($matchedTokensCount < $translatorTokensCount) {
486 18
                if ($this->tokensEqual($token, $translatorTokens[$matchedTokensCount])) {
487 18
                    $matchedTokensCount++;
488 18
                } else {
489 18
                    $matchedTokensCount = 0;
490
                }
491 18
            } elseif ($matchedTokensCount === $translatorTokensCount) {
492
                // translator found
493
494
                // end of function call
495 18
                if ($this->tokensEqual(')', $token)) {
496 18
                    $pendingParenthesisCount--;
497
498 18
                    if ($pendingParenthesisCount === 0) {
499
                        // end of translator call or end of something that we can't extract
500 18
                        if (isset($buffer[0][0], $buffer[1], $buffer[2][0]) && $buffer[0][0] === T_CONSTANT_ENCAPSED_STRING && $buffer[1] === ',' && $buffer[2][0] === T_CONSTANT_ENCAPSED_STRING) {
501
                            // is valid call we can extract
502 18
                            $category = stripcslashes($buffer[0][1]);
503 18
                            $category = mb_substr($category, 1, mb_strlen($category) - 2);
504
505 18
                            if (!$this->isCategoryIgnored($category, $ignoreCategories)) {
506 18
                                $message = stripcslashes($buffer[2][1]);
507 18
                                $message = mb_substr($message, 1, mb_strlen($message) - 2);
508
509 18
                                $messages[$category][] = $message;
510 18
                            }
511
512 18
                            $nestedTokens = array_slice($buffer, 3);
513 18
                            if (count($nestedTokens) > $translatorTokensCount) {
514
                                // search for possible nested translator calls
515 2
                                $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($nestedTokens, $translatorTokens, $ignoreCategories));
516 2
                            }
517 18
                        } else {
518
                            // invalid call or dynamic call we can't extract
519
                            $line = Console::ansiFormat($this->getLine($buffer), [Console::FG_CYAN]);
520
                            $skipping = Console::ansiFormat('Skipping line', [Console::FG_YELLOW]);
521
                            $this->stdout("$skipping $line. Make sure both category and message are static strings.\n");
522
                        }
523
524
                        // prepare for the next match
525 18
                        $matchedTokensCount = 0;
526 18
                        $pendingParenthesisCount = 0;
527 18
                        $buffer = [];
528 18
                    } else {
529 2
                        $buffer[] = $token;
530
                    }
531 18
                } elseif ($this->tokensEqual('(', $token)) {
532
                    // count beginning of function call, skipping translator beginning
533 18
                    if ($pendingParenthesisCount > 0) {
534 2
                        $buffer[] = $token;
535 2
                    }
536 18
                    $pendingParenthesisCount++;
537 18
                } elseif (isset($token[0]) && !in_array($token[0], [T_WHITESPACE, T_COMMENT])) {
538
                    // ignore comments and whitespaces
539 18
                    $buffer[] = $token;
540 18
                }
541 18
            }
542 18
        }
543
544 18
        return $messages;
545
    }
546
547
    /**
548
     * The method checks, whether the $category is ignored according to $ignoreCategories array.
549
     * Examples:
550
     *
551
     * - `myapp` - will be ignored only `myapp` category;
552
     * - `myapp*` - will be ignored by all categories beginning with `myapp` (`myapp`, `myapplication`, `myapprove`, `myapp/widgets`, `myapp.widgets`, etc).
553
     *
554
     * @param string $category category that is checked
555
     * @param array $ignoreCategories message categories to ignore.
556
     * @return boolean
557
     * @since 2.0.7
558
     */
559 18
    protected function isCategoryIgnored($category, array $ignoreCategories)
560
    {
561 18
        $result = false;
562
563 18
        if (!empty($ignoreCategories)) {
564 2
            if (in_array($category, $ignoreCategories, true)) {
565 2
                $result = true;
566 2
            } else {
567 2
                foreach ($ignoreCategories as $pattern) {
568 2
                    if (strpos($pattern, '*') > 0 && strpos($category, rtrim($pattern, '*')) === 0) {
569 2
                        $result = true;
570 2
                        break;
571
                    }
572 2
                }
573
            }
574 2
        }
575
576 18
        return $result;
577
    }
578
579
    /**
580
     * Finds out if two PHP tokens are equal
581
     *
582
     * @param array|string $a
583
     * @param array|string $b
584
     * @return boolean
585
     * @since 2.0.1
586
     */
587 18
    protected function tokensEqual($a, $b)
588
    {
589 18
        if (is_string($a) && is_string($b)) {
590 18
            return $a === $b;
591 18
        } elseif (isset($a[0], $a[1], $b[0], $b[1])) {
592 18
            return $a[0] === $b[0] && $a[1] == $b[1];
593
        }
594 18
        return false;
595
    }
596
597
    /**
598
     * Finds out a line of the first non-char PHP token found
599
     *
600
     * @param array $tokens
601
     * @return integer|string
602
     * @since 2.0.1
603
     */
604
    protected function getLine($tokens)
605
    {
606
        foreach ($tokens as $token) {
607
            if (isset($token[2])) {
608
                return $token[2];
609
            }
610
        }
611
        return 'unknown';
612
    }
613
614
    /**
615
     * Writes messages into PHP files
616
     *
617
     * @param array $messages
618
     * @param string $dirName name of the directory to write to
619
     * @param boolean $overwrite if existing file should be overwritten without backup
620
     * @param boolean $removeUnused if obsolete translations should be removed
621
     * @param boolean $sort if translations should be sorted
622
     * @param boolean $markUnused if obsolete translations should be marked
623
     */
624 9
    protected function saveMessagesToPHP($messages, $dirName, $overwrite, $removeUnused, $sort, $markUnused)
625
    {
626 9
        foreach ($messages as $category => $msgs) {
627 9
            $file = str_replace("\\", '/', "$dirName/$category.php");
628 9
            $path = dirname($file);
629 9
            FileHelper::createDirectory($path);
630 9
            $msgs = array_values(array_unique($msgs));
631 9
            $coloredFileName = Console::ansiFormat($file, [Console::FG_CYAN]);
632 9
            $this->stdout("Saving messages to $coloredFileName...\n");
633 9
            $this->saveMessagesCategoryToPHP($msgs, $file, $overwrite, $removeUnused, $sort, $category, $markUnused);
634 9
        }
635 9
    }
636
637
    /**
638
     * Writes category messages into PHP file
639
     *
640
     * @param array $messages
641
     * @param string $fileName name of the file to write to
642
     * @param boolean $overwrite if existing file should be overwritten without backup
643
     * @param boolean $removeUnused if obsolete translations should be removed
644
     * @param boolean $sort if translations should be sorted
645
     * @param string $category message category
646
     * @param boolean $markUnused if obsolete translations should be marked
647
     */
648 9
    protected function saveMessagesCategoryToPHP($messages, $fileName, $overwrite, $removeUnused, $sort, $category, $markUnused)
649
    {
650 9
        if (is_file($fileName)) {
651 6
            $rawExistingMessages = require($fileName);
652 6
            $existingMessages = $rawExistingMessages;
653 6
            sort($messages);
654 6
            ksort($existingMessages);
655 6
            if (array_keys($existingMessages) === $messages && (!$sort || array_keys($rawExistingMessages) === $messages)) {
656 3
                $this->stdout("Nothing new in \"$category\" category... Nothing to save.\n\n", Console::FG_GREEN);
657 3
                return;
658
            }
659 4
            unset($rawExistingMessages);
660 4
            $merged = [];
661 4
            $untranslated = [];
662 4
            foreach ($messages as $message) {
663 4
                if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
664 2
                    $merged[$message] = $existingMessages[$message];
665 2
                } else {
666 4
                    $untranslated[] = $message;
667
                }
668 4
            }
669 4
            ksort($merged);
670 4
            sort($untranslated);
671 4
            $todo = [];
672 4
            foreach ($untranslated as $message) {
673 4
                $todo[$message] = '';
674 4
            }
675 4
            ksort($existingMessages);
676 4
            foreach ($existingMessages as $message => $translation) {
677 4
                if (!$removeUnused && !isset($merged[$message]) && !isset($todo[$message])) {
678 1
                    if (!empty($translation) && (!$markUnused || (strncmp($translation, '@@', 2) === 0 && substr_compare($translation, '@@', -2, 2) === 0))) {
679
                        $todo[$message] = $translation;
680
                    } else {
681 1
                        $todo[$message] = '@@' . $translation . '@@';
682
                    }
683 1
                }
684 4
            }
685 4
            $merged = array_merge($todo, $merged);
686 4
            if ($sort) {
687
                ksort($merged);
688
            }
689 4
            if (false === $overwrite) {
690
                $fileName .= '.merged';
691
            }
692 4
            $this->stdout("Translation merged.\n");
693 4
        } else {
694 6
            $merged = [];
695 6
            foreach ($messages as $message) {
696 6
                $merged[$message] = '';
697 6
            }
698 6
            ksort($merged);
699
        }
700
701
702 9
        $array = VarDumper::export($merged);
703
        $content = <<<EOD
704
<?php
705
/**
706
 * Message translations.
707
 *
708 9
 * This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
709
 * It contains the localizable messages extracted from source code.
710
 * You may modify this file by translating the extracted messages.
711
 *
712
 * Each array element represents the translation (value) of a message (key).
713
 * If the value is empty, the message is considered as not translated.
714
 * Messages that no longer need translation will have their translations
715
 * enclosed between a pair of '@@' marks.
716
 *
717
 * Message string can be used with plural forms format. Check i18n section
718
 * of the guide for details.
719
 *
720
 * NOTE: this file must be saved in UTF-8 encoding.
721
 */
722 9
return $array;
723
724 9
EOD;
725
726 9
        if (file_put_contents($fileName, $content) !== false) {
727 9
            $this->stdout("Translation saved.\n\n", Console::FG_GREEN);
728 9
            return self::EXIT_CODE_NORMAL;
729
        } else {
730
            $this->stdout("Translation was NOT saved.\n\n", Console::FG_RED);
731
            return self::EXIT_CODE_ERROR;
732
        }
733
    }
734
735
    /**
736
     * Writes messages into PO file
737
     *
738
     * @param array $messages
739
     * @param string $dirName name of the directory to write to
740
     * @param boolean $overwrite if existing file should be overwritten without backup
741
     * @param boolean $removeUnused if obsolete translations should be removed
742
     * @param boolean $sort if translations should be sorted
743
     * @param string $catalog message catalog
744
     * @param boolean $markUnused if obsolete translations should be marked
745
     */
746 9
    protected function saveMessagesToPO($messages, $dirName, $overwrite, $removeUnused, $sort, $catalog, $markUnused)
747
    {
748 9
        $file = str_replace("\\", '/', "$dirName/$catalog.po");
749 9
        FileHelper::createDirectory(dirname($file));
750 9
        $this->stdout("Saving messages to $file...\n");
751
752 9
        $poFile = new GettextPoFile();
753
754
755 9
        $merged = [];
756 9
        $todos = [];
757
758 9
        $hasSomethingToWrite = false;
759 9
        foreach ($messages as $category => $msgs) {
760 9
            $notTranslatedYet = [];
761 9
            $msgs = array_values(array_unique($msgs));
762
763 9
            if (is_file($file)) {
764 6
                $existingMessages = $poFile->load($file, $category);
765
766 6
                sort($msgs);
767 6
                ksort($existingMessages);
768 6
                if (array_keys($existingMessages) == $msgs) {
769 3
                    $this->stdout("Nothing new in \"$category\" category...\n");
770
771 3
                    sort($msgs);
772 3
                    foreach ($msgs as $message) {
773 3
                        $merged[$category . chr(4) . $message] = $existingMessages[$message];
774 3
                    }
775 3
                    ksort($merged);
776 3
                    continue;
777
                }
778
779
                // merge existing message translations with new message translations
780 4
                foreach ($msgs as $message) {
781 4
                    if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
782 2
                        $merged[$category . chr(4) . $message] = $existingMessages[$message];
783 2
                    } else {
784 4
                        $notTranslatedYet[] = $message;
785
                    }
786 4
                }
787 4
                ksort($merged);
788 4
                sort($notTranslatedYet);
789
790
                // collect not yet translated messages
791 4
                foreach ($notTranslatedYet as $message) {
792 4
                    $todos[$category . chr(4) . $message] = '';
793 4
                }
794
795
                // add obsolete unused messages
796 4
                foreach ($existingMessages as $message => $translation) {
797 4
                    if (!$removeUnused && !isset($merged[$category . chr(4) . $message]) && !isset($todos[$category . chr(4) . $message])) {
798 1
                        if (!empty($translation) && (!$markUnused || (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@'))) {
799
                            $todos[$category . chr(4) . $message] = $translation;
800
                        } else {
801 1
                            $todos[$category . chr(4) . $message] = '@@' . $translation . '@@';
802
                        }
803 1
                    }
804 4
                }
805
806 4
                $merged = array_merge($todos, $merged);
807 4
                if ($sort) {
808
                    ksort($merged);
809
                }
810
811 4
                if ($overwrite === false) {
812
                    $file .= '.merged';
813
                }
814 4
            } else {
815 6
                sort($msgs);
816 6
                foreach ($msgs as $message) {
817 6
                    $merged[$category . chr(4) . $message] = '';
818 6
                }
819 6
                ksort($merged);
820
            }
821 9
            $this->stdout("Category \"$category\" merged.\n");
822 9
            $hasSomethingToWrite = true;
823 9
        }
824 9
        if ($hasSomethingToWrite) {
825 9
            $poFile->save($file, $merged);
826 9
            $this->stdout("Translation saved.\n", Console::FG_GREEN);
827 9
        } else {
828 2
            $this->stdout("Nothing to save.\n", Console::FG_GREEN);
829
        }
830 9
    }
831
832
    /**
833
     * Writes messages into POT file
834
     *
835
     * @param array $messages
836
     * @param string $dirName name of the directory to write to
837
     * @param string $catalog message catalog
838
     * @since 2.0.6
839
     */
840
    protected function saveMessagesToPOT($messages, $dirName, $catalog)
841
    {
842
        $file = str_replace("\\", '/', "$dirName/$catalog.pot");
843
        FileHelper::createDirectory(dirname($file));
844
        $this->stdout("Saving messages to $file...\n");
845
846
        $poFile = new GettextPoFile();
847
848
        $merged = [];
849
850
        $hasSomethingToWrite = false;
851
        foreach ($messages as $category => $msgs) {
852
            $msgs = array_values(array_unique($msgs));
853
854
            sort($msgs);
855
            foreach ($msgs as $message) {
856
                $merged[$category . chr(4) . $message] = '';
857
            }
858
            ksort($merged);
859
            $this->stdout("Category \"$category\" merged.\n");
860
            $hasSomethingToWrite = true;
861
        }
862
        if ($hasSomethingToWrite) {
863
            $poFile->save($file, $merged);
864
            $this->stdout("Translation saved.\n", Console::FG_GREEN);
865
        } else {
866
            $this->stdout("Nothing to save.\n", Console::FG_GREEN);
867
        }
868
    }
869
}
870