GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( f9fd4d...f5c98f )
by Robert
11:43
created

MessageController::optionAliases()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 0
cts 3
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 16
nc 1
nop 0
crap 2
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\Exception;
12
use yii\console\ExitCode;
13
use yii\db\Connection;
14
use yii\db\Query;
15
use yii\di\Instance;
16
use yii\helpers\Console;
17
use yii\helpers\FileHelper;
18
use yii\helpers\VarDumper;
19
use yii\i18n\GettextPoFile;
20
21
/**
22
 * Extracts messages to be translated from source files.
23
 *
24
 * The extracted messages can be saved the following depending on `format`
25
 * setting in config file:
26
 *
27
 * - PHP message source files.
28
 * - ".po" files.
29
 * - Database.
30
 *
31
 * Usage:
32
 * 1. Create a configuration file using the 'message/config' command:
33
 *    yii message/config /path/to/myapp/messages/config.php
34
 * 2. Edit the created config file, adjusting it for your web application needs.
35
 * 3. Run the 'message/extract' command, using created config:
36
 *    yii message /path/to/myapp/messages/config.php
37
 *
38
 * @author Qiang Xue <[email protected]>
39
 * @since 2.0
40
 */
41
class MessageController extends \yii\console\Controller
42
{
43
    /**
44
     * @var string controller default action ID.
45
     */
46
    public $defaultAction = 'extract';
47
    /**
48
     * @var string required, root directory of all source files.
49
     */
50
    public $sourcePath = '@yii';
51
    /**
52
     * @var string required, root directory containing message translations.
53
     */
54
    public $messagePath = '@yii/messages';
55
    /**
56
     * @var array required, list of language codes that the extracted messages
57
     * should be translated to. For example, ['zh-CN', 'de'].
58
     */
59
    public $languages = [];
60
    /**
61
     * @var string the name of the function for translating messages.
62
     * Defaults to 'Yii::t'. This is used as a mark to find the messages to be
63
     * translated. You may use a string for single function name or an array for
64
     * multiple function names.
65
     */
66
    public $translator = 'Yii::t';
67
    /**
68
     * @var bool whether to sort messages by keys when merging new messages
69
     * with the existing ones. Defaults to false, which means the new (untranslated)
70
     * messages will be separated from the old (translated) ones.
71
     */
72
    public $sort = false;
73
    /**
74
     * @var bool whether the message file should be overwritten with the merged messages
75
     */
76
    public $overwrite = true;
77
    /**
78
     * @var bool whether to remove messages that no longer appear in the source code.
79
     * Defaults to false, which means these messages will NOT be removed.
80
     */
81
    public $removeUnused = false;
82
    /**
83
     * @var bool whether to mark messages that no longer appear in the source code.
84
     * Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks.
85
     */
86
    public $markUnused = true;
87
    /**
88
     * @var array list of patterns that specify which files/directories should NOT be processed.
89
     * If empty or not set, all files/directories will be processed.
90
     * See helpers/FileHelper::findFiles() description for pattern matching rules.
91
     * If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
92
     */
93
    public $except = [
94
        '.svn',
95
        '.git',
96
        '.gitignore',
97
        '.gitkeep',
98
        '.hgignore',
99
        '.hgkeep',
100
        '/messages',
101
        '/BaseYii.php', // contains examples about Yii:t()
102
    ];
103
    /**
104
     * @var array list of patterns that specify which files (not directories) should be processed.
105
     * If empty or not set, all files will be processed.
106
     * See helpers/FileHelper::findFiles() description for pattern matching rules.
107
     * If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
108
     */
109
    public $only = ['*.php'];
110
    /**
111
     * @var string generated file format. Can be "php", "db", "po" or "pot".
112
     */
113
    public $format = 'php';
114
    /**
115
     * @var string connection component ID for "db" format.
116
     */
117
    public $db = 'db';
118
    /**
119
     * @var string custom name for source message table for "db" format.
120
     */
121
    public $sourceMessageTable = '{{%source_message}}';
122
    /**
123
     * @var string custom name for translation message table for "db" format.
124
     */
125
    public $messageTable = '{{%message}}';
126
    /**
127
     * @var string name of the file that will be used for translations for "po" format.
128
     */
129
    public $catalog = 'messages';
130
    /**
131
     * @var array message categories to ignore. For example, 'yii', 'app*', 'widgets/menu', etc.
132
     * @see isCategoryIgnored
133
     */
134
    public $ignoreCategories = [];
135
    /**
136
     * @var string File header in generated PHP file with messages. This property is used only if [[$format]] is "php".
137
     * @since 2.0.13
138
     */
139
    public $phpFileHeader = '';
140
    /**
141
     * @var string|null DocBlock used for messages array in generated PHP file. If `null`, default DocBlock will be used.
142
     * This property is used only if [[$format]] is "php".
143
     * @since 2.0.13
144
     */
145
    public $phpDocBlock;
146
147
    /**
148
     * @var array Config for messages extraction.
149
     * @see actionExtract()
150
     * @see initConfig()
151
     * @since 2.0.13
152
     */
153
    protected $config;
154
155
156
    /**
157
     * @inheritdoc
158
     */
159 45
    public function options($actionID)
160
    {
161 45
        return array_merge(parent::options($actionID), [
162 45
            'sourcePath',
163
            'messagePath',
164
            'languages',
165
            'translator',
166
            'sort',
167
            'overwrite',
168
            'removeUnused',
169
            'markUnused',
170
            'except',
171
            'only',
172
            'format',
173
            'db',
174
            'sourceMessageTable',
175
            'messageTable',
176
            'catalog',
177
            'ignoreCategories',
178
            'phpFileHeader',
179
            'phpDocBlock',
180
        ]);
181
    }
182
183
    /**
184
     * @inheritdoc
185
     * @since 2.0.8
186
     */
187
    public function optionAliases()
188
    {
189
        return array_merge(parent::optionAliases(), [
190
            'c' => 'catalog',
191
            'e' => 'except',
192
            'f' => 'format',
193
            'i' => 'ignoreCategories',
194
            'l' => 'languages',
195
            'u' => 'markUnused',
196
            'p' => 'messagePath',
197
            'o' => 'only',
198
            'w' => 'overwrite',
199
            'S' => 'sort',
200
            't' => 'translator',
201
            'm' => 'sourceMessageTable',
202
            's' => 'sourcePath',
203
            'r' => 'removeUnused',
204
        ]);
205
    }
206
207
    /**
208
     * Creates a configuration file for the "extract" command using command line options specified.
209
     *
210
     * The generated configuration file contains parameters required
211
     * for source code messages extraction.
212
     * You may use this configuration file with the "extract" command.
213
     *
214
     * @param string $filePath output file name or alias.
215
     * @return int CLI exit code
216
     * @throws Exception on failure.
217
     */
218 3
    public function actionConfig($filePath)
219
    {
220 3
        $filePath = Yii::getAlias($filePath);
221 3
        if (file_exists($filePath)) {
222
            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...
223
                return ExitCode::OK;
224
            }
225
        }
226
227 3
        $array = VarDumper::export($this->getOptionValues($this->action->id));
228
        $content = <<<EOD
229
<?php
230
/**
231 3
 * Configuration file for 'yii {$this->id}/{$this->defaultAction}' command.
232
 *
233 3
 * This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
234
 * It contains parameters for source code messages extraction.
235
 * You may modify this file to suit your needs.
236
 *
237 3
 * You can use 'yii {$this->id}/{$this->action->id}-template' command to create
238
 * template configuration file with detailed description for each parameter.
239
 */
240 3
return $array;
241
242
EOD;
243
244 3
        if (file_put_contents($filePath, $content) === false) {
245
            $this->stdout("Configuration file was NOT created: '{$filePath}'.\n\n", Console::FG_RED);
246
            return ExitCode::UNSPECIFIED_ERROR;
247
        }
248
249 3
        $this->stdout("Configuration file created: '{$filePath}'.\n\n", Console::FG_GREEN);
250 3
        return ExitCode::OK;
251
    }
252
253
    /**
254
     * Creates a configuration file template for the "extract" command.
255
     *
256
     * The created configuration file contains detailed instructions on
257
     * how to customize it to fit for your needs. After customization,
258
     * you may use this configuration file with the "extract" command.
259
     *
260
     * @param string $filePath output file name or alias.
261
     * @return int CLI exit code
262
     * @throws Exception on failure.
263
     */
264
    public function actionConfigTemplate($filePath)
265
    {
266
        $filePath = Yii::getAlias($filePath);
267
268
        if (file_exists($filePath)) {
269
            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...
270
                return ExitCode::OK;
271
            }
272
        }
273
274
        if (!copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath)) {
275
            $this->stdout("Configuration file template was NOT created at '{$filePath}'.\n\n", Console::FG_RED);
276
            return ExitCode::UNSPECIFIED_ERROR;
277
        }
278
279
        $this->stdout("Configuration file template created at '{$filePath}'.\n\n", Console::FG_GREEN);
280
        return ExitCode::OK;
281
    }
282
283
    /**
284
     * Extracts messages to be translated from source code.
285
     *
286
     * This command will search through source code files and extract
287
     * messages that need to be translated in different languages.
288
     *
289
     * @param string $configFile the path or alias of the configuration file.
290
     * You may use the "yii message/config" command to generate
291
     * this file and then customize it for your needs.
292
     * @throws Exception on failure.
293
     */
294 42
    public function actionExtract($configFile = null)
295
    {
296 42
        $this->initConfig($configFile);
297
298 39
        $files = FileHelper::findFiles(realpath($this->config['sourcePath']), $this->config);
299
300 39
        $messages = [];
301 39
        foreach ($files as $file) {
302 39
            $messages = array_merge_recursive($messages, $this->extractMessages($file, $this->config['translator'], $this->config['ignoreCategories']));
303
        }
304
305 39
        $catalog = isset($this->config['catalog']) ? $this->config['catalog'] : 'messages';
306
307 39
        if (in_array($this->config['format'], ['php', 'po'])) {
308 27
            foreach ($this->config['languages'] as $language) {
309 27
                $dir = $this->config['messagePath'] . DIRECTORY_SEPARATOR . $language;
310 27
                if (!is_dir($dir) && !@mkdir($dir)) {
311
                    throw new Exception("Directory '{$dir}' can not be created.");
312
                }
313 27
                if ($this->config['format'] === 'po') {
314 13
                    $this->saveMessagesToPO($messages, $dir, $this->config['overwrite'], $this->config['removeUnused'], $this->config['sort'], $catalog, $this->config['markUnused']);
315
                } else {
316 27
                    $this->saveMessagesToPHP($messages, $dir, $this->config['overwrite'], $this->config['removeUnused'], $this->config['sort'], $this->config['markUnused']);
317
                }
318
            }
319 12
        } elseif ($this->config['format'] === 'db') {
320
            /** @var Connection $db */
321 12
            $db = Instance::ensure($this->config['db'], Connection::className());
322 12
            $sourceMessageTable = isset($this->config['sourceMessageTable']) ? $this->config['sourceMessageTable'] : '{{%source_message}}';
323 12
            $messageTable = isset($this->config['messageTable']) ? $this->config['messageTable'] : '{{%message}}';
324 12
            $this->saveMessagesToDb(
325 12
                $messages,
326 12
                $db,
327 12
                $sourceMessageTable,
328 12
                $messageTable,
329 12
                $this->config['removeUnused'],
330 12
                $this->config['languages'],
331 12
                $this->config['markUnused']
332
            );
333
        } elseif ($this->config['format'] === 'pot') {
334
            $this->saveMessagesToPOT($messages, $this->config['messagePath'], $catalog);
335
        }
336 39
    }
337
338
    /**
339
     * Saves messages to database.
340
     *
341
     * @param array $messages
342
     * @param Connection $db
343
     * @param string $sourceMessageTable
344
     * @param string $messageTable
345
     * @param bool $removeUnused
346
     * @param array $languages
347
     * @param bool $markUnused
348
     */
349 12
    protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages, $markUnused)
350
    {
351 12
        $currentMessages = [];
352 12
        $rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db);
353 12
        foreach ($rows as $row) {
354 11
            $currentMessages[$row['category']][$row['id']] = $row['message'];
355
        }
356
357 12
        $currentLanguages = [];
358 12
        $rows = (new Query())->select(['language'])->from($messageTable)->groupBy('language')->all($db);
359 12
        foreach ($rows as $row) {
360 11
            $currentLanguages[] = $row['language'];
361
        }
362 12
        $missingLanguages = [];
363 12
        if (!empty($currentLanguages)) {
364 11
            $missingLanguages = array_diff($languages, $currentLanguages);
365
        }
366
367 12
        $new = [];
368 12
        $obsolete = [];
369
370 12
        foreach ($messages as $category => $msgs) {
371 12
            $msgs = array_unique($msgs);
372
373 12
            if (isset($currentMessages[$category])) {
374 9
                $new[$category] = array_diff($msgs, $currentMessages[$category]);
375 9
                $obsolete += array_diff($currentMessages[$category], $msgs);
376
            } else {
377 12
                $new[$category] = $msgs;
378
            }
379
        }
380
381 12
        foreach (array_diff(array_keys($currentMessages), array_keys($messages)) as $category) {
382 7
            $obsolete += $currentMessages[$category];
383
        }
384
385 12
        if (!$removeUnused) {
386 11
            foreach ($obsolete as $pk => $msg) {
387 8
                if (mb_substr($msg, 0, 2) === '@@' && mb_substr($msg, -2) === '@@') {
388 8
                    unset($obsolete[$pk]);
389
                }
390
            }
391
        }
392
393 12
        $obsolete = array_keys($obsolete);
394 12
        $this->stdout('Inserting new messages...');
395 12
        $savedFlag = false;
396
397 12
        foreach ($new as $category => $msgs) {
398 12
            foreach ($msgs as $msg) {
399 10
                $savedFlag = true;
400 10
                $lastPk = $db->schema->insert($sourceMessageTable, ['category' => $category, 'message' => $msg]);
401 10
                foreach ($languages as $language) {
402 10
                    $db->createCommand()
403 10
                       ->insert($messageTable, ['id' => $lastPk['id'], 'language' => $language])
404 12
                       ->execute();
405
                }
406
            }
407
        }
408
409 12
        if (!empty($missingLanguages)) {
410 1
            $updatedMessages = [];
411 1
            $rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db);
412 1
            foreach ($rows as $row) {
413 1
                $updatedMessages[$row['category']][$row['id']] = $row['message'];
414
            }
415 1
            foreach ($updatedMessages as $category => $msgs) {
416 1
                foreach ($msgs as $id => $msg) {
417 1
                    $savedFlag = true;
418 1
                    foreach ($missingLanguages as $language) {
419 1
                        $db->createCommand()
420 1
                            ->insert($messageTable, ['id' => $id, 'language' => $language])
421 1
                            ->execute();
422
                    }
423
                }
424
            }
425
        }
426
427 12
        $this->stdout($savedFlag ? "saved.\n" : "Nothing to save.\n");
428 12
        $this->stdout($removeUnused ? 'Deleting obsoleted messages...' : 'Updating obsoleted messages...');
429
430 12
        if (empty($obsolete)) {
431 6
            $this->stdout("Nothing obsoleted...skipped.\n");
432 6
            return;
433
        }
434
435 9
        if ($removeUnused) {
436 1
            $db->createCommand()
437 1
               ->delete($sourceMessageTable, ['in', 'id', $obsolete])
438 1
               ->execute();
439 1
            $this->stdout("deleted.\n");
440 8
        } elseif ($markUnused) {
441 6
            $rows = (new Query())
442 6
                ->select(['id', 'message'])
443 6
                ->from($sourceMessageTable)
444 6
                ->where(['in', 'id', $obsolete])
445 6
                ->all($db);
446
447 6
            foreach ($rows as $row) {
448 6
                $db->createCommand()->update(
449 6
                    $sourceMessageTable,
450 6
                    ['message' => '@@' . $row['message'] . '@@'],
451 6
                    ['id' => $row['id']]
452 6
                )->execute();
453
            }
454 6
            $this->stdout("updated.\n");
455
        } else {
456 2
            $this->stdout("kept untouched.\n");
457
        }
458 9
    }
459
460
    /**
461
     * Extracts messages from a file.
462
     *
463
     * @param string $fileName name of the file to extract messages from
464
     * @param string $translator name of the function used to translate messages
465
     * @param array $ignoreCategories message categories to ignore.
466
     * This parameter is available since version 2.0.4.
467
     * @return array
468
     */
469 39
    protected function extractMessages($fileName, $translator, $ignoreCategories = [])
470
    {
471 39
        $this->stdout('Extracting messages from ');
472 39
        $this->stdout($fileName, Console::FG_CYAN);
473 39
        $this->stdout("...\n");
474
475 39
        $subject = file_get_contents($fileName);
476 39
        $messages = [];
477 39
        $tokens = token_get_all($subject);
478 39
        foreach ((array) $translator as $currentTranslator) {
479 39
            $translatorTokens = token_get_all('<?php ' . $currentTranslator);
480 39
            array_shift($translatorTokens);
481 39
            $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($tokens, $translatorTokens, $ignoreCategories));
482
        }
483
484 39
        $this->stdout("\n");
485
486 39
        return $messages;
487
    }
488
489
    /**
490
     * Extracts messages from a parsed PHP tokens list.
491
     * @param array $tokens tokens to be processed.
492
     * @param array $translatorTokens translator tokens.
493
     * @param array $ignoreCategories message categories to ignore.
494
     * @return array messages.
495
     */
