35 Commits

Author SHA1 Message Date
c112b225dc Update byTag method to specify void return type
Added an explicit void return type to the byTag method for clarity and strict type enforcement.
2025-02-14 23:33:39 +05:00
e3194cef48 Update MDHub image version to 0.4.0 in production setup 2025-02-14 21:52:17 +05:00
41a7343338 Added the ability to automatically translate simple text. 2025-02-13 22:43:39 +05:00
90aa909b7f Fix incorrect property access in CompletionChecker
Corrected the property access from array syntax to object syntax for `$hash->field` in the `CompletionChecker.php` file. This ensures proper handling of fields and prevents potential runtime errors.
2025-02-13 22:42:40 +05:00
8b35a5691f Refactor language filter to use conditional query builder
Removed redundant `whereIn` clause to simplify the query logic.
2025-02-12 21:38:52 +05:00
844264fb62 Added automatic translation about the project. 2025-02-09 21:39:18 +05:00
d8d17064d0 Mark DocumentationCategoryContentService as readonly. 2025-02-09 20:55:40 +05:00
4dc6060fb3 Added support for automatic translation of documentation categories. 2025-02-09 20:00:34 +05:00
58a256e1e4 Added the ability to automatically translate documentation content. 2025-02-09 18:58:51 +05:00
25faa3d62b Added settings for translation into other languages ​​in the project. 2025-02-09 18:53:08 +05:00
c4817a675a Standardize Dockerfile stage naming convention
Updated all Dockerfile stage names to adhere to a consistent lowercase naming convention with underscores. This improves readability and aligns with best practices for naming Docker build stages.
2025-02-09 18:26:05 +05:00
cb2161356e Remove commented-out alternate image references in configs
Cleaned up unused image lines referencing MDHub in multiple docker-compose files.
2025-02-09 18:25:09 +05:00
b729d057a9 Add writable .composer directory for Composer cache.
This commit ensures a writable .composer directory is created with appropriate permissions. This change resolves potential permission issues when using Composer inside the container.
2024-12-10 22:22:50 +05:00
d36507b180 Changed the version of my-projects-website:0.3.0 to my-projects-website:0.3.1. 2024-08-04 13:49:56 +05:00
8159145e07 Added prism 'Bash', 'Sh', 'Shell', 'TOML', 'Json'. 2024-08-04 12:38:38 +05:00
4e414a952e Fixed. Docker Production CMD ["/usr/local/bin/start"]. 2024-07-27 02:57:17 +05:00
3902f5d36f my-projects-website:0.3.0. 2024-07-27 01:40:45 +05:00
46da639055 Added the ability to send notifications by email when a new review is added. 2024-07-27 00:29:28 +05:00
c84ed9f12b Configured the removal of old files that were not attached to the model or were marked as deleted. 2024-07-26 22:18:01 +05:00
dde792b97a Made it possible to disable captcha. 2024-07-26 22:16:34 +05:00
618c925dfa Fixed an issue with displaying the translation of site.Menu if the project language did not match the system language. 2024-07-26 22:15:33 +05:00
8bc4c0d8d0 Made it possible to disable captcha. 2024-07-26 22:13:14 +05:00
a5e3c5ed25 Fixed category display error from documentation. Previously, I showed all categories of documentation. 2024-07-26 22:11:54 +05:00
22bb840705 Added prism-yaml. 2024-07-26 22:06:38 +05:00
707762d29b Site. Fixed an error in javascript when the page does not have a tag with the .documentation-version__button class. 2024-07-26 22:06:21 +05:00
b33362a235 Added the ability to upload pictures in the editor. 2024-07-26 22:04:39 +05:00
4d36821ecc Fixed UNIT_SOURCE. 2024-06-12 00:43:36 +05:00
da5201dd9a Updated to korelf/my-projects-website:0.2.1. 2024-06-12 00:01:01 +05:00
2910509218 Added environment UNIT_SOURCE and redis volumes. 2024-06-11 23:44:51 +05:00
74f7b88bce Improved docker-entrypoint_*.sh scripts. 2024-06-11 23:43:37 +05:00
6998424b78 Added ignore .env.testing. 2024-06-11 23:42:54 +05:00
05f566c115 Added RoleSyncPermissionsCommandHandlerException. 2024-06-11 23:40:45 +05:00
c5fc106761 Added autocomplete="off" attribute to input password. 2024-06-11 22:39:39 +05:00
d7dc80cca4 Deleted sweetalert2. 2024-06-11 22:35:04 +05:00
6d3891979c Removed extra code. 2024-06-11 22:34:44 +05:00
147 changed files with 7287 additions and 1213 deletions

View File

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

View File

@@ -1,17 +1,34 @@
APP_NAME=Laravel APP_NAME="My Projects Website"
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://localhost APP_URL=http://localhost
APP_CAPTCHA=false
CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081 CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081
CAPTCHA_PRIVATE_TOKEN= CAPTCHA_PRIVATE_TOKEN=
CAPTCHA_STATIC_PATH=http://your-domain-captcha-or-IP:8081/captcha CAPTCHA_STATIC_PATH=http://your-domain-captcha-or-IP:8081/captcha
CAPTCHA_PUBLIC_TOKEN= CAPTCHA_PUBLIC_TOKEN=
# Don't forget to configure MAIL_MAILER to send mail.
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_FORCE_HTTPS=false
#UNIT_SOURCE="\"172.16.0.0/12\""
APP_DEFAULT_LOCALE=ru APP_DEFAULT_LOCALE=ru
APP_FAKER_LOCALE=ru_RU APP_FAKER_LOCALE=ru_RU
@@ -59,7 +76,7 @@ REDIS_HOST=app-redis
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=log MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1 MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null

