Merge pull request 'Version 1.0.0' (#1) from develop into main
Reviewed-on: #1
This commit is contained in:
commit
2af65cb5db
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 kor-elf
|
Copyright (c) 2024 Leonid Nikitin (kor-elf)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
13
README.md
13
README.md
@ -1,2 +1,15 @@
|
|||||||
# translate-laravel
|
# translate-laravel
|
||||||
|
|
||||||
|
Библиотека для Laravel, что бы общаться с сервисом перевода от <a href="https://yandex.cloud/ru/docs/translate/" target="_blank">yandex</a>. В будущем (скорей всего в далёком будущем) планирую расширить поддержку разных сервисов перевода. Что бы можно было общаться через один интерфейс с разными сервисами.
|
||||||
|
|
||||||
|
Параметры в **.env**:
|
||||||
|
|
||||||
|
# TRANSLATE_SERVICE
|
||||||
|
По умолчанию через какой сервис обращаться за переводом. На данный момент доступен только один сервис - **yandex**.
|
||||||
|
|
||||||
|
# TRANSLATE_YANDEX_FOLDER_ID
|
||||||
|
ID folder. Код можно увидеть в адресе console.yandex.cloud/folders/**{тут будет код}**.
|
||||||
|
|
||||||
|
# TRANSLATE_YANDEX_AUTHORIZED_KEY_PATH
|
||||||
|
Абсолютный путь в рамках приложения Laravel к файлу **authorized_key.json**.
|
||||||
|
Например: положили файл в корень Laravel. Тогда будет путь такой: **/authorized_key.json**.
|
38
composer.json
Normal file
38
composer.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "kor-elf/translate-laravel",
|
||||||
|
"description": "Translator for laravel",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "library",
|
||||||
|
"keywords": [
|
||||||
|
"Translate",
|
||||||
|
"Laravel",
|
||||||
|
"Yandex Translate"
|
||||||
|
],
|
||||||
|
"homepage": "https://git.kor-elf.net/kor-elf/translate-laravel",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Leonid Nikitin",
|
||||||
|
"email": "i@kor-elf.net",
|
||||||
|
"homepage": "https://git.kor-elf.net/kor-elf",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"illuminate/support": "^10.0|^11.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.0.1",
|
||||||
|
"web-token/jwt-framework": "^3.0"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"korElf\\TranslateLaravel\\TranslateLaravelProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"korElf\\TranslateLaravel\\": "src/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
config/translate.php
Normal file
34
config/translate.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2024
|
||||||
|
* author: Leonid Nikitin - i@kor-elf.net
|
||||||
|
* web: https://git.kor-elf.net/kor-elf
|
||||||
|
* Initial version created on: 15.09.2024
|
||||||
|
* MIT license: https://git.kor-elf.net/kor-elf/translate-laravel/src/branch/main/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Translate Service
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default translate service that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a translation operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('TRANSLATE_SERVICE', 'yandex'),
|
||||||
|
|
||||||
|
'services' => [
|
||||||
|
'yandex' => [
|
||||||
|
'driver' => '\korElf\TranslateLaravel\Translate\YandexDriver',
|
||||||
|
'config' => [
|
||||||
|
'folder_id' => env('TRANSLATE_YANDEX_FOLDER_ID'),
|
||||||
|
'authorized_key_path' => base_path(env('TRANSLATE_YANDEX_AUTHORIZED_KEY_PATH')),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user