Версия 0.4.0 #7

Merged
kor-elf merged 13 commits from develop into main 2025-02-15 00:07:48 +05:00
28 changed files with 2716 additions and 799 deletions
Showing only changes of commit 25faa3d62b - Show all commits

View File

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

View File

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

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

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

View File

@ -0,0 +1,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

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

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

View File

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

View File

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

View File

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('project_translation_service', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('language_id')->unique();
$table->foreign('language_id')->references('id')->on('project_languages');
$table->unsignedBigInteger('source_language_id')->nullable()->index();
$table->foreign('source_language_id')->references('id')->on('project_languages');
$table->string('code', 50);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_translation_service');
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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