LinkStackOrg /
LinkStack
| 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
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
|
|||||||
| 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
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
Loading history...
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
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
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. 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
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. 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
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. 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
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. 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 | } |