13 Commits

Author SHA1 Message Date
caa6ecc2bf Merge pull request 'Версия 0.4.0' (#7) from develop into main
Reviewed-on: #7
2025-02-15 00:07:47 +05:00
c112b225dc Update byTag method to specify void return type
Added an explicit void return type to the byTag method for clarity and strict type enforcement.
2025-02-14 23:33:39 +05:00
e3194cef48 Update MDHub image version to 0.4.0 in production setup 2025-02-14 21:52:17 +05:00
41a7343338 Added the ability to automatically translate simple text. 2025-02-13 22:43:39 +05:00
90aa909b7f Fix incorrect property access in CompletionChecker
Corrected the property access from array syntax to object syntax for `$hash->field` in the `CompletionChecker.php` file. This ensures proper handling of fields and prevents potential runtime errors.
2025-02-13 22:42:40 +05:00
8b35a5691f Refactor language filter to use conditional query builder
Removed redundant `whereIn` clause to simplify the query logic.
2025-02-12 21:38:52 +05:00
844264fb62 Added automatic translation about the project. 2025-02-09 21:39:18 +05:00
d8d17064d0 Mark DocumentationCategoryContentService as readonly. 2025-02-09 20:55:40 +05:00
c2266f2a67 Merge pull request 'Changed the version of my-projects-website:0.3.0 to my-projects-website:0.3.1.' (#6) from develop into main
Reviewed-on: #6
2024-08-04 13:50:34 +05:00
0e98d67c49 Merge pull request 'Версия 0.3.1' (#5) from develop into main
Reviewed-on: #5
2024-08-04 13:44:20 +05:00
17e84ae0a3 Merge pull request 'Fixed. Docker Production CMD ["/usr/local/bin/start"].' (#4) from develop into main
Reviewed-on: #4
2024-07-27 03:07:41 +05:00
907a41e057 Merge pull request 'Версия 0.3.0' (#3) from develop into main
Reviewed-on: #3
2024-07-27 02:09:13 +05:00
417ce35fc8 Merge pull request 'Версия 0.2.1' (#2) from develop into main
Reviewed-on: #2
2024-06-12 00:03:10 +05:00
35 changed files with 979 additions and 11 deletions

View File

@@ -11,6 +11,7 @@ final readonly class StoreUpdate extends Dto
private string $title, private string $title,
private string $description, private string $description,
private Storages $storages, private Storages $storages,
private bool $isTranslateAutomatically = false,
) { } ) { }
public function getTitle(): string public function getTitle(): string
@@ -27,4 +28,9 @@ final readonly class StoreUpdate extends Dto
{ {
return $this->storages; return $this->storages;
} }
public function isTranslateAutomatically(): bool
{
return $this->isTranslateAutomatically;
}
} }

View File

@@ -8,10 +8,16 @@ final readonly class Update extends Dto
{ {
public function __construct( public function __construct(
private Translations $translations, private Translations $translations,
private bool $isTranslateAutomatically = false,
) { } ) { }
public function getTranslations(): Translations public function getTranslations(): Translations
{ {
return $this->translations; return $this->translations;
} }
public function isTranslateAutomatically(): bool
{
return $this->isTranslateAutomatically;
}
} }

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\ProjectTranslationServiceTextHash;
final class Codes
{
private array $codes = [];
public function add(string $code, string $value): self
{
$this->codes[$code] = $value;
return $this;
}
public function getCodes(): array
{
return $this->codes;
}
public function getCodeNames(): array
{
return \array_keys($this->codes);
}
}

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\ProjectTranslationServiceTextHash;
final class HashStatusWaiting
{
private array $hash = [];
public function add(string $code, bool $isWaiting): void
{
$this->hash[$code] = $isWaiting;
}
public function isStatusWaiting(string $code): bool
{
return $this->hash[$code] ?? false;
}
}

View File

@@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\ProjectTranslationServiceTextHash;
final class Hashes
{
private array $hashes = [];
private array $ids = [];
public function add(int $hashId, string $code, string $hash): void
{
$this->hashes[$hashId] = [
'code' => $code,
'hash' => $hash,
];
$this->ids[] = $hashId;
}
public function getHash(int $hashId): ?array
{
return $this->hashes[$hashId] ?? null;
}
public function getIds(): array
{
return $this->ids;
}
}

View File

@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\ProjectTranslationServiceTextHash;
final class TranslateCodes
{
private array $codes = [];
private array $hashes = [];
public function add(int $languageId, string $code, string $hash, int $hashId): self
{
if (!isset($this->codes[$languageId])) {
$this->codes[$languageId] = [];
}
$this->codes[$languageId][] = $code;
if (!isset($this->hashes[$languageId])) {
$this->hashes[$languageId] = [];
}
$this->hashes[$languageId][$code] = [
'hash' => $hash,
'hashId' => $hashId,
];
return $this;
}
public function getCodes(): array
{
return $this->codes;
}
public function getHashesByLanguage(int $languageId): array
{
return $this->hashes[$languageId] ?? [];
}
}

View File

@@ -26,6 +26,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
'title' => ['required', 'string', 'max:255',], 'title' => ['required', 'string', 'max:255',],
'description' => ['nullable', 'string',], 'description' => ['nullable', 'string',],
'storage.content_images.*.file' => ['numeric', 'min:1'], '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'), title: $this->input('title'),
description: $this->input('description'), description: $this->input('description'),
storages: $this->storages(), storages: $this->storages(),
isTranslateAutomatically: (bool) $this->input('translate-automatically', false),
); );
} }

