Implemented translation via Yandex service.

This commit is contained in:
2024-09-16 22:22:24 +05:00
parent 82dc0b9f99
commit 0d13d602a7
12 changed files with 491 additions and 1 deletions

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel\Contracts;
use korElf\TranslateLaravel\DTO\Languages;
use Illuminate\Contracts\Foundation\Application;
interface Translate
{
public static function init(Application $app, array $config = []): self;
public function translateText(string|array $text, string $targetLanguageCode, ?string $sourceLanguageCode = null): string|array;
public function translateHtml(string|array $text, string $targetLanguageCode, ?string $sourceLanguageCode = null): string|array;
public function listLanguages(): Languages;
}

18
src/DTO/Languages.php Normal file
View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel\DTO;
final class Languages
{
private array $languages;
public function add(string $code, string $name): void
{
$this->languages[$code] = $name;
}
public function getLanguages(): array
{
return $this->languages;
}
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel\Exceptions;
use RuntimeException;
class TranslateException extends RuntimeException
{
}

24
src/Facades/Translate.php Normal file
View File

@@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static \korElf\TranslateLaravel\Contracts\Translate service(?string $name = null)
* @method static \korElf\TranslateLaravel\Contracts\Translate resolve(string $name)
* @method static string|array translateText(string|array $text, string $targetLanguageCode, ?string $sourceLanguageCode = null)
* @method static string|array translateHtml(string|array $text, string $targetLanguageCode, ?string $sourceLanguageCode = null)
* @method static \korElf\TranslateLaravel\DTO\Languages listLanguages()
* @method static string getDefaultDriver()
* @method static void setDefaultDriver(string $name)
* @method static void purge(?string $name)
*/
final class Translate extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'translate';
}
}

View File

@@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel\Translate;
use Illuminate\Contracts\Foundation\Application;
use korElf\TranslateLaravel\Contracts\Translate;
use InvalidArgumentException;
final class TranslateManager
{
/**
* The array of resolved translation services.
*/
private array $translates = [];
public function __construct(
private Application $app,
) { }
public function service(?string $name = null): Translate
{
$name = $name ?: $this->getDefaultDriver();
return $this->translates[$name] ??= $this->resolve($name);
}
public function getDefaultDriver(): string
{
return $this->app['config']['translate.default'];
}
public function setDefaultDriver(string $name): void
{
$this->app['config']['translate.default'] = $name;
}
/**
* Resolve the given translate service.
*
* @throws InvalidArgumentException
*/
public function resolve(string $name): Translate
{
$config = $this->getConfig($name);
if (\is_null($config)) {
throw new InvalidArgumentException("Translate service [{$name}] is not defined.");
}
return $config['driver']::init($this->app, $config['config']);
}
/**
* Disconnect the given driver and remove from local cache.
*/
public function purge(?string $name = null): void
{
$name ??= $this->getDefaultDriver();
unset($this->translates[$name]);
}
/**
* Dynamically call the default driver instance.
*/
public function __call(string $method, mixed $parameters): mixed
{
return $this->service()->$method(...$parameters);
}
private function getConfig(string $name): ?array
{
return $this->app['config']["translate.services.{$name}"] ?? null;
}
}

View File

@@ -0,0 +1,113 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel\Translate\Yandex;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Carbon;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\PS256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer;
use korElf\TranslateLaravel\Exceptions\TranslateException;
final class Connection
{
private readonly string $path;
private readonly string $folderId;
private readonly string $privateKey;
private readonly string $keyId;
private readonly string $serviceAccountId;
public function __construct(string $path, string $folderId, string $privateKey, string $keyId, string $serviceAccountId)
{
$this->path = $path;
$this->folderId = $folderId;
// Need to remove header/metadata from private key
if (\strpos($privateKey, "PLEASE DO NOT REMOVE THIS LINE!") === 0) {
$privateKey = \substr($privateKey, \strpos($privateKey, "\n") + 1);
}
$this->privateKey = $privateKey;
$this->keyId = $keyId;
$this->serviceAccountId = $serviceAccountId;
}
public function post(string $path, array $post): array
{
$token = $this->getToken();
$path = $this->path . $path;
$post['folderId'] = $this->folderId;
$response = Http::withToken($token)
->timeout(30)
->post($path, $post);
$data = $response->json();
if ($data !== null && $response->status() === 200) {
return $data;
}
$message = $data['message'] ?? 'Error in request';
throw new TranslateException($message, $response->status());
}
private function getToken(): string
{
$cacheName = self::class . '_token';
$token = Cache::get($cacheName);
if ($token !== null) {
return $token;
}
$token = $this->createNewToken();
Cache::put($cacheName, $token['iamToken'], Carbon::createFromDate($token['expiresAt']));
return $token['iamToken'];
}
private function getJWTToken(): string
{
$jwk = JWKFactory::createFromKey(
$this->privateKey,
null,
[
'alg' => 'PS256',
'use' => 'sig',
'kid' => $this->keyId,
]
);
$algorithmManager = new AlgorithmManager([new PS256()]);
$jwsBuilder = new JWSBuilder($algorithmManager);
$payload = \json_encode([
'iss' => $this->serviceAccountId,
'aud' => "https://iam.api.cloud.yandex.net/iam/v1/tokens",
'iat' => time(),
'nbf' => time(),
'exp' => time() + 600,
]);
$jws = $jwsBuilder
->create()
->withPayload($payload)
->addSignature($jwk, ['alg' => 'PS256', 'typ'=>'JWT', 'kid' => $this->keyId])
->build();
$serializer = new CompactSerializer();
return $serializer->serialize($jws, 0);
}
private function createNewToken(): array
{
$jwtToken = $this->getJWTToken();
$token = Http::timeout(30)
->post('https://iam.api.cloud.yandex.net/iam/v1/tokens', ['jwt' => $jwtToken]);
return $token->json();
}
}

View File

@@ -0,0 +1,113 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel\Translate;
use korElf\TranslateLaravel\Contracts\Translate;
use korElf\TranslateLaravel\DTO\Languages;
use korElf\TranslateLaravel\Exceptions\TranslateException;
use korElf\TranslateLaravel\Translate\Yandex\Connection;
use Illuminate\Contracts\Foundation\Application;
final class YandexDriver implements Translate
{
public static function init(Application $app, array $config = []): self
{
$keyData = json_decode(file_get_contents($config['authorized_key_path']), true);
$path = 'https://translate.api.cloud.yandex.net/translate/v2/';
return new self(
$app->make(Connection::class, [
'path' => $path,
'folderId' => $config['folder_id'],
'privateKey' => $keyData['private_key'],
'keyId' => $keyData['id'],
'serviceAccountId' => $keyData['service_account_id'],
]),
);
}
public function __construct(
private readonly Connection $connection
) { }
public function translateText(string|array $text, string $targetLanguageCode, ?string $sourceLanguageCode = null): string|array
{
$format = 'PLAIN_TEXT';
$result = $this->translate($text, $format, $targetLanguageCode, $sourceLanguageCode);
if (\is_string($text)) {
return array_shift($result);
}
return $result;
}
public function translateHtml(string|array $text, string $targetLanguageCode, ?string $sourceLanguageCode = null): string|array
{
$format = 'HTML';
$result = $this->translate($text, $format, $targetLanguageCode, $sourceLanguageCode);
if (\is_string($text)) {
return array_shift($result);
}
return $result;
}
public function listLanguages(): Languages
{
$params = [];
try {
$result = $this->connection->post('languages', $params);
if (! isset($result['languages'])) {
$message = $result['message'] ?? 'Missing value languages in array';
throw new TranslateException($message);
}
$languages = new Languages();
foreach ($result['languages'] as $language) {
$languages->add($language['code'], $language['name'] ?? $language['code']);
}
return $languages;
} catch (TranslateException $e) {
throw $e;
} catch (\Throwable $exception) {
throw new TranslateException($exception->getMessage());
}
}
private function translate(string|array $text, string $format, string $targetLanguageCode, ?string $sourceLanguageCode = null): array
{
if (\is_string($text)) {
$text = [$text];
}
$params = [
'targetLanguageCode' => $targetLanguageCode,
'format' => $format,
'texts' => $text,
'speller' => true,
];
if ($sourceLanguageCode) {
$params['sourceLanguageCode'] = $sourceLanguageCode;
}
try {
$result = $this->connection->post('translate', $params);
if (! isset($result['translations'])) {
$message = $result['message'] ?? 'Missing value translations in array';
throw new TranslateException($message);
}
$translations = [];
foreach ($result['translations'] as $translation) {
$translations[] = $translation['text'];
}
return $translations;
} catch (TranslateException $e) {
throw $e;
} catch (\Throwable $exception) {
throw new TranslateException($exception->getMessage());
}
}
}

View File

@@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace korElf\TranslateLaravel;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Foundation\Application;
use korElf\TranslateLaravel\Translate\TranslateManager;
final class TranslateLaravelProvider extends ServiceProvider
{
public function boot(): Void
{
$this->publishes([
__DIR__ . '/../config/translate.php' => config_path('translate.php'),
], 'config');
}
/**
* Register the service provider.
*
* @return void
*/
public function register(): Void
{
$this->mergeConfigFrom(
__DIR__ . '/../config/translate.php',
'translate'
);
$this->app->singleton('translate', function (Application $app) {
return new TranslateManager($app);
});
}
}