View File

@@ -8,6 +8,7 @@
.env .env
.env.backup .env.backup
.env.production .env.production
.env.testing
.phpunit.result.cache .phpunit.result.cache
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml

View File

@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\Console\Commands\Files;
use App\Services\Commands\DeleteOldFilesService;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
final class DeleteOldFilesFromStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'files:delete-old-files-from-storage';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove temporary files or files that have been deleted from the storage table';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle(DeleteOldFilesService $deleteOldFilesService): void
{
$temporaryBeforeDate = Carbon::now()->subDays(3);
$deletedBeforeDate = Carbon::now()->subDays(7);
$result = $deleteOldFilesService->fromStorage($temporaryBeforeDate, $deletedBeforeDate);
if ($result->isError()) {
$this->error($result->getMessage());
return;
}
$this->info($result->getMessage());
}
}

View File

@@ -3,12 +3,15 @@
namespace App\Dto\Service\Admin\Project\About; namespace App\Dto\Service\Admin\Project\About;
use App\Dto\Service\Dto; use App\Dto\Service\Dto;
use App\Dto\Service\Storage\Storages;
final readonly class StoreUpdate extends Dto final readonly class StoreUpdate extends Dto
{ {
public function __construct( public function __construct(
private string $title, private string $title,
private string $description, private string $description,
private Storages $storages,
private bool $isTranslateAutomatically = false,
) { } ) { }
public function getTitle(): string public function getTitle(): string
@@ -20,4 +23,14 @@ final readonly class StoreUpdate extends Dto
{ {
return $this->description; return $this->description;
} }
public function getStorages(): Storages
{
return $this->storages;
}
public function isTranslateAutomatically(): bool
{
return $this->isTranslateAutomatically;
}
} }

View File

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

View File

@@ -2,12 +2,16 @@
namespace App\Dto\Service\Admin\Project\DocumentationContent; namespace App\Dto\Service\Admin\Project\DocumentationContent;
use App\Dto\Service\Storage\Storages;
final readonly class Content final readonly class Content
{ {
public function __construct( public function __construct(
private int $languageId, private int $languageId,
private string $title, private string $title,
private string $content, private string $content,
private Storages $storages,
private bool $isTranslateAutomatically = false,
) { } ) { }
public function getLanguageId(): int public function getLanguageId(): int
@@ -24,4 +28,14 @@ final readonly class Content
{ {
return $this->content; return $this->content;
} }
public function getStorages(): Storages
{
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( public function __construct(
private Translations $translations, private Translations $translations,
private bool $isTranslateAutomatically = false,
) { } ) { }
public function getTranslations(): Translations public function getTranslations(): Translations
{ {
return $this->translations; return $this->translations;
} }
public function isTranslateAutomatically(): bool
{
return $this->isTranslateAutomatically;
}
} }

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\DocumentationContent;
use App\Dto\Service\Storage\Storages;
use App\Models\DocumentationContent;
final class StorageDto
{
/**
* @var array [][DocumentationContent documentationContent, Storages storages]
*/
private array $storages = [];
public function add(DocumentationContent $documentationContent, Storages $storages): void
{
$this->storages[] = [
'documentationContent' => $documentationContent,
'storages' => $storages
];
}
/**
* @return array [][DocumentationContent documentationContent, Storages storages]
*/
public function getStorages(): array
{
return $this->storages;
}
}

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

@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\View\Volt\Form;
use App\Enums\Morph;
use App\Helpers\Helpers;
final readonly class WysiwygStorageUpload
{
public function __construct(
private string $inputName,
private Morph $morph,
) { }
public function getInputName(): string
{
return $this->inputName;
}
public function getRequestInputName(): string
{
return Helpers::formatAttributeNameToRequestName($this->getInputName());
}
public function getMorph(): Morph
{
return $this->morph;
}
}

View File

@@ -2,16 +2,25 @@
namespace App\Enums; namespace App\Enums;
use App\Models\DocumentationCategoryContent;
use App\Models\DocumentationContent;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectContent;
enum Morph: int enum Morph: int
{ {
case Project = 1; case Project = 1;
case DocumentationContent = 2;
case ProjectContent = 3;
case DocumentationCategoryContent = 4;
public function getPathModel(): string public function getPathModel(): string
{ {
return match ($this) { return match ($this) {
self::Project => Project::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 => [ self::AdminPanel => [
'view' => __('permissions.Administrative panel allowed'), 'view' => __('permissions.Administrative panel allowed'),
], ],
self::Project => array_merge($this->getBasePermissions(), [
'Setting up automatic translation' => __('permissions.Setting up automatic translation'),
]),
self::ProjectContent => [ self::ProjectContent => [
'view' => __('permissions.Allowed to watch'), 'view' => __('permissions.Allowed to watch'),
'create' => __('permissions.Allowed to create'), '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

@@ -9,39 +9,43 @@ use App\Contracts\StorageType\Video;
enum StorageType: int implements Image, Video, Audio enum StorageType: int implements Image, Video, Audio
{ {
case Logo = 1; case Logo = 1;
case ContentImages = 2;
public function getTitle(): string public function getTitle(): string
{ {
return match ($this) { return match ($this) {
self::Logo => __('validation.attributes.logo'), self::Logo => __('validation.attributes.logo'),
self::ContentImages => __('validation.attributes.content_images'),
}; };
} }
public function getAcceptMimes(): array public function getAcceptMimes(): array
{ {
return match ($this) { return match ($this) {
self::Logo => ['jpeg', 'jpg', 'png'], self::Logo => ['jpeg', 'jpg', 'png'],
self::ContentImages => ['jpeg', 'jpg', 'png'],
}; };
} }
public function isImage(): bool public function isImage(): bool
{ {
return match ($this) { return match ($this) {
self::Logo => true, self::Logo => true,
self::ContentImages => true,
default => false default => false
}; };
} }
public function isVideo(): bool public function isVideo(): bool
{ {
return match ($this->name) { return match ($this) {
default => false default => false
}; };
} }
public function isAudio(): bool public function isAudio(): bool
{ {
return match ($this->name) { return match ($this) {
default => false default => false
}; };
} }

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Services\DocumentationContent;
use App\ServiceResults\ServiceResultError;
final class StorageCommandException extends \Exception
{
public function __construct(private readonly ServiceResultError $resultError, string $message = "", int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public function getResultError(): ServiceResultError
{
return $this->resultError;
}
}

View File

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

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

@@ -19,7 +19,9 @@ final class AuthController extends Controller
public function login(): View public function login(): View
{ {
return view('login'); return view('login', [
'captcha' => config('app.captcha', false),
]);
} }
public function authorization(AuthorizationRequest $request): RedirectResponse public function authorization(AuthorizationRequest $request): RedirectResponse

View File

@@ -19,6 +19,7 @@ final class FeedbackController extends Controller
return view('site.feedback.index', [ return view('site.feedback.index', [
'project' => $request->get('project'), 'project' => $request->get('project'),
'websiteTranslations' => $request->get('websiteTranslations'), 'websiteTranslations' => $request->get('websiteTranslations'),
'captcha' => config('app.captcha', false),
]); ]);
} }

View File

@@ -4,10 +4,19 @@ namespace App\Http\Requests\Admin\Projects\About;
use App\Contracts\FormRequestDto; use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Project\About\StoreUpdate; use App\Dto\Service\Admin\Project\About\StoreUpdate;
use App\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{ {
public function attributes(): array
{
return [
'storage.content_images.*.file' => __('validation.attributes.content_images'),
];
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
*/ */
@@ -16,6 +25,8 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
return [ return [
'title' => ['required', 'string', 'max:255',], 'title' => ['required', 'string', 'max:255',],
'description' => ['nullable', 'string',], 'description' => ['nullable', 'string',],
'storage.content_images.*.file' => ['numeric', 'min:1'],
'translate-automatically' => ['nullable', 'boolean'],
]; ];
} }
@@ -24,6 +35,19 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
return new StoreUpdate( return new StoreUpdate(
title: $this->input('title'), title: $this->input('title'),
description: $this->input('description'), description: $this->input('description'),
storages: $this->storages(),
isTranslateAutomatically: (bool) $this->input('translate-automatically', false),
); );
} }
private function storages(): Storages
{
$storages = new Storages();
$content = $this->input('storage', []);
$images = $content['content_images'] ?? [];
$storages->addMany($images, StorageType::ContentImages);
return $storages;
}
} }

View File

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

View File

@@ -6,10 +6,19 @@ use App\Contracts\FormRequestDto;
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\Content;
use App\Dto\Service\Admin\Project\DocumentationContent\Contents; use App\Dto\Service\Admin\Project\DocumentationContent\Contents;
use App\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{ {
public function attributes(): array
{
return [
'content.*.content_images.*.file' => __('validation.attributes.content_images'),
];
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
*/ */
@@ -22,6 +31,8 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
'category_id' => ['nullable', 'integer', 'exists:documentation_categories,id'], 'category_id' => ['nullable', 'integer', 'exists:documentation_categories,id'],
'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'],
'translate-automatically.*' => ['nullable', 'boolean'],
]; ];
} }
@@ -49,8 +60,20 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
languageId: (int) $languageId, languageId: (int) $languageId,
title: $content['title'], title: $content['title'],
content: $content['content'] ?? '', content: $content['content'] ?? '',
storages: $this->contentStorages($content),
isTranslateAutomatically: (bool) $this->input('translate-automatically.' . $languageId, false),
)); ));
} }
return $contents; return $contents;
} }
private function contentStorages(array $content): Storages
{
$storages = new Storages();
$images = $content['content_images'] ?? [];
$storages->addMany($images, StorageType::ContentImages);
return $storages;
}
} }

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

View File

@@ -13,12 +13,17 @@ final class AuthorizationRequest extends FormRequest implements FormRequestDto
*/ */
public function rules(): array public function rules(): array
{ {
return [ $rules = [
'email' => ['required', 'email', 'max:255'], 'email' => ['required', 'email', 'max:255'],
'password' => ['required', 'min:3'], 'password' => ['required', 'min:3'],
'captcha-verified' => ['captcha'],
'remember' => ['nullable', 'boolean'], 'remember' => ['nullable', 'boolean'],
]; ];
if (config('app.captcha', false)) {
$rules['captcha-verified'] = ['captcha'];
}
return $rules;
} }
public function getDto(): Authorization public function getDto(): Authorization

View File

@@ -32,12 +32,17 @@ final class SendRequest extends FormRequest implements FormRequestDto
*/ */
public function rules(): array public function rules(): array
{ {
return [ $rules = [
'name' => ['nullable', 'string', 'max:255'], 'name' => ['nullable', 'string', 'max:255'],
'email' => ['nullable', 'string', 'max:255', 'email'], 'email' => ['nullable', 'string', 'max:255', 'email'],
'message' => ['required', 'string', 'max:5000'], 'message' => ['required', 'string', 'max:5000'],
'captcha-verified' => ['captcha'],
]; ];
if (config('app.captcha', false)) {
$rules['captcha-verified'] = ['captcha'];
}
return $rules;
} }
public function getDto(): Send public function getDto(): Send

View File

@@ -6,7 +6,9 @@ use App\Contracts\FormRequestDto;
use App\Dto\Service\Storage\Upload; use App\Dto\Service\Storage\Upload;
use App\Enums\Morph; use App\Enums\Morph;
use App\Enums\StorageType; use App\Enums\StorageType;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Rules\Enum;
final class ImageRequest extends FormRequest implements FormRequestDto final class ImageRequest extends FormRequest implements FormRequestDto
@@ -37,4 +39,20 @@ final class ImageRequest extends FormRequest implements FormRequestDto
morph: Morph::from((int) $this->input('morph')), morph: Morph::from((int) $this->input('morph')),
); );
} }
/**
* Get the error messages for the defined validation rules.*
* @return array
*/
protected function failedValidation(Validator $validator): array
{
/**
* To always return json
*/
throw new HttpResponseException(response()->json([
'errors' => $validator->errors(),
'status' => true
], 422));
}
} }

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

@@ -2,13 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Contracts\Models\Storage as StorageContract;
use App\Models\Traits\StorageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
final class DocumentationContent extends Model final class DocumentationContent extends Model implements StorageContract
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes, StorageTrait;
protected $table = 'documentation_content'; protected $table = 'documentation_content';

View File

@@ -2,18 +2,21 @@
namespace App\Models; namespace App\Models;
use App\Contracts\Models\Storage as StorageContract;
use App\Models\Traits\StorageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
final class ProjectContent extends Model final class ProjectContent extends Model implements StorageContract
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes, StorageTrait;
protected $table = 'project_content'; protected $table = 'project_content';
protected $fillable = [ protected $fillable = [
'title', 'title',
'description', 'description',
'language_id',
]; ];
} }

View File

@@ -7,6 +7,7 @@ use App\Models\Scopes\SortScope;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Attributes\ScopedBy; use Illuminate\Database\Eloquent\Attributes\ScopedBy;
@@ -70,4 +71,9 @@ final class ProjectLanguage extends Model
}, },
)->shouldCache(); )->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

@@ -0,0 +1,60 @@
<?php declare(strict_types=1);
namespace App\Notifications;
use App\Enums\Site\ProjectSection;
use App\Models\ProjectFeedback;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
final class ReviewAdded extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct(
private readonly ProjectFeedback $feedback
) { }
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
$project = $this->feedback->project;
return (new MailMessage)
->subject(__('notification.Review Added: :name', ['name' => $project->name]))
->line(__('notification.Added a new review.'))
->action(__('notification.Project :name', ['name' => $project->name]), \url(ProjectSection::Home->url($project)))
->line( __('site.attributes.name') . ': ' . $this->feedback->name)
->line( __('site.attributes.email') . ': ' . $this->feedback->email)
->line(__('site.attributes.message') . ': ' . $this->feedback->message);
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
final readonly class DocumentationContentPolicy extends Policy
{
public function upload(User $user): bool
{
if ($user->hasPermission('documentation.create') || $user->hasPermission('documentation.update')) {
return true;
}
return false;
}
}

View File

@@ -26,4 +26,13 @@ final readonly class ProjectContentPolicy extends Policy
{ {
return $user->hasPermission('project-content.update'); return $user->hasPermission('project-content.update');
} }
public function upload(User $user): bool
{
if ($user->hasPermission('project-content.create') || $user->hasPermission('project-content.update')) {
return true;
}
return false;
}
} }

View File

@@ -32,6 +32,11 @@ final readonly class ProjectPolicy extends Policy
return $user->hasPermission('project.delete'); 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 public function upload(User $user): bool
{ {
if ($user->hasPermission('project.create') || $user->hasPermission('project.update')) { if ($user->hasPermission('project.create') || $user->hasPermission('project.update')) {

View File

@@ -3,8 +3,10 @@
namespace App\Providers; namespace App\Providers;
use App\Enums\Morph; use App\Enums\Morph;
use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler;
use App\Services\Search\CreateSearchInstanceCommand; use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search; use App\Services\Search\Search;
use App\Services\Site\FeedbackService;
use App\Services\Storage\Image\ResizeCommandHandler; use App\Services\Storage\Image\ResizeCommandHandler;
use App\Services\Storage\ImageService; use App\Services\Storage\ImageService;
use App\Services\Storage\StorageCommandHandler; use App\Services\Storage\StorageCommandHandler;
@@ -31,7 +33,15 @@ class AppServiceProvider extends ServiceProvider
}); });
$this->app->bind(StorageCommandHandler::class, function () { $this->app->bind(StorageCommandHandler::class, function () {
return new StorageCommandHandler(disc: (string) config('storage.disk')); return new StorageCommandHandler(disc: (string) \config('storage.disk'));
});
$this->app->bind(FeedbackService::class, function (Application $app) {
return new FeedbackService(
$app->make(ProjectFeedbackCommandHandler::class),
(bool) \config('feedback.mail_notifications', false),
\config('feedback.mail_to', null),
);
}); });
$this->app->bind(ImageService::class, function (Application $app) { $this->app->bind(ImageService::class, function (Application $app) {

View File

@@ -11,7 +11,6 @@ use App\Services\Search\CreateSearchInstanceCommand;
use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto; use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
final readonly class DocumentationCategoryRepository final readonly class DocumentationCategoryRepository
@@ -63,7 +62,7 @@ final readonly class DocumentationCategoryRepository
->exists(); ->exists();
} }
public function getForSelect(?ProjectLanguage $defaultLanguage, ?DocumentationCategory $exceptCategory = null, array $withExcepts = []): array public function getForSelect(DocumentationVersion $version, ?ProjectLanguage $defaultLanguage, ?DocumentationCategory $exceptCategory = null, array $withExcepts = []): array
{ {
$with = [ $with = [
'content' => function (HasOne $hasOne) use ($defaultLanguage) { 'content' => function (HasOne $hasOne) use ($defaultLanguage) {
@@ -73,7 +72,7 @@ final readonly class DocumentationCategoryRepository
} }
]; ];
$categories = DocumentationCategory::query() $categories = $version->categories()
->with($with) ->with($with)
->when($exceptCategory, function (Builder $query, DocumentationCategory $exceptCategory) { ->when($exceptCategory, function (Builder $query, DocumentationCategory $exceptCategory) {
$query->whereNotIn( $query->whereNotIn(

View File

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

View File

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

@@ -4,6 +4,8 @@ namespace App\Services\Admin\Project;
use App\Contracts\ServiceResultError; use App\Contracts\ServiceResultError;
use App\Dto\Service\Admin\Project\About\StoreUpdate; use App\Dto\Service\Admin\Project\About\StoreUpdate;
use App\Enums\Morph;
use App\Jobs\Translate\ProcessProjectContent;
use App\Models\ProjectContent; use App\Models\ProjectContent;
use App\Models\User; use App\Models\User;
use App\Repositories\ProjectContentRepository; use App\Repositories\ProjectContentRepository;
@@ -13,6 +15,7 @@ use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\StoreUpdateResult; use App\ServiceResults\StoreUpdateResult;
use App\Services\ProjectContent\ProjectContentCommandHandler; use App\Services\ProjectContent\ProjectContentCommandHandler;
use App\Services\Service; use App\Services\Service;
use App\Services\Storage\StorageService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
final class AboutService extends Service final class AboutService extends Service
@@ -22,6 +25,7 @@ final class AboutService extends Service
private readonly ProjectLanguageRepository $projectLanguageRepository, private readonly ProjectLanguageRepository $projectLanguageRepository,
private readonly ProjectContentRepository $projectContentRepository, private readonly ProjectContentRepository $projectContentRepository,
private readonly ProjectContentCommandHandler $projectContentCommandHandler, private readonly ProjectContentCommandHandler $projectContentCommandHandler,
private readonly StorageService $storageService,
) { } ) { }
public function languages(int $projectId, User $user): ServiceResultError | ServiceResultArray public function languages(int $projectId, User $user): ServiceResultError | ServiceResultArray
@@ -66,6 +70,7 @@ final class AboutService extends Service
'project' => $project, 'project' => $project,
'language' => $language, 'language' => $language,
'content' => $content, 'content' => $content,
'serviceTranslationEnable' => config('translation_service.enable', false),
]); ]);
} }
@@ -88,12 +93,23 @@ final class AboutService extends Service
if ($user->cannot('create', ProjectContent::class)) { if ($user->cannot('create', ProjectContent::class)) {
return $this->errFobidden(__('Access is denied')); return $this->errFobidden(__('Access is denied'));
} }
$storages = $this->storageService->getStoragesAndValidate($data->getStorages(), Morph::ProjectContent);
if (!$storages->isSuccess()) {
return $storages;
}
try { try {
$aboutProject = DB::transaction(function () use ($data, $projectId, $languageId) { $aboutProject = DB::transaction(function () use ($data, $projectId, $languageId, $storages) {
$dataAboutProject = $this->getDataAboutProject($data); $dataAboutProject = $this->getDataAboutProject($data);
return $this->projectContentCommandHandler->handleStore($projectId, $languageId, $dataAboutProject); $aboutProject = $this->projectContentCommandHandler->handleStore($projectId, $languageId, $dataAboutProject);
$this->storageService->saveAndDelete($aboutProject, $storages);
return $aboutProject;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($aboutProject, $data);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -108,12 +124,23 @@ final class AboutService extends Service
return $this->errFobidden(__('Access is denied')); return $this->errFobidden(__('Access is denied'));
} }
$storages = $this->storageService->getStoragesAndValidate($data->getStorages(), Morph::ProjectContent);
if (!$storages->isSuccess()) {
return $storages;
}
try { try {
$aboutProject = DB::transaction(function () use ($data, $content) { $aboutProject = DB::transaction(function () use ($data, $content, $storages) {
$dataAboutProject = $this->getDataAboutProject($data); $dataAboutProject = $this->getDataAboutProject($data);
return $this->projectContentCommandHandler->handleUpdate($content, $dataAboutProject); $aboutProject = $this->projectContentCommandHandler->handleUpdate($content, $dataAboutProject);
$this->storageService->saveAndDelete($aboutProject, $storages);
return $aboutProject;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($aboutProject, $data);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -129,4 +156,15 @@ final class AboutService extends Service
'description' => $data->getDescription(), 'description' => $data->getDescription(),
]; ];
} }
private function translateContent(ProjectContent $projectContent, StoreUpdate $data): void
{
if (! $data->isTranslateAutomatically()) {
return;
}
$translateExceptLanguages = [$projectContent->language_id];
$translateContentIds = [$projectContent->id];
ProcessProjectContent::dispatch($projectContent->project_id, $translateContentIds, $translateExceptLanguages);
}
} }

View File

@@ -5,8 +5,11 @@ namespace App\Services\Admin\Project;
use App\Dto\QuerySettingsDto; use App\Dto\QuerySettingsDto;
use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto; use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto;
use App\Dto\Service\Admin\Project\DocumentationCategory\StoreUpdate; 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\DocumentationCategory\ParentException;
use App\Exceptions\Services\ServiceException; use App\Exceptions\Services\ServiceException;
use App\Jobs\Translate\ProcessProjectDocumentationCategoryContent;
use App\Models\DocumentationCategory; use App\Models\DocumentationCategory;
use App\Models\DocumentationCategoryContent; use App\Models\DocumentationCategoryContent;
use App\Models\ProjectLanguage; use App\Models\ProjectLanguage;
@@ -89,7 +92,8 @@ final class DocumentationCategoryService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'category' => new DocumentationCategory(), 'category' => new DocumentationCategory(),
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage), 'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
'serviceTranslationEnable' => config('translation_service.enable', false),
]); ]);
} }
@@ -119,7 +123,8 @@ final class DocumentationCategoryService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'category' => $category, 'category' => $category,
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage, $category, $withCategories), '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; return $category;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($category, $data->getContents());
}
} catch (ServiceException $e) { } catch (ServiceException $e) {
return $e->getServiceResultError(); return $e->getServiceResultError();
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -201,6 +210,10 @@ final class DocumentationCategoryService extends Service
return $category; return $category;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($category, $data->getContents());
}
} catch (ServiceException $e) { } catch (ServiceException $e) {
return $e->getServiceResultError(); return $e->getServiceResultError();
} catch (ParentException $e) { } catch (ParentException $e) {
@@ -254,4 +267,27 @@ final class DocumentationCategoryService extends Service
'parent_id' => $data->getParentId(), '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,13 +5,16 @@ 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\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;
use App\Repositories\DocumentationCategoryRepository; use App\Repositories\DocumentationCategoryRepository;
use App\Repositories\DocumentationRepository; use App\Repositories\DocumentationRepository;
use App\Repositories\DocumentationVersionRepository; use App\Repositories\DocumentationVersionRepository;
use App\Repositories\ProjectRepository;
use App\ServiceResults\ServiceResultArray; use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\ServiceResultSuccess;
@@ -89,7 +92,8 @@ final class DocumentationService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'documentation' => new Documentation(), 'documentation' => new Documentation(),
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage), 'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
'serviceTranslationEnable' => config('translation_service.enable', false),
]); ]);
} }
@@ -119,7 +123,8 @@ final class DocumentationService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'documentation' => $documentation, 'documentation' => $documentation,
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage, null, $withCategories), 'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, null, $withCategories),
'serviceTranslationEnable' => config('translation_service.enable', false),
]); ]);
} }
@@ -150,6 +155,12 @@ final class DocumentationService extends Service
return $documentation; return $documentation;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($documentation, $data->getContents());
}
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -190,6 +201,12 @@ final class DocumentationService extends Service
return $documentation; return $documentation;
}); });
if (\config('translation_service.enable', false)) {
$this->translateContent($documentation, $data->getContents());
}
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -236,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);
}
} }

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

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace App\Services\Commands;
use App\Models\Storage as StorageModel;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\Service;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage as StorageSupport;
final class DeleteOldFilesService extends Service
{
/**
* $temporaryBeforeDate = date of deletion of temporary files
* $deletedBeforeDate = date of deletion files that were marked as deleted
*
* @param Carbon $temporaryBeforeDate
* @param Carbon $deletedBeforeDate
* @return ServiceResultError|ServiceResultSuccess
*/
public function fromStorage(Carbon $temporaryBeforeDate, Carbon $deletedBeforeDate): ServiceResultError|ServiceResultSuccess
{
$disk = config('storage.disk');
StorageModel::withTrashed()
->where(function (Builder $query) use($temporaryBeforeDate) {
$query->whereNull('morph_id')->where('updated_at', '<', $temporaryBeforeDate);
})->orWhere(function (Builder $query) use($deletedBeforeDate) {
$query->whereNotNull('deleted_at')->where('deleted_at', '<', $deletedBeforeDate);
})->chunkById(100, function ($items) use($disk) {
$deleteIds = [];
foreach ($items as $item) {
$deleteIds[] = $item->id;
if (StorageSupport::disk($disk)->exists($item->file)) {
StorageSupport::disk($disk)->delete($item->file);
}
}
StorageModel::withTrashed()->whereIn('id', $deleteIds)->forceDelete();
});
return $this->ok(__('Old Files deleted.'));
}
}

View File

@@ -4,18 +4,33 @@ namespace App\Services\DocumentationContent;
use App\Dto\Service\Admin\Project\DocumentationContent\Content; use App\Dto\Service\Admin\Project\DocumentationContent\Content;
use App\Dto\Service\Admin\Project\DocumentationContent\Contents; use App\Dto\Service\Admin\Project\DocumentationContent\Contents;
use App\Dto\Service\DocumentationContent\StorageDto;
use App\Exceptions\Services\DocumentationContent\ContentSaveException; use App\Exceptions\Services\DocumentationContent\ContentSaveException;
use App\Exceptions\Services\DocumentationContent\StorageCommandException;
use App\Models\Documentation; use App\Models\Documentation;
use App\Models\DocumentationContent;
use App\Models\Project; use App\Models\Project;
final readonly class ModelSyncCommand final readonly class ModelSyncCommand
{ {
public function __construct(
private StorageCommand $storageCommand,
) { }
/**
* @throws StorageCommandException
* @throws ContentSaveException
*/
public function execute(Project $project, Documentation $documentation, Contents $contents): void public function execute(Project $project, Documentation $documentation, Contents $contents): void
{ {
$storageDto = new StorageDto();
$languages = $project->languages; $languages = $project->languages;
$documentationContents = $documentation->contents; $documentationContents = $documentation->contents;
$newContents = []; $newContents = [];
$contentsStorageCreated = [];
$contentLanguages = [];
foreach ($contents->getContents() as $content) { foreach ($contents->getContents() as $content) {
/** @var Content $content */ /** @var Content $content */
$language = $languages->firstWhere('id', $content->getLanguageId()); $language = $languages->firstWhere('id', $content->getLanguageId());
@@ -26,15 +41,29 @@ final readonly class ModelSyncCommand
$model = $documentationContents->firstWhere('language_id', $language->id); $model = $documentationContents->firstWhere('language_id', $language->id);
$data = $this->getData($content); $data = $this->getData($content);
if (\is_null($model)) { if (\is_null($model)) {
$contentsStorageCreated[$content->getLanguageId()] = $content->getStorages();
$newContents[] = array_merge(['language_id' => $content->getLanguageId()], $data); $newContents[] = array_merge(['language_id' => $content->getLanguageId()], $data);
$contentLanguages[] = $content->getLanguageId();
continue; continue;
} }
$storageDto->add($model, $content->getStorages());
$model->update($data); $model->update($data);
} }
if (!empty($newContents)) { if (!empty($newContents)) {
$documentation->contents()->createMany($newContents); $documentation->contents()->createMany($newContents);
$contents = $documentation->contents()->whereIn('language_id', $contentLanguages)->get();
foreach ($contents as $content) {
/** @var DocumentationContent $content */
if (!isset($contentsStorageCreated[$content->language_id])) {
continue;
}
$storageDto->add($content, $contentsStorageCreated[$content->language_id]);
}
} }
$this->storageCommand->execute($storageDto);
} }
private function getData(Content $content): array private function getData(Content $content): array

View File

@@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Services\DocumentationContent;
use App\Dto\Service\DocumentationContent\StorageDto;
use App\Enums\Morph;
use App\Exceptions\Services\DocumentationContent\StorageCommandException;
use App\Services\Storage\StorageService;
final readonly class StorageCommand
{
public function __construct(
private StorageService $storageService,
) { }
/**
* @throws StorageCommandException
*/
public function execute(StorageDto $storageDto): void
{
foreach ($storageDto->getStorages() as $storage) {
$storages = $this->storageService->getStoragesAndValidate($storage['storages'], Morph::DocumentationContent, $storage['documentationContent']->id);
if (!$storages->isSuccess()) {
throw new StorageCommandException($storages, 'Error when adding a file to storage: ' . $storages->getMessage());
}
$this->storageService->saveAndDelete($storage['documentationContent'], $storages);
}
}
}

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

@@ -3,7 +3,7 @@
namespace App\Services\Role; namespace App\Services\Role;
use App\Enums\Permission; use App\Enums\Permission;
use App\Exceptions\Rule\RoleSyncPermissionsCommandHandlerException; use App\Exceptions\Services\Rule\RoleSyncPermissionsCommandHandlerException;
use App\Models\Role; use App\Models\Role;
use App\Models\RolePermission; use App\Models\RolePermission;

View File

@@ -5,32 +5,44 @@ namespace App\Services\Site;
use App\Dto\Service\Site\Feedback\Send; use App\Dto\Service\Site\Feedback\Send;
use App\Models\Project; use App\Models\Project;
use App\Models\User; use App\Models\User;
use App\Notifications\ReviewAdded;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\ServiceResultSuccess;
use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler; use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler;
use App\Services\Service; use App\Services\Service;
use App\Services\WebsiteTranslations; use App\Services\WebsiteTranslations;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification;
final class FeedbackService extends Service final class FeedbackService extends Service
{ {
public function __construct( public function __construct(
private readonly ProjectFeedbackCommandHandler $feedbackCommandHandler, private readonly ProjectFeedbackCommandHandler $feedbackCommandHandler,
private readonly bool $isNotifications = false,
private readonly ?string $mailNotifications = null,
) { } ) { }
public function send(Send $send, Project $project, WebsiteTranslations $websiteTranslations, ?User $user = null): ServiceResultError | ServiceResultSuccess public function send(Send $send, Project $project, WebsiteTranslations $websiteTranslations, ?User $user = null): ServiceResultError | ServiceResultSuccess
{ {
try { try {
DB::transaction(function () use ($send, $project, $user) { $feedback = DB::transaction(function () use ($send, $project, $user) {
$data = $this->getDataFeedback($send); $data = $this->getDataFeedback($send);
$data['user_id'] = $user?->id; $data['user_id'] = $user?->id;
$this->feedbackCommandHandler->handleStore($project, $data); return $this->feedbackCommandHandler->handleStore($project, $data);
}); });
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService($websiteTranslations->translate('Server Error')); return $this->errService($websiteTranslations->translate('Server Error'));
} }
try {
if ($this->isNotifications) {
Notification::route('mail', $this->mailNotifications)->notify(new ReviewAdded($feedback));
}
} catch (\Throwable $e) {
report($e);
}
return $this->ok($websiteTranslations->translate('site.Message sent successfully')); return $this->ok($websiteTranslations->translate('site.Message sent successfully'));
} }

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

@@ -2,15 +2,17 @@
namespace App\View\Components\Volt\Forms; namespace App\View\Components\Volt\Forms;
use App\Dto\View\Volt\Form\WysiwygStorageUpload;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\View\View; use Illuminate\View\View;
final class TextareaWysiwyg extends Form final class TextareaWysiwyg extends Form
{ {
public function __construct( public function __construct(
private readonly string $title, private readonly string $title,
private readonly string $name, private readonly string $name,
private readonly ?string $value = '', private readonly ?string $value = '',
private readonly ?WysiwygStorageUpload $storageUpload = null,
) { } ) { }
protected function getName(): string protected function getName(): string
@@ -28,6 +30,11 @@ final class TextareaWysiwyg extends Form
return (string) old($this->getRequestName(), $this->value); return (string) old($this->getRequestName(), $this->value);
} }
public function getStorageUpload(): ?WysiwygStorageUpload
{
return $this->storageUpload;
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -37,10 +44,11 @@ final class TextareaWysiwyg extends Form
return view('components.volt.forms.textarea-wysiwyg', [ return view('components.volt.forms.textarea-wysiwyg', [
'tinymceLicenseKey' => $tinymceLicenseKey, 'tinymceLicenseKey' => $tinymceLicenseKey,
'title' => $this->getTitle(), 'title' => $this->getTitle(),
'name' => $this->getName(), 'name' => $this->getName(),
'requestName' => $this->getRequestName(), 'requestName' => $this->getRequestName(),
'value' => $this->getValue(), 'value' => $this->getValue(),
'storageUpload' => $this->getStorageUpload(),
]); ]);
} }
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,15 @@ return [
'env' => env('APP_ENV', 'production'), 'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Сaptcha
|--------------------------------------------------------------------------
|
| Enables or disables captcha.
*/
'captcha' => (bool) env('APP_CAPTCHA', false),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application Debug Mode | Application Debug Mode

View File

@@ -0,0 +1,9 @@
<?php
return [
/**
* Enable new review alerts.
*/
'mail_notifications' => (bool) env('FEEDBACK_MAIL_NOTIFICATIONS', false),
'mail_to' => env('FEEDBACK_MAIL_TO', null),
];

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::table('storage', function (Blueprint $table) {
$table->dropIndex(['morph_id']);
$table->index(['morph_id', 'updated_at']);
$table->index(['deleted_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('storage', function (Blueprint $table) {
$table->dropIndex(['morph_id', 'updated_at']);
$table->dropIndex(['deleted_at']);
$table->index(['morph_id']);
});
}
};

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", "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",
@@ -267,5 +268,9 @@
"Documentation successfully removed": "Documentation successfully removed", "Documentation successfully removed": "Documentation successfully removed",
"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",
"Automatic translation": "Automatic translation",
"The settings were saved successfully": "The settings were saved successfully",
"Category not found": "Category not found"
} }

Some files were not shown because too many files have changed in this diff Show More