Merge pull request 'Версия 0.4.0' (#7) from develop into main

Reviewed-on: #7
This commit is contained in:
Leonid Nikitin 2025-02-15 00:07:47 +05:00
commit caa6ecc2bf
93 changed files with 4773 additions and 833 deletions

View File

@ -7,6 +7,7 @@
**/storage/framework/sessions/*
**/storage/framework/views/*
**/storage/framework/testing/*
**/storage/translation_service/*
**/storage/logs/*
**/vendor/
**/node_modules/

View File

@ -15,6 +15,19 @@ CAPTCHA_PUBLIC_TOKEN=
FEEDBACK_MAIL_NOTIFICATIONS=false
FEEDBACK_MAIL_TO=
TRANSLATION_SERVICE_ENABLE=false
# yandex or log
TRANSLATE_SERVICE=log
TRANSLATE_YANDEX_FOLDER_ID=
TRANSLATE_YANDEX_AUTHORIZED_KEY_PATH=/storage/translation_service/authorized_key.json
TRANSLATE_YANDEX_LIMIT_MAX_REQUEST=20
TRANSLATE_YANDEX_LIMIT_RATE_SECONDS=1
TRANSLATE_YANDEX_LIMIT_MAX_SYMBOLS=9000
TRANSLATE_LOG_LIMIT_MAX_REQUEST=20
TRANSLATE_LOG_LIMIT_RATE_SECONDS=1
TRANSLATE_LOG_LIMIT_MAX_SYMBOLS=9000
APP_FORCE_HTTPS=false
APP_DEFAULT_LOCALE=ru

View File

@ -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;
}
}

View File

@ -7,6 +7,7 @@ final readonly class Content
public function __construct(
private int $languageId,
private string $title,
private bool $isTranslateAutomatically = false,
) { }
public function getLanguageId(): int
@ -18,4 +19,9 @@ final readonly class Content
{
return $this->title;
}
public function isTranslateAutomatically(): bool
{
return $this->isTranslateAutomatically;
}
}

View File