View File

@@ -20,6 +20,7 @@ final class UpdateRequest extends FormRequest implements FormRequestDto
'translations' => ['nullable', 'array'], 'translations' => ['nullable', 'array'],
'translations.*.code' => ['required', 'string', new In(Translations::getTranslationCodes())], 'translations.*.code' => ['required', 'string', new In(Translations::getTranslationCodes())],
'translations.*.text' => ['nullable', 'string', 'max:1000'], 'translations.*.text' => ['nullable', 'string', 'max:1000'],
'translate-automatically' => ['nullable', 'boolean'],
]; ];
} }
@@ -33,6 +34,9 @@ final class UpdateRequest extends FormRequest implements FormRequestDto
); );
} }
return new Update($translations); return new Update(
translations: $translations,
isTranslateAutomatically: (bool) $this->input('translate-automatically', false),
);
} }
} }

View File

@@ -0,0 +1,58 @@
<?php declare(strict_types=1);
namespace App\Jobs\Translate;
use App\Services\Translate\Project\ProjectContentService;
use Exception;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\WithoutOverlapping;
final class ProcessProjectContent implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, Queueable;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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());
}
}
}

View File

@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace App\Jobs\Translate;
use App\Services\Translate\Project\TranslationTextService;
use Exception;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\WithoutOverlapping;
final class ProcessTranslationText implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, Queueable;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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 int $languageId,
private readonly array $translateTextCode = [],
private readonly array $exceptLanguages = [],
) { }
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'ProcessTranslationText-' . $this->projectId;
}
/**
* Execute the job.
* @throws Exception
*/
public function handle(TranslationTextService $translationTextService): void
{
$result = $translationTextService->translate($this->projectId, $this->languageId, $this->translateTextCode, $this->exceptLanguages);
if ($result->isError() && $result->getCode() !== 404) {
cache()->lock($this->uniqueId())->forceRelease();
throw new Exception($result->getMessage());
}
}
}

View File

@@ -17,5 +17,6 @@ final class ProjectContent extends Model implements StorageContract
protected $fillable = [ protected $fillable = [
'title', 'title',
'description', 'description',
'language_id',
]; ];
} }

View File

@@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\ProjectTranslationServiceHashes\Status;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
final class ProjectTranslationServiceTextHash extends Model
{
use HasFactory;
protected $table = 'project_translation_service_text_hashes';
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'status' => Status::Waiting,
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'language_id',
'code',
'status',
'hash',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => Status::class,
];
}
}

View File

