Версия 0.4.0 #7
@ -11,6 +11,7 @@ final readonly class Content
|
|||||||
private string $title,
|
private string $title,
|
||||||
private string $content,
|
private string $content,
|
||||||
private Storages $storages,
|
private Storages $storages,
|
||||||
|
private bool $isTranslateAutomatically = false,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public function getLanguageId(): int
|
public function getLanguageId(): int
|
||||||
@ -32,4 +33,9 @@ final readonly class Content
|
|||||||
{
|
{
|
||||||
return $this->storages;
|
return $this->storages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isTranslateAutomatically(): bool
|
||||||
|
{
|
||||||
|
return $this->isTranslateAutomatically;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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] ?? [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums\ProjectTranslationServiceHashes;
|
||||||
|
|
||||||
|
enum Status: int
|
||||||
|
{
|
||||||
|
case Waiting = 0;
|
||||||
|
case Success = 10;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exceptions\Services\Translate;
|
||||||
|
|
||||||
|
final class CompletedException extends \Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -32,6 +32,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
|
|||||||
'content.*.title' => ['required', 'string', 'max:255'],
|
'content.*.title' => ['required', 'string', 'max:255'],
|
||||||
'content.*.content' => ['nullable', 'string'],
|
'content.*.content' => ['nullable', 'string'],
|
||||||
'content.*.content_images.*.file' => ['numeric', 'min:1'],
|
'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'],
|
title: $content['title'],
|
||||||
content: $content['content'] ?? '',
|
content: $content['content'] ?? '',
|
||||||
storages: $this->contentStorages($content),
|
storages: $this->contentStorages($content),
|
||||||
|
isTranslateAutomatically: (bool) $this->input('translate-automatically.' . $languageId, false),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return $contents;
|
return $contents;
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
app/application/app/Models/ProjectTranslationServiceHash.php
Normal file
51
app/application/app/Models/ProjectTranslationServiceHash.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?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)
|
||||||
|
->whereIn('language_id', $languages)
|
||||||
|
->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);
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,10 @@ namespace App\Services\Admin\Project;
|
|||||||
use App\Dto\Builder\Documentation as DocumentationBuilderDto;
|
use App\Dto\Builder\Documentation as DocumentationBuilderDto;
|
||||||
use App\Dto\QuerySettingsDto;
|
use App\Dto\QuerySettingsDto;
|
||||||
use App\Dto\Service\Admin\Project\Documentation\StoreUpdate;
|
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\Exceptions\Services\DocumentationContent\StorageCommandException;
|
||||||
|
use App\Jobs\Translate\ProcessProjectDocumentationContent;
|
||||||
use App\Models\Documentation;
|
use App\Models\Documentation;
|
||||||
use App\Models\ProjectLanguage;
|
use App\Models\ProjectLanguage;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@ -90,6 +93,7 @@ final class DocumentationService extends Service
|
|||||||
'project' => $project,
|
'project' => $project,
|
||||||
'documentation' => new Documentation(),
|
'documentation' => new Documentation(),
|
||||||
'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
|
'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
|
||||||
|
'serviceTranslationEnable' => config('translation_service.enable', false),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +124,7 @@ final class DocumentationService extends Service
|
|||||||
'project' => $project,
|
'project' => $project,
|
||||||
'documentation' => $documentation,
|
'documentation' => $documentation,
|
||||||
'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, null, $withCategories),
|
'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;
|
return $documentation;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (\config('translation_service.enable', false)) {
|
||||||
|
$this->translateContent($documentation, $data->getContents());
|
||||||
|
}
|
||||||
} catch (StorageCommandException $e) {
|
} catch (StorageCommandException $e) {
|
||||||
return $e->getResultError();
|
return $e->getResultError();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@ -192,6 +201,10 @@ final class DocumentationService extends Service
|
|||||||
|
|
||||||
return $documentation;
|
return $documentation;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (\config('translation_service.enable', false)) {
|
||||||
|
$this->translateContent($documentation, $data->getContents());
|
||||||
|
}
|
||||||
} catch (StorageCommandException $e) {
|
} catch (StorageCommandException $e) {
|
||||||
return $e->getResultError();
|
return $e->getResultError();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@ -240,4 +253,27 @@ final class DocumentationService extends Service
|
|||||||
'category_id' => $data->getCategoryId(),
|
'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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
@ -256,6 +256,7 @@
|
|||||||
"The link has been deleted": "The link has been deleted",
|
"The link has been deleted": "The link has been deleted",
|
||||||
"Language not found": "Language not found",
|
"Language not found": "Language not found",
|
||||||
"Project not found": "Project not found",
|
"Project not found": "Project not found",
|
||||||
|
"Documentation not found": "Documentation not found",
|
||||||
"Translations successfully updated": "Translations successfully updated",
|
"Translations successfully updated": "Translations successfully updated",
|
||||||
"allowed characters:": "allowed characters:",
|
"allowed characters:": "allowed characters:",
|
||||||
"Documentation version created successfully": "Documentation version created successfully",
|
"Documentation version created successfully": "Documentation version created successfully",
|
||||||
@ -268,5 +269,7 @@
|
|||||||
"Category successfully created": "Category successfully created",
|
"Category successfully created": "Category successfully created",
|
||||||
"Category updated successfully": "Category updated successfully",
|
"Category updated successfully": "Category updated successfully",
|
||||||
"Category successfully deleted": "Category successfully deleted",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -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": "Документация не найдена",
|
||||||
"Translations successfully updated": "Переводы успешно обновлены",
|
"Translations successfully updated": "Переводы успешно обновлены",
|
||||||
"allowed characters:": "разрешенные символы:",
|
"allowed characters:": "разрешенные символы:",
|
||||||
"Documentation version created successfully": "Версия документации успешно создана",
|
"Documentation version created successfully": "Версия документации успешно создана",
|
||||||
@ -268,5 +269,7 @@
|
|||||||
"Category successfully created": "Категория успешно создана",
|
"Category successfully created": "Категория успешно создана",
|
||||||
"Category updated successfully": "Категория успешно обновлена",
|
"Category updated successfully": "Категория успешно обновлена",
|
||||||
"Category successfully deleted": "Категория успешно удалена",
|
"Category successfully deleted": "Категория успешно удалена",
|
||||||
"Old Files deleted": "Старые файлы удалены"
|
"Old Files deleted": "Старые файлы удалены",
|
||||||
|
"Automatic translation": "Автоматический перевод",
|
||||||
|
"The settings were saved successfully": "Настройки успешно сохранены"
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,13 @@
|
|||||||
role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab">
|
role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab">
|
||||||
<x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id"
|
<x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id"
|
||||||
:user-value="($index === 0) ? 1 : 0" class="content-enable" checkbox-value="1"
|
: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')"
|
<x-volt.forms.input :title="__('validation.attributes.title')"
|
||||||
:name="'content[' . $language->id . '][title]'" type="text" class="language-content"
|
:name="'content[' . $language->id . '][title]'" type="text" class="language-content"
|
||||||
:disabled="$index !== 0" :value="$content?->title ?? ''" required/>
|
:disabled="$index !== 0" :value="$content?->title ?? ''" required/>
|
||||||
|
@ -40,6 +40,15 @@
|
|||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<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') }}">
|
<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">
|
<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"/>
|
<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"/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user