Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
65cfbd88d8
|
|||
|
fe90500e44
|
|||
|
92fd2cab1b
|
|||
|
e2eb80f140
|
|||
|
91be5052ed
|
|||
|
e63ded1708
|
|||
|
31da4eff78
|
|||
|
5abddfbd57
|
|||
|
7e05777385
|
|||
|
4fdf7fd6f9
|
@@ -0,0 +1,3 @@
|
||||
/.git export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.gitattributes export-ignore
|
||||
@@ -5,7 +5,10 @@
|
||||
# Параметры в **.env**:
|
||||
|
||||
### TRANSLATE_SERVICE
|
||||
По умолчанию через какой сервис обращаться за переводом. На данный момент доступен только один сервис - **yandex**.
|
||||
По умолчанию через какой сервис обращаться за переводом. На данный момент доступен только сервисы - **yandex**, **log**.
|
||||
|
||||
**yandex** - https://aistudio.yandex.ru/docs/ru/translate/concepts/<br>
|
||||
**log** - для проверки и внедрения в свой продукт.
|
||||
|
||||
### TRANSLATE_YANDEX_FOLDER_ID
|
||||
ID folder. Код можно увидеть в адресе console.yandex.cloud/folders/**{тут будет код}**.
|
||||
@@ -23,6 +26,16 @@ ID folder. Код можно увидеть в адресе console.yandex.cloud
|
||||
### 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(
|
||||
@@ -57,12 +70,12 @@ ID folder. Код можно увидеть в адресе console.yandex.cloud
|
||||
|
||||
### Перевести с помощью очередей (Queues)
|
||||
> \KorElf\TranslateLaravel\Facades\Translate::runJob(
|
||||
> <br> \KorElf\TranslateLaravel\DTO\RunTranslateDto \$params,
|
||||
> <br> \KorElf\TranslateLaravel\Contracts\TranslationCompletedListener \$completedListener
|
||||
> <br> \KorElf\TranslateLaravel\DTO\RunTranslateDto \$params,
|
||||
> <br> \KorElf\TranslateLaravel\DTO\AfterTranslateDto \$afterTranslateDto
|
||||
<br>): \Illuminate\Foundation\Bus\PendingDispatch
|
||||
|
||||
**\$params** - параметры перевода.
|
||||
<br>**\$completedListener** - после завершения перевода отправляет результат в этот объект. Объект должен соблюдать контракт **\KorElf\TranslateLaravel\Contracts\TranslationCompletedListener**.
|
||||
<br>**\$afterTranslateDto** - после завершения перевода отправляет результат в объект \$afterTranslateDto->\$className. Объект должен соблюдать контракт **\KorElf\TranslateLaravel\Contracts\TranslationCompletedListener**.
|
||||
|
||||
**Пример:**
|
||||
> Создаём файл TranslationListener например в папке app/Services
|
||||
@@ -76,25 +89,41 @@ ID folder. Код можно увидеть в адресе console.yandex.cloud
|
||||
>
|
||||
> final class TranslationListener implements TranslationCompletedListener
|
||||
> {
|
||||
> public function onTranslationCompleted(array $translatedText): void
|
||||
> /**
|
||||
> * Объекты создаются и пробрасываются автоматически через контейнер `$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(\App\Services\TranslationListener $translationListener): View
|
||||
> 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, $translationListener);
|
||||
> \KorElf\TranslateLaravel\Facades\Translate::runJob($params, $afterTranslateDto);
|
||||
>
|
||||
> return view('success');
|
||||
> }
|
||||
|
||||
+5
-4
@@ -19,11 +19,12 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"illuminate/support": "^10.0|^11.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"web-token/jwt-framework": "^3.0",
|
||||
"ext-libxml": "^2.11.7",
|
||||
"ext-dom": "^2.11.7"
|
||||
"web-token/jwt-framework": "^4.1",
|
||||
"ext-libxml": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
|
||||
@@ -35,5 +35,16 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'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),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -4,5 +4,5 @@ namespace KorElf\TranslateLaravel\Contracts;
|
||||
|
||||
interface TranslationCompletedListener
|
||||
{
|
||||
public function onTranslationCompleted(array $translatedText): void;
|
||||
public function onTranslationCompleted(array $translatedText, array $data = []): void;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ final readonly class ProcessTranslateDto
|
||||
private string $groupName,
|
||||
private string $key,
|
||||
private int $part,
|
||||
private string $text,
|
||||
private PartText $text,
|
||||
private TextType $textType,
|
||||
private string $targetLanguageCode,
|
||||
private ?string $sourceLanguageCode = null,
|
||||
@@ -32,7 +32,7 @@ final readonly class ProcessTranslateDto
|
||||
return $this->part;
|
||||
}
|
||||
|
||||
public function getText(): string
|
||||
public function getText(): PartText
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace KorElf\TranslateLaravel\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
|
||||
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
|
||||
use KorElf\TranslateLaravel\DTO\RunTranslateDto;
|
||||
|
||||
/**
|
||||
@@ -11,7 +11,7 @@ use KorElf\TranslateLaravel\DTO\RunTranslateDto;
|
||||
* @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, TranslationCompletedListener $completedListener)
|
||||
* @method static \Illuminate\Foundation\Bus\PendingDispatch runJob(RunTranslateDto $params, AfterTranslateDto $afterTranslateDto)
|
||||
* @method static \KorElf\TranslateLaravel\DTO\Languages listLanguages()
|
||||
* @method static string getDefaultDriver()
|
||||
* @method static void setDefaultDriver(string $name)
|
||||
|
||||
+13
-11
@@ -5,10 +5,11 @@ 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\Contracts\TranslationCompletedListener;
|
||||
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
|
||||
use KorElf\TranslateLaravel\DTO\Translated;
|
||||
use KorElf\TranslateLaravel\Exceptions\AfterTranslateException;
|
||||
|
||||
@@ -20,15 +21,12 @@ final class AfterTranslate implements ShouldQueue, ShouldBeEncrypted
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $groupName,
|
||||
private readonly TranslationCompletedListener $listener,
|
||||
private readonly Translated $translated,
|
||||
private readonly string $groupName,
|
||||
private readonly Translated $translated,
|
||||
private readonly AfterTranslateDto $afterTranslateDto
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle(Application $application): void
|
||||
{
|
||||
$translated = [];
|
||||
$data = Cache::get($this->groupName, []);
|
||||
@@ -43,11 +41,15 @@ final class AfterTranslate implements ShouldQueue, ShouldBeEncrypted
|
||||
$translated[$key] = implode(' ', $data[$key]);
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
if (empty($data) || !empty($errors)) {
|
||||
throw new AfterTranslateException('Part or all of the text has not been translated. Keys: ' . implode(', ', $errors));
|
||||
}
|
||||
Cache::forget($this->groupName);
|
||||
$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());
|
||||
|
||||
$this->listener->onTranslationCompleted($translated);
|
||||
Cache::forget($this->groupName);
|
||||
}
|
||||
}
|
||||
@@ -59,28 +59,62 @@ final class ProcessTranslate implements ShouldQueue, ShouldBeEncrypted, ProcessT
|
||||
{
|
||||
$param = $this->param;
|
||||
$groupName = $param->getGroupName();
|
||||
$key = $param->getKey();
|
||||
$part = $param->getPart();
|
||||
|
||||
$translated = Cache::get($groupName, []);
|
||||
if (!isset($translated[$param->getKey()])) {
|
||||
$translated[$param->getKey()] = [];
|
||||
}
|
||||
|
||||
$translate = Translate::service($param->getDriver());
|
||||
$function = $param->getTextType()->functionName();
|
||||
$key = $param->getKey();
|
||||
$part = $param->getPart();
|
||||
|
||||
$translated[$key][$part] = $param->getText();
|
||||
if (\trim($param->getText()) !== '') {
|
||||
|
||||
$translated[$key][$part] = $translate->{$function}(
|
||||
$param->getText(),
|
||||
$param->getTargetLanguageCode(),
|
||||
$param->getSourceLanguageCode()
|
||||
);
|
||||
|
||||
}
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Str;
|
||||
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
|
||||
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
|
||||
use KorElf\TranslateLaravel\DTO\ProcessTranslateDto;
|
||||
use korElf\TranslateLaravel\DTO\RunTranslateDto;
|
||||
use KorElf\TranslateLaravel\DTO\Translated;
|
||||
@@ -24,8 +24,8 @@ final class RunTranslate implements ShouldQueue, ShouldBeEncrypted
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly RunTranslateDto $runTranslateDto,
|
||||
private readonly TranslationCompletedListener $translationCompletedListener,
|
||||
private readonly RunTranslateDto $runTranslateDto,
|
||||
private readonly AfterTranslateDto $afterTranslateDto,
|
||||
) { }
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ final class RunTranslate implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
$translated->add($key, count($texts));
|
||||
}
|
||||
$chains[] = new AfterTranslate($groupName, $this->translationCompletedListener, $translated);
|
||||
$chains[] = new AfterTranslate($groupName, $translated, $this->afterTranslateDto);
|
||||
|
||||
Bus::chain($chains)->dispatch();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -2,114 +2,56 @@
|
||||
|
||||
namespace KorElf\TranslateLaravel\Translate;
|
||||
|
||||
use KorElf\TranslateLaravel\DTO\PartText;
|
||||
use KorElf\TranslateLaravel\Enums\TextType;
|
||||
use KorElf\TranslateLaravel\Facades\Translate;
|
||||
use DOMDocument;
|
||||
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)) {
|
||||
return [$text];
|
||||
$part = new PartText();
|
||||
$part->add($text);
|
||||
return [$part];
|
||||
}
|
||||
|
||||
if ($textType === TextType::Html) {
|
||||
return $this->splitHtmlText($text, $maxLength);
|
||||
return $this->splitHtml($text, $maxLength);
|
||||
}
|
||||
|
||||
return $this->splitTextBySentences($text, $maxLength);
|
||||
return $this->splitText($text, $maxLength);
|
||||
}
|
||||
|
||||
private function splitTextBySentences(string $text, int $maxLength): array
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $maxLength
|
||||
* @return array<int, PartText>
|
||||
*/
|
||||
private function splitText(string $text, int $maxLength): array
|
||||
{
|
||||
// Common expression for searching for sentences.
|
||||
$sentenceEndings = '/(?<=[.?!])\s+(?=[A-ZА-Я])/u';
|
||||
|
||||
//Dividing the text into sentences
|
||||
$sentences = preg_split($sentenceEndings, $text, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
$parts = [];
|
||||
$currentPart = '';
|
||||
|
||||
foreach ($sentences as $sentence) {
|
||||
// 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)) {
|
||||
$parts[] = $currentPart;
|
||||
$currentPart = '';
|
||||
}
|
||||
$currentPart = $sentence;
|
||||
}
|
||||
|
||||
if (!empty($currentPart)) {
|
||||
$parts[] = $currentPart;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
$splitTextCommand = new SplitTextCommand();
|
||||
return $splitTextCommand->execute($text, $maxLength);
|
||||
}
|
||||
|
||||
private function splitHtmlText(string $html, int $maxLength): array
|
||||
/**
|
||||
* @param string $html
|
||||
* @param int $maxLength
|
||||
* @return array<int, PartText>
|
||||
*/
|
||||
private function splitHtml(string $html, int $maxLength): array
|
||||
{
|
||||
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);
|
||||
|
||||
$parts = [];
|
||||
$currentPart = new DOMDocument();
|
||||
$currentLength = 0;
|
||||
|
||||
// Iterate through all child nodes of the root element
|
||||
foreach ($dom->documentElement->childNodes as $node) {
|
||||
if ($node->nodeType === XML_TEXT_NODE || $node->nodeType === XML_CDATA_SECTION_NODE) {
|
||||
// Dividing text nodes into sentences
|
||||
$sentences = preg_split('/(?<=[.?!])\s+(?=[A-ZА-Я])/', $node->textContent, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
foreach ($sentences as $sentence) {
|
||||
$sentenceHtml = htmlspecialchars($sentence);
|
||||
if ($currentLength + mb_strlen($sentenceHtml) > $maxLength) {
|
||||
$parts[] = $currentPart->saveHTML();
|
||||
|
||||
// Start new part
|
||||
$currentPart = new DOMDocument();
|
||||
$currentLength = 0;
|
||||
}
|
||||
|
||||
$textNode = $currentPart->createTextNode($sentence . ' ');
|
||||
$currentPart->appendChild($textNode);
|
||||
$currentLength += mb_strlen($sentenceHtml);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$nodeHtml = $dom->saveHTML($node);
|
||||
|
||||
if ($currentLength + mb_strlen($nodeHtml) > $maxLength) {
|
||||
$parts[] = $currentPart->saveHTML();
|
||||
|
||||
// Start new part
|
||||
$currentPart = new DOMDocument();
|
||||
$currentLength = mb_strlen($nodeHtml);
|
||||
}
|
||||
|
||||
$currentPart->appendChild($currentPart->importNode($node, true));
|
||||
$currentLength += mb_strlen($nodeHtml);
|
||||
}
|
||||
|
||||
if ($currentPart->hasChildNodes()) {
|
||||
$parts[] = $currentPart->saveHTML();
|
||||
}
|
||||
|
||||
libxml_clear_errors();
|
||||
|
||||
return $parts;
|
||||
$splitHTMLCommand = new SplitHTMLCommand($maxLength);
|
||||
return $splitHTMLCommand->execute($html);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ namespace KorElf\TranslateLaravel\Translate;
|
||||
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Foundation\Bus\PendingDispatch;
|
||||
use KorElf\TranslateLaravel\Contracts\CallbackAfterTranslated;
|
||||
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;
|
||||
|
||||
@@ -68,9 +68,15 @@ final class TranslateManager
|
||||
/**
|
||||
* Run through queues.
|
||||
*/
|
||||
public function runJob(RunTranslateDto $params, TranslationCompletedListener $completedListener): PendingDispatch
|
||||
public function runJob(RunTranslateDto $params, AfterTranslateDto $afterTranslateDto): PendingDispatch
|
||||
{
|
||||
return RunTranslate::dispatch($params, $completedListener);
|
||||
$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
|
||||
|
||||
Reference in New Issue
Block a user