496 39
    protected function extractMessagesFromTokens(array $tokens, array $translatorTokens, array $ignoreCategories)
497
    {
498 39
        $messages = [];
499 39
        $translatorTokensCount = count($translatorTokens);
500 39
        $matchedTokensCount = 0;
501 39
        $buffer = [];
502 39
        $pendingParenthesisCount = 0;
503
504 39
        foreach ($tokens as $token) {
505
            // finding out translator call
506 39
            if ($matchedTokensCount < $translatorTokensCount) {
507 39
                if ($this->tokensEqual($token, $translatorTokens[$matchedTokensCount])) {
508 39
                    $matchedTokensCount++;
509
                } else {
510 39
                    $matchedTokensCount = 0;
511
                }
512 39
            } elseif ($matchedTokensCount === $translatorTokensCount) {
513
                // translator found
514
515
                // end of function call
516 39
                if ($this->tokensEqual(')', $token)) {
517 39
                    $pendingParenthesisCount--;
518
519 39
                    if ($pendingParenthesisCount === 0) {
520
                        // end of translator call or end of something that we can't extract
521 39
                        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) {
522
                            // is valid call we can extract
523 39
                            $category = stripcslashes($buffer[0][1]);
524 39
                            $category = mb_substr($category, 1, -1);
525
526 39
                            if (!$this->isCategoryIgnored($category, $ignoreCategories)) {
527 39
                                $fullMessage = mb_substr($buffer[2][1], 1, -1);
528 39
                                $i = 3;
529 39
                                while ($i < count($buffer) - 1 && !is_array($buffer[$i]) && $buffer[$i] === '.') {
530 3
                                    $fullMessage .= mb_substr($buffer[$i + 1][1], 1, -1);
531 3
                                    $i += 2;
532
                                }
533
534 39
                                $message = stripcslashes($fullMessage);
535 39
                                $messages[$category][] = $message;
536
                            }
537
538 39
                            $nestedTokens = array_slice($buffer, 3);
539 39
                            if (count($nestedTokens) > $translatorTokensCount) {
540
                                // search for possible nested translator calls
541 39
                                $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($nestedTokens, $translatorTokens, $ignoreCategories));
542
                            }
543
                        } else {
544
                            // invalid call or dynamic call we can't extract
545
                            $line = Console::ansiFormat($this->getLine($buffer), [Console::FG_CYAN]);
546
                            $skipping = Console::ansiFormat('Skipping line', [Console::FG_YELLOW]);
547
                            $this->stdout("$skipping $line. Make sure both category and message are static strings.\n");
548
                        }
549
550
                        // prepare for the next match
551 39
                        $matchedTokensCount = 0;
552 39
                        $pendingParenthesisCount = 0;
553 39
                        $buffer = [];
554
                    } else {
555 39
                        $buffer[] = $token;
556
                    }
557 39
                } elseif ($this->tokensEqual('(', $token)) {
558
                    // count beginning of function call, skipping translator beginning
559 39
                    if ($pendingParenthesisCount > 0) {
560 3
                        $buffer[] = $token;
561
                    }
562 39
                    $pendingParenthesisCount++;
563 39
                } elseif (isset($token[0]) && !in_array($token[0], [T_WHITESPACE, T_COMMENT])) {
564
                    // ignore comments and whitespaces
565 39
                    $buffer[] = $token;
566
                }
567
            }
568
        }
569
570 39
        return $messages;
571
    }
572
573
    /**
574
     * The method checks, whether the $category is ignored according to $ignoreCategories array.
575
     *
576
     * Examples:
577
     *
578
     * - `myapp` - will be ignored only `myapp` category;
579
     * - `myapp*` - will be ignored by all categories beginning with `myapp` (`myapp`, `myapplication`, `myapprove`, `myapp/widgets`, `myapp.widgets`, etc).
580
     *
581
     * @param string $category category that is checked
582
     * @param array $ignoreCategories message categories to ignore.
583
     * @return bool
584
     * @since 2.0.7
585
     */
586 39
    protected function isCategoryIgnored($category, array $ignoreCategories)
587
    {
588 39
        if (!empty($ignoreCategories)) {
589 3
            if (in_array($category, $ignoreCategories, true)) {
590 3
                return true;
591
            }
592 3
            foreach ($ignoreCategories as $pattern) {
593 3
                if (strpos($pattern, '*') > 0 && strpos($category, rtrim($pattern, '*')) === 0) {
594 3
                    return true;
595
                }
596
            }
597
        }
598
599 39
        return false;
600
    }
601
602
    /**
603
     * Finds out if two PHP tokens are equal.
604
     *
605
     * @param array|string $a
606
     * @param array|string $b
607
     * @return bool
608
     * @since 2.0.1
609
     */
610 39
    protected function tokensEqual($a, $b)
611
    {
612 39
        if (is_string($a) && is_string($b)) {
613 39
            return $a === $b;
614
        }
615 39
        if (isset($a[0], $a[1], $b[0], $b[1])) {
616 39
            return $a[0] === $b[0] && $a[1] == $b[1];
617
        }
618
619 39
        return false;
620
    }
621
622
    /**
623
     * Finds out a line of the first non-char PHP token found.
624
     *
625
     * @param array $tokens
626
     * @return int|string
627
     * @since 2.0.1
628
     */
629
    protected function getLine($tokens)
630
    {
631
        foreach ($tokens as $token) {
632
            if (isset($token[2])) {
633
                return $token[2];
634
            }
635
        }
636
637
        return 'unknown';
638
    }
639
640
    /**
641
     * Writes messages into PHP files.
642
     *
643
     * @param array $messages
644
     * @param string $dirName name of the directory to write to
645
     * @param bool $overwrite if existing file should be overwritten without backup
646
     * @param bool $removeUnused if obsolete translations should be removed
647
     * @param bool $sort if translations should be sorted
648
     * @param bool $markUnused if obsolete translations should be marked
649
     */
650 14
    protected function saveMessagesToPHP($messages, $dirName, $overwrite, $removeUnused, $sort, $markUnused)
651
    {
652 14
        foreach ($messages as $category => $msgs) {
653 14
            $file = str_replace('\\', '/', "$dirName/$category.php");
654 14
            $path = dirname($file);
655 14
            FileHelper::createDirectory($path);
656 14
            $msgs = array_values(array_unique($msgs));
657 14
            $coloredFileName = Console::ansiFormat($file, [Console::FG_CYAN]);
658 14
            $this->stdout("Saving messages to $coloredFileName...\n");
659 14
            $this->saveMessagesCategoryToPHP($msgs, $file, $overwrite, $removeUnused, $sort, $category, $markUnused);
660
        }
661 14
    }
662
663
    /**
664
     * Writes category messages into PHP file.
665
     *
666
     * @param array $messages
667
     * @param string $fileName name of the file to write to
668
     * @param bool $overwrite if existing file should be overwritten without backup
669
     * @param bool $removeUnused if obsolete translations should be removed
670
     * @param bool $sort if translations should be sorted
671
     * @param string $category message category
672
     * @param bool $markUnused if obsolete translations should be marked
673
     * @return int exit code
674
     */
675 14
    protected function saveMessagesCategoryToPHP($messages, $fileName, $overwrite, $removeUnused, $sort, $category, $markUnused)
676
    {
677 14
        if (is_file($fileName)) {
678 9
            $rawExistingMessages = require $fileName;
679 9
            $existingMessages = $rawExistingMessages;
680 9
            sort($messages);
681 9
            ksort($existingMessages);
682 9
            if (array_keys($existingMessages) === $messages && (!$sort || array_keys($rawExistingMessages) === $messages)) {
683 4
                $this->stdout("Nothing new in \"$category\" category... Nothing to save.\n\n", Console::FG_GREEN);
684 4
                return ExitCode::OK;
685
            }
686 6
            unset($rawExistingMessages);
687 6
            $merged = [];
688 6
            $untranslated = [];
689 6
            foreach ($messages as $message) {
690 6
                if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
691 3
                    $merged[$message] = $existingMessages[$message];
692
                } else {
693 6
                    $untranslated[] = $message;
694
                }
695
            }
696 6
            ksort($merged);
697 6
            sort($untranslated);
698 6
            $todo = [];
699 6
            foreach ($untranslated as $message) {
700 5
                $todo[$message] = '';
701
            }
702 6
            ksort($existingMessages);
703 6
            foreach ($existingMessages as $message => $translation) {
704 6
                if (!$removeUnused && !isset($merged[$message]) && !isset($todo[$message])) {
705 3
                    if (!$markUnused || (!empty($translation) && (strncmp($translation, '@@', 2) === 0 && substr_compare($translation, '@@', -2, 2) === 0))) {
706 2
                        $todo[$message] = $translation;
707
                    } else {
708 6
                        $todo[$message] = '@@' . $translation . '@@';
709
                    }
710
                }
711
            }
712 6
            $merged = array_merge($todo, $merged);
713 6
            if ($sort) {
714
                ksort($merged);
715
            }
716 6
            if (false === $overwrite) {
717
                $fileName .= '.merged';
718
            }
719 6
            $this->stdout("Translation merged.\n");
720
        } else {
721 9
            $merged = [];
722 9
            foreach ($messages as $message) {
723 9
                $merged[$message] = '';
724
            }
725 9
            ksort($merged);
726
        }
