Completed
Push — master ( 625d55...2d12e1 )
by Carsten
12:37
created

MessageController::saveMessagesToPO()   D

Complexity

Conditions 21
Paths 18

Size

Total Lines 84
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 21.0896

Importance

Changes 0
Metric Value
dl 0
loc 84
ccs 48
cts 51
cp 0.9412
rs 4.821
c 0
b 0
f 0
cc 21
eloc 54
nc 18
nop 7
crap 21.0896

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