Completed
Push — master ( 3c8880...e2c218 )
by Dmitry
06:42 queued 02:46
created

TranslateController::saveMessagesToPHPEnhanced()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 6
dl 0
loc 12
ccs 0
cts 11
cp 0
crap 6
rs 9.4285
1
<?php
2
3
/*
4
 * HiPanel core package
5
 *
6
 * @link      https://hipanel.com/
7
 * @package   hipanel-core
8
 * @license   BSD-3-Clause
9
 * @copyright Copyright (c) 2014-2016, HiQDev (http://hiqdev.com/)
10
 */
11
12
namespace hipanel\console;
13
14
use Stichoza\GoogleTranslate\TranslateClient;
15
use Yii;
16
use yii\console\controllers\MessageController;
17
use yii\console\Exception;
18
use yii\helpers\Console;
19
use yii\helpers\FileHelper;
20
use yii\helpers\VarDumper;
21
22
/**
23
 * Usage: ./yii translate/google_extract /home/tofid/www/hipanel.dev/config/i18n.php
24
 * Class TranslateController.
25
 */
26
class TranslateController extends MessageController
27
{
28
    public $configFile = '@hipanel/config/i18n.php';
29
30
    public function actionIndex($message = 'hello {world} apple beta {gamma {horse, cat}} this {mysite} this is a draw {apple {beta,{cat,dog} dog} house} farm')
0 ignored issues
show
Unused Code introduced by
The parameter $message is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
31
    {
32
        //        echo GoogleTranslate::staticTranslate('hello world', "en", "ru") . "\n";
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
33
        echo TranslateClient::translate('en', 'ru', 'Hello again') . "\n";
0 ignored issues
show
Bug introduced by
The method translate() does not exist on Stichoza\GoogleTranslate\TranslateClient. Did you maybe mean instanceTranslate()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
34
        $message = 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.';
35
        print_r($this->parse_safe_translate($message));
36
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
37
        $message='{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}';
38
       print_r($this->parse_safe_translate($message));
39
       $message='The "apple" {is} good.';
40
       print_r($this->parse_safe_translate($message));
41
       $message='The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.';
42
       print_r($this->parse_safe_translate($message));
43
       var_dump($this->parse_safe_translate('Are you sure you want to delete this item?'));
44
       var_dump($this->parse_safe_translate('Create {modelClass}'));
45
       */
46
    }
47
48
    /**
49
     * Extracts messages to be translated from source code.
50
     *
51
     * This command will search through source code files and extract
52
     * messages that need to be translated in different languages.
53
     *
54
     * @param string $configFile the path or alias of the configuration file.
55
     * You may use the "yii message/config" command to generate
56
     * this file and then customize it for your needs.
57
     * @throws Exception on failure.
58
     */
59
    public function actionGoogleExtract($configFile = null)
60
    {
61
        $configFile = Yii::getAlias($configFile ?: $this->configFile);
62
        if (!is_file($configFile)) {
63
            throw new Exception("The configuration file does not exist: $configFile");
64
        }
65
        $config = array_merge([
66
            'translator' => 'Yii::t',
67
            'overwrite' => false,
68
            'removeUnused' => false,
69
            'sort' => false,
70
            'format' => 'php',
71
        ], require_once($configFile));
72
        if (!isset($config['sourcePath'], $config['languages'])) {
73
            throw new Exception('The configuration file must specify "sourcePath" and "languages".');
74
        }
75
        if (!is_dir($config['sourcePath'])) {
76
            throw new Exception("The source path {$config['sourcePath']} is not a valid directory.");
77
        }
78
        if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'], true)) {
79
            throw new Exception('Format should be either "php", "po" or "db".');
80
        }
81
        if (in_array($config['format'], ['php', 'po'], true)) {
82
            if (!isset($config['messagePath'])) {
83
                throw new Exception('The configuration file must specify "messagePath".');
84
            } elseif (!is_dir($config['messagePath'])) {
85
                throw new Exception("The message path {$config['messagePath']} is not a valid directory.");
86
            }
87
        }
88
        if (empty($config['languages'])) {
89
            throw new Exception('Languages cannot be empty.');
90
        }
91
        $files = FileHelper::findFiles(realpath($config['sourcePath']), $config);
92
        $messages = [];
93
        foreach ($files as $file) {
94
            $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator']));
95
        }
96
        if (in_array($config['format'], ['php', 'po'], true)) {
97
            foreach ($config['languages'] as $language) {
98
                $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language;
99
                if (!is_dir($dir)) {
100
                    @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...
101
                }
102
                if ($config['format'] === 'po') {
103
                    $catalog = isset($config['catalog']) ? $config['catalog'] : 'messages';
104
                    $this->saveMessagesToPO($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $catalog);
0 ignored issues
show
Bug introduced by
The call to saveMessagesToPO() misses a required argument $markUnused.

This check looks for function calls that miss required arguments.

Loading history...
105
                } else {
106
                    $this->saveMessagesToPHPEnhanced($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $language);
107
                }
108
            }
109
        } elseif ($config['format'] === 'db') {
110
            $db = \Yii::$app->get(isset($config['db']) ? $config['db'] : 'db');
111
            if (!$db instanceof \yii\db\Connection) {
112
                throw new Exception('The "db" option must refer to a valid database application component.');
113
            }
114
            $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}';
115
            $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}';
116
            $this->saveMessagesToDb(
0 ignored issues
show
Bug introduced by
The call to saveMessagesToDb() misses a required argument $markUnused.

This check looks for function calls that miss required arguments.

Loading history...
117
                $messages,
118
                $db,
119
                $sourceMessageTable,
120
                $messageTable,
121
                $config['removeUnused'],
122
                $config['languages']
0 ignored issues
show
Documentation introduced by
$config['languages'] is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
123
            );
124
        }
125
    }
126
127
    public function parse_safe_translate($s)
128
    {
129
        $debug = false;
130
        $result = [];
131
        $start = 0;
132
        $nest = 0;
133
        $ptr_first_curly = 0;
134
        $total_len = strlen($s);
135
        for ($i = 0; $i < $total_len; ++$i) {
136
            if ($s[$i] === '{') {
137
                // found left curly
138
                if ($nest === 0) {
139
                    // it was the first one, nothing is nested yet
140
                    $ptr_first_curly = $i;
141
                }
142
                // increment nesting
143
                $nest += 1;
144
            } elseif ($s[$i] === '}') {
145
                // found right curly
146
                // reduce nesting
147
                $nest -= 1;
148
                if ($nest === 0) {
149
                    // end of nesting
150
                    if ($ptr_first_curly - $start >= 0) {
151
                        // push string leading up to first left curly
152
                        $prefix = substr($s, $start, $ptr_first_curly - $start);
153
                        if (strlen($prefix) > 0) {
154
                            array_push($result, $prefix);
155
                        }
156
                    }
157
                    // push (possibly nested) curly string
158
                    $suffix = substr($s, $ptr_first_curly, $i - $ptr_first_curly + 1);
159
                    if (strlen($suffix) > 0) {
160
                        array_push($result, $suffix);
161
                    }
162
                    if ($debug) {
163
                        echo '|' . substr($s, $start, $ptr_first_curly - $start - 1) . "|\n";
164
                        echo '|' . substr($s, $ptr_first_curly, $i - $ptr_first_curly + 1) . "|\n";
165
                    }
166
                    $start = $i + 1;
167
                    $ptr_first_curly = 0;
168
                    if ($debug) {
169
                        echo 'next start: ' . $start . "\n";
170
                    }
171
                }
172
            }
173
        }
174
        $suffix = substr($s, $start, $total_len - $start);
175
        if ($debug) {
176
            echo 'Start:' . $start . "\n";
177
            echo 'Pfc:' . $ptr_first_curly . "\n";
178
            echo $suffix . "\n";
179
        }
180
        if (strlen($suffix) > 0) {
181
            array_push($result, substr($s, $start, $total_len - $start));
182
        }
183
        return $result;
184
    }
185
186
    public function getGoogleTranslation($message, $language)
187
    {
188
        $arr_parts = $this->parse_safe_translate($message);
189
        $translation = '';
190
        foreach ($arr_parts as $str) {
191
            if (!stristr($str, '{')) {
192
                if (strlen($translation) > 0 and substr($translation, -1) === '}') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
193
                    $translation .= ' ';
194
                }
195
                $translation .= TranslateClient::translate(Yii::$app->language, $language, $str); // GoogleTranslate::staticTranslate($str, Yii::$app->language, $language);
0 ignored issues
show
Bug introduced by
The method translate() does not exist on Stichoza\GoogleTranslate\TranslateClient. Did you maybe mean instanceTranslate()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
196
            } else {
197
                // add space prefix unless it's first
198
                if (strlen($translation) > 0) {
199
                    $translation .= ' ' . $str;
200
                } else {
201
                    $translation .= $str;
202
                }
203
            }
204
        }
205
        print_r($translation);
206
        return $translation;
207
    }
208
209
    protected function saveMessagesToPHPEnhanced($messages, $dirName, $overwrite, $removeUnused, $sort, $language)
210
    {
211
        foreach ($messages as $category => $msgs) {
212
            $file = str_replace('\\', '/', "$dirName/$category.php");
213
            $path = dirname($file);
214
            FileHelper::createDirectory($path);
215
            $msgs = array_values(array_unique($msgs));
216
            $coloredFileName = Console::ansiFormat($file, [Console::FG_CYAN]);
217
            $this->stdout("Saving messages to $coloredFileName...\n");
218
            $this->saveMessagesCategoryToPHPEnhanced($msgs, $file, $overwrite, $removeUnused, $sort, $category, $language);
219
        }
220
    }
221
222
    protected function saveMessagesCategoryToPHPEnhanced($messages, $fileName, $overwrite, $removeUnused, $sort, $category, $language, $force = true)
223
    {
224
        if (is_file($fileName)) {
225
            $existingMessages = require $fileName;
226
            sort($messages);
227
            ksort($existingMessages);
228
            if (!$force) {
229
                if (array_keys($existingMessages) === $messages) {
230
                    $this->stdout("Nothing new in \"$category\" category... Nothing to save.\n\n", Console::FG_GREEN);
231
                    return;
232
                }
233
            }
234
            $merged = [];
235
            $untranslated = [];
236
            foreach ($messages as $message) {
237
                if (array_key_exists($message, $existingMessages) && strlen($existingMessages[$message]) > 0) {
238
                    $merged[$message] = $existingMessages[$message];
239
                } else {
240
                    $untranslated[] = $message;
241
                }
242
            }
243
            ksort($merged);
244
            sort($untranslated);
245
            $todo = [];
246
            foreach ($untranslated as $message) {
247
                $todo[$message] = $this->getGoogleTranslation($message, $language);
248
            }
249
            ksort($existingMessages);
250
            foreach ($existingMessages as $message => $translation) {
251
                if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeUnused) {
252
                    if (!empty($translation) && strncmp($translation, '@@', 2) === 0 && substr_compare($translation, '@@', -2, 2) === 0) {
253
                        $todo[$message] = $translation;
254
                    } else {
255
                        $todo[$message] = '@@' . $translation . '@@';
256
                    }
257
                }
258
            }
259
260
            $merged = array_merge($todo, $merged);
261
            if ($sort) {
262
                ksort($merged);
263
            }
264
            if (false === $overwrite) {
265
                $fileName .= '.merged';
266
            }
267
            $this->stdout("Translation merged.\n");
268
        } else {
269
            $merged = [];
270
            foreach ($messages as $message) {
271
                $merged[$message] = '';
272
            }
273
            ksort($merged);
274
        }
275
276
        $array = VarDumper::export($merged);
277
        $content = <<<EOD
278
<?php
279
/**
280
* Message translations.
281
*
282
* This file is automatically generated by 'yii {$this->id}' command.
283
* It contains the localizable messages extracted from source code.
284
* You may modify this file by translating the extracted messages.
285
*
286
* Each array element represents the translation (value) of a message (key).
287
* If the value is empty, the message is considered as not translated.
288
* Messages that no longer need translation will have their translations
289
* enclosed between a pair of '@@' marks.
290
*
291
* Message string can be used with plural forms format. Check i18n section
292
* of the guide for details.
293
*
294
* NOTE: this file must be saved in UTF-8 encoding.
295
*/
296
return $array;
297
EOD;
298
299
        file_put_contents($fileName, $content);
300
        $this->stdout("Translation saved.\n\n", Console::FG_GREEN);
301
    }
302
}
303