727
728 14
        $array = VarDumper::export($merged);
729
        $content = <<<EOD
730
<?php
731 14
{$this->config['phpFileHeader']}{$this->config['phpDocBlock']}
732 14
return $array;
733
734
EOD;
735
736 14
        if (file_put_contents($fileName, $content) === false) {
737
            $this->stdout("Translation was NOT saved.\n\n", Console::FG_RED);
738
            return ExitCode::UNSPECIFIED_ERROR;
739
        }
740
741 14
        $this->stdout("Translation saved.\n\n", Console::FG_GREEN);
742 14
        return ExitCode::OK;
743
    }
744
745
    /**
746
     * Writes messages into PO file.
747
     *
748
     * @param array $messages
749
     * @param string $dirName name of the directory to write to
750
     * @param bool $overwrite if existing file should be overwritten without backup
751
     * @param bool $removeUnused if obsolete translations should be removed
752
     * @param bool $sort if translations should be sorted
753
     * @param string $catalog message catalog
754
     * @param bool $markUnused if obsolete translations should be marked
755
     */
756 13
    protected function saveMessagesToPO($messages, $dirName, $overwrite, $removeUnused, $sort, $catalog, $markUnused)
757
    {
758 13
        $file = str_replace('\\', '/', "$dirName/$catalog.po");
759 13
        FileHelper::createDirectory(dirname($file));
760 13
        $this->stdout("Saving messages to $file...\n");
761
762 13
        $poFile = new GettextPoFile();
763
764 13
        $merged = [];
765 13
        $todos = [];
766
767 13
        $hasSomethingToWrite = false;
768 13
        foreach ($messages as $category => $msgs) {
769 13
            $notTranslatedYet = [];
770 13
            $msgs = array_values(array_unique($msgs));
771
772 13
            if (is_file($file)) {
773 9
                $existingMessages = $poFile->load($file, $category);
774
775 9
                sort($msgs);
776 9
                ksort($existingMessages);
777 9
                if (array_keys($existingMessages) == $msgs) {
778 4
                    $this->stdout("Nothing new in \"$category\" category...\n");
779
780 4
                    sort($msgs);
781 4
                    foreach ($msgs as $message) {
782 4
                        $merged[$category . chr(4) . $message] = $existingMessages[$message];
783
                    }
784 4
                    ksort($merged);
785 4
                    continue;
786
                }
787
788
                // merge existing message translations with new message translations
789 6
                foreach ($msgs as $message) {
790 6
                    if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
791 3
                        $merged[$category . chr(4) . $message] = $existingMessages[$message];
792
                    } else {
793 6
                        $notTranslatedYet[] = $message;
794
                    }
795
                }
796 6
                ksort($merged);
797 6
                sort($notTranslatedYet);
798
799
                // collect not yet translated messages
800 6
                foreach ($notTranslatedYet as $message) {
801 5
                    $todos[$category . chr(4) . $message] = '';
802
                }
803
804
                // add obsolete unused messages
805 6
                foreach ($existingMessages as $message => $translation) {
806 6
                    if (!$removeUnused && !isset($merged[$category . chr(4) . $message]) && !isset($todos[$category . chr(4) . $message])) {
807 3
                        if (!$markUnused || (!empty($translation) && ((substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@')))) {
808 2
                            $todos[$category . chr(4) . $message] = $translation;
809
                        } else {
810 6
                            $todos[$category . chr(4) . $message] = '@@' . $translation . '@@';
811
                        }
812
                    }
813
                }
814
815 6
                $merged = array_merge($todos, $merged);
816 6
                if ($sort) {
817
                    ksort($merged);
818
                }
819
820 6
                if ($overwrite === false) {
821 6
                    $file .= '.merged';
822
                }
823
            } else {
824 8
                sort($msgs);
825 8
                foreach ($msgs as $message) {
826 8
                    $merged[$category . chr(4) . $message] = '';
827
                }
828 8
                ksort($merged);
829
            }
830 13
            $this->stdout("Category \"$category\" merged.\n");
831 13
            $hasSomethingToWrite = true;
832
        }
833 13
        if ($hasSomethingToWrite) {
834 13
            $poFile->save($file, $merged);
835 13
            $this->stdout("Translation saved.\n", Console::FG_GREEN);
836
        } else {
837 3
            $this->stdout("Nothing to save.\n", Console::FG_GREEN);
838
        }
839 13
    }
840
841
    /**
842
     * Writes messages into POT file.
843
     *
844
     * @param array $messages
845
     * @param string $dirName name of the directory to write to
846
     * @param string $catalog message catalog
847
     * @since 2.0.6
848
     */
849
    protected function saveMessagesToPOT($messages, $dirName, $catalog)
850
    {
851
        $file = str_replace('\\', '/', "$dirName/$catalog.pot");
852
        FileHelper::createDirectory(dirname($file));
853
        $this->stdout("Saving messages to $file...\n");
854
855
        $poFile = new GettextPoFile();
856
857
        $merged = [];
858
859
        $hasSomethingToWrite = false;
860
        foreach ($messages as $category => $msgs) {
861
            $msgs = array_values(array_unique($msgs));
862
863
            sort($msgs);
864
            foreach ($msgs as $message) {
865
                $merged[$category . chr(4) . $message] = '';
866
            }
867
            $this->stdout("Category \"$category\" merged.\n");
868
            $hasSomethingToWrite = true;
869
        }
870
        if ($hasSomethingToWrite) {
871
            ksort($merged);
872
            $poFile->save($file, $merged);
873
            $this->stdout("Translation saved.\n", Console::FG_GREEN);
874
        } else {
875
            $this->stdout("Nothing to save.\n", Console::FG_GREEN);
876
        }
877
    }
878
879
    /**
880
     * @param string $configFile
881
     * @throws Exception If configuration file does not exists.
882
     * @since 2.0.13
883
     */
884 42
    protected function initConfig($configFile)
885
    {
886 42
        $configFileContent = [];
887 42
        if ($configFile !== null) {
888 42
            $configFile = Yii::getAlias($configFile);
889 42
            if (!is_file($configFile)) {
890 3
                throw new Exception("The configuration file does not exist: $configFile");
891
            }
892 39
            $configFileContent = require $configFile;
893
        }
894
895 39
        $this->config = array_merge(
896 39
            $this->getOptionValues($this->action->id),
897 39
            $configFileContent,
898 39
            $this->getPassedOptionValues()
899
        );
900 39
        $this->config['sourcePath'] = Yii::getAlias($this->config['sourcePath']);
901 39
        $this->config['messagePath'] = Yii::getAlias($this->config['messagePath']);
902
903 39
        if (!isset($this->config['sourcePath'], $this->config['languages'])) {
904
            throw new Exception('The configuration file must specify "sourcePath" and "languages".');
905
        }
906 39
        if (!is_dir($this->config['sourcePath'])) {
907
            throw new Exception("The source path {$this->config['sourcePath']} is not a valid directory.");
908
        }
909 39
        if (empty($this->config['format']) || !in_array($this->config['format'], ['php', 'po', 'pot', 'db'])) {
910
            throw new Exception('Format should be either "php", "po", "pot" or "db".');
911
        }
912 39
        if (in_array($this->config['format'], ['php', 'po', 'pot'])) {
913 27
            if (!isset($this->config['messagePath'])) {
914
                throw new Exception('The configuration file must specify "messagePath".');
915
            }
916 27
            if (!is_dir($this->config['messagePath'])) {
917
                throw new Exception("The message path {$this->config['messagePath']} is not a valid directory.");
918
            }
919
        }
920 39
        if (empty($this->config['languages'])) {
921
            throw new Exception('Languages cannot be empty.');
922
        }
923
924 39
        if ($this->config['format'] === 'php' && $this->config['phpDocBlock'] === null) {
925
            $this->config['phpDocBlock'] = <<<DOCBLOCK
926
/**
927
 * Message translations.
928
 *
929
 * This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
930
 * It contains the localizable messages extracted from source code.
931
 * You may modify this file by translating the extracted messages.
932
 *
933
 * Each array element represents the translation (value) of a message (key).
934
 * If the value is empty, the message is considered as not translated.
935
 * Messages that no longer need translation will have their translations
936
 * enclosed between a pair of '@@' marks.
937
 *
938
 * Message string can be used with plural forms format. Check i18n section
939
 * of the guide for details.
940
 *
941
 * NOTE: this file must be saved in UTF-8 encoding.
942
 */
943
DOCBLOCK;
944
        }
945 39
    }
946
}
947