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