@@ -22,4 +22,13 @@ final readonly class ProjectTranslationRepository
$query = ProjectTranslation::query()->withTrashed()->where('project_id', $projectId)->where('language_id', $languageId); $query = ProjectTranslation::query()->withTrashed()->where('project_id', $projectId)->where('language_id', $languageId);
return $this->createSearchInstanceCommand->execute($query); return $this->createSearchInstanceCommand->execute($query);
} }
public function getProjectTranslationByCodes(int $projectId, int $languageId, array $codes): Search
{
$query = ProjectTranslation::query()
->where('project_id', $projectId)
->where('language_id', $languageId)
->whereIn('code', $codes);
return $this->createSearchInstanceCommand->execute($query);
}
} }

View File

@@ -19,7 +19,6 @@ final readonly class ProjectTranslationServiceHashRepository
$query = ProjectTranslationServiceHash::query() $query = ProjectTranslationServiceHash::query()
->where('morph_type', $morph) ->where('morph_type', $morph)
->where('morph_id', $morphId) ->where('morph_id', $morphId)
->whereIn('language_id', $languages)
->when($languages, function (Builder $query) use ($languages) { ->when($languages, function (Builder $query) use ($languages) {
$query->whereIn('language_id', $languages); $query->whereIn('language_id', $languages);
}) })

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Models\ProjectTranslationServiceTextHash;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Database\Eloquent\Builder;
final readonly class ProjectTranslationServiceTextHashRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
) { }
public function getHashes(array $codes, ?array $languages = null): Search
{
$query = ProjectTranslationServiceTextHash::query()
->whereIn('code', $codes)
->when($languages, function (Builder $query) use ($languages) {
$query->whereIn('language_id', $languages);
});
return $this->createSearchInstanceCommand->execute($query);
}
public function getHashesByIds(array $ids): Search
{
$query = ProjectTranslationServiceTextHash::query()->whereIn('id', $ids);
return $this->createSearchInstanceCommand->execute($query);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Services\Admin\Project;
use App\Contracts\ServiceResultError; use App\Contracts\ServiceResultError;
use App\Dto\Service\Admin\Project\About\StoreUpdate; use App\Dto\Service\Admin\Project\About\StoreUpdate;
use App\Enums\Morph; use App\Enums\Morph;
use App\Jobs\Translate\ProcessProjectContent;
use App\Models\ProjectContent; use App\Models\ProjectContent;
use App\Models\User; use App\Models\User;
use App\Repositories\ProjectContentRepository; use App\Repositories\ProjectContentRepository;
@@ -69,6 +70,7 @@ final class AboutService extends Service
'project' => $project, 'project' => $project,
'language' => $language, 'language' => $language,
'content' => $content, 'content' => $content,
'serviceTranslationEnable' => config('translation_service.enable', false),
]); ]);
} }
@@ -105,6 +107,9 @@ final class AboutService extends Service
$this->storageService->saveAndDelete($aboutProject, $storages); $this->storageService->saveAndDelete($aboutProject, $storages);
return $aboutProject; return $aboutProject;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($aboutProject, $data);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -133,6 +138,9 @@ final class AboutService extends Service
return $aboutProject; return $aboutProject;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($aboutProject, $data);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -148,4 +156,15 @@ final class AboutService extends Service
'description' => $data->getDescription(), '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);
}
} }

View File

@@ -3,8 +3,10 @@
namespace App\Services\Admin\Project; namespace App\Services\Admin\Project;
use App\Dto\Service\Admin\Project\Translation\Translations; use App\Dto\Service\Admin\Project\Translation\Translations;
use App\Dto\Service\Admin\Project\Translation\Translation;
use App\Dto\Service\Admin\Project\Translation\Update; use App\Dto\Service\Admin\Project\Translation\Update;
use App\Enums\CacheTag; use App\Enums\CacheTag;
use App\Jobs\Translate\ProcessTranslationText;
use App\Models\ProjectTranslation; use App\Models\ProjectTranslation;
use App\Models\User; use App\Models\User;
use App\Repositories\ProjectRepository; use App\Repositories\ProjectRepository;
@@ -60,6 +62,7 @@ final class TranslationService extends Service
'language' => $language, 'language' => $language,
'projectTranslations' => $this->projectTranslationRepository->getProjectTranslations($projectId, $languageId)->all()->pluck('text', 'code')->toArray(), 'projectTranslations' => $this->projectTranslationRepository->getProjectTranslations($projectId, $languageId)->all()->pluck('text', 'code')->toArray(),
'translations' => Translations::getTranslationCodes(), 'translations' => Translations::getTranslationCodes(),
'serviceTranslationEnable' => config('translation_service.enable', false),
]); ]);
} }
@@ -81,6 +84,9 @@ final class TranslationService extends Service
$this->translationModelSyncCommand->execute($project, $language, $data->getTranslations()); $this->translationModelSyncCommand->execute($project, $language, $data->getTranslations());
}); });
$this->clearCacheCommandHandler->byTag(CacheTag::ProjectTranslation); $this->clearCacheCommandHandler->byTag(CacheTag::ProjectTranslation);
if (\config('translation_service.enable', false)) {
$this->translateContent($projectId, $languageId, $data);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -88,4 +94,19 @@ final class TranslationService extends Service
return $this->ok(__('Translations successfully updated')); return $this->ok(__('Translations successfully updated'));
} }
private function translateContent(int $projectId, int $languageId, Update $data): void
{
if (! $data->isTranslateAutomatically()) {
return;
}
$translateExceptLanguages = [$languageId];
$translateTextCode = [];
foreach ($data->getTranslations()->getTranslations() as $translation) {
/** @var Translation $translation */
$translateTextCode[] = $translation->getCode();
}
ProcessTranslationText::dispatch($projectId, $languageId, $translateTextCode, $translateExceptLanguages);
}
} }

View File

@@ -6,7 +6,7 @@ use App\Enums\CacheTag;
final readonly class ClearCacheCommandHandler final readonly class ClearCacheCommandHandler
{ {
public function byTag(CacheTag $tag) public function byTag(CacheTag $tag): void
{ {
$tag->getCache()->flush(); $tag->getCache()->flush();
} }

View File

@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslation;
use App\Models\ProjectLanguage;
final readonly class TranslationText
{
public function __construct(
private ProjectLanguage $language,
private array $translations,
) { }
public static function init(ProjectLanguage $language, array $translations): self
{
return new self($language, $translations);
}
public function translate(string $code): string
{
return $this->translations[$code] ?? __($code, [], $this->language->system_lang?->getLocale());
}
}

View File

@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslation;
use App\Models\Project;
use App\Models\ProjectLanguage;
use App\Repositories\ProjectTranslationRepository;
final readonly class TranslationTextCommand
{
public function __construct(
private ProjectTranslationRepository $translationRepository,
) { }
public function execute(Project $project, ProjectLanguage $language, array $codes): TranslationText
{
$translations = $this->translationRepository
->getProjectTranslationByCodes($project->id, $language->id, $codes)
->all()->pluck('text', 'code');
return TranslationText::init($language, $translations->toArray());
}
}

View File

@@ -30,7 +30,7 @@ final readonly class CompletionChecker
$isWaiting = true; $isWaiting = true;
$hashSuccessIds[] = $hash->id; $hashSuccessIds[] = $hash->id;
} }
$hashStatusWaiting->add($hash['field'], $isWaiting); $hashStatusWaiting->add($hash->field, $isWaiting);
} }
$this->commandHandler->handleSetStatusById($hashSuccessIds, Status::Success); $this->commandHandler->handleSetStatusById($hashSuccessIds, Status::Success);

View File

@@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslationServiceTextHash;
use App\Dto\Service\ProjectTranslationServiceTextHash\Hashes;
use App\Dto\Service\ProjectTranslationServiceTextHash\HashStatusWaiting;
use App\Enums\ProjectTranslationServiceHashes\Status;
use App\Repositories\ProjectTranslationServiceTextHashRepository;
final readonly class CompletionChecker
{
public function __construct(
private ProjectTranslationServiceTextHashRepository $hashRepository,
private ProjectTranslationServiceTextHashCommandHandler $commandHandler,
) { }
public function execute(Hashes $hashesDto): HashStatusWaiting
{
$hashes = $this->hashRepository->getHashesByIds($hashesDto->getIds())->all();
$hashStatusWaiting = new HashStatusWaiting();
$hashSuccessIds = [];
foreach ($hashes as $hash) {
$dataHash = $hashesDto->getHash($hash->id);
if ($dataHash === null) {
continue;
}
$isWaiting = false;
if ($hash->status === Status::Waiting && $hash->hash === $dataHash['hash'] && $hash->code === $dataHash['code']) {
$isWaiting = true;
$hashSuccessIds[] = $hash->id;
}
$hashStatusWaiting->add($hash->code, $isWaiting);
}
$this->commandHandler->handleSetStatusById($hashSuccessIds, Status::Success);
return $hashStatusWaiting;
}
}

View File

@@ -0,0 +1,66 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslationServiceTextHash;
use App\Dto\Service\ProjectTranslationServiceTextHash\Codes;
use App\Dto\Service\ProjectTranslationServiceTextHash\TranslateCodes;
use App\Enums\ProjectTranslationServiceHashes\Status;
use App\Models\ProjectTranslationServiceTextHash;
use App\Repositories\ProjectTranslationServiceTextHashRepository;
final readonly class HashTrackerCommand
{
public function __construct(
private ProjectTranslationServiceTextHashRepository $hashRepository,
private ProjectTranslationServiceTextHashCommandHandler $commandHandler,
) { }
public function execute(array $languages, Codes $codes): TranslateCodes
{
$hashes = $this->hashRepository->getHashes($codes->getCodeNames(), $languages)->all();
$translateCodes = new TranslateCodes();
foreach ($codes->getCodes() as $code => $value) {
$textHash = $this->generateHashFromText((string) $value);
foreach ($languages as $language) {
$modelHash = $hashes->where('language_id', $language)->firstWhere('code', $code);
if ($modelHash === null) {
$modelHash = $this->commandHandler->handleStore([
'language_id' => $language,
'code' => $code,
'hash' => $textHash,
'status' => Status::Waiting,
]);
$translateCodes->add($language, $code, $textHash, $modelHash->id);
continue;
}
if ($modelHash->hash === $textHash && $this->isSuccess($modelHash)) {
continue;
}
$translateCodes->add($language, $code, $textHash, $modelHash->id);
$this->commandHandler->handleUpdate($modelHash, [
'hash' => $textHash,
'status' => Status::Waiting,
]);
}
}
return $translateCodes;
}
private function generateHashFromText(string $text): string
{
return \hash('sha256', $text);
}
private function isSuccess(ProjectTranslationServiceTextHash $modelHash): bool
{
if ($modelHash->status === Status::Success) {
return true;
}
return ($modelHash->updated_at >= now()->subMinutes(10));
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslationServiceTextHash;
use App\Enums\ProjectTranslationServiceHashes\Status;
use App\Models\ProjectTranslationServiceTextHash;
final readonly class ProjectTranslationServiceTextHashCommandHandler
{
public function handleStore(array $data): ProjectTranslationServiceTextHash
{
return ProjectTranslationServiceTextHash::create($data);
}
public function handleUpdate(ProjectTranslationServiceTextHash $hash, array $data): ProjectTranslationServiceTextHash
{
$hash->update($data);
$hash->touch();
return $hash;
}
public function handleSetStatusById(array $ids, Status $status): void
{
ProjectTranslationServiceTextHash::query()->whereIn('id', $ids)->update(['status' => $status->value]);
}
}

View File

@@ -13,7 +13,7 @@ use App\Services\ProjectTranslationServiceHash\CompletionChecker;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener; use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
final class DocumentationCategoryContentService implements TranslationCompletedListener final readonly class DocumentationCategoryContentService implements TranslationCompletedListener
{ {
public function __construct( public function __construct(
private DocumentationCategoryRepository $categoryRepository, private DocumentationCategoryRepository $categoryRepository,

View File

@@ -0,0 +1,89 @@
<?php declare(strict_types=1);
namespace App\Services\Translate\Completed\Project;
use App\Dto\Service\ProjectTranslationServiceHash\Hashes;
use App\Exceptions\Services\Translate\CompletedException;
use App\Jobs\Translate\ProcessProjectContent;
use App\Models\Project;
use App\Models\ProjectContent;
use App\Repositories\ProjectRepository;
use App\Services\ProjectTranslationService\NoTranslateAttributeHandler;
use App\Services\ProjectTranslationServiceHash\CompletionChecker;
use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
final readonly class ProjectContentService implements TranslationCompletedListener
{
public function __construct(
private ProjectRepository $projectRepository,
private NoTranslateAttributeHandler $noTranslateAttributeHandler,
private CompletionChecker $completionChecker,
) { }
/**
* @throws CompletedException
*/
public function onTranslationCompleted(array $translatedText, array $data = []): void
{
if (
!isset($data['projectId'])
|| !isset($data['languageId'])
|| !isset($data['hashes'])
) {
throw new CompletedException('Required data is missing: projectId, languageId, or hashes.');
}
$project = $this->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;
}
}

View File

@@ -0,0 +1,94 @@
<?php declare(strict_types=1);
namespace App\Services\Translate\Completed\Project;
use App\Dto\Service\ProjectTranslationServiceTextHash\Hashes;
use App\Exceptions\Services\Translate\CompletedException;
use App\Jobs\Translate\ProcessTranslationText;
use App\Models\Project;
use App\Models\ProjectTranslation;
use App\Repositories\ProjectRepository;
use App\Repositories\ProjectTranslationRepository;
use App\Services\ProjectTranslationServiceTextHash\CompletionChecker;
use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
final readonly class TranslationTextService implements TranslationCompletedListener
{
public function __construct(
private ProjectRepository $projectRepository,
private CompletionChecker $completionChecker,
private ProjectTranslationRepository $projectTranslationRepository,
) { }
/**
* @throws CompletedException
*/
public function onTranslationCompleted(array $translatedText, array $data = []): void
{
if (
!isset($data['projectId'])
|| !isset($data['languageId'])
|| !isset($data['hashes'])
) {
throw new CompletedException('Required data is missing: projectId, languageId, or hashes.');
}
$project = $this->projectRepository->getProjectById((int) $data['projectId']);
if ($project === null) {
return;
}
$translations = $this->projectTranslationRepository->getProjectTranslationByCodes($project->id, $data['languageId'], \array_keys($translatedText))->all();
$translateTextCode = DB::transaction(function () use ($data, $translatedText, $translations) {
$translateTextCode = [];
$hashes = $this->completionChecker->execute(
$this->getHashes($data['hashes']),
);
foreach ($translatedText as $translatedTextKey => $translatedTextValue) {
if ($hashes->isStatusWaiting($translatedTextKey) !== true) {
continue;
}
$translateTextCode[] = $translatedTextKey;
$translation = $translations->firstWhere('code', $translatedTextKey);
if ($translation !== null) {
$translation->update([
'text' => $translatedTextValue,
]);
continue;
}
ProjectTranslation::create([
'project_id' => (int) $data['projectId'],
'language_id' => (int) $data['languageId'],
'code' => $translatedTextKey,
'text' => $translatedTextValue,
]);
}
return $translateTextCode;
});
if (\config('translation_service.enable', false) && !empty($translateTextCode)) {
$this->translateContent($project, $translateTextCode, $data);
}
}
private function translateContent(Project $project, array $translateTextCode, array $data): void
{
$translateExceptLanguages = $data['exceptLanguages'] ?? [];
$translateExceptLanguages[] = $data['languageId'];
ProcessTranslationText::dispatch($project->id, $data['languageId'], $translateTextCode, $translateExceptLanguages);
}
private function getHashes(array $hashes): Hashes
{
$hashesDto = new Hashes();
foreach ($hashes as $code => $hash) {
$hashesDto->add((int) $hash['hashId'], $code, $hash['hash']);
}
return $hashesDto;
}
}

View File

@@ -0,0 +1,95 @@
<?php declare(strict_types=1);
namespace App\Services\Translate\Project;
use App\Dto\Service\ProjectTranslationServiceHash\Fields;
use App\Dto\Service\ProjectTranslationServiceHash\TranslateFields;
use App\Enums\Morph;
use App\Repositories\ProjectRepository;
use App\Repositories\ProjectTranslationServiceRepository;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\ProjectTranslationService\NoTranslateAttributeHandler;
use App\Services\ProjectTranslationServiceHash\HashTrackerCommand;
use App\Services\Service;
use App\Services\Translate\Completed\Project\ProjectContentService as ProjectContentServiceCompleted;
use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\Facades\Translate;
final class ProjectContentService extends Service
{
public function __construct(
private readonly ProjectRepository $projectRepository,
private readonly ProjectTranslationServiceRepository $projectTranslationServiceRepository,
private readonly HashTrackerCommand $hashTrackerCommand,
private readonly NoTranslateAttributeHandler $noTranslateAttributeHandler,
) {}
public function translate(int $projectId, array $contentIds, array $exceptLanguages): ServiceResultSuccess | ServiceResultError
{
try {
$project = $this->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();
}
}

View File

@@ -0,0 +1,94 @@
<?php declare(strict_types=1);
namespace App\Services\Translate\Project;
use App\Dto\Service\ProjectTranslationServiceTextHash\Codes;
use App\Dto\Service\ProjectTranslationServiceTextHash\TranslateCodes;
use App\Repositories\ProjectRepository;
use App\Repositories\ProjectTranslationServiceRepository;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\ProjectTranslation\TranslationText;
use App\Services\ProjectTranslation\TranslationTextCommand;
use App\Services\ProjectTranslationServiceTextHash\HashTrackerCommand;
use App\Services\Translate\Completed\Project\TranslationTextService as TranslationTextServiceCompleted;
use App\Services\Service;
use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\Facades\Translate;
final class TranslationTextService extends Service
{
public function __construct(
private readonly ProjectRepository $projectRepository,
private readonly TranslationTextCommand $translationTextCommand,
private readonly ProjectTranslationServiceRepository $projectTranslationServiceRepository,
private readonly HashTrackerCommand $hashTrackerCommand,
) { }
public function translate(int $projectId, int $languageId, array $translateTextCode, array $exceptLanguages): ServiceResultSuccess | ServiceResultError
{
try {
$project = $this->projectRepository->getProjectById($projectId);
if ($project === null) {
return $this->errNotFound(__('Project not found'));
}
$language = $project->languages()->firstWhere('id', $languageId);
if ($language === null) {
return $this->errNotFound(__('Language not found'));
}
$translateIntoLanguage = $this->projectTranslationServiceRepository
->getLanguagesBySourceLanguage($languageId, $exceptLanguages)
->all()->pluck('code', 'language_id')->toArray();
if (empty($translateIntoLanguage)) {
return $this->ok();
}
$translationText = $this->translationTextCommand->execute($project, $language, $translateTextCode);
$codes = $this->getCodes($translationText, $translateTextCode);
$translations = DB::transaction(function () use ($translateIntoLanguage, $codes) {
return $this->hashTrackerCommand->execute(\array_keys($translateIntoLanguage), $codes);
});
/** @var TranslateCodes $translations */
unset($codes);
$sourceLanguageCode = $this->projectTranslationServiceRepository->getLanguageCodeByLanguageId($language->id);
foreach ($translations->getCodes() as $currentLanguageId => $codes) {
$params = new \KorElf\TranslateLaravel\DTO\RunTranslateDto();
foreach ($codes as $code) {
$text = $translationText->translate($code);
$params->addParamHtml($code, $text, $translateIntoLanguage[$currentLanguageId], $sourceLanguageCode);
}
$afterTranslateDto = new AfterTranslateDto(TranslationTextServiceCompleted::class, [
'projectId' => $projectId,
'languageId' => $currentLanguageId,
'hashes' => $translations->getHashesByLanguage($currentLanguageId),
'exceptLanguages' => $exceptLanguages,
]);
Translate::runJob($params, $afterTranslateDto);
unset($params, $afterTranslateDto);
}
unset($translations, $translateIntoLanguage, $project, $language, $translationText, $sourceLanguageCode);
} catch (\Throwable $e) {
\report($e);
return $this->errService($e->getMessage());
}
return $this->ok();
}
private function getCodes(TranslationText $translationText, array $translateTextCode): Codes
{
$codes = new Codes();
foreach ($translateTextCode as $code) {
$codes->add($code, $translationText->translate($code));
}
return $codes;
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('project_translation_service_text_hashes', function (Blueprint $table) {
$table->id();
$table->string('code');
$table->unsignedBigInteger('language_id')->index();
$table->foreign('language_id')->references('id')->on('project_languages');
$table->unsignedInteger('status')->index()->default(0);
$table->char('hash', 64);
$table->timestamps();
$table->unique(['language_id', 'code'], 'unique_language_code');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_translation_service_text_hashes');
}
};

View File

@@ -310,6 +310,7 @@ return [
'content_images' => 'content images', 'content_images' => 'content images',
'source_language_id' => 'source language identifier', 'source_language_id' => 'source language identifier',
'translate_from_language' => 'translate from language', 'translate_from_language' => 'translate from language',
'translate-automatically' => 'translate automatically',
'translate-automatically.*' => 'translate automatically', 'translate-automatically.*' => 'translate automatically',
], ],
]; ];

View File

@@ -310,6 +310,7 @@ return [
'content_images' => 'изображения контента', 'content_images' => 'изображения контента',
'source_language_id' => 'идентификатор исходного языка', 'source_language_id' => 'идентификатор исходного языка',
'translate_from_language' => 'перевести с языка', 'translate_from_language' => 'перевести с языка',
'translate-automatically' => 'переводить автоматически',
'translate-automatically.*' => 'переводить автоматически', 'translate-automatically.*' => 'переводить автоматически',
], ],
]; ];

View File

@@ -1,4 +1,9 @@
@csrf @csrf
@if($serviceTranslationEnable)
<x-volt.forms.checkbox :title="__('Automatic translation')" name="translate-automatically"
:user-value="1" class="language-content" checkbox-value="1"
not-checked-value="0"/>
@endif
<x-volt.forms.input :title="__('validation.attributes.title')" name="title" type="text" :value="$content->title" required autofocus /> <x-volt.forms.input :title="__('validation.attributes.title')" name="title" type="text" :value="$content->title" required autofocus />
<x-volt.forms.textarea-wysiwyg <x-volt.forms.textarea-wysiwyg
:title="__('validation.attributes.description')" :title="__('validation.attributes.description')"

View File

@@ -8,6 +8,11 @@
<h3 id="category" class="mb-4">{{ __('admin-sections.Translations') }}</h3> <h3 id="category" class="mb-4">{{ __('admin-sections.Translations') }}</h3>
<div class="table-responsive"> <div class="table-responsive">
<form method="post" action="{{ route('admin.projects.translations.update', ['project' => $project->id, 'language' => $language->id]) }}"> <form method="post" action="{{ route('admin.projects.translations.update', ['project' => $project->id, 'language' => $language->id]) }}">
@if($serviceTranslationEnable)
<x-volt.forms.checkbox :title="__('Automatic translation')" name="translate-automatically"
:user-value="1" class="language-content" checkbox-value="1"
not-checked-value="0"/><br>
@endif
<table class="table table-centered table-nowrap mb-0 rounded"> <table class="table table-centered table-nowrap mb-0 rounded">
<thead class="thead-light"> <thead class="thead-light">
<tr> <tr>

View File

@@ -1,8 +1,8 @@
version: '3.7' version: '3.7'
services: services:
app: app:
# image: korelf/my-projects-website:0.3.1 # docker hub # image: korelf/my-projects-website:0.4.0 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.3.1 # MDHub image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.4.0 # MDHub
# restart: always # restart: always
depends_on: depends_on:
- db - db
@@ -20,8 +20,8 @@ services:
# - ./app/translate/authorized_key.json:/var/www/html/storage/translation_service/authorized_key.json # - ./app/translate/authorized_key.json:/var/www/html/storage/translation_service/authorized_key.json
queue: queue:
# image: korelf/my-projects-website:0.3.1 # docker hub # image: korelf/my-projects-website:0.4.0 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.3.1 # MDHub image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.4.0 # MDHub
# restart: always # restart: always
depends_on: depends_on:
- db - db
@@ -35,8 +35,8 @@ services:
# - ./app/translate/authorized_key.json:/var/www/html/storage/translation_service/authorized_key.json # - ./app/translate/authorized_key.json:/var/www/html/storage/translation_service/authorized_key.json
scheduler: scheduler:
# image: korelf/my-projects-website:0.3.1 # docker hub # image: korelf/my-projects-website:0.4.0 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.3.1 # MDHub image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.4.0 # MDHub
# restart: always # restart: always
depends_on: depends_on:
- db - db