1 Commits

Author SHA1 Message Date
kor-elf 2af65cb5db Merge pull request 'Version 1.0.0' (#1) from develop into main
Reviewed-on: #1
2024-09-16 22:37:07 +05:00
32 changed files with 38 additions and 1223 deletions
-3
View File
@@ -1,3 +0,0 @@
/.git export-ignore
/.gitignore export-ignore
/.gitattributes export-ignore
-1
View File
@@ -1 +0,0 @@
/vendor
+6 -120
View File
@@ -2,128 +2,14 @@
Библиотека для Laravel, что бы общаться с сервисом перевода от <a href="https://yandex.cloud/ru/docs/translate/" target="_blank">yandex</a>. В будущем (скорей всего в далёком будущем) планирую расширить поддержку разных сервисов перевода. Что бы можно было общаться через один интерфейс с разными сервисами.
# Параметры в **.env**:
Параметры в **.env**:
### TRANSLATE_SERVICE
По умолчанию через какой сервис обращаться за переводом. На данный момент доступен только сервисы - **yandex**, **log**.
# TRANSLATE_SERVICE
По умолчанию через какой сервис обращаться за переводом. На данный момент доступен только один сервис - **yandex**.
**yandex** - https://aistudio.yandex.ru/docs/ru/translate/concepts/<br>
**log** - для проверки и внедрения в свой продукт.
### TRANSLATE_YANDEX_FOLDER_ID
# TRANSLATE_YANDEX_FOLDER_ID
ID folder. Код можно увидеть в адресе console.yandex.cloud/folders/**{тут будет код}**.
### TRANSLATE_YANDEX_AUTHORIZED_KEY_PATH
# TRANSLATE_YANDEX_AUTHORIZED_KEY_PATH
Абсолютный путь в рамках приложения Laravel к файлу **authorized_key.json**.
Например: положили файл в корень Laravel. Тогда будет путь такой: **/authorized_key.json**.
### TRANSLATE_YANDEX_LIMIT_MAX_REQUEST
Максимальное количество запросов в период, который указан в TRANSLATE_YANDEX_LIMIT_RATE_SECONDS. По умолчанию 20 запросов в секунду. **На данный момент работает только перевод через систему очередей.**
### TRANSLATE_YANDEX_LIMIT_RATE_SECONDS
Период в котором считается максимальное количество запросов. По умолчанию стоит секунда. **На данный момент работает только перевод через систему очередей.**
### TRANSLATE_YANDEX_LIMIT_MAX_SYMBOLS
Максимальное количество символов за один запрос. Если превышает, то делится на две части и делает по очереди два запроса. По умолчанию стоит 9000 символов. **На данный момент работает только перевод через систему очередей.**
### TRANSLATE_LOG_LIMIT_MAX_REQUEST
Максимальное количество запросов в период, который указан в TRANSLATE_YANDEX_LIMIT_RATE_SECONDS. По умолчанию 20 запросов в секунду. **На данный момент работает только перевод через систему очередей.**
### TRANSLATE_LOG_LIMIT_RATE_SECONDS
Период в котором считается максимальное количество запросов. По умолчанию стоит секунда. **На данный момент работает только перевод через систему очередей.**
### TRANSLATE_LOG_LIMIT_MAX_SYMBOLS
Максимальное количество символов за один запрос. Если превышает, то делится на две части и делает по очереди два запроса. По умолчанию стоит 9000 символов. **На данный момент работает только перевод через систему очередей.**
# Методы
### Перевести обычный текст
> \KorElf\TranslateLaravel\Facades\Translate::translateText(
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string | array \$text,
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\$targetLanguageCode,
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;?string &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\$sourceLanguageCode = null
> <br>): string | array
**\$text** - текст который нужно перевести.
<br>**\$targetLanguageCode** - язык на который надо перевести.
<br>**\$sourceLanguageCode** - язык с которого надо перевести. Если **null**, то сервис сам пытается определить язык.
<br>
### Перевести текст в формате HTML
> \KorElf\TranslateLaravel\Facades\Translate::translateHtml(
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string | array \$text,
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\$targetLanguageCode,
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;?string &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\$sourceLanguageCode = null
> <br>): string | array
**\$text** - текст который нужно перевести.
<br>**\$targetLanguageCode** - язык на который надо перевести.
<br>**\$sourceLanguageCode** - язык с которого надо перевести. Если **null**, то сервис сам пытается определить язык.
<br>
### Получить список языков, который сервис поддерживает
> \KorElf\TranslateLaravel\Facades\Translate::listLanguages(): \KorElf\TranslateLaravel\DTO\Languages
<br>
### Перевести с помощью очередей (Queues)
> \KorElf\TranslateLaravel\Facades\Translate::runJob(
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\KorElf\TranslateLaravel\DTO\RunTranslateDto &nbsp;&nbsp;\$params,
> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\KorElf\TranslateLaravel\DTO\AfterTranslateDto &nbsp;\$afterTranslateDto
<br>): \Illuminate\Foundation\Bus\PendingDispatch
**\$params** - параметры перевода.
<br>**\$afterTranslateDto** - после завершения перевода отправляет результат в объект \$afterTranslateDto->\$className. Объект должен соблюдать контракт **\KorElf\TranslateLaravel\Contracts\TranslationCompletedListener**.
**Пример:**
> Создаём файл TranslationListener например в папке app/Services
>
> <?php declare(strict_types=1);
>
> namespace App\Services;
>
> use Illuminate\Support\Facades\Log;
> use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
>
> final class TranslationListener implements TranslationCompletedListener
> {
> /**
> * Объекты создаются и пробрасываются автоматически через контейнер `$application->make`,
> * благодаря механизму автоматического внедрения зависимостей (Dependency Injection).
> * Вы можете передавать свои собственные классы, поместив их в качестве параметров конструктора.
> * Контейнер создаст эти объекты автоматически, основываясь на разрешении типа (type-hint).
> */
> // public function __construct(
> // private readonly SaveContentCommand $saveContentCommand
> // ) { }
>
> public function onTranslationCompleted(array $translatedText, array $data = []): void
> {
> Log::info($data);
> foreach ($translatedText as $translatedTextKey => $translatedTextValue) {
> Log::info($translatedTextKey . ': ' . $translatedTextValue);
> }
> // $this->saveContentCommand->execute($data['contentId'], $translatedText);
> }
> }
>
> Потом например в каком-то контроллере (а лучше конечно в сервисе) пишем такой метод
>
> public function sendingForTranslation(): View
> {
> // Вначале создаём объект с параметрами
> $params = (new \KorElf\TranslateLaravel\DTO\RunTranslateDto)
> ->addParamText('title', 'Заголовок', 'en', 'ru')
> ->addParamHtml('content', '<p>Привет, Мир!</p>', 'en', 'ru');
>
> $translationCompletedListener = \App\Services\TranslationListener::class;
> $data = ['contentId' => 1];
> $afterTranslateDto = new \KorElf\TranslateLaravel\DTO\AfterTranslateDto($translationCompletedListener, $data);
>
> // Отправляем на очередь
> \KorElf\TranslateLaravel\Facades\Translate::runJob($params, $afterTranslateDto);
>
> return view('success');
> }
Например: положили файл в корень Laravel. Тогда будет путь такой: **/authorized_key.json**.
+4 -7
View File
@@ -19,23 +19,20 @@
],
"require": {
"php": "^8.2",
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^10.0|^11.0",
"guzzlehttp/guzzle": "^7.0.1",
"web-token/jwt-framework": "^4.1",
"ext-libxml": "*",
"ext-dom": "*",
"ext-mbstring": "*"
"web-token/jwt-framework": "^3.0"
},
"extra": {
"laravel": {
"providers": [
"KorElf\\TranslateLaravel\\TranslateLaravelProvider"
"korElf\\TranslateLaravel\\TranslateLaravelProvider"
]
}
},
"autoload": {
"psr-4": {
"KorElf\\TranslateLaravel\\": "src/"
"korElf\\TranslateLaravel\\": "src/"
}
}
}
+1 -17
View File
@@ -24,26 +24,10 @@ return [
'services' => [
'yandex' => [
'driver' => '\KorElf\TranslateLaravel\Translate\YandexDriver',
'driver' => '\korElf\TranslateLaravel\Translate\YandexDriver',
'config' => [
'folder_id' => env('TRANSLATE_YANDEX_FOLDER_ID'),
'authorized_key_path' => base_path(env('TRANSLATE_YANDEX_AUTHORIZED_KEY_PATH')),
'limit' => [
'max_request' => (int) env('TRANSLATE_YANDEX_LIMIT_MAX_REQUEST', 20),
'rate_seconds' => (int) env('TRANSLATE_YANDEX_LIMIT_RATE_SECONDS', 1),
'max_symbols' => (int) env('TRANSLATE_YANDEX_LIMIT_MAX_SYMBOLS', 9000),
],
],
],
'log' => [
'driver' => '\KorElf\TranslateLaravel\Translate\LogDriver',
'config' => [
'limit' => [
'max_request' => (int) env('TRANSLATE_LOG_LIMIT_MAX_REQUEST', 20),
'rate_seconds' => (int) env('TRANSLATE_LOG_LIMIT_RATE_SECONDS', 1),
'max_symbols' => (int) env('TRANSLATE_LOG_LIMIT_MAX_SYMBOLS', 9000),
],
],
],
],
@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Contracts;
use KorElf\TranslateLaravel\DTO\ProcessTranslateLimit;
interface ProcessTranslateContract
{
public function getLimited(): ProcessTranslateLimit;
}
+2 -2
View File
@@ -1,8 +1,8 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Contracts;
namespace korElf\TranslateLaravel\Contracts;
use KorElf\TranslateLaravel\DTO\Languages;
use korElf\TranslateLaravel\DTO\Languages;
use Illuminate\Contracts\Foundation\Application;
interface Translate
@@ -1,8 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Contracts;
interface TranslationCompletedListener
{
public function onTranslationCompleted(array $translatedText, array $data = []): void;
}
-32
View File
@@ -1,32 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
final readonly class AfterTranslateDto
{
/**
* @param string $className The fully qualified class name (FQN) of a class that implements the TranslationCompletedListener interface.
* @param array $data Additional data associated with the translation process.
*/
public function __construct(
private string $className,
private array $data = [],
) { }
/**
* Get the fully qualified class name (FQN) of a class that implements the TranslationCompletedListener interface.
*
* @return class-string<\KorElf\TranslateLaravel\Contracts\TranslationCompletedListener>
*/
public function getClassName(): string
{
return $this->className;
}
public function getData(): array
{
return $this->data;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
namespace korElf\TranslateLaravel\DTO;
final class Languages
{
-41
View File
@@ -1,41 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
use KorElf\TranslateLaravel\Enums\TextType;
final readonly class Parameter
{
public function __construct(
private string $text,
private TextType $textType,
private string $targetLanguageCode,
private ?string $sourceLanguageCode = null,
private ?string $driver = null
) { }
public function getText(): string
{
return $this->text;
}
public function getTextType(): TextType
{
return $this->textType;
}
public function getTargetLanguageCode(): string
{
return $this->targetLanguageCode;
}
public function getSourceLanguageCode(): ?string
{
return $this->sourceLanguageCode;
}
public function getDriver(): ?string
{
return $this->driver;
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
final class PartText
{
private array $texts = [];
private array $beforeTexts = [];
private array $afterTexts = [];
private int $part = 0;
public function add(string $text, ?string $beforeText = null, ?string $afterText = null): void
{
$this->texts[$this->part] = $text;
$this->beforeTexts[$this->part] = $beforeText;
$this->afterTexts[$this->part] = $afterText;
$this->part++;
}
public function getTextsForTranslation(): array
{
return $this->texts;
}
public function getTextsAfterTranslation(array $texts): string
{
$result = '';
foreach ($texts as $key => $text) {
if ($this->beforeTexts[$key] !== null) {
$result .= $this->beforeTexts[$key];
}
$result .= $text;
if ($this->afterTexts[$key] !== null) {
$result .= $this->afterTexts[$key];
}
}
return $result;
}
}
-59
View File
@@ -1,59 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
use KorElf\TranslateLaravel\Enums\TextType;
final readonly class ProcessTranslateDto
{
public function __construct(
private string $groupName,
private string $key,
private int $part,
private PartText $text,
private TextType $textType,
private string $targetLanguageCode,
private ?string $sourceLanguageCode = null,
private ?string $driver = null
) { }
public function getGroupName(): string
{
return $this->groupName;
}
public function getKey(): string
{
return $this->key;
}
public function getPart(): int
{
return $this->part;
}
public function getText(): PartText
{
return $this->text;
}
public function getTextType(): TextType
{
return $this->textType;
}
public function getTargetLanguageCode(): string
{
return $this->targetLanguageCode;
}
public function getSourceLanguageCode(): ?string
{
return $this->sourceLanguageCode;
}
public function getDriver(): ?string
{
return $this->driver;
}
}
-27
View File
@@ -1,27 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
final readonly class ProcessTranslateLimit
{
public function __construct(
private int $maxRequest,
private int $rateSeconds,
private string $driver,
) { }
public function getMaxRequest(): int
{
return $this->maxRequest;
}
public function getRateSeconds(): int
{
return $this->rateSeconds;
}
public function getDriver(): string
{
return $this->driver;
}
}
-65
View File
@@ -1,65 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
use KorElf\TranslateLaravel\Enums\TextType;
use KorElf\TranslateLaravel\Exceptions\RunTranslateDtoException;
final class RunTranslateDto
{
/**
* @var array [key => \KorElf\TranslateLaravel\DTO\Parameter]
*/
private array $params = [];
public function addParam(
string $key,
string $text,
TextType $textType,
string $targetLanguageCode,
?string $sourceLanguageCode = null,
?string $driver = null
): self
{
if (isset($this->params[$key])) {
throw new RunTranslateDtoException('Duplicate key: ' . $key);
}
$this->params[$key] = new Parameter(
$text,
$textType,
$targetLanguageCode,
$sourceLanguageCode,
$driver,
);
return $this;
}
public function addParamHtml(
string $key,
string $text,
string $targetLanguageCode,
?string $sourceLanguageCode = null,
?string $driver = null
): self
{
return $this->addParam($key, $text, TextType::Html, $targetLanguageCode, $sourceLanguageCode, $driver);
}
public function addParamText(
string $key,
string $text,
string $targetLanguageCode,
?string $sourceLanguageCode = null,
?string $driver = null
): self
{
return $this->addParam($key, $text, TextType::Text, $targetLanguageCode, $sourceLanguageCode, $driver);
}
public function getParams(): array
{
return $this->params;
}
}
-20
View File
@@ -1,20 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\DTO;
final class Translated
{
private array $data = [];
public function add(string $key, int $parts): self
{
$this->data[$key] = $parts;
return $this;
}
public function toArray(): array
{
return $this->data;
}
}
-17
View File
@@ -1,17 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Enums;
enum TextType: string
{
case Text = 'text';
case Html = 'html';
public function functionName(): string
{
return match ($this) {
self::Text => 'translateText',
self::Html => 'translateHtml',
};
}
}
@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Exceptions;
use RuntimeException;
final class AfterTranslateException extends RuntimeException
{
}
@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Exceptions;
use RuntimeException;
final class RunTranslateDtoException extends RuntimeException
{
}
+2 -2
View File
@@ -1,10 +1,10 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Exceptions;
namespace korElf\TranslateLaravel\Exceptions;
use RuntimeException;
final class TranslateException extends RuntimeException
class TranslateException extends RuntimeException
{
}
+4 -8
View File
@@ -1,22 +1,18 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Facades;
namespace korElf\TranslateLaravel\Facades;
use Illuminate\Support\Facades\Facade;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\DTO\RunTranslateDto;
/**
* @method static \KorElf\TranslateLaravel\Contracts\Translate service(?string $name = null)
* @method static \KorElf\TranslateLaravel\Contracts\Translate resolve(string $name)
* @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 \Illuminate\Foundation\Bus\PendingDispatch runJob(RunTranslateDto $params, AfterTranslateDto $afterTranslateDto)
* @method static \KorElf\TranslateLaravel\DTO\Languages listLanguages()
* @method static \korElf\TranslateLaravel\DTO\Languages listLanguages()
* @method static string getDefaultDriver()
* @method static void setDefaultDriver(string $name)
* @method static void purge(?string $name)
* @method static array getLimit(?string $driver)
*/
final class Translate extends Facade
-55
View File
@@ -1,55 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Cache;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\DTO\Translated;
use KorElf\TranslateLaravel\Exceptions\AfterTranslateException;
final class AfterTranslate implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable;
/**
* Create a new job instance.
*/
public function __construct(
private readonly string $groupName,
private readonly Translated $translated,
private readonly AfterTranslateDto $afterTranslateDto
) { }
public function handle(Application $application): void
{
$translated = [];
$data = Cache::get($this->groupName, []);
$errors = [];
foreach ($this->translated->toArray() as $key => $parts) {
if (!isset($data[$key]) || count($data[$key]) !== $parts) {
$errors[] = $key;
continue;
}
$translated[$key] = implode(' ', $data[$key]);
}
if (empty($data) || !empty($errors)) {
throw new AfterTranslateException('Part or all of the text has not been translated. Keys: ' . implode(', ', $errors));
}
$objectAfterTranslate = $application->make($this->afterTranslateDto->getClassName());
if (!$objectAfterTranslate instanceof \KorElf\TranslateLaravel\Contracts\TranslationCompletedListener) {
throw new AfterTranslateException('The class must implement the \KorElf\TranslateLaravel\Contracts\TranslationCompletedListener interface.');
}
$objectAfterTranslate->onTranslationCompleted($translated, $this->afterTranslateDto->getData());
Cache::forget($this->groupName);
}
}
-120
View File
@@ -1,120 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Support\Facades\Cache;
use KorElf\TranslateLaravel\Contracts\ProcessTranslateContract;
use KorElf\TranslateLaravel\DTO\ProcessTranslateDto;
use KorElf\TranslateLaravel\DTO\ProcessTranslateLimit;
use KorElf\TranslateLaravel\Facades\Translate;
final class ProcessTranslate implements ShouldQueue, ShouldBeEncrypted, ProcessTranslateContract
{
use Dispatchable, InteractsWithQueue, Queueable;
/**
* Create a new job instance.
*/
public function __construct(
private readonly ProcessTranslateDto $param
) { }
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [
new RateLimited('kor-elf-translate'),
];
}
public function getLimited(): ProcessTranslateLimit
{
$driver = $this->param->getDriver();
if ($driver === null) {
$driver = Translate::getDefaultDriver();
}
$limit = Translate::getLimit($driver);
return new ProcessTranslateLimit(
maxRequest: $limit['max_request'] ?? 1000000000,
rateSeconds: $limit['rate_seconds'] ?? 1,
driver: $driver,
);
}
/**
* Execute the job.
*/
public function handle(): void
{
$param = $this->param;
$groupName = $param->getGroupName();
$key = $param->getKey();
$part = $param->getPart();
$translated = Cache::get($groupName, []);
if (!isset($translated[$param->getKey()])) {
$translated[$param->getKey()] = [];
}
$translated[$key][$part] = $this->translate();
Cache::put($groupName, $translated, 86400);
}
private function translate(): string
{
$param = $this->param;
$translate = Translate::service($param->getDriver());
$function = $param->getTextType()->functionName();
$partText = $param->getText();
$originalTexts = $partText->getTextsForTranslation();
$textsForTranslation = [];
$originalKeysByTranslationIndex = [];
foreach ($originalTexts as $originalKey => $text) {
if (\trim($text) === '') {
continue;
}
$translationIndex = count($textsForTranslation);
$textsForTranslation[$translationIndex] = $text;
$originalKeysByTranslationIndex[$translationIndex] = $originalKey;
}
if ($textsForTranslation === []) {
return $partText->getTextsAfterTranslation($originalTexts);
}
$translatedTexts = $translate->{$function}(
$textsForTranslation,
$param->getTargetLanguageCode(),
$param->getSourceLanguageCode()
);
foreach ($translatedTexts as $translationIndex => $translatedText) {
if (!isset($originalKeysByTranslationIndex[$translationIndex])) {
continue;
}
$originalKey = $originalKeysByTranslationIndex[$translationIndex];
$originalTexts[$originalKey] = $translatedText;
}
return $partText->getTextsAfterTranslation($originalTexts);
}
}
-62
View File
@@ -1,62 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Str;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\DTO\ProcessTranslateDto;
use korElf\TranslateLaravel\DTO\RunTranslateDto;
use KorElf\TranslateLaravel\DTO\Translated;
use KorElf\TranslateLaravel\Translate\SplitTextIntoPartsCommand;
final class RunTranslate implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
private readonly RunTranslateDto $runTranslateDto,
private readonly AfterTranslateDto $afterTranslateDto,
) { }
/**
* Execute the job.
*/
public function handle(SplitTextIntoPartsCommand $splitTextIntoPartsCommand): void
{
$chains = [];
$groupName = 'KorElf\TranslateLaravel\Jobs-' . Str::uuid()->toString();
$translated = new Translated();
foreach ($this->runTranslateDto->getParams() as $key => $param) {
/* @var \KorElf\TranslateLaravel\DTO\Parameter $param */
$texts = $splitTextIntoPartsCommand->execute($param->getText(), $param->getTextType(), $param->getDriver());
foreach ($texts as $part => $partText) {
$processTranslateDto = new ProcessTranslateDto(
$groupName,
$key,
$part,
$partText,
$param->getTextType(),
$param->getTargetLanguageCode(),
$param->getSourceLanguageCode(),
$param->getDriver(),
);
$chains[] = new ProcessTranslate($processTranslateDto);
}
$translated->add($key, count($texts));
}
$chains[] = new AfterTranslate($groupName, $translated, $this->afterTranslateDto);
Bus::chain($chains)->dispatch();
}
}
-50
View File
@@ -1,50 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Translate;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Log;
use KorElf\TranslateLaravel\Contracts\Translate;
use KorElf\TranslateLaravel\DTO\Languages;
final class LogDriver implements Translate
{
/**
* @throws BindingResolutionException
*/
public static function init(Application $app, array $config = []): Translate
{
return new self(
$app->make(Log::class),
);
}
public function __construct(
private readonly Log $log
) { }
public function translateText(array|string $text, string $targetLanguageCode, ?string $sourceLanguageCode = null): string|array
{
$this->log::info('Translate text', ['text' => $text, 'targetLanguageCode' => $targetLanguageCode, 'sourceLanguageCode' => $sourceLanguageCode]);
return $text;
}
public function translateHtml(array|string $text, string $targetLanguageCode, ?string $sourceLanguageCode = null): string|array
{
$this->log::info('Translate html', ['text' => $text, 'targetLanguageCode' => $targetLanguageCode, 'sourceLanguageCode' => $sourceLanguageCode]);
return $text;
}
public function listLanguages(): Languages
{
$languages = new Languages();
$languages->add('ru', 'Russian');
$languages->add('en', 'English');
$languages->add('de', 'Deutsch');
return $languages;
}
}
@@ -1,270 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Translate\SplitTextIntoParts;
use DOMNodeList;
use KorElf\TranslateLaravel\DTO\PartText;
use DOMNode;
use DOMDocument;
final class SplitHTMLCommand
{
private array $parts = [];
private int $currentPart = 0;
private int $currentLength = 0;
private string $currentText = '';
private string $currentBeforeText = '';
private string $currentAfterText = '';
public function __construct(private readonly int $maxLength)
{
}
/**
* @param string $html
* @return array<int, PartText>
*/
public function execute(string $html): array
{
$this->currentPart = 0;
$this->currentLength = 0;
$this->currentText = '';
$this->parts = [
0 => new PartText(),
];
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->loadHTML(
mb_convert_encoding('<body>' . $html . '</body>', 'HTML-ENTITIES', 'UTF-8'),
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
$this->processNodes($dom, $dom->documentElement->childNodes);
if ($this->currentLength > 0) {
$this->addTextToPart();
$this->currentText = '';
$this->currentBeforeText = '';
$this->currentAfterText = '';
$this->currentLength = 0;
$this->currentPart++;
}
libxml_clear_errors();
return $this->parts;
}
private function processNodes(DOMDocument $dom, DOMNodeList $nodes): void
{
/** @var DOMNode $node */
foreach ($nodes as $node) {
if ($node->nodeType === XML_TEXT_NODE || $node->nodeType === XML_CDATA_SECTION_NODE) {
$this->processText($node->textContent);
continue;
}
if ($node->nodeType === XML_ELEMENT_NODE) {
$this->processHtml($dom, $node);
continue;
}
}
}
private function processText(string $text): void
{
$currentLength = mb_strlen($text);
if ($this->currentLength + $currentLength <= $this->maxLength) {
$this->currentText .= $text;
$this->currentLength += $currentLength;
return;
}
if ($this->currentLength > 0) {
$this->addTextToPart();
$this->newPart();
}
if ($currentLength <= $this->maxLength) {
$this->currentText .= $text;
$this->currentLength += $currentLength;
return;
}
$this->splitLongText($text);
}
private function splitLongText(string $text): void
{
// Common expression for searching for sentences.
$sentenceEndings = '/(?<=[.?!])[ \t]+(?=[A-ZА-Я])/u';
//Dividing the text into sentences
$sentences = preg_split($sentenceEndings, $text, -1, PREG_SPLIT_NO_EMPTY);
foreach ($sentences as $sentence) {
$currentLength = mb_strlen($sentence);
if ($this->currentLength > 0) {
$currentLength += 1;
}
// If adding a sentence does not exceed the limit, add it to the current part.
if ($this->currentLength + $currentLength <= $this->maxLength) {
$this->currentText .= (empty($this->currentText) ? '' : ' ') . $sentence;
$this->currentLength += $currentLength;
continue;
}
$this->addTextToPart();
$this->newPart();
$this->currentText = $sentence;
$this->currentLength = $currentLength;
}
}
private function processHtml(DOMDocument $dom, DOMNode $node): void
{
$html = $dom->saveHTML($node);
$currentLength = mb_strlen($html);
if ($this->currentLength + $currentLength <= $this->maxLength) {
$this->currentText .= $html;
$this->currentLength += $currentLength;
return;
}
if ($this->currentLength > 0) {
$this->addTextToPart();
$this->newPart();
}
if ($currentLength <= $this->maxLength) {
$this->currentText .= $html;
$this->currentLength += $currentLength;
return;
}
if ($node->hasChildNodes()) {
$tag = strtolower($node->nodeName);
if ($tag === 'table') {
$this->splitTable($dom, $node);
return;
}
$this->currentBeforeText .= '<' . $tag . $this->attributesToString($node) . '>';
$this->processNodes($dom, $node->childNodes);
$this->currentAfterText .= '</' . $tag . '>';
$this->addTextToPart();
$this->newPart();
return;
}
}
private function splitTable(DOMDocument $dom, DOMNode $node): void
{
if ($this->currentLength > 0) {
$this->addTextToPart();
$this->newPart();
}
$this->currentBeforeText .= '<table' . $this->attributesToString($node) . '>';
if ($node->hasChildNodes()) {
foreach ($node->childNodes as $childNode) {
if ($childNode->nodeType === XML_TEXT_NODE || $childNode->nodeType === XML_CDATA_SECTION_NODE) {
continue;
}
if ($childNode->nodeType !== XML_ELEMENT_NODE) {
continue;
}
$this->processTableSection($dom, $childNode);
}
}
$this->currentAfterText .= '</table>';
$this->addTextToPart();
$this->newPart();
}
private function processTableSection(DOMDocument $dom, DOMNode $node): void
{
$tag = strtolower($node->nodeName);
if ($tag === 'tr') {
$this->processTableTr($dom, $node);
}
if (in_array($tag, ['thead', 'tbody', 'tfoot'], true)) {
$this->currentBeforeText .= '<' . $tag . $this->attributesToString($node) . '>';
foreach ($node->childNodes as $childNode) {
$tagNode = strtolower($childNode->nodeName);
if ($tagNode === 'tr') {
$this->processTableTr($dom, $childNode);
}
}
$this->currentAfterText .= '</' . $tag . '>';
}
}
private function processTableTr(DOMDocument $dom, DOMNode $node): void
{
$this->currentBeforeText .= '<tr' . $this->attributesToString($node) . '>';
foreach ($node->childNodes as $childNode) {
$tagNode = strtolower($childNode->nodeName);
if ($tagNode === 'td') {
$this->processTableTd($dom, $childNode);
}
}
$this->currentAfterText .= '</tr>';
$this->addTextToPart();
}
private function processTableTd(DOMDocument $dom, DOMNode $node): void
{
$this->currentBeforeText .= '<td' . $this->attributesToString($node) . '>';
$this->processNodes($dom, $node->childNodes);
$this->currentAfterText .= '</td>';
$this->addTextToPart();
}
private function attributesToString(DOMNode $node): string
{
if (!$node->hasAttributes()) {
return '';
}
$parts = [];
foreach ($node->attributes as $attr) {
$parts[] = sprintf(
' %s="%s"',
$attr->nodeName,
htmlspecialchars($attr->nodeValue ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
);
}
return implode('', $parts);
}
private function addTextToPart(): void
{
$this->parts[$this->currentPart]->add($this->currentText, $this->currentBeforeText, $this->currentAfterText);
$this->currentText = '';
$this->currentBeforeText = '';
$this->currentAfterText = '';
}
private function newPart(): void
{
$this->currentText = '';
$this->currentBeforeText = '';
$this->currentAfterText = '';
$this->currentLength = 0;
$this->currentPart++;
$this->parts[$this->currentPart] = new PartText();
}
}
@@ -1,50 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Translate\SplitTextIntoParts;
use KorElf\TranslateLaravel\DTO\PartText;
final readonly class SplitTextCommand
{
/**
* @param string $text
* @param int $maxLength
* @return array<int, PartText>
*/
public function execute(string $text, int $maxLength): array
{
// Common expression for searching for sentences.
$sentenceEndings = '/(?<=[.?!])[ \t]+(?=[A-ZА-Я])/u';
//Dividing the text into sentences
$sentences = preg_split($sentenceEndings, $text, -1, PREG_SPLIT_NO_EMPTY);
$parts = [];
$currentPart = '';
foreach ($sentences as $sentence) {
$part = new PartText();
// If adding a sentence does not exceed the limit, add it to the current part.
if (mb_strlen($currentPart . ' ' . $sentence) <= $maxLength) {
$currentPart .= (empty($currentPart) ? '' : ' ') . $sentence;
continue;
}
// Otherwise, save the current part and start a new one.
if (!empty($currentPart)) {
$part->add($currentPart);
$parts[] = $part;
}
$currentPart = $sentence;
}
if (!empty($currentPart)) {
$part = new PartText();
$part->add($currentPart);
$parts[] = $part;
}
return $parts;
}
}
@@ -1,57 +0,0 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Translate;
use KorElf\TranslateLaravel\DTO\PartText;
use KorElf\TranslateLaravel\Enums\TextType;
use KorElf\TranslateLaravel\Facades\Translate;
use KorElf\TranslateLaravel\Translate\SplitTextIntoParts\SplitHTMLCommand;
use KorElf\TranslateLaravel\Translate\SplitTextIntoParts\SplitTextCommand;
final readonly class SplitTextIntoPartsCommand
{
/**
* @param string $text
* @param TextType $textType
* @param string|null $driver
* @return array<int, PartText>
*/
public function execute(string $text, TextType $textType, ?string $driver): array
{
$maxLength = Translate::getLimit($driver);
$maxLength = $maxLength['max_symbols'] ?? null;
if ($maxLength === null || $maxLength > mb_strlen($text)) {
$part = new PartText();
$part->add($text);
return [$part];
}
if ($textType === TextType::Html) {
return $this->splitHtml($text, $maxLength);
}
return $this->splitText($text, $maxLength);
}
/**
* @param string $text
* @param int $maxLength
* @return array<int, PartText>
*/
private function splitText(string $text, int $maxLength): array
{
$splitTextCommand = new SplitTextCommand();
return $splitTextCommand->execute($text, $maxLength);
}
/**
* @param string $html
* @param int $maxLength
* @return array<int, PartText>
*/
private function splitHtml(string $html, int $maxLength): array
{
$splitHTMLCommand = new SplitHTMLCommand($maxLength);
return $splitHTMLCommand->execute($html);
}
}
+3 -33
View File
@@ -1,15 +1,10 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Translate;
namespace korElf\TranslateLaravel\Translate;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Bus\PendingDispatch;
use korElf\TranslateLaravel\Contracts\Translate;
use InvalidArgumentException;
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
use KorElf\TranslateLaravel\Contracts\Translate;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\DTO\RunTranslateDto;
use KorElf\TranslateLaravel\Jobs\RunTranslate;
final class TranslateManager
{
@@ -49,7 +44,7 @@ final class TranslateManager
$config = $this->getConfig($name);
if (\is_null($config)) {
throw new InvalidArgumentException('Translate service [' . $name . '] is not defined.');
throw new InvalidArgumentException("Translate service [{$name}] is not defined.");
}
return $config['driver']::init($this->app, $config['config']);
@@ -65,31 +60,6 @@ final class TranslateManager
unset($this->translates[$name]);
}
/**
* Run through queues.
*/
public function runJob(RunTranslateDto $params, AfterTranslateDto $afterTranslateDto): PendingDispatch
{
$className = $afterTranslateDto->getClassName();
if (!is_subclass_of($className, TranslationCompletedListener::class)) {
throw new InvalidArgumentException('The class ' . $className . ' must implement the \KorElf\TranslateLaravel\Contracts\Translate\TranslationCompletedListener interface.');
}
unset($className);
return RunTranslate::dispatch($params, $afterTranslateDto);
}
public function getLimit(?string $driver = null): array
{
if ($driver === null) {
$driver = $this->getDefaultDriver();
}
$config = $this->getConfig($driver);
return $config['config']['limit'] ?? [];
}
/**
* Dynamically call the default driver instance.
*/
+7 -7
View File
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Translate\Yandex;
namespace korElf\TranslateLaravel\Translate\Yandex;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
@@ -10,15 +10,15 @@ 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;
use korElf\TranslateLaravel\Exceptions\TranslateException;
final class Connection
{
private string $path;
private string $folderId;
private string $privateKey;
private string $keyId;
private string $serviceAccountId;
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)
{
+5 -5
View File
@@ -1,11 +1,11 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel\Translate;
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 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
+3 -14
View File
@@ -1,13 +1,10 @@
<?php declare(strict_types=1);
namespace KorElf\TranslateLaravel;
namespace korElf\TranslateLaravel;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Foundation\Application;
use KorElf\TranslateLaravel\Contracts\ProcessTranslateContract;
use KorElf\TranslateLaravel\Translate\TranslateManager;
use korElf\TranslateLaravel\Translate\TranslateManager;
final class TranslateLaravelProvider extends ServiceProvider
{
@@ -16,15 +13,6 @@ final class TranslateLaravelProvider extends ServiceProvider
$this->publishes([
__DIR__ . '/../config/translate.php' => config_path('translate.php'),
], 'config');
RateLimiter::for('kor-elf-translate', function (ProcessTranslateContract $job) {
$limited = $job->getLimited();
return Limit::perSecond(
$limited->getMaxRequest(),
$limited->getRateSeconds()
)->by($limited->getDriver());
});
}
/**
@@ -38,6 +26,7 @@ final class TranslateLaravelProvider extends ServiceProvider
__DIR__ . '/../config/translate.php',
'translate'
);
$this->app->singleton('translate', function (Application $app) {
return new TranslateManager($app);
});