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 ' );
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 )) {
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 ),
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 ()),
288
+ 413 => throw new LargeTextException ($ e ->getMessage (), $ e ->getCode ()),
289
+ default => throw new TranslationRequestException ($ e ->getMessage (), $ e ->getCode ()),
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 ' );
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
+ }
0 commit comments