Translate   A
last analyzed

Complexity

Total Complexity 7

Size/Duplication

Total Lines 42
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 42
rs 10
c 0
b 0
f 0
wmc 7

1 Method

Rating   Name   Duplication   Size   Complexity  
B handle() 0 36 7
1
<?php
2
3
namespace App\Console\Commands;
4
use Illuminate\Console\Command;
5
use Illuminate\Support\Facades\File;
6
use GuzzleHttp\Client;
7
use GuzzleHttp\Exception\GuzzleException;
8
use JsonException;
9
use Throwable;
10
11
interface TokenProviderInterface
12
{
13
14
    public function generateToken(string $source, string $target, string $text): string;
15
}
16
17
class GoogleTokenGenerator implements TokenProviderInterface
18
{
19
20
    public function generateToken(string $source, string $target, string $text): string
21
    {
22
        $tkk = ['406398', 2087938574];
23
24
        for ($d = [], $e = 0, $f = 0; $f < $this->length($text); $f++) {
25
            $g = $this->charCodeAt($text, $f);
26
            if ($g < 128) {
27
                $d[$e++] = $g;
28
            } else {
29
                if ($g < 2048) {
30
                    $d[$e++] = $g >> 6 | 192;
31
                } else {
32
                    if ($g & 64512 === 55296 && $f + 1 < $this->length($text) && ($this->charCodeAt($text, $f + 1) & 64512) === 56320) {
33
                        $g = 65536 + (($g & 1023) << 10) + ($this->charCodeAt($text, ++$f) & 1023);
34
                        $d[$e++] = $g >> 18 | 240;
35
                        $d[$e++] = $g >> 12 & 63 | 128;
36
                    } else {
37
                        $d[$e++] = $g >> 12 | 224;
38
                    }
39
                    $d[$e++] = $g >> 6 & 63 | 128;
40
                }
41
                $d[$e++] = $g & 63 | 128;
42
            }
43
        }
44
45
        $a = $tkk[0];
46
        foreach ($d as $value) {
47
            $a += $value;
48
            $a = $this->rl($a, '+-a^+6');
49
        }
50
        $a = $this->rl($a, '+-3^+b+-f');
0 ignored issues
show
Bug introduced by
It seems like $a can also be of type string; however, parameter $a of App\Console\Commands\GoogleTokenGenerator::rl() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

50
        $a = $this->rl(/** @scrutinizer ignore-type */ $a, '+-3^+b+-f');
Loading history...
51
        $a ^= $tkk[1];
52
        if ($a < 0) {
53
            $a = ($a & 2147483647) + 2147483648;
54
        }
55
        $a = fmod($a, 1000000);
56
57
        return $a . '.' . ($a ^ $tkk[0]);
58
    }
59
60
    private function rl(int $a, string $b): int
61
    {
62
        for ($c = 0; $c < strlen($b) - 2; $c += 3) {
63
            $d = $b[$c + 2];
64
            $d = $d >= 'a' ? ord($d[0]) - 87 : (int) $d;
65
            $d = $b[$c + 1] === '+' ? $this->unsignedRightShift($a, $d) : $a << $d;
66
            $a = $b[$c] === '+' ? ($a + $d & 4294967295) : $a ^ $d;
67
        }
68
69
        return $a;
70
    }
71
72
    private function unsignedRightShift(int $a, int $b): int
73
    {
74
        if ($b >= 32 || $b < -32) {
75
            $m = (int) ($b / 32);
76
            $b -= ($m * 32);
77
        }
78
79
        if ($b < 0) {
80
            $b += 32;
81
        }
82
83
        if ($b === 0) {
84
            return (($a >> 1) & 0x7fffffff) * 2 + (($a >> $b) & 1);
85
        }
86
87
        if ($a < 0) {
88
            $a >>= 1;
89
            $a &= 2147483647;
90
            $a |= 0x40000000;
91
            $a >>= ($b - 1);
92
        } else {
93
            $a >>= $b;
94
        }
95
96
        return $a;
97
    }
98
99
    private function charCodeAt(string $string, int $index): int
100
    {
101
        return mb_ord(mb_substr($string, $index, 1));
102
    }
103
104
    private function length(string $string): int
105
    {
106
        return mb_strlen($string);
107
    }
108
}
109
110
class GoogleTranslate
111
{
112
113
    protected Client $client;
114
115
    protected ?string $source;
116
117
    protected ?string $target;
118
119
    protected ?string $lastDetectedSource;
120
121
    protected string $url = 'https://translate.google.com/translate_a/single';
122
123
    protected array $options = [];
124
125
    protected array $urlParams = [
126
        'client'   => 'gtx',
127
        'hl'       => 'en',
128
        'dt'       => [
129
            't',   
130
            'bd',  
131
            'at',  
132
            'ex',  
133
            'ld',  
134
            'md',  
135
            'qca', 
136
            'rw',  
137
            'rm',  
138
            'ss'   
139
        ],
140
        'sl'       => null, 
141
        'tl'       => null, 
142
        'q'        => null, 
143
        'ie'       => 'UTF-8', 
144
        'oe'       => 'UTF-8', 
145
        'multires' => 1,
146
        'otf'      => 0,
147
        'pc'       => 1,
148
        'trs'      => 1,
149
        'ssel'     => 0,
150
        'tsel'     => 0,
151
        'kc'       => 1,
152
        'tk'       => null,
153
    ];
154
155
    protected array $resultRegexes = [
156
        '/,+/'  => ',',
157
        '/\[,/' => '[',
158
    ];
159
160
    protected TokenProviderInterface $tokenProvider;
161
162
    public function __construct(string $target = 'en', string $source = null, array $options = [], TokenProviderInterface $tokenProvider = null)
163
    {
164
        $this->client = new Client();
165
        $this->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator)
166
            ->setOptions($options) 
167
            ->setSource($source)
168
            ->setTarget($target);
169
    }
170
171
    public function setTarget(string $target): self
172
    {
173
        $this->target = $target;
174
        return $this;
175
    }
176
177
    public function setSource(string $source = null): self
178
    {
179
        $this->source = $source ?? 'auto';
180
        return $this;
181
    }
182
183
    public function setUrl(string $url): self
184
    {
185
        $this->url = $url;
186
        return $this;
187
    }
188
189
    public function setClient(string $client): self
190
    {
191
        $this->urlParams['client'] = $client;
192
        return $this;
193
    }
194
195
    public function setOptions(array $options = []): self
196
    {
197
        $this->options = $options;
198
        return $this;
199
    }
200
201
    public function setTokenProvider(TokenProviderInterface $tokenProvider): self
202
    {
203
        $this->tokenProvider = $tokenProvider;
204
        return $this;
205
    }
206
207
    public function getLastDetectedSource(): ?string
208
    {
209
        return $this->lastDetectedSource;
210
    }
211
212
    public static function trans(string $string, string $target = 'en', string $source = null, array $options = [], TokenProviderInterface $tokenProvider = null): ?string
213
    {
214
        return (new self)
215
            ->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator)
216
            ->setOptions($options) 
217
            ->setSource($source)
218
            ->setTarget($target)
219
            ->translate($string);
220
    }
221
222
    public function translate(string $string): ?string
223
    {
224
225
        if ($this->source === $this->target) {
226
            return $string;
227
        }
228
229
        $responseArray = $this->getResponse($string);
230
231
        if (empty($responseArray[0])) {
232
            return null;
233
        }
234
235
        $detectedLanguages = [];
236
237
        foreach ($responseArray as $item) {
238
            if (is_string($item)) {
239
                $detectedLanguages[] = $item;
240
            }
241
        }
242
243
        if (isset($responseArray[count($responseArray) - 2][0][0])) {
244
            $detectedLanguages[] = $responseArray[count($responseArray) - 2][0][0];
245
        }
246
247
        $this->lastDetectedSource = null;
248
249
        foreach ($detectedLanguages as $lang) {
250
            if ($this->isValidLocale($lang)) {
251
                $this->lastDetectedSource = $lang;
252
                break;
253
            }
254
        }
255
256
        if (is_string($responseArray)) {
0 ignored issues
show
introduced by
The condition is_string($responseArray) is always false.
Loading history...
257
            return $responseArray;
258
        }
259
260
        if (is_array($responseArray[0])) {
261
            return (string) array_reduce($responseArray[0], static function ($carry, $item) {
262
                $carry .= $item[0];
263
                return $carry;
264
            });
265
        }
266
267
        return (string) $responseArray[0];
268
    }
269
270
    public function getResponse(string $string): array
271
    {
272
        $queryArray = array_merge($this->urlParams, [
273
            'sl'   => $this->source,
274
            'tl'   => $this->target,
275
            'tk'   => $this->tokenProvider->generateToken($this->source, $this->target, $string),
0 ignored issues
show
Bug introduced by
It seems like $this->source can also be of type null; however, parameter $source of App\Console\Commands\Tok...erface::generateToken() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
            'tk'   => $this->tokenProvider->generateToken(/** @scrutinizer ignore-type */ $this->source, $this->target, $string),
Loading history...
Bug introduced by
It seems like $this->target can also be of type null; however, parameter $target of App\Console\Commands\Tok...erface::generateToken() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
            'tk'   => $this->tokenProvider->generateToken($this->source, /** @scrutinizer ignore-type */ $this->target, $string),
Loading history...
276
            'q'    => $string
277
        ]);
278
279
        $queryUrl = preg_replace('/%5B\d+%5D=/', '=', http_build_query($queryArray));
280
281
        try {
282
            $response = $this->client->get($this->url, [
283
                    'query' => $queryUrl,
284
                ] + $this->options);
285
        } catch (GuzzleException $e) {
286
            match ($e->getCode()) {
287
                429, 503 => throw new RateLimitException($e->getMessage(), $e->getCode()),
0 ignored issues
show
Bug introduced by
The type App\Console\Commands\RateLimitException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
288
                413 => throw new LargeTextException($e->getMessage(), $e->getCode()),
0 ignored issues
show
Bug introduced by
The type App\Console\Commands\LargeTextException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
289
                default => throw new TranslationRequestException($e->getMessage(), $e->getCode()),
0 ignored issues
show
Bug introduced by
The type App\Console\Commands\TranslationRequestException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
290
            };
291
        } catch (Throwable $e) {
292
            throw new TranslationRequestException($e->getMessage(), $e->getCode());
293
        }
294
295
        $body = $response->getBody(); 
296
297
        $bodyJson = preg_replace(array_keys($this->resultRegexes), array_values($this->resultRegexes), $body);
298
299
        try {
300
            $bodyArray = json_decode($bodyJson, true, flags: JSON_THROW_ON_ERROR);
301
        } catch (JsonException) {
302
            throw new TranslationDecodingException('Data cannot be decoded or it is deeper than the recursion limit');
0 ignored issues
show
Bug introduced by
The type App\Console\Commands\TranslationDecodingException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
303
        }
304
305
        return $bodyArray;
306
    }
307
308
    protected function isValidLocale(string $lang): bool
309
    {
310
        return (bool) preg_match('/^([a-z]{2,3})(-[A-Za-z]{2,4})?$/', $lang);
311
    }
312
}
313
314
class Translate extends Command
315
{
316
    protected $signature = 'translate';
317
318
    protected $description = 'Translate language files';
319
320
    public function handle()
321
    {
322
        $locales = config('app.supported_locales'); 
323
        $sourceLocale = 'en'; 
324
325
        foreach ($locales as $locale) {
326
            $langPath = resource_path("lang/{$locale}");
327
            if (!File::exists($langPath)) {
328
                File::makeDirectory($langPath, 0755, true);
329
            }
330
        }
331
332
        $sourceFile = resource_path("lang/{$sourceLocale}/messages.php");
333
        $sourceTranslations = require($sourceFile);
334
335
        foreach ($locales as $locale) {
336
            $targetFile = resource_path("lang/{$locale}/messages.php");
337
            $targetTranslations = File::exists($targetFile) ? require($targetFile) : [];
338
339
            $tr = new GoogleTranslate();
340
            $tr->setSource($sourceLocale);
341
            $tr->setTarget($locale);
342
343
            foreach ($sourceTranslations as $key => $value) {
344
                if (!array_key_exists($key, $targetTranslations)) {
345
                    $translatedValue = $tr->translate($value);
346
                    $targetTranslations[$key] = $translatedValue;
347
                }
348
            }
349
350
            $content = '<?php' . PHP_EOL . PHP_EOL;
351
            $content .= 'return ' . var_export($targetTranslations, true) . ';' . PHP_EOL;
352
353
            file_put_contents($targetFile, $content);
354
355
            $this->info("Translations for '{$locale}' created successfully.");
356
        }
357
    }
358
}