@ -11,6 +11,7 @@ final readonly class Content
private string $title,
private string $content,
private Storages $storages,
private bool $isTranslateAutomatically = false,
) { }
public function getLanguageId(): int
@ -32,4 +33,9 @@ final readonly class Content
{
return $this->storages;
}
public function isTranslateAutomatically(): bool
{
return $this->isTranslateAutomatically;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project\ServiceTranslate;
use App\Dto\Service\Dto;
final readonly class Translation extends Dto
{
public function __construct(
private int $languageId,
private ?int $sourceLanguageId,
private ?string $code,
) { }
public function getLanguageId(): int
{
return $this->languageId;
}
public function getSourceLanguageId(): ?int
{
return $this->sourceLanguageId;
}
public function getCode(): ?string
{
return $this->code;
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project\ServiceTranslate;
final class Translations
{
private array $translations;
public function add(Translation $translation): void
{
$this->translations[] = $translation;
}
public function getTranslations(): array
{
return $this->translations;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project\ServiceTranslate;
use App\Dto\Service\Dto;
final readonly class Update extends Dto
{
public function __construct(
private Translations $translations,
) { }
public function getTranslations(): Translations
{
return $this->translations;
}
}

View File

@ -8,10 +8,16 @@ final readonly class Update extends Dto
{
public function __construct(
private Translations $translations,
private bool $isTranslateAutomatically = false,
) { }
public function getTranslations(): 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\ProjectTranslationServiceHash;
final class Fields
{
private array $fields = [];
public function add(string $name, string $value): self
{
$this->fields[$name] = $value;
return $this;
}
public function getFields(): array
{
return $this->fields;
}
public function getNames(): array
{
return \array_keys($this->fields);
}
}

View File

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

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\ProjectTranslationServiceHash;
final class Hashes
{
private array $hashes = [];
private array $ids = [];
public function add(int $hashId, string $fieldName, string $hash): void
{
$this->hashes[$hashId] = [
'field' => $fieldName,
'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\ProjectTranslationServiceHash;
final class TranslateFields
{
private array $fields = [];
private array $hashes = [];
public function add(int $languageId, string $fieldName, string $hash, int $hashId): self
{
if (!isset($this->fields[$languageId])) {
$this->fields[$languageId] = [];
}
$this->fields[$languageId][] = $fieldName;
if (!isset($this->hashes[$languageId])) {
$this->hashes[$languageId] = [];
}
$this->hashes[$languageId][$fieldName] = [
'hash' => $hash,
'hashId' => $hashId,
];
return $this;
}
public function getFields(): array
{
return $this->fields;
}
public function getHashesByLanguage(int $languageId): array
{
return $this->hashes[$languageId] ?? [];
}
}

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

@ -2,6 +2,7 @@
namespace App\Enums;
use App\Models\DocumentationCategoryContent;
use App\Models\DocumentationContent;
use App\Models\Project;
use App\Models\ProjectContent;
@ -11,13 +12,15 @@ enum Morph: int
case Project = 1;
case DocumentationContent = 2;
case ProjectContent = 3;
case DocumentationCategoryContent = 4;
public function getPathModel(): string
{
return match ($this) {
self::Project => Project::class,
self::DocumentationContent => DocumentationContent::class,
self::ProjectContent => ProjectContent::class,
self::Project => Project::class,
self::DocumentationContent => DocumentationContent::class,
self::ProjectContent => ProjectContent::class,
self::DocumentationCategoryContent => DocumentationCategoryContent::class,
};
}

View File

@ -21,6 +21,9 @@ enum Permission: string
self::AdminPanel => [
'view' => __('permissions.Administrative panel allowed'),
],
self::Project => array_merge($this->getBasePermissions(), [
'Setting up automatic translation' => __('permissions.Setting up automatic translation'),
]),
self::ProjectContent => [
'view' => __('permissions.Allowed to watch'),
'create' => __('permissions.Allowed to create'),

View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace App\Enums\ProjectTranslationServiceHashes;
enum Status: int
{
case Waiting = 0;
case Success = 10;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Services\Translate;
final class CompletedException extends \Exception
{
}

View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin\Projects;
use App\Http\Controllers\Admin\Controller;
use App\Http\Requests\Admin\Projects\ServiceTranslate\UpdateRequest;
use App\Services\Admin\Project\ServiceTranslateService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ServiceTranslateController extends Controller
{
public function __construct(
private readonly ServiceTranslateService $serviceTranslateService,
) { }
public function view(int $projectId, Request $request): View
{
$user = $request->user();
$result = $this->serviceTranslateService->view($projectId, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin.projects.service-translate.view', $result->getData());
}
public function update(int $projectId, UpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->serviceTranslateService->update($projectId, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.projects.service-translate.view', ['project' => $projectId])->withSuccess($result->getMessage());
}
}

View File

@ -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),
);
}

View File

@ -21,6 +21,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
'sort' => ['required', 'integer', 'min:-1000', 'max:1000'],
'parent_id' => ['nullable', 'integer', 'exists:documentation_categories,id'],
'content.*.title' => ['required', 'string', 'max:255'],
'translate-automatically.*' => ['nullable', 'boolean'],
];
}
@ -47,6 +48,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
$contents->addContent(new Content(
languageId: (int) $languageId,
title: $content['title'],
isTranslateAutomatically: (bool) $this->input('translate-automatically.' . $languageId, false),
));
}
return $contents;

View File

@ -32,6 +32,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
'content.*.title' => ['required', 'string', 'max:255'],
'content.*.content' => ['nullable', 'string'],
'content.*.content_images.*.file' => ['numeric', 'min:1'],
'translate-automatically.*' => ['nullable', 'boolean'],
];
}
@ -60,6 +61,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
title: $content['title'],
content: $content['content'] ?? '',
storages: $this->contentStorages($content),
isTranslateAutomatically: (bool) $this->input('translate-automatically.' . $languageId, false),
));
}
return $contents;

View File

@ -0,0 +1,56 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Projects\ServiceTranslate;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Project\ServiceTranslate\Translation;
use App\Dto\Service\Admin\Project\ServiceTranslate\Translations;
use App\Dto\Service\Admin\Project\ServiceTranslate\Update;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateRequest extends FormRequest implements FormRequestDto
{
public function attributes(): array
{
return [
'language.*.id' => __('validation.attributes.language_id'),
'language.*.code' => __('validation.attributes.code'),
'language.*.source_language_id' => __('validation.attributes.source_language_id'),
];
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'language.*.id' => ['required', 'numeric', 'min:1'],
'language.*.code' => ['nullable', 'string', 'min:2', 'max:50'],
'language.*.source_language_id' => ['nullable', 'numeric', 'min:1', 'different:language.*.id'],
];
}
public function getDto(): Update
{
$translations = new Translations();
foreach ($this->input('language', []) as $language) {
$sourceLanguageId = $language['source_language_id'] ?? null;
if ($sourceLanguageId) {
$sourceLanguageId = (int) $sourceLanguageId;
}
$translation = new Translation(
languageId: (int) $language['id'],
sourceLanguageId: $sourceLanguageId,
code: $language['code'] ?? null,
);
$translations->add($translation);
}
return new Update(
translations: $translations,
);
}
}

View File

@ -20,6 +20,7 @@ final class UpdateRequest extends FormRequest implements FormRequestDto
'translations' => ['nullable', 'array'],
'translations.*.code' => ['required', 'string', new In(Translations::getTranslationCodes())],
'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,58 @@
<?php declare(strict_types=1);
namespace App\Jobs\Translate;
use App\Services\Translate\Project\DocumentationCategoryContentService;
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 ProcessProjectDocumentationCategoryContent 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 $projectDocumentCategoryId,
private readonly array $contentIds = [],
private readonly array $exceptLanguages = [],
) { }
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'documentation-category-' . $this->projectDocumentCategoryId;
}
/**
* Execute the job.
* @throws Exception
*/
public function handle(DocumentationCategoryContentService $categoryContentService): void
{
$result = $categoryContentService->translate($this->projectDocumentCategoryId, $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,58 @@
<?php declare(strict_types=1);
namespace App\Jobs\Translate;
use App\Services\Translate\Project\DocumentationContentService;
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 ProcessProjectDocumentationContent 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 $projectDocumentId,
private readonly array $contentIds = [],
private readonly array $exceptLanguages = [],
) { }
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'documentation-' . $this->projectDocumentId;
}
/**
* Execute the job.
* @throws Exception
*/
public function handle(DocumentationContentService $documentationContentService): void
{
$result = $documentationContentService->translate($this->projectDocumentId, $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 = [
'title',
'description',
'language_id',
];
}

View File

@ -7,6 +7,7 @@ use App\Models\Scopes\SortScope;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
@ -70,4 +71,9 @@ final class ProjectLanguage extends Model
},
)->shouldCache();
}
public function serviceTranslate(): HasOne
{
return $this->hasOne(ProjectTranslationService::class, 'language_id', 'id');
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
final class ProjectTranslationService extends Model
{
use HasFactory;
protected $table = 'project_translation_service';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'code',
'language_id',
'source_language_id',
];
}

View File

@ -0,0 +1,51 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\Morph;
use App\Enums\ProjectTranslationServiceHashes\Status;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
final class ProjectTranslationServiceHash extends Model
{
use HasFactory;
protected $table = 'project_translation_service_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',
'field',
'status',
'hash',
'morph_type',
'morph_id',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => Status::class,
'morph_type' => Morph::class,
];
}
}

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

@ -32,6 +32,11 @@ final readonly class ProjectPolicy extends Policy
return $user->hasPermission('project.delete');
}
public function settingUpAutomaticTranslation(User $user, Project $project): bool
{
return $user->hasPermission('project.setting-up-automatic-translation');
}
public function upload(User $user): bool
{
if ($user->hasPermission('project.create') || $user->hasPermission('project.update')) {

View File

@ -22,4 +22,13 @@ final readonly class ProjectTranslationRepository
$query = ProjectTranslation::query()->withTrashed()->where('project_id', $projectId)->where('language_id', $languageId);
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

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Enums\Morph;
use App\Models\ProjectTranslationServiceHash;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Database\Eloquent\Builder;
final readonly class ProjectTranslationServiceHashRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
) { }
public function getHashes(Morph $morph, int $morphId, ?array $languages = null, ?array $fields = null): Search
{
$query = ProjectTranslationServiceHash::query()
->where('morph_type', $morph)
->where('morph_id', $morphId)
->when($languages, function (Builder $query) use ($languages) {
$query->whereIn('language_id', $languages);
})
->when($fields, function (Builder $query) use ($fields) {
$query->whereIn('field', $fields);
});
return $this->createSearchInstanceCommand->execute($query);
}
public function getHashesByIds(array $ids): Search
{
$query = ProjectTranslationServiceHash::query()->whereIn('id', $ids);
return $this->createSearchInstanceCommand->execute($query);
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Models\ProjectTranslationService;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use Illuminate\Database\Eloquent\Builder;
final readonly class ProjectTranslationServiceRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
) { }
public function getLanguagesBySourceLanguage(int $sourceLanguage, ?array $excludeLanguages = null): Search
{
$query = ProjectTranslationService::query()
->select('language_id', 'code')
->where('source_language_id', $sourceLanguage)
->when($excludeLanguages, function (Builder $query) use ($excludeLanguages) {
$query->whereNotIn('language_id', $excludeLanguages);
});
return $this->createSearchInstanceCommand->execute($query);
}
public function getLanguageCodeByLanguageId(int $languageId): ?string
{
return ProjectTranslationService::query()
->select('code')
->where('language_id', $languageId)
->first()?->code ?? null;
}
}

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\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);
}
}

View File

@ -5,8 +5,11 @@ namespace App\Services\Admin\Project;
use App\Dto\QuerySettingsDto;
use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto;
use App\Dto\Service\Admin\Project\DocumentationCategory\StoreUpdate;
use App\Dto\Service\Admin\Project\DocumentationCategoryContent\Content;
use App\Dto\Service\Admin\Project\DocumentationCategoryContent\Contents;
use App\Exceptions\Services\DocumentationCategory\ParentException;
use App\Exceptions\Services\ServiceException;
use App\Jobs\Translate\ProcessProjectDocumentationCategoryContent;
use App\Models\DocumentationCategory;
use App\Models\DocumentationCategoryContent;
use App\Models\ProjectLanguage;
@ -90,6 +93,7 @@ final class DocumentationCategoryService extends Service
'project' => $project,
'category' => new DocumentationCategory(),
'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
'serviceTranslationEnable' => config('translation_service.enable', false),
]);
}
@ -120,6 +124,7 @@ final class DocumentationCategoryService extends Service
'project' => $project,
'category' => $category,
'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, $category, $withCategories),
'serviceTranslationEnable' => config('translation_service.enable', false),
]);
}
@ -151,6 +156,10 @@ final class DocumentationCategoryService extends Service
return $category;
});
if (\config('translation_service.enable', false)) {
$this->translateContent($category, $data->getContents());
}
} catch (ServiceException $e) {
return $e->getServiceResultError();
} catch (\Throwable $e) {
@ -201,6 +210,10 @@ final class DocumentationCategoryService extends Service
return $category;
});
if (\config('translation_service.enable', false)) {
$this->translateContent($category, $data->getContents());
}
} catch (ServiceException $e) {
return $e->getServiceResultError();
} catch (ParentException $e) {
@ -254,4 +267,27 @@ final class DocumentationCategoryService extends Service
'parent_id' => $data->getParentId(),
];
}
private function translateContent(DocumentationCategory $category, Contents $contents): void
{
$translateExceptLanguages = [];
$translateLanguages = [];
foreach ($contents->getContents() as $content) {
/** @var Content $content */
$translateExceptLanguages[] = $content->getLanguageId();
if ($content->isTranslateAutomatically()) {
$translateLanguages[] = $content->getLanguageId();
}
}
if (empty($translateLanguages)) {
return;
}
$translateContentIds = $category->contents()->select('id')
->whereIn('language_id', $translateLanguages)
->get()->pluck('id')->toArray();
ProcessProjectDocumentationCategoryContent::dispatch($category->id, $translateContentIds, $translateExceptLanguages);
}
}

View File

@ -5,7 +5,10 @@ namespace App\Services\Admin\Project;
use App\Dto\Builder\Documentation as DocumentationBuilderDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Service\Admin\Project\Documentation\StoreUpdate;
use App\Dto\Service\Admin\Project\DocumentationContent\Content;
use App\Dto\Service\Admin\Project\DocumentationContent\Contents;
use App\Exceptions\Services\DocumentationContent\StorageCommandException;
use App\Jobs\Translate\ProcessProjectDocumentationContent;
use App\Models\Documentation;
use App\Models\ProjectLanguage;
use App\Models\User;
@ -90,6 +93,7 @@ final class DocumentationService extends Service
'project' => $project,
'documentation' => new Documentation(),
'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
'serviceTranslationEnable' => config('translation_service.enable', false),
]);
}
@ -120,6 +124,7 @@ final class DocumentationService extends Service
'project' => $project,
'documentation' => $documentation,
'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, null, $withCategories),
'serviceTranslationEnable' => config('translation_service.enable', false),
]);
}
@ -150,6 +155,10 @@ final class DocumentationService extends Service
return $documentation;
});
if (\config('translation_service.enable', false)) {
$this->translateContent($documentation, $data->getContents());
}
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) {
@ -192,6 +201,10 @@ final class DocumentationService extends Service
return $documentation;
});
if (\config('translation_service.enable', false)) {
$this->translateContent($documentation, $data->getContents());
}
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) {
@ -240,4 +253,27 @@ final class DocumentationService extends Service
'category_id' => $data->getCategoryId(),
];
}
private function translateContent(Documentation $documentation, Contents $contents): void
{
$translateExceptLanguages = [];
$translateLanguages = [];
foreach ($contents->getContents() as $content) {
/** @var Content $content */
$translateExceptLanguages[] = $content->getLanguageId();
if ($content->isTranslateAutomatically()) {
$translateLanguages[] = $content->getLanguageId();
}
}
if (empty($translateLanguages)) {
return;
}
$translateContentIds = $documentation->contents()->select('id')
->whereIn('language_id', $translateLanguages)
->get()->pluck('id')->toArray();
ProcessProjectDocumentationContent::dispatch($documentation->id, $translateContentIds, $translateExceptLanguages);
}
}

View File

@ -0,0 +1,67 @@
<?php declare(strict_types=1);
namespace App\Services\Admin\Project;
use App\Dto\Service\Admin\Project\ServiceTranslate\Update;
use App\Models\User;
use App\Repositories\ProjectRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\ProjectTranslationService\ModelSyncCommand as TranslationServiceModelSyncCommand;
use App\Services\Service;
use Illuminate\Support\Facades\DB;
final class ServiceTranslateService extends Service
{
public function __construct(
private readonly ProjectRepository $projectRepository,
private readonly TranslationServiceModelSyncCommand $translationServiceModelSyncCommand,
) { }
public function view(int $projectId, User $user): ServiceResultError | ServiceResultArray
{
$project = $this->projectRepository->getProjectById($projectId);
if (\is_null($project)) {
return $this->errNotFound(__('Not Found'));
}
if (
config('translation_service.enable', false) === false
|| $user->cannot('settingUpAutomaticTranslation', $project)
) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'project' => $project,
'languages' => $project->languages()->with(['serviceTranslate'])->get(),
]);
}
public function update(int $projectId, Update $data, User $user): ServiceResultError | ServiceResultSuccess
{
$project = $this->projectRepository->getProjectById($projectId);
if (\is_null($project)) {
return $this->errNotFound(__('Not Found'));
}
if (
config('translation_service.enable', false) === false
|| $user->cannot('settingUpAutomaticTranslation', $project)
) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($data, $project) {
$this->translationServiceModelSyncCommand->execute($project, $data->getTranslations());
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The settings were saved successfully'));
}
}

View File

@ -3,8 +3,10 @@
namespace App\Services\Admin\Project;
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\Enums\CacheTag;
use App\Jobs\Translate\ProcessTranslationText;
use App\Models\ProjectTranslation;
use App\Models\User;
use App\Repositories\ProjectRepository;
@ -60,6 +62,7 @@ final class TranslationService extends Service
'language' => $language,
'projectTranslations' => $this->projectTranslationRepository->getProjectTranslations($projectId, $languageId)->all()->pluck('text', 'code')->toArray(),
'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->clearCacheCommandHandler->byTag(CacheTag::ProjectTranslation);
if (\config('translation_service.enable', false)) {
$this->translateContent($projectId, $languageId, $data);
}
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
@ -88,4 +94,19 @@ final class TranslationService extends Service
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

@ -50,6 +50,7 @@ final class ProjectService extends Service
return $this->result([
'projects' => $projects,
'serviceTranslationEnable' => config('translation_service.enable', false),
]);
}

View File

@ -6,7 +6,7 @@ use App\Enums\CacheTag;
final readonly class ClearCacheCommandHandler
{
public function byTag(CacheTag $tag)
public function byTag(CacheTag $tag): void
{
$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

@ -0,0 +1,54 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslationService;
use App\Models\Project;
use App\Dto\Service\Admin\Project\ServiceTranslate\Translations;
use App\Models\ProjectTranslationService;
final readonly class ModelSyncCommand
{
public function execute(Project $project, Translations $translations): void
{
$insert = [];
$delete = [];
$languages = $project->languages()->with(['serviceTranslate'])->get();
foreach ($translations->getTranslations() as $translation) {
/* @var \App\Dto\Service\Admin\Project\ServiceTranslate\Translation $translation */
$language = $languages->firstWhere('id', '=', $translation->getLanguageId());
if ($language === null) {
continue;
}
$serviceTranslate = $language->serviceTranslate;
if ($serviceTranslate === null) {
if ($translation->getCode() !== null) {
$insert[] = [
'language_id' => $translation->getLanguageId(),
'code' => $translation->getCode(),
'source_language_id' => $translation->getSourceLanguageId(),
];
}
continue;
}
if ($translation->getCode() === null) {
$delete[] = [$serviceTranslate->id];
continue;
}
$serviceTranslate->update([
'code' => $translation->getCode(),
'source_language_id' => $translation->getSourceLanguageId(),
]);
}
if (!empty($insert)) {
ProjectTranslationService::query()->insert($insert);
}
if (!empty($delete)) {
ProjectTranslationService::query()->whereIn('id', $delete)->delete();
}
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslationService;
use Illuminate\Support\Str;
final readonly class NoTranslateAttributeHandler
{
public function handleAddAttribute(string $text): string
{
return Str::replace('<code', '<code translate="no"', $text);
}
public function handleRemoveAttribute(string $text): string
{
return Str::replace('<code translate="no"', '<code', $text);
}
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslationServiceHash;
use App\Dto\Service\ProjectTranslationServiceHash\Hashes;
use App\Dto\Service\ProjectTranslationServiceHash\HashStatusWaiting;
use App\Enums\ProjectTranslationServiceHashes\Status;
use App\Repositories\ProjectTranslationServiceHashRepository;
final readonly class CompletionChecker
{
public function __construct(
private ProjectTranslationServiceHashRepository $hashRepository,
private ProjectTranslationServiceHashCommandHandler $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->field === $dataHash['field']) {
$isWaiting = true;
$hashSuccessIds[] = $hash->id;
}
$hashStatusWaiting->add($hash->field, $isWaiting);
}
$this->commandHandler->handleSetStatusById($hashSuccessIds, Status::Success);
return $hashStatusWaiting;
}
}

View File

@ -0,0 +1,69 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectTranslationServiceHash;
use App\Dto\Service\ProjectTranslationServiceHash\Fields;
use App\Dto\Service\ProjectTranslationServiceHash\TranslateFields;
use App\Enums\Morph;
use App\Enums\ProjectTranslationServiceHashes\Status;
use App\Models\ProjectTranslationServiceHash;
use App\Repositories\ProjectTranslationServiceHashRepository;
final readonly class HashTrackerCommand
{
public function __construct(
private ProjectTranslationServiceHashRepository $hashRepository,
private ProjectTranslationServiceHashCommandHandler $commandHandler,
) { }
public function execute(Morph $morph, int $morphId, array $languages, Fields $fields): TranslateFields
{
$hashes = $this->hashRepository->getHashes($morph, $morphId, $languages, $fields->getNames())->all();
$translateFields = new TranslateFields();
foreach ($fields->getFields() as $fieldName => $fieldValue) {
$textHash = $this->generateHashFromText((string) $fieldValue);
foreach ($languages as $language) {
$modelHash = $hashes->where('language_id', $language)->firstWhere('field', $fieldName);
if ($modelHash === null) {
$modelHash = $this->commandHandler->handleStore([
'language_id' => $language,
'morph_type' => $morph,
'morph_id' => $morphId,
'field' => $fieldName,
'hash' => $textHash,
'status' => Status::Waiting,
]);
$translateFields->add($language, $fieldName, $textHash, $modelHash->id);
continue;
}
if ($modelHash->hash === $textHash && $this->isSuccess($modelHash)) {
continue;
}
$translateFields->add($language, $fieldName, $textHash, $modelHash->id);
$this->commandHandler->handleUpdate($modelHash, [
'hash' => $textHash,
'status' => Status::Waiting,
]);
}
}
return $translateFields;
}
private function generateHashFromText(string $text): string
{
return \hash('sha256', $text);
}
private function isSuccess(ProjectTranslationServiceHash $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\ProjectTranslationServiceHash;
use App\Enums\ProjectTranslationServiceHashes\Status;
use App\Models\ProjectTranslationServiceHash;
final readonly class ProjectTranslationServiceHashCommandHandler
{
public function handleStore(array $data): ProjectTranslationServiceHash
{
return ProjectTranslationServiceHash::create($data);
}
public function handleUpdate(ProjectTranslationServiceHash $hash, array $data): ProjectTranslationServiceHash
{
$hash->update($data);
$hash->touch();
return $hash;
}
public function handleSetStatusById(array $ids, Status $status): void
{
ProjectTranslationServiceHash::query()->whereIn('id', $ids)->update(['status' => $status->value]);
}
}

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

@ -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\ProcessProjectDocumentationCategoryContent;
use App\Models\DocumentationCategory;
use App\Models\DocumentationCategoryContent;
use App\Repositories\DocumentationCategoryRepository;
use App\Services\ProjectTranslationService\NoTranslateAttributeHandler;
use App\Services\ProjectTranslationServiceHash\CompletionChecker;
use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
final readonly class DocumentationCategoryContentService implements TranslationCompletedListener
{
public function __construct(
private DocumentationCategoryRepository $categoryRepository,
private NoTranslateAttributeHandler $noTranslateAttributeHandler,
private CompletionChecker $completionChecker,
) { }
/**
* @throws CompletedException
*/
public function onTranslationCompleted(array $translatedText, array $data = []): void
{
if (
!isset($data['categoryId'])
|| !isset($data['languageId'])
|| !isset($data['hashes'])
) {
throw new CompletedException('Required data is missing: categoryId, languageId, or hashes.');
}
$category = $this->categoryRepository->getCategoryById((int) $data['categoryId']);
if ($category === null) {
return;
}
$categoryContent = DB::transaction(function () use ($data, $category, $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;
}
$categoryContent = $category->contents()->where('language_id', $data['languageId'])->first();
if ($categoryContent !== null) {
$categoryContent->update($values);
return $categoryContent;
}
$values['language_id'] = $data['languageId'];
return $category->contents()->create($values);
});
if (\config('translation_service.enable', false) && $categoryContent !== null) {
$this->translateContent($category, $categoryContent, $data);
}
}
private function translateContent(DocumentationCategory $category, DocumentationCategoryContent $categoryContent, array $data): void
{
$translateExceptLanguages = $data['exceptLanguages'] ?? [];
$translateExceptLanguages[] = $data['languageId'];
ProcessProjectDocumentationCategoryContent::dispatch($category->id, [$categoryContent->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,90 @@
<?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\ProcessProjectDocumentationContent;
use App\Models\Documentation;
use App\Models\DocumentationContent;
use App\Repositories\DocumentationRepository;
use App\Services\ProjectTranslationService\NoTranslateAttributeHandler;
use App\Services\ProjectTranslationServiceHash\CompletionChecker;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener;
final readonly class DocumentationContentService implements TranslationCompletedListener
{
public function __construct(
private DocumentationRepository $documentationRepository,
private NoTranslateAttributeHandler $noTranslateAttributeHandler,
private CompletionChecker $completionChecker,
) { }
/**
* @throws CompletedException
*/
public function onTranslationCompleted(array $translatedText, array $data = []): void
{
if (
!isset($data['projectDocumentId'])
|| !isset($data['languageId'])
|| !isset($data['hashes'])
) {
throw new CompletedException('Required data is missing: projectDocumentId, languageId, or hashes.');
}
$documentation = $this->documentationRepository->getDocumentationById((int) $data['projectDocumentId']);
if ($documentation === null) {
return;
}
$documentationContent = DB::transaction(function () use ($data, $documentation, $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;
}
$documentationContent = $documentation->contents()->where('language_id', $data['languageId'])->first();
if ($documentationContent !== null) {
$documentationContent->update($values);
return $documentationContent;
}
$values['language_id'] = $data['languageId'];
return $documentation->contents()->create($values);
});
if (\config('translation_service.enable', false) && $documentationContent !== null) {
$this->translateContent($documentation, $documentationContent, $data);
}
}
private function translateContent(Documentation $documentation, DocumentationContent $documentationContent, array $data): void
{
$translateExceptLanguages = $data['exceptLanguages'] ?? [];
$translateExceptLanguages[] = $data['languageId'];
ProcessProjectDocumentationContent::dispatch($documentation->id, [$documentationContent->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,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,94 @@
<?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\DocumentationCategoryRepository;
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\Translate\Completed\Project\DocumentationCategoryContentService as DocumentationCategoryContentServiceCompleted;
use App\Services\Service;
use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\Facades\Translate;
final class DocumentationCategoryContentService extends Service
{
public function __construct(
private readonly DocumentationCategoryRepository $documentationCategoryRepository,
private readonly ProjectTranslationServiceRepository $projectTranslationServiceRepository,
private readonly HashTrackerCommand $hashTrackerCommand,
private readonly NoTranslateAttributeHandler $noTranslateAttributeHandler,
) {}
public function translate(int $categoryId, array $contentIds, array $exceptLanguages): ServiceResultSuccess | ServiceResultError
{
try {
$category = $this->documentationCategoryRepository->getCategoryById($categoryId);
if ($category === null) {
return $this->errNotFound(__('Category not found'));
}
$contents = $category->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 ?? '');
$translations = DB::transaction(function () use ($content, $translateIntoLanguage, $fields) {
return $this->hashTrackerCommand->execute(Morph::DocumentationCategoryContent, $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(DocumentationCategoryContentServiceCompleted::class, [
'categoryId' => $categoryId,
'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,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\DocumentationRepository;
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\DocumentationContentService as DocumentationContentServiceCompleted;
use Illuminate\Support\Facades\DB;
use KorElf\TranslateLaravel\DTO\AfterTranslateDto;
use KorElf\TranslateLaravel\Facades\Translate;
final class DocumentationContentService extends Service
{
public function __construct(
private readonly DocumentationRepository $documentationRepository,
private readonly ProjectTranslationServiceRepository $projectTranslationServiceRepository,
private readonly HashTrackerCommand $hashTrackerCommand,
private readonly NoTranslateAttributeHandler $noTranslateAttributeHandler,
) {}
public function translate(int $projectDocumentId, array $contentIds, array $exceptLanguages): ServiceResultSuccess | ServiceResultError
{
try {
$documentation = $this->documentationRepository->getDocumentationById($projectDocumentId);
if ($documentation === null) {
return $this->errNotFound(__('Documentation not found'));
}
$contents = $documentation->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('content', $content->content ?? '');
$translations = DB::transaction(function () use ($content, $translateIntoLanguage, $fields) {
return $this->hashTrackerCommand->execute(Morph::DocumentationContent, $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(DocumentationContentServiceCompleted::class, [
'projectDocumentId' => $projectDocumentId,
'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,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

@ -8,6 +8,7 @@
"php": "^8.3",
"intervention/image-laravel": "^1.2",
"kor-elf/captcha-rule-for-laravel": "^1.0",
"kor-elf/translate-laravel": "1.3.0",
"laravel/framework": "^11.0",
"laravel/tinker": "^2.9",
"staudenmeir/laravel-adjacency-list": "^1.0"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Translate service
|--------------------------------------------------------------------------
|
| Enables or disables translate service.
*/
'enable' => (bool) env('TRANSLATION_SERVICE_ENABLE', false),
];

View File

@ -0,0 +1,32 @@
<?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', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('language_id')->unique();
$table->foreign('language_id')->references('id')->on('project_languages');
$table->unsignedBigInteger('source_language_id')->nullable()->index();
$table->foreign('source_language_id')->references('id')->on('project_languages');
$table->string('code', 50);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_translation_service');
}
};

View File

@ -0,0 +1,36 @@
<?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_hashes', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('language_id')->index();
$table->foreign('language_id')->references('id')->on('project_languages');
$table->unsignedInteger('morph_type');
$table->unsignedBigInteger('morph_id');
$table->string('field', 255);
$table->unsignedInteger('status')->index()->default(0);
$table->char('hash', 64);
$table->timestamps();
$table->index(['morph_type', 'morph_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_translation_service_hashes');
}
};

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

@ -256,6 +256,7 @@
"The link has been deleted": "The link has been deleted",
"Language not found": "Language not found",
"Project not found": "Project not found",
"Documentation not found": "Documentation not found",
"Translations successfully updated": "Translations successfully updated",
"allowed characters:": "allowed characters:",
"Documentation version created successfully": "Documentation version created successfully",
@ -268,5 +269,8 @@
"Category successfully created": "Category successfully created",
"Category updated successfully": "Category updated successfully",
"Category successfully deleted": "Category successfully deleted",
"Old Files deleted": "Old Files deleted"
"Old Files deleted": "Old Files deleted",
"Automatic translation": "Automatic translation",
"The settings were saved successfully": "The settings were saved successfully",
"Category not found": "Category not found"
}

View File

@ -16,4 +16,5 @@ return [
'Documentation version' => 'Documentation version',
'Documentation' => 'Documentation',
'Categories' => 'Categories',
'Setting up automatic translation' => 'Setting up automatic translation',
];

View File

@ -9,6 +9,8 @@ return [
'Allowed to edit' => 'Allowed to edit',
'Allowed to delete' => 'Allowed to delete',
'Setting up automatic translation' => 'Setting up automatic translation',
'Administrative panel allowed' => 'Administrative panel allowed',
'AdminPanel' => 'Administrative panel allowed',

View File

@ -308,5 +308,9 @@ return [
'content.*.content' => 'content',
'category_id' => 'category',
'content_images' => 'content images',
'source_language_id' => 'source language identifier',
'translate_from_language' => 'translate from language',
'translate-automatically' => 'translate automatically',
'translate-automatically.*' => 'translate automatically',
],
];

View File

@ -256,6 +256,7 @@
"The link has been deleted": "Ссылка удалена",
"Language not found": "Язык не найден",
"Project not found": "Проект не найден",
"Documentation not found": "Документация не найдена",
"Translations successfully updated": "Переводы успешно обновлены",
"allowed characters:": "разрешенные символы:",
"Documentation version created successfully": "Версия документации успешно создана",
@ -268,5 +269,8 @@
"Category successfully created": "Категория успешно создана",
"Category updated successfully": "Категория успешно обновлена",
"Category successfully deleted": "Категория успешно удалена",
"Old Files deleted": "Старые файлы удалены"
"Old Files deleted": "Старые файлы удалены",
"Automatic translation": "Автоматический перевод",
"The settings were saved successfully": "Настройки успешно сохранены",
"Category not found": "Категория не найдена"
}

View File

@ -16,4 +16,5 @@ return [
'Documentation version' => 'Версия документации',
'Documentation' => 'Документация',
'Categories' => 'Категории',
'Setting up automatic translation' => 'Настройка автоматического перевода',
];

View File

@ -9,6 +9,8 @@ return [
'Allowed to edit' => 'Разрешено редактировать',
'Allowed to delete' => 'Разрешено удалять',
'Setting up automatic translation' => 'Настройка автоматического перевода',
'Administrative panel allowed' => 'Административная панель разрешена',
'AdminPanel' => 'Административная панель разрешена',

View File

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

View File

@ -1,4 +1,9 @@
@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.textarea-wysiwyg
:title="__('validation.attributes.description')"

View File

@ -20,6 +20,11 @@
@endphp
<div class="tab-pane fade @if($index === 0) show active @endif" id="language-{{ $language->id }}" role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab">
<x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id" :user-value="($index === 0) ? 1 : 0" class="content-enable" checkbox-value="1" notCheckedValue="0"/>
@if($serviceTranslationEnable)
<x-volt.forms.checkbox :title="__('Automatic translation')" :name="'translate-automatically[' . $language->id . ']'"
:user-value="1" class="language-content" checkbox-value="1"
not-checked-value="0"/>
@endif
<x-volt.forms.input :title="__('validation.attributes.title')" :name="'content[' . $language->id . '][title]'" type="text" class="language-content" :disabled="$index !== 0" :value="$content?->title ?? ''" required />
</div>
@endforeach

View File

@ -29,7 +29,13 @@
role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab">
<x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id"
:user-value="($index === 0) ? 1 : 0" class="content-enable" checkbox-value="1"
notCheckedValue="0"/>
not-checked-value="0"/>
@if($serviceTranslationEnable)
<x-volt.forms.checkbox :title="__('Automatic translation')" :name="'translate-automatically[' . $language->id . ']'"
:user-value="1" class="language-content" checkbox-value="1"
not-checked-value="0"/>
@endif
<x-volt.forms.input :title="__('validation.attributes.title')"
:name="'content[' . $language->id . '][title]'" type="text" class="language-content"
:disabled="$index !== 0" :value="$content?->title ?? ''" required/>

View File

@ -40,6 +40,15 @@
@endif
</td>
<td>
@if($serviceTranslationEnable)
@can('settingUpAutomaticTranslation', $project)
<a href="{{ route('admin.projects.service-translate.view', $project) }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Automatic translation') }}">
<svg class="align-text-top" width="16" height="16" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"></path>
</svg>
</a>
@endcan
@endif
<a href="{{ route('admin.projects.edit', $project) }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Edit') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>

View File

@ -0,0 +1,46 @@
@section('meta_title', __('admin-sections.Setting up automatic translation') . '. ' . __('admin-sections.Project') . ': ' . $project->name)
@section('h1', __('admin-sections.Setting up automatic translation') . '. ' . __('admin-sections.Project') . ': ' . $project->name)
<x-admin.layout>
<div class="row">
<div class="col-12 mb-4">
<div class="card border-0 shadow components-section">
<div class="card-body">
<form method="post" action="{{ route('admin.projects.service-translate.update', ['project' => $project->id]) }}">
@csrf
<div class="table-responsive">
<table class="table table-centered table-nowrap mb-0 rounded">
<thead class="thead-light">
<tr>
<th class="border-0">{{ __('validation.attributes.lang') }}</th>
<th class="border-0">{{ __('validation.attributes.code') }}</th>
<th class="border-0">{{ __('validation.attributes.translate_from_language') }}</th>
<th class="border-0 rounded-end" style="width: 150px"></th>
</tr>
</thead>
<tbody>
@foreach($languages as $index => $language)
<tr>
<td>{{ $language->title }}</td>
<td>
<x-volt.forms.input title="" name="language[{{ $index }}][code]" type="text" :value="$language->serviceTranslate?->code" />
</td>
<td>
<x-volt.forms.select title="" name="language[{{ $index }}][source_language_id]" :value="(string) $language->serviceTranslate?->source_language_id" :list="$languages->pluck('title', 'id')->toArray()">
<option value=""></option>
</x-volt.forms.select>
</td>
<td>
<input type="hidden" value="{{ $language->id }}" name="language[{{ $index }}][id]" />
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
</form>
</div>
</div>
</div>
</div>
</x-admin.layout>

View File

@ -8,6 +8,11 @@
<h3 id="category" class="mb-4">{{ __('admin-sections.Translations') }}</h3>
<div class="table-responsive">
<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">
<thead class="thead-light">
<tr>

View File

@ -1,5 +1,7 @@
<div class="mb-4">
<label for="form-select-{{ $requestName }}">{{ $title }}</label>
@if(!empty($title))
<label for="form-select-{{ $requestName }}">{{ $title }}</label>
@endif
<select id="form-select-{{ $requestName }}" aria-label="{{ $title }}" class="form-select @error($requestName) is-invalid @enderror" name="{{ $name }}" {{ $attributes }}>
{{ $slot }}
@foreach($list as $elementKey => $elementValue)

View File

@ -25,6 +25,9 @@ Route::middleware(['auth', 'verified', \App\Http\Middleware\UserLocale::class])-
Route::get('about/{language}', [\App\Http\Controllers\Admin\Projects\AboutController::class, 'edit'])->name('about.edit')->where(['language' => '[0-9]+']);
Route::put('about/{language}', [\App\Http\Controllers\Admin\Projects\AboutController::class, 'update'])->name('about.update')->where(['language' => '[0-9]+']);
Route::get('service-translate', [\App\Http\Controllers\Admin\Projects\ServiceTranslateController::class, 'view'])->name('service-translate.view');
Route::post('service-translate', [\App\Http\Controllers\Admin\Projects\ServiceTranslateController::class, 'update'])->name('service-translate.update');
Route::resource('links', \App\Http\Controllers\Admin\Projects\LinksController::class)->except(['show'])->where(['link' => '[0-9]+']);
Route::get('translations', [\App\Http\Controllers\Admin\Projects\TranslationsController::class, 'languages'])->name('translations.languages');

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1,4 +1,4 @@
FROM docker.io/php:8.3-zts-alpine3.18 AS UNIT_BUILDER
FROM docker.io/php:8.3-zts-alpine3.18 AS unit_builder
ARG UNIT_VERSION=1.31.1
@ -12,10 +12,10 @@ RUN apk --no-cache add pcre2-dev gcc git musl-dev make && \
make && \
make install
FROM docker.io/php:8.3-zts-alpine3.18 as BUILD
FROM docker.io/php:8.3-zts-alpine3.18 AS build
COPY --from=UNIT_BUILDER /var/sbin/unitd /usr/sbin/unitd
COPY --from=UNIT_BUILDER /var/lib/unit/ /var/lib/unit/
COPY --from=unit_builder /var/sbin/unitd /usr/sbin/unitd
COPY --from=unit_builder /var/lib/unit/ /var/lib/unit/
COPY docker/unit-config.json /docker-entrypoint.d/config.json
@ -48,7 +48,7 @@ RUN apk --no-cache add pcre2 libbz2 libpng libwebp libjpeg-turbo icu-libs freety
&& ln -sf /dev/stdout /var/log/unit.log \
&& addgroup -S unit && adduser -S unit -G unit
FROM BUILD as APP_BUILD_FOR_PRODUCTION
FROM build AS app_build_for_production
WORKDIR /home/app
COPY application /home/app
@ -60,9 +60,9 @@ RUN apk --no-cache add git nodejs npm \
&& rm -rf /home/app/node_modules /home/app/.env
FROM BUILD AS PRODUCTION
FROM build AS production
COPY --from=APP_BUILD_FOR_PRODUCTION /home/app /var/www/html
COPY --from=app_build_for_production /home/app /var/www/html
COPY docker/docker-entrypoint_prod.sh /home/unit/docker-entrypoint.sh
COPY docker/start.sh /usr/local/bin/start
@ -78,7 +78,7 @@ EXPOSE 9000
CMD ["/usr/local/bin/start"]
FROM BUILD AS DEVELOP
FROM build AS develop
WORKDIR /var/www/html
@ -99,22 +99,23 @@ CMD ["/usr/local/bin/start"]
FROM BUILD AS ARTISAN
FROM build AS artisan
WORKDIR /var/www/html
STOPSIGNAL SIGTERM
ENTRYPOINT ["php", "/var/www/html/artisan"]
FROM BUILD AS COMPOSER
FROM build AS composer
WORKDIR /var/www/html
STOPSIGNAL SIGTERM
RUN apk --no-cache add git \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN mkdir "/.composer" && chmod -R 0777 "/.composer"
ENTRYPOINT ["composer"]
FROM BUILD AS NPM
FROM build AS npm
RUN mkdir "/.npm" && chmod -R 0777 "/.npm"
WORKDIR /var/www/html
STOPSIGNAL SIGTERM

View File

@ -48,7 +48,6 @@ services:
app-redis:
image: redis:3.0-alpine # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/redis:3.0-alpine # MDHub
# restart: always
volumes:
- ./redis/data:/data
@ -103,13 +102,11 @@ services:
env_file: captcha-app/.env
captcha-redis:
image: redis:3.0-alpine # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/redis:3.0-alpine # MDHub
# restart: always
volumes:
- ./captcha-app/redis/data:/data
db:
image: docker.io/mysql:8.0.33 # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/mysql:8.0.33 # MDHub
command: --default-authentication-plugin=mysql_native_password
#restart: always
ports:

View File

@ -1,8 +1,8 @@
version: '3.7'
services:
app:
# image: korelf/my-projects-website:0.3.1 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.3.1 # MDHub
# image: korelf/my-projects-website:0.4.0 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.4.0 # MDHub
# restart: always
depends_on:
- db
@ -17,10 +17,11 @@ services:
volumes:
- ./app/storage/app:/var/www/html/storage/app
- ./app/storage/logs:/var/www/html/storage/logs
# - ./app/translate/authorized_key.json:/var/www/html/storage/translation_service/authorized_key.json
queue:
# image: korelf/my-projects-website:0.3.1 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.3.1 # MDHub
# image: korelf/my-projects-website:0.4.0 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.4.0 # MDHub
# restart: always
depends_on:
- db
@ -31,10 +32,11 @@ services:
volumes:
- ./app/storage/app:/var/www/html/storage/app
- ./app/storage/logs:/var/www/html/storage/logs
# - ./app/translate/authorized_key.json:/var/www/html/storage/translation_service/authorized_key.json
scheduler:
# image: korelf/my-projects-website:0.3.1 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.3.1 # MDHub
# image: korelf/my-projects-website:0.4.0 # docker hub
image: docker.mdhub.kor-elf.net/kor-elf/my-projects-website:0.4.0 # MDHub
# restart: always
depends_on:
- db
@ -45,10 +47,10 @@ services:
volumes:
- ./app/storage/app:/var/www/html/storage/app
- ./app/storage/logs:/var/www/html/storage/logs
# - ./app/translate/authorized_key.json:/var/www/html/storage/translation_service/authorized_key.json
app-redis:
image: redis:3.0-alpine # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/redis:3.0-alpine # MDHub
# restart: always
volumes:
- ./redis/data:/data
@ -103,13 +105,11 @@ services:
env_file: captcha-app/.env
captcha-redis:
image: redis:3.0-alpine # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/redis:3.0-alpine # MDHub
# restart: always
volumes:
- ./captcha-app/redis/data:/data
db:
image: docker.io/mysql:8.0.33 # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/mysql:8.0.33 # MDHub
command: --default-authentication-plugin=mysql_native_password
#restart: always
ports:

View File

@ -42,7 +42,6 @@ services:
app-redis:
image: redis:3.0-alpine # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/redis:3.0-alpine # MDHub
volumes:
- ./redis/data:/data
@ -104,12 +103,10 @@ services:
- ./captcha-app/app/storage/logs:/var/www/html/storage/logs
captcha-redis:
image: redis:3.0-alpine # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/redis:3.0-alpine # MDHub
volumes:
- ./captcha-app/redis/data:/data
db:
image: docker.io/mysql:8.0.33 # docker hub
# image: docker.mdhub.kor-elf.net/kor-elf/mysql:8.0.33 # MDHub
command: --default-authentication-plugin=mysql_native_password
#restart: always
ports: