Implemented translation via Yandex service.
This commit is contained in:
17
src/Contracts/Translate.php
Normal file
17
src/Contracts/Translate.php
Normal 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
18
src/DTO/Languages.php
Normal 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;
|
||||
}
|
||||
}
|
10
src/Exceptions/TranslateException.php
Normal file
10
src/Exceptions/TranslateException.php
Normal 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
24
src/Facades/Translate.php
Normal 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';
|
||||
}
|
||||
}
|
75
src/Translate/TranslateManager.php
Normal file
75
src/Translate/TranslateManager.php
Normal 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;
|
||||
}
|
||||
}
|
113
src/Translate/Yandex/Connection.php
Normal file
113
src/Translate/Yandex/Connection.php
Normal 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();
|
||||
}
|
||||
}
|
113
src/Translate/YandexDriver.php
Normal file
113
src/Translate/YandexDriver.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
35
src/TranslateLaravelProvider.php
Normal file
35
src/TranslateLaravelProvider.php
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user