From 844264fb6254526abc2ce331cb4b31c225dc9d9d Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 9 Feb 2025 21:39:18 +0500 Subject: [PATCH] Added automatic translation about the project. --- .../Admin/Project/About/StoreUpdate.php | 6 ++ .../Projects/About/StoreUpdateRequest.php | 2 + .../Jobs/Translate/ProcessProjectContent.php | 58 +++++++++++ app/application/app/Models/ProjectContent.php | 1 + .../Services/Admin/Project/AboutService.php | 19 ++++ .../Project/ProjectContentService.php | 89 +++++++++++++++++ .../Project/ProjectContentService.php | 95 +++++++++++++++++++ app/application/lang/en/validation.php | 1 + app/application/lang/ru/validation.php | 1 + .../admin/projects/about/_from.blade.php | 5 + 10 files changed, 277 insertions(+) create mode 100644 app/application/app/Jobs/Translate/ProcessProjectContent.php create mode 100644 app/application/app/Services/Translate/Completed/Project/ProjectContentService.php create mode 100644 app/application/app/Services/Translate/Project/ProjectContentService.php diff --git a/app/application/app/Dto/Service/Admin/Project/About/StoreUpdate.php b/app/application/app/Dto/Service/Admin/Project/About/StoreUpdate.php index 3ac74b1..2f8094d 100644 --- a/app/application/app/Dto/Service/Admin/Project/About/StoreUpdate.php +++ b/app/application/app/Dto/Service/Admin/Project/About/StoreUpdate.php @@ -11,6 +11,7 @@ final readonly class StoreUpdate extends Dto private string $title, private string $description, private Storages $storages, + private bool $isTranslateAutomatically = false, ) { } public function getTitle(): string @@ -27,4 +28,9 @@ final readonly class StoreUpdate extends Dto { return $this->storages; } + + public function isTranslateAutomatically(): bool + { + return $this->isTranslateAutomatically; + } } diff --git a/app/application/app/Http/Requests/Admin/Projects/About/StoreUpdateRequest.php b/app/application/app/Http/Requests/Admin/Projects/About/StoreUpdateRequest.php index abc6556..55394c2 100644 --- a/app/application/app/Http/Requests/Admin/Projects/About/StoreUpdateRequest.php +++ b/app/application/app/Http/Requests/Admin/Projects/About/StoreUpdateRequest.php @@ -26,6 +26,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto 'title' => ['required', 'string', 'max:255',], 'description' => ['nullable', 'string',], 'storage.content_images.*.file' => ['numeric', 'min:1'], + 'translate-automatically' => ['nullable', 'boolean'], ]; } @@ -35,6 +36,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto title: $this->input('title'), description: $this->input('description'), storages: $this->storages(), + isTranslateAutomatically: (bool) $this->input('translate-automatically', false), ); } diff --git a/app/application/app/Jobs/Translate/ProcessProjectContent.php b/app/application/app/Jobs/Translate/ProcessProjectContent.php new file mode 100644 index 0000000..a6d74c4 --- /dev/null +++ b/app/application/app/Jobs/Translate/ProcessProjectContent.php @@ -0,0 +1,58 @@ + + */ + public function middleware(): array + { + return [ + (new WithoutOverlapping($this->uniqueId()))->expireAfter(180), + ]; + } + + /** + * Create a new job instance. + */ + public function __construct( + private readonly int $projectId, + private readonly array $contentIds = [], + private readonly array $exceptLanguages = [], + ) { } + + /** + * Get the unique ID for the job. + */ + public function uniqueId(): string + { + return 'project-' . $this->projectId; + } + + /** + * Execute the job. + * @throws Exception + */ + public function handle(ProjectContentService $projectContentService): void + { + $result = $projectContentService->translate($this->projectId, $this->contentIds, $this->exceptLanguages); + if ($result->isError() && $result->getCode() !== 404) { + cache()->lock($this->uniqueId())->forceRelease(); + throw new Exception($result->getMessage()); + } + } +} diff --git a/app/application/app/Models/ProjectContent.php b/app/application/app/Models/ProjectContent.php index 71644e7..ec9a35b 100644 --- a/app/application/app/Models/ProjectContent.php +++ b/app/application/app/Models/ProjectContent.php @@ -17,5 +17,6 @@ final class ProjectContent extends Model implements StorageContract protected $fillable = [ 'title', 'description', + 'language_id', ]; } diff --git a/app/application/app/Services/Admin/Project/AboutService.php b/app/application/app/Services/Admin/Project/AboutService.php index 611a99d..30b126e 100644 --- a/app/application/app/Services/Admin/Project/AboutService.php +++ b/app/application/app/Services/Admin/Project/AboutService.php @@ -5,6 +5,7 @@ namespace App\Services\Admin\Project; use App\Contracts\ServiceResultError; use App\Dto\Service\Admin\Project\About\StoreUpdate; use App\Enums\Morph; +use App\Jobs\Translate\ProcessProjectContent; use App\Models\ProjectContent; use App\Models\User; use App\Repositories\ProjectContentRepository; @@ -69,6 +70,7 @@ final class AboutService extends Service 'project' => $project, 'language' => $language, 'content' => $content, + 'serviceTranslationEnable' => config('translation_service.enable', false), ]); } @@ -105,6 +107,9 @@ final class AboutService extends Service $this->storageService->saveAndDelete($aboutProject, $storages); return $aboutProject; }); + if (\config('translation_service.enable', false)) { + $this->translateContent($aboutProject, $data); + } } catch (\Throwable $e) { report($e); return $this->errService(__('Server Error')); @@ -133,6 +138,9 @@ final class AboutService extends Service return $aboutProject; }); + if (\config('translation_service.enable', false)) { + $this->translateContent($aboutProject, $data); + } } catch (\Throwable $e) { report($e); return $this->errService(__('Server Error')); @@ -148,4 +156,15 @@ final class AboutService extends Service 'description' => $data->getDescription(), ]; } + + private function translateContent(ProjectContent $projectContent, StoreUpdate $data): void + { + if (! $data->isTranslateAutomatically()) { + return; + } + $translateExceptLanguages = [$projectContent->language_id]; + $translateContentIds = [$projectContent->id]; + + ProcessProjectContent::dispatch($projectContent->project_id, $translateContentIds, $translateExceptLanguages); + } } diff --git a/app/application/app/Services/Translate/Completed/Project/ProjectContentService.php b/app/application/app/Services/Translate/Completed/Project/ProjectContentService.php new file mode 100644 index 0000000..b9400e2 --- /dev/null +++ b/app/application/app/Services/Translate/Completed/Project/ProjectContentService.php @@ -0,0 +1,89 @@ +projectRepository->getProjectById((int) $data['projectId']); + if ($project === null) { + return; + } + + $projectContent = DB::transaction(function () use ($data, $project, $translatedText) { + $values = []; + $hashes = $this->completionChecker->execute( + $this->getHashes($data['hashes']), + ); + + foreach ($translatedText as $translatedTextKey => $translatedTextValue) { + if ($hashes->isStatusWaiting($translatedTextKey) !== true) { + continue; + } + + $translatedTextValue = $this->noTranslateAttributeHandler->handleRemoveAttribute($translatedTextValue); + $values[$translatedTextKey] = $translatedTextValue; + } + if (\count($values) === 0) { + return null; + } + + $projectContent = $project->contents()->where('language_id', $data['languageId'])->first(); + if ($projectContent !== null) { + $projectContent->update($values); + return $projectContent; + } + + $values['language_id'] = $data['languageId']; + return $project->contents()->create($values); + }); + if (\config('translation_service.enable', false) && $projectContent !== null) { + $this->translateContent($project, $projectContent, $data); + } + } + + private function translateContent(Project $project, ProjectContent $projectContent, array $data): void + { + $translateExceptLanguages = $data['exceptLanguages'] ?? []; + $translateExceptLanguages[] = $data['languageId']; + ProcessProjectContent::dispatch($project->id, [$projectContent->id], $translateExceptLanguages); + } + + private function getHashes(array $hashes): Hashes + { + $hashesDto = new Hashes(); + foreach ($hashes as $field => $hash) { + $hashesDto->add((int) $hash['hashId'], $field, $hash['hash']); + } + return $hashesDto; + } +} diff --git a/app/application/app/Services/Translate/Project/ProjectContentService.php b/app/application/app/Services/Translate/Project/ProjectContentService.php new file mode 100644 index 0000000..8a4266c --- /dev/null +++ b/app/application/app/Services/Translate/Project/ProjectContentService.php @@ -0,0 +1,95 @@ +projectRepository->getProjectById($projectId); + if ($project === null) { + return $this->errNotFound(__('Project not found')); + } + + $contents = $project->contents() + ->whereIn('id', $contentIds) + ->cursor(); + + $sourceLanguagesCode = []; + foreach ($contents as $content) { + if (!isset($sourceLanguagesCode[$content->language_id])) { + $sourceLanguagesCode[$content->language_id] = $this->projectTranslationServiceRepository->getLanguageCodeByLanguageId($content->language_id) ?? 'null'; + } + $translateIntoLanguage = $this->projectTranslationServiceRepository + ->getLanguagesBySourceLanguage($content->language_id, $exceptLanguages) + ->all()->pluck('code', 'language_id')->toArray(); + + if (empty($translateIntoLanguage)) { + continue; + } + + $fields = (new Fields()) + ->add('title', $content->title ?? '') + ->add('description', $content->description ?? ''); + $translations = DB::transaction(function () use ($content, $translateIntoLanguage, $fields) { + return $this->hashTrackerCommand->execute(Morph::ProjectContent, $content->id, \array_keys($translateIntoLanguage), $fields); + }); + /** @var TranslateFields $translations */ + unset($fields); + + foreach ($translations->getFields() as $languageId => $fields) { + $params = new \KorElf\TranslateLaravel\DTO\RunTranslateDto(); + $sourceLanguageCode = null; + if (isset($sourceLanguagesCode[$content->language_id]) && $sourceLanguagesCode[$content->language_id] !== 'null') { + $sourceLanguageCode = $sourceLanguagesCode[$content->language_id]; + } + foreach ($fields as $field) { + $text = $this->noTranslateAttributeHandler->handleAddAttribute((string) $content->{$field} ?? ''); + $params->addParamHtml($field, $text, $translateIntoLanguage[$languageId], $sourceLanguageCode); + } + + $afterTranslateDto = new AfterTranslateDto(ProjectContentServiceCompleted::class, [ + 'projectId' => $projectId, + 'languageId' => $languageId, + 'hashes' => $translations->getHashesByLanguage($languageId), + 'exceptLanguages' => $exceptLanguages, + ]); + Translate::runJob($params, $afterTranslateDto); + unset($params, $afterTranslateDto); + } + + unset($translations, $translateIntoLanguage); + } + unset($contents, $documentation); + + } catch (\Throwable $e) { + \report($e); + return $this->errService($e->getMessage()); + } + + return $this->ok(); + } +} diff --git a/app/application/lang/en/validation.php b/app/application/lang/en/validation.php index e952264..70cf9a4 100644 --- a/app/application/lang/en/validation.php +++ b/app/application/lang/en/validation.php @@ -310,6 +310,7 @@ return [ 'content_images' => 'content images', 'source_language_id' => 'source language identifier', 'translate_from_language' => 'translate from language', + 'translate-automatically' => 'translate automatically', 'translate-automatically.*' => 'translate automatically', ], ]; diff --git a/app/application/lang/ru/validation.php b/app/application/lang/ru/validation.php index e8fc506..091718c 100644 --- a/app/application/lang/ru/validation.php +++ b/app/application/lang/ru/validation.php @@ -310,6 +310,7 @@ return [ 'content_images' => 'изображения контента', 'source_language_id' => 'идентификатор исходного языка', 'translate_from_language' => 'перевести с языка', + 'translate-automatically' => 'переводить автоматически', 'translate-automatically.*' => 'переводить автоматически', ], ]; diff --git a/app/application/resources/views/admin/projects/about/_from.blade.php b/app/application/resources/views/admin/projects/about/_from.blade.php index f3ca3a9..e83216f 100644 --- a/app/application/resources/views/admin/projects/about/_from.blade.php +++ b/app/application/resources/views/admin/projects/about/_from.blade.php @@ -1,4 +1,9 @@ @csrf +@if($serviceTranslationEnable) + +@endif