TranslateController   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 7
dl 0
loc 280
ccs 0
cts 233
cp 0
rs 3.44
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A actionIndex() 0 17 1
D actionGoogleExtract() 0 67 22
C parse_safe_translate() 0 59 13
B getGoogleTranslation() 0 23 6
A saveMessagesToPHPEnhanced() 0 12 2
D saveMessagesCategoryToPHPEnhanced() 0 81 18

How to fix   Complexity   

Complex Class

Complex classes like TranslateController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TranslateController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * HiPanel core package
4
 *
5
 * @link      https://hipanel.com/
6
 * @package   hipanel-core
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2014-2019, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hipanel\console;
12
13
use Stichoza\GoogleTranslate\TranslateClient;
14
use Yii;
15
use yii\console\controllers\MessageController;
16
use yii\console\Exception;
17
use yii\helpers\Console;
18
use yii\helpers\FileHelper;
19
use yii\helpers\VarDumper;
20
21
/**
22
 * Usage: ./yii translate/google_extract /home/tofid/www/hipanel.dev/config/i18n.php
23
 * Class TranslateController.
24
 */
25
class TranslateController extends MessageController
26
{
27
    public $configFile = __DIR__ . '/../../config/i18n.php';
28
29
    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...
30
    {
31
        //        echo GoogleTranslate::staticTranslate('hello world', "en", "ru") . "\n";
32
        echo TranslateClient::translate('en', 'ru', 'Hello again') . "\n";
0 ignored issues
show
Unused Code introduced by
The call to TranslateClient::translate() has too many arguments starting with 'ru'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
33
        $message = 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.';
34
        print_r($this->parse_safe_translate($message));
35
        /*
36
        $message='{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}';
37
       print_r($this->parse_safe_translate($message));
38
       $message='The "apple" {is} good.';
39
       print_r($this->parse_safe_translate($message));
40
       $message='The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.';
41
       print_r($this->parse_safe_translate($message));
42
       var_dump($this->parse_safe_translate('Are you sure you want to delete this item?'));
43
       var_dump($this->parse_safe_translate('Create {modelClass}'));
44
       */
45
    }
46
47
    /**
48
     * Extracts messages to be translated from source code.
49
     *
50
     * This command will search through source code files and extract
51
     * messages that need to be translated in different languages.
52
     *
53
     * @param string $configFile the path or alias of the configuration file.
54
     * You may use the "yii message/config" command to generate
55
     * this file and then customize it for your needs.
56
     * @throws Exception on failure
57
     */
58
    public function actionGoogleExtract($configFile = null)
59
    {
60
        $configFile = Yii::getAlias($configFile ?: $this->configFile);
61
        if (!is_file($configFile)) {
62
            throw new Exception("The configuration file does not exist: $configFile");
63
        }
64
        $config = array_merge([
65
            'translator' => 'Yii::t',
66
            'overwrite' => false,
67
            'removeUnused' => false,
68
            'sort' => false,
69
            'format' => 'php',
70
        ], require_once($configFile));
71
        if (!isset($config['sourcePath'], $config['languages'])) {
72
            throw new Exception('The configuration file must specify "sourcePath" and "languages".');
73
        }
74
        if (!is_dir($config['sourcePath'])) {
75
            throw new Exception("The source path {$config['sourcePath']} is not a valid directory.");
76
        }
77
        if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'], true)) {
78
            throw new Exception('Format should be either "php", "po" or "db".');
79
        }
80
        if (in_array($config['format'], ['php', 'po'], true)) {
81
            if (!isset($config['messagePath'])) {
82
                throw new Exception('The configuration file must specify "messagePath".');
83
            } elseif (!is_dir($config['messagePath'])) {
84
                throw new Exception("The message path {$config['messagePath']} is not a valid directory.");
85
            }
86
        }
87
        if (empty($config['languages'])) {
88
            throw new Exception('Languages cannot be empty.');
89
        }
90
        $files = FileHelper::findFiles(realpath($config['sourcePath']), $config);
91
        $messages = [];
92
        foreach ($files as $file) {
93
            $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator']));
94
        }
95
        if (in_array($config['format'], ['php', 'po'], true)) {
96
            foreach ($config['languages'] as $language) {
97
                $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language;
98
                if (!is_dir($dir)) {
99
                    @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...
100
                }
101
                if ($config['format'] === 'po') {
102
                    $catalog = isset($config['catalog']) ? $config['catalog'] : 'messages';
103
                    $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...
104
                } else {
105
                    $this->saveMessagesToPHPEnhanced($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $language);
106
                }
107
            }
108
        } elseif ($config['format'] === 'db') {
109
            $db = \Yii::$app->get(isset($config['db']) ? $config['db'] : 'db');
110
            if (!$db instanceof \yii\db\Connection) {
111
                throw new Exception('The "db" option must refer to a valid database application component.');
112
            }
113
            $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}';
114
            $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}';
115
            $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...
116
                $messages,
117
                $db,
118
                $sourceMessageTable,
119
                $messageTable,
120
                $config['removeUnused'],
121
                $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...
122
            );
123
        }
124
    }
125
126
    public function parse_safe_translate($s)
127
    {
128
        $debug = false;
129
        $result = [];
130
        $start = 0;
131
        $nest = 0;
132
        $ptr_first_curly = 0;
133
        $total_len = strlen($s);
134
        for ($i = 0; $i < $total_len; ++$i) {
135
            if ($s[$i] === '{') {
136
                // found left curly
137
                if ($nest === 0) {
138
                    // it was the first one, nothing is nested yet
139
                    $ptr_first_curly = $i;
140
                }
141
                // increment nesting
142
                ++$nest;
143
            } elseif ($s[$i] === '}') {
144
                // found right curly
145
                // reduce nesting
146
                --$nest;
147
                if ($nest === 0) {
148
                    // end of nesting
149
                    if ($ptr_first_curly - $start >= 0) {
150
                        // push string leading up to first left curly
151
                        $prefix = substr($s, $start, $ptr_first_curly - $start);
152
                        if (strlen($prefix) > 0) {
153
                            array_push($result, $prefix);
154
                        }
155
                    }
156
                    // push (possibly nested) curly string
157
                    $suffix = substr($s, $ptr_first_curly, $i - $ptr_first_curly + 1);
158
                    if (strlen($suffix) > 0) {
159
                        array_push($result, $suffix);
160
                    }
161
                    if ($debug) {
162
                        echo '|' . substr($s, $start, $ptr_first_curly - $start - 1) . "|\n";
163
                        echo '|' . substr($s, $ptr_first_curly, $i - $ptr_first_curly + 1) . "|\n";
164
                    }
165
                    $start = $i + 1;
166
                    $ptr_first_curly = 0;
167
                    if ($debug) {
168
                        echo 'next start: ' . $start . "\n";
169
                    }
170
                }
171
            }
172
        }
173
        $suffix = substr($s, $start, $total_len - $start);
174
        if ($debug) {
175
            echo 'Start:' . $start . "\n";
176
            echo 'Pfc:' . $ptr_first_curly . "\n";
177
            echo $suffix . "\n";
178
        }
179
        if (strlen($suffix) > 0) {
180
            array_push($result, substr($s, $start, $total_len - $start));
181
        }
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) === '}') {
193
                    $translation .= ' ';
194
                }
195
                $translation .= TranslateClient::translate(Yii::$app->language, $language, $str); // GoogleTranslate::staticTranslate($str, Yii::$app->language, $language);
0 ignored issues
show
Unused Code introduced by
The call to TranslateClient::translate() has too many arguments starting with $language.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

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