Compare commits

...

41 Commits
0.2.0 ... main

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

View File

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

View File

@ -8,6 +8,7 @@
.env
.env.backup
.env.production
.env.testing
.phpunit.result.cache
Homestead.json
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;
use App\Dto\Service\Dto;
use App\Dto\Service\Storage\Storages;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $title,
private string $description,
private Storages $storages,
private bool $isTranslateAutomatically = false,
) { }
public function getTitle(): string
@ -20,4 +23,14 @@ final readonly class StoreUpdate extends Dto
{
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(
private int $languageId,
private string $title,
private bool $isTranslateAutomatically = false,
) { }
public function getLanguageId(): int
@ -18,4 +19,9 @@ final readonly class Content
{
return $this->title;
}
public function isTranslateAutomatically(): bool
{
return $this->isTranslateAutomatically;
}
}

View File

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

View File

@ -0,0 +1,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;
use App\Models\DocumentationCategoryContent;
use App\Models\DocumentationContent;
use App\Models\Project;
use App\Models\ProjectContent;
enum Morph: int
{
case Project = 1;
case DocumentationContent = 2;
case ProjectContent = 3;
case DocumentationCategoryContent = 4;
public function getPathModel(): string
{
return match ($this) {
self::Project => Project::class,
self::Project => Project::class,
self::DocumentationContent => DocumentationContent::class,
self::ProjectContent => ProjectContent::class,
self::DocumentationCategoryContent => DocumentationCategoryContent::class,
};
}

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ final class FeedbackController extends Controller
return view('site.feedback.index', [
'project' => $request->get('project'),
'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\Dto\Service\Admin\Project\About\StoreUpdate;
use App\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest;
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.
*/
@ -16,6 +25,8 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
return [
'title' => ['required', 'string', 'max:255',],
'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(
title: $this->input('title'),
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'],
'parent_id' => ['nullable', 'integer', 'exists:documentation_categories,id'],
'content.*.title' => ['required', 'string', 'max:255'],
'translate-automatically.*' => ['nullable', 'boolean'],
];
}
@ -47,6 +48,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
$contents->addContent(new Content(
languageId: (int) $languageId,
title: $content['title'],
isTranslateAutomatically: (bool) $this->input('translate-automatically.' . $languageId, false),
));
}
return $contents;

View File

@ -6,10 +6,19 @@ use App\Contracts\FormRequestDto;
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\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest;
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.
*/
@ -22,6 +31,8 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
'category_id' => ['nullable', 'integer', 'exists:documentation_categories,id'],
'content.*.title' => ['required', 'string', 'max:255'],
'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,
title: $content['title'],
content: $content['content'] ?? '',
storages: $this->contentStorages($content),
isTranslateAutomatically: (bool) $this->input('translate-automatically.' . $languageId, false),
));
}
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.*.code' => ['required', 'string', new In(Translations::getTranslationCodes())],
'translations.*.text' => ['nullable', 'string', 'max:1000'],
'translate-automatically' => ['nullable', 'boolean'],
];
}
@ -33,6 +34,9 @@ final class UpdateRequest extends FormRequest implements FormRequestDto
);
}
return new Update($translations);
return new Update(
translations: $translations,
isTranslateAutomatically: (bool) $this->input('translate-automatically', false),
);
}
}

View File

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

View File

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

View File

@ -6,7 +6,9 @@ use App\Contracts\FormRequestDto;
use App\Dto\Service\Storage\Upload;
use App\Enums\Morph;
use App\Enums\StorageType;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Validation\Rules\Enum;
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')),
);
}
/**
* 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;
use App\Contracts\Models\Storage as StorageContract;
use App\Models\Traits\StorageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
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';

View File

@ -2,18 +2,21 @@
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\Model;
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 $fillable = [
'title',
'description',
'language_id',
];
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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');
}
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');
}
public function settingUpAutomaticTranslation(User $user, Project $project): bool
{
return $user->hasPermission('project.setting-up-automatic-translation');
}
public function upload(User $user): bool
{
if ($user->hasPermission('project.create') || $user->hasPermission('project.update')) {

View File

@ -3,8 +3,10 @@
namespace App\Providers;
use App\Enums\Morph;
use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use App\Services\Site\FeedbackService;
use App\Services\Storage\Image\ResizeCommandHandler;
use App\Services\Storage\ImageService;
use App\Services\Storage\StorageCommandHandler;
@ -31,7 +33,15 @@ class AppServiceProvider extends ServiceProvider
});
$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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,16 @@ namespace App\Services\Admin\Project;
use App\Dto\Builder\Documentation as DocumentationBuilderDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Service\Admin\Project\Documentation\StoreUpdate;
use App\Dto\Service\Admin\Project\DocumentationContent\Content;
use App\Dto\Service\Admin\Project\DocumentationContent\Contents;
use App\Exceptions\Services\DocumentationContent\StorageCommandException;
use App\Jobs\Translate\ProcessProjectDocumentationContent;
use App\Models\Documentation;
use App\Models\ProjectLanguage;
use App\Models\User;
use App\Repositories\DocumentationCategoryRepository;
use App\Repositories\DocumentationRepository;
use App\Repositories\DocumentationVersionRepository;
use App\Repositories\ProjectRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
@ -89,7 +92,8 @@ final class DocumentationService extends Service
'version' => $version,
'project' => $project,
'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,
'project' => $project,
'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;
});
if (\config('translation_service.enable', false)) {
$this->translateContent($documentation, $data->getContents());
}
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
@ -190,6 +201,12 @@ final class DocumentationService extends Service
return $documentation;
});
if (\config('translation_service.enable', false)) {
$this->translateContent($documentation, $data->getContents());
}
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
@ -236,4 +253,27 @@ final class DocumentationService extends Service
'category_id' => $data->getCategoryId(),
];
}
private function translateContent(Documentation $documentation, Contents $contents): void
{
$translateExceptLanguages = [];
$translateLanguages = [];
foreach ($contents->getContents() as $content) {
/** @var Content $content */
$translateExceptLanguages[] = $content->getLanguageId();
if ($content->isTranslateAutomatically()) {
$translateLanguages[] = $content->getLanguageId();
}
}
if (empty($translateLanguages)) {
return;
}
$translateContentIds = $documentation->contents()->select('id')
->whereIn('language_id', $translateLanguages)
->get()->pluck('id')->toArray();
ProcessProjectDocumentationContent::dispatch($documentation->id, $translateContentIds, $translateExceptLanguages);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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;
use App\Enums\Permission;
use App\Exceptions\Rule\RoleSyncPermissionsCommandHandlerException;
use App\Exceptions\Services\Rule\RoleSyncPermissionsCommandHandlerException;
use App\Models\Role;
use App\Models\RolePermission;

View File

@ -5,32 +5,44 @@ namespace App\Services\Site;
use App\Dto\Service\Site\Feedback\Send;
use App\Models\Project;
use App\Models\User;
use App\Notifications\ReviewAdded;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler;
use App\Services\Service;
use App\Services\WebsiteTranslations;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification;
final class FeedbackService extends Service
{
public function __construct(
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
{
try {
DB::transaction(function () use ($send, $project, $user) {
$feedback = DB::transaction(function () use ($send, $project, $user) {
$data = $this->getDataFeedback($send);
$data['user_id'] = $user?->id;
$this->feedbackCommandHandler->handleStore($project, $data);
return $this->feedbackCommandHandler->handleStore($project, $data);
});
} catch (\Throwable $e) {
report($e);
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'));
}

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,15 @@ return [
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Сaptcha
|--------------------------------------------------------------------------
|
| Enables or disables captcha.
*/
'captcha' => (bool) env('APP_CAPTCHA', false),
/*
|--------------------------------------------------------------------------
| 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",
"Language not found": "Language not found",
"Project not found": "Project not found",
"Documentation not found": "Documentation not found",
"Translations successfully updated": "Translations successfully updated",
"allowed characters:": "allowed characters:",
"Documentation version created successfully": "Documentation version created successfully",
@ -267,5 +268,9 @@
"Documentation successfully removed": "Documentation successfully removed",
"Category successfully created": "Category successfully created",
"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