From 491249c8d8b2b6069fac2f042f8869c66f3ee691 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 22 Apr 2024 23:52:04 +0500 Subject: [PATCH] Added the ability to dynamically translate on the project website. --- .../Dto/Service/Admin/Project/Language.php | 23 ++++- .../Admin/Project/Translation/Translation.php | 23 +++++ .../Project/Translation/Translations.php | 35 +++++++ .../Admin/Project/Translation/Update.php | 17 ++++ app/application/app/Enums/CacheTag.php | 1 + app/application/app/Enums/Permission.php | 5 + .../Transaction/TranslationsException.php | 8 ++ .../Admin/Projects/TranslationsController.php | 54 +++++++++++ .../app/Http/Controllers/Site/Controller.php | 2 +- .../Controllers/Site/ProjectsController.php | 15 ++- .../{Project.php => ProjectAndLanguage.php} | 40 ++++---- ...omain.php => ProjectDomainAndLanguage.php} | 21 +++-- .../app/Http/Middleware/ProjectLanguage.php | 43 +++++++++ .../Admin/Projects/StoreUpdateRequest.php | 10 ++ .../Projects/Translations/UpdateRequest.php | 38 ++++++++ .../app/Models/ProjectLanguage.php | 27 +++++- .../app/Models/ProjectTranslation.php | 26 ++++++ .../app/Policies/ProjectTranslationPolicy.php | 24 +++++ .../ProjectTranslationRepository.php | 25 +++++ .../Site/PagePossibleWithoutTranslation.php | 8 +- .../Admin/Project/TranslationService.php | 91 +++++++++++++++++++ .../ProjectLanguage/ModelSyncCommand.php | 2 + .../ProjectTranslation/ModelSyncCommand.php | 60 ++++++++++++ app/application/app/Services/Service.php | 6 +- .../app/Services/Site/ProjectService.php | 17 ++-- .../app/Services/WebsiteTranslations.php | 23 +++++ .../View/Components/Site/ChooseLanguage.php | 24 +++-- .../app/View/Components/Site/Layout.php | 6 +- .../View/Components/Volt/Forms/Languages.php | 16 +++- app/application/bootstrap/app.php | 7 +- ...em_language_to_project_languages_table.php | 30 ++++++ ..._19_084614_create_project_translations.php | 36 ++++++++ app/application/lang/en.json | 3 +- app/application/lang/en/admin-sections.php | 1 + app/application/lang/en/permissions.php | 1 + app/application/lang/en/validation.php | 7 ++ app/application/lang/ru.json | 3 +- app/application/lang/ru/admin-sections.php | 1 + app/application/lang/ru/permissions.php | 1 + app/application/lang/ru/validation.php | 7 ++ app/application/resources/site/scss/app.scss | 15 ++- .../views/admin/projects/links/edit.blade.php | 2 +- .../views/admin/projects/show.blade.php | 12 +++ .../projects/translations/edit.blade.php | 42 +++++++++ .../projects/translations/languages.blade.php | 36 ++++++++ .../components/site/choose-language.blade.php | 4 +- .../components/volt/forms/input.blade.php | 12 ++- .../components/volt/forms/languages.blade.php | 6 +- .../volt/forms/languages/input.blade.php | 2 +- .../volt/forms/languages/language.blade.php | 35 +++++-- .../forms/languages/system_lang.blade.php | 9 ++ .../resources/views/layout/site.blade.php | 12 +-- .../site/page-without-translation.blade.php | 4 +- .../views/site/projects/about.blade.php | 4 +- app/application/routes/web.php | 4 + 55 files changed, 867 insertions(+), 119 deletions(-) create mode 100644 app/application/app/Dto/Service/Admin/Project/Translation/Translation.php create mode 100644 app/application/app/Dto/Service/Admin/Project/Translation/Translations.php create mode 100644 app/application/app/Dto/Service/Admin/Project/Translation/Update.php create mode 100644 app/application/app/Exceptions/Dto/Admin/Project/Transaction/TranslationsException.php create mode 100644 app/application/app/Http/Controllers/Admin/Projects/TranslationsController.php rename app/application/app/Http/Middleware/{Project.php => ProjectAndLanguage.php} (56%) rename app/application/app/Http/Middleware/{ProjectDomain.php => ProjectDomainAndLanguage.php} (52%) create mode 100644 app/application/app/Http/Middleware/ProjectLanguage.php create mode 100644 app/application/app/Http/Requests/Admin/Projects/Translations/UpdateRequest.php create mode 100644 app/application/app/Models/ProjectTranslation.php create mode 100644 app/application/app/Policies/ProjectTranslationPolicy.php create mode 100644 app/application/app/Repositories/ProjectTranslationRepository.php create mode 100644 app/application/app/Services/Admin/Project/TranslationService.php create mode 100644 app/application/app/Services/ProjectTranslation/ModelSyncCommand.php create mode 100644 app/application/app/Services/WebsiteTranslations.php create mode 100644 app/application/database/migrations/2024_04_18_144547_add_system_language_to_project_languages_table.php create mode 100644 app/application/database/migrations/2024_04_19_084614_create_project_translations.php create mode 100644 app/application/resources/views/admin/projects/translations/edit.blade.php create mode 100644 app/application/resources/views/admin/projects/translations/languages.blade.php create mode 100644 app/application/resources/views/components/volt/forms/languages/system_lang.blade.php diff --git a/app/application/app/Dto/Service/Admin/Project/Language.php b/app/application/app/Dto/Service/Admin/Project/Language.php index c8fb97d..468db45 100644 --- a/app/application/app/Dto/Service/Admin/Project/Language.php +++ b/app/application/app/Dto/Service/Admin/Project/Language.php @@ -3,15 +3,18 @@ namespace App\Dto\Service\Admin\Project; use App\Dto\Service\Dto; +use App\Enums\Lang; final readonly class Language extends Dto { public function __construct( - private string $title, - private string $code, - private int $sort, - private bool $isDefault, - private ?int $id, + private string $title, + private string $code, + private int $sort, + private bool $isDefault, + private ?string $isoCode = null, + private ?Lang $systemLang = null, + private ?int $id = null, ) { } public function getTitle(): string @@ -38,4 +41,14 @@ public function getId(): ?int { return $this->id; } + + public function getIsoCode(): ?string + { + return $this->isoCode; + } + + public function getSystemLang(): ?Lang + { + return $this->systemLang; + } } diff --git a/app/application/app/Dto/Service/Admin/Project/Translation/Translation.php b/app/application/app/Dto/Service/Admin/Project/Translation/Translation.php new file mode 100644 index 0000000..0dc89fb --- /dev/null +++ b/app/application/app/Dto/Service/Admin/Project/Translation/Translation.php @@ -0,0 +1,23 @@ +code; + } + + public function getText(): ?string + { + return $this->text; + } +} diff --git a/app/application/app/Dto/Service/Admin/Project/Translation/Translations.php b/app/application/app/Dto/Service/Admin/Project/Translation/Translations.php new file mode 100644 index 0000000..a0ac031 --- /dev/null +++ b/app/application/app/Dto/Service/Admin/Project/Translation/Translations.php @@ -0,0 +1,35 @@ +translations[] = new Translation($code, $text); + } + + public function getTranslations(): array + { + return $this->translations; + } + + public static function getTranslationCodes(): array + { + return [ + 'site.Menu', + 'site.Powered by service', + 'site.About project', + 'site.Choose language', + 'site.Page without translation', + 'site.Project', + ]; + } +} diff --git a/app/application/app/Dto/Service/Admin/Project/Translation/Update.php b/app/application/app/Dto/Service/Admin/Project/Translation/Update.php new file mode 100644 index 0000000..3ce9e24 --- /dev/null +++ b/app/application/app/Dto/Service/Admin/Project/Translation/Update.php @@ -0,0 +1,17 @@ +translations; + } +} diff --git a/app/application/app/Enums/CacheTag.php b/app/application/app/Enums/CacheTag.php index 1ff3862..316900c 100644 --- a/app/application/app/Enums/CacheTag.php +++ b/app/application/app/Enums/CacheTag.php @@ -8,6 +8,7 @@ enum CacheTag: string { case Project = 'project'; + case ProjectTranslation = 'project_translation'; public function getCache(): TaggedCache { diff --git a/app/application/app/Enums/Permission.php b/app/application/app/Enums/Permission.php index bdf8d14..652a7d5 100644 --- a/app/application/app/Enums/Permission.php +++ b/app/application/app/Enums/Permission.php @@ -10,6 +10,7 @@ enum Permission: string case Project = 'project'; case ProjectContent = 'project-content'; case ProjectLink = 'project-link'; + case ProjectTranslation = 'project-translation'; public function getPermissions(): array { @@ -22,6 +23,10 @@ public function getPermissions(): array 'create' => __('permissions.Allowed to create'), 'update' => __('permissions.Allowed to edit'), ], + self::ProjectTranslation => [ + 'view' => __('permissions.Allowed to watch'), + 'update' => __('permissions.Allowed to edit'), + ], default => $this->getBasePermissions() }; diff --git a/app/application/app/Exceptions/Dto/Admin/Project/Transaction/TranslationsException.php b/app/application/app/Exceptions/Dto/Admin/Project/Transaction/TranslationsException.php new file mode 100644 index 0000000..b0b04a7 --- /dev/null +++ b/app/application/app/Exceptions/Dto/Admin/Project/Transaction/TranslationsException.php @@ -0,0 +1,8 @@ +user(); + $result = $this->translationService->languages($projectId, $user); + if ($result->isError()) { + $this->errors($result); + } + + return view('admin/projects/translations/languages', $result->getData()); + } + + public function edit(int $projectId, int $languageId, Request $request): View + { + $user = $request->user(); + $result = $this->translationService->edit($projectId, $languageId, $user); + if ($result->isError()) { + $this->errors($result); + } + + return view('admin/projects/translations/edit', $result->getData()); + } + + public function update(int $projectId, int $languageId, UpdateRequest $request): RedirectResponse + { + $user = $request->user(); + $data = $request->getDto(); + $result = $this->translationService->update($projectId, $languageId, $data, $user); + if ($result->isError()) { + return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage()); + } + + return redirect()->route('admin.projects.translations.edit', [ + 'project' => $projectId, + 'language' => $languageId, + ])->withSuccess($result->getMessage()); + } +} diff --git a/app/application/app/Http/Controllers/Site/Controller.php b/app/application/app/Http/Controllers/Site/Controller.php index 28edf22..1a23afd 100644 --- a/app/application/app/Http/Controllers/Site/Controller.php +++ b/app/application/app/Http/Controllers/Site/Controller.php @@ -12,7 +12,7 @@ protected function viewPageWithoutTranslation(PagePossibleWithoutTranslation $re { return \view('site.page-without-translation', [ 'project' => $result->getProject(), - 'language' => $result->getLanguage(), + 'websiteTranslations' => $result->getWebsiteTranslations(), ]); } } diff --git a/app/application/app/Http/Controllers/Site/ProjectsController.php b/app/application/app/Http/Controllers/Site/ProjectsController.php index 8febecd..cd227ac 100644 --- a/app/application/app/Http/Controllers/Site/ProjectsController.php +++ b/app/application/app/Http/Controllers/Site/ProjectsController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\Site; -use App\Enums\Site\ProjectSection; use App\Services\Site\ProjectService; use Illuminate\Http\Request; use Illuminate\View\View; @@ -13,15 +12,13 @@ public function __construct( private readonly ProjectService $projectService, ) { } - public function index(Request $request, ?string $language = null): View + public function index(Request $request): View { - $user = $request->user(); - - if (\is_null($request->project)) { - if (!\is_null($language)) { - abort(404); - } + $user = $request->user(); + $project = $request->get('project'); + $websiteTranslations = $request->get('websiteTranslations'); + if (\is_null($project)) { $with = ['storage']; $result = $this->projectService->getProjects($user, $with); if ($result->isError()) { @@ -31,7 +28,7 @@ public function index(Request $request, ?string $language = null): View return \view('site.projects.index', $result->getData()); } - $result = $this->projectService->getAboutByProject($request->project, $language, $request->user()); + $result = $this->projectService->getAboutByProject($project, $websiteTranslations, $request->user()); if ($result->isError()) { $this->errors($result); } diff --git a/app/application/app/Http/Middleware/Project.php b/app/application/app/Http/Middleware/ProjectAndLanguage.php similarity index 56% rename from app/application/app/Http/Middleware/Project.php rename to app/application/app/Http/Middleware/ProjectAndLanguage.php index a374785..15215af 100644 --- a/app/application/app/Http/Middleware/Project.php +++ b/app/application/app/Http/Middleware/ProjectAndLanguage.php @@ -3,23 +3,17 @@ namespace App\Http\Middleware; use App\Enums\CacheTag; -use App\Repositories\ProjectRepository; use Illuminate\Http\Request; -use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Response; use Closure; -class Project +class ProjectAndLanguage extends ProjectLanguage { - public function __construct( - private readonly ProjectRepository $projectRepository, - ) { } - public function handle(Request $request, Closure $next): Response { $projectCode = $request->route()?->parameter('project'); if ($projectCode === null) { - abort(404); + abort(Response::HTTP_NOT_FOUND); } $seconds = 3600; @@ -27,23 +21,27 @@ public function handle(Request $request, Closure $next): Response return $this->projectRepository->getProjectByCode($projectCode) ?? false; }); if ($project === false) { - abort(404); + abort(Response::HTTP_NOT_FOUND); + } + + if ( + $project->http_host !== null + && $project->http_host !== $request->getSchemeAndHttpHost() + ) { + return redirect($project->http_host, 302); + } + + $languageCode = $request->route()?->parameter('language'); + $websiteTranslations = $this->getWebsiteTranslations($project, $languageCode); + if (\is_null($websiteTranslations)) { + abort(Response::HTTP_NOT_FOUND); } unset($request->route()->parameters['project']); + unset($request->route()->parameters['language']); - $routeName = $request->route()->getName(); - if ( - $routeName !== null - && $project->http_host !== null - && $project->http_host !== $request->getSchemeAndHttpHost() - ) { - $routeName = Str::of($routeName)->replaceStart('project.', '')->toString(); - $route = \route($routeName, $request->route()->parameters(), false); - return redirect($project->http_host . $route, 302); - } - - $request->merge(['project' => $project]); + $request->attributes->set('project', $project); + $request->attributes->set('websiteTranslations', $websiteTranslations); return $next($request); } diff --git a/app/application/app/Http/Middleware/ProjectDomain.php b/app/application/app/Http/Middleware/ProjectDomainAndLanguage.php similarity index 52% rename from app/application/app/Http/Middleware/ProjectDomain.php rename to app/application/app/Http/Middleware/ProjectDomainAndLanguage.php index be4a7a5..6503c95 100644 --- a/app/application/app/Http/Middleware/ProjectDomain.php +++ b/app/application/app/Http/Middleware/ProjectDomainAndLanguage.php @@ -3,17 +3,12 @@ namespace App\Http\Middleware; use App\Enums\CacheTag; -use App\Repositories\ProjectRepository; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; -final class ProjectDomain +final class ProjectDomainAndLanguage extends ProjectLanguage { - public function __construct( - private readonly ProjectRepository $projectRepository, - ) { } - public function handle(Request $request, Closure $next): Response { $httpHost = $request->getSchemeAndHttpHost(); @@ -25,7 +20,19 @@ public function handle(Request $request, Closure $next): Response if ($project === false) { $project = null; } - $request->merge(['project' => $project]); + + $websiteTranslations = null; + if ($project !== null) { + $languageCode = $request->route()?->parameter('language'); + $websiteTranslations = $this->getWebsiteTranslations($project, $languageCode); + if (\is_null($websiteTranslations)) { + abort(Response::HTTP_NOT_FOUND); + } + } + + unset($request->route()->parameters['language']); + $request->attributes->set('project', $project); + $request->attributes->set('websiteTranslations', $websiteTranslations); return $next($request); } diff --git a/app/application/app/Http/Middleware/ProjectLanguage.php b/app/application/app/Http/Middleware/ProjectLanguage.php new file mode 100644 index 0000000..efaea63 --- /dev/null +++ b/app/application/app/Http/Middleware/ProjectLanguage.php @@ -0,0 +1,43 @@ +getCache()->remember(self::class . $project->id . '-' . $languageCode, $seconds, function () use ($project, $languageCode) { + return $this->projectLanguageRepository->getProjectLanguageByCodeOrDefault($project, $languageCode) ?? false; + }); + if ($language === false) { + return null; + } + if ($language !== null) { + if ($language->system_lang) { + App::setLocale($language->system_lang->getLocale()); + } + } + + $seconds = 3600 * 24; + $translations = CacheTag::ProjectTranslation->getCache()->remember(self::class . '-translations-' . $project->id . '-' . $language->id, $seconds, function () use ($project, $language) { + return $this->projectTranslationRepository->getProjectTranslations($project->id, $language->id)->all()->pluck('text', 'code')->toArray(); + }); + + return new WebsiteTranslations($language, $translations); + } +} diff --git a/app/application/app/Http/Requests/Admin/Projects/StoreUpdateRequest.php b/app/application/app/Http/Requests/Admin/Projects/StoreUpdateRequest.php index 6af4e5e..9917b3f 100644 --- a/app/application/app/Http/Requests/Admin/Projects/StoreUpdateRequest.php +++ b/app/application/app/Http/Requests/Admin/Projects/StoreUpdateRequest.php @@ -7,9 +7,11 @@ use App\Dto\Service\Admin\Project\Languages; use App\Dto\Service\Admin\Project\StoreUpdate; use App\Dto\Service\Storage\Storages; +use App\Enums\Lang; use App\Enums\StorageType; use App\Rules\HttpHost; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rules\Enum; class StoreUpdateRequest extends FormRequest implements FormRequestDto { @@ -33,6 +35,8 @@ public function rules(): array 'languages.items.*.title' => ['required', 'string', 'max:255'], 'languages.items.*.code' => ['required', 'string', 'min:2', 'max:30', 'regex:/^[a-z_]+$/'], 'languages.items.*.sort' => ['required', 'numeric', 'min:-1000', 'max:1000'], + 'languages.items.*.system_lang' => ['nullable', new Enum(Lang::class)], + 'languages.items.*.iso_code' => ['nullable', 'string', 'max:30', 'regex:/^[a-zA-Z-]+$/'], 'languages.default' => ['required', 'numeric', function (string $attribute, mixed $value, \Closure $fail) { $languages = $this->input('languages.items', []); if (!isset($languages[$value])) { @@ -81,11 +85,17 @@ private function languages(): Languages if ($languageId !== null) { $languageId = (int) $languageId; } + $systemLang = null; + if ($lang['system_lang'] !== null) { + $systemLang = Lang::tryFrom((int) $lang['system_lang']); + } $language = new Language( title: $lang['title'], code: $lang['code'], sort: (int) $lang['sort'], isDefault: ($default === $index), + isoCode: $lang['iso_code'] ?? null, + systemLang: $systemLang, id: $languageId, ); $languages->addLanguage($language); diff --git a/app/application/app/Http/Requests/Admin/Projects/Translations/UpdateRequest.php b/app/application/app/Http/Requests/Admin/Projects/Translations/UpdateRequest.php new file mode 100644 index 0000000..f185a2d --- /dev/null +++ b/app/application/app/Http/Requests/Admin/Projects/Translations/UpdateRequest.php @@ -0,0 +1,38 @@ + ['nullable', 'array'], + 'translations.*.code' => ['required', 'string', new In(Translations::getTranslationCodes())], + 'translations.*.text' => ['nullable', 'string', 'max:1000'], + ]; + } + + public function getDto(): Update + { + $translations = new Translations(); + foreach ($this->input('translations', []) as $translation) { + $translations->addTranslation( + code: $translation['code'], + text: $translation['text'] ?? null, + ); + } + + return new Update($translations); + } +} diff --git a/app/application/app/Models/ProjectLanguage.php b/app/application/app/Models/ProjectLanguage.php index a7b8f77..b6268ff 100644 --- a/app/application/app/Models/ProjectLanguage.php +++ b/app/application/app/Models/ProjectLanguage.php @@ -2,7 +2,9 @@ namespace App\Models; +use App\Enums\Lang; 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\SoftDeletes; @@ -33,6 +35,8 @@ final class ProjectLanguage extends Model 'code', 'is_default', 'sort', + 'system_lang', + 'iso_code', ]; /** @@ -43,8 +47,27 @@ final class ProjectLanguage extends Model protected function casts(): array { return [ - 'is_default' => 'boolean', - 'sort' => 'integer', + 'is_default' => 'boolean', + 'sort' => 'integer', + 'system_lang' => Lang::class, ]; } + + protected function attributeLang(): Attribute + { + return Attribute::make( + get: function () { + + if ($this->iso_code) { + return $this->iso_code; + } + + if ($this->system_lang) { + return $this->system_lang->getLocale(); + } + + return $this->code; + }, + )->shouldCache(); + } } diff --git a/app/application/app/Models/ProjectTranslation.php b/app/application/app/Models/ProjectTranslation.php new file mode 100644 index 0000000..4961a96 --- /dev/null +++ b/app/application/app/Models/ProjectTranslation.php @@ -0,0 +1,26 @@ +hasPermission('project.view'); + } + + public function view(User $user, ProjectTranslation $translation): bool + { + return $user->hasPermission('project-translation.view'); + } + + public function update(User $user): bool + { + return $user->hasPermission('project-translation.update'); + } +} diff --git a/app/application/app/Repositories/ProjectTranslationRepository.php b/app/application/app/Repositories/ProjectTranslationRepository.php new file mode 100644 index 0000000..10bce6a --- /dev/null +++ b/app/application/app/Repositories/ProjectTranslationRepository.php @@ -0,0 +1,25 @@ +where('project_id', $projectId)->where('language_id', $languageId); + return $this->createSearchInstanceCommand->execute($query); + } + + public function getProjectTranslationsWithTrashed(int $projectId, int $languageId): Search + { + $query = ProjectTranslation::query()->withTrashed()->where('project_id', $projectId)->where('language_id', $languageId); + return $this->createSearchInstanceCommand->execute($query); + } +} diff --git a/app/application/app/ServiceResults/Site/PagePossibleWithoutTranslation.php b/app/application/app/ServiceResults/Site/PagePossibleWithoutTranslation.php index af3e03d..4ebc4fc 100644 --- a/app/application/app/ServiceResults/Site/PagePossibleWithoutTranslation.php +++ b/app/application/app/ServiceResults/Site/PagePossibleWithoutTranslation.php @@ -3,14 +3,14 @@ namespace app\ServiceResults\Site; use App\Models\Project; -use App\Models\ProjectLanguage; use App\ServiceResults\ServiceResult; +use App\Services\WebsiteTranslations; final class PagePossibleWithoutTranslation extends ServiceResult { public function __construct( private readonly Project $project, - private readonly ProjectLanguage $language, + private readonly WebsiteTranslations $websiteTranslations, private readonly array $data, private readonly bool $isTranslation, ) { } @@ -30,8 +30,8 @@ public function getProject(): Project return $this->project; } - public function getLanguage(): ProjectLanguage + public function getWebsiteTranslations(): WebsiteTranslations { - return $this->language; + return $this->websiteTranslations; } } diff --git a/app/application/app/Services/Admin/Project/TranslationService.php b/app/application/app/Services/Admin/Project/TranslationService.php new file mode 100644 index 0000000..b8482e5 --- /dev/null +++ b/app/application/app/Services/Admin/Project/TranslationService.php @@ -0,0 +1,91 @@ +projectRepository->getProjectById($projectId); + if (\is_null($project)) { + return $this->errNotFound(__('Not Found')); + } + + if ($user->cannot('viewAny', ProjectTranslation::class)) { + return $this->errFobidden(__('Access is denied')); + } + + return $this->result([ + 'project' => $project, + ]); + } + + public function edit(int $projectId, int $languageId, User $user): ServiceResultError | ServiceResultArray + { + $project = $this->projectRepository->getProjectById($projectId); + $language = $project?->languages()->firstWhere('id', $languageId); + + if (\is_null($project) || \is_null($language)) { + return $this->errNotFound(__('Not Found')); + } + + if ($user->cannot('viewAny', ProjectTranslation::class)) { + return $this->errFobidden(__('Access is denied')); + } + + return $this->result([ + 'project' => $project, + 'language' => $language, + 'projectTranslations' => $this->projectTranslationRepository->getProjectTranslations($projectId, $languageId)->all()->pluck('text', 'code')->toArray(), + 'translations' => Translations::getTranslationCodes(), + ]); + } + + public function update(int $projectId, int $languageId, Update $data, User $user): ServiceResultError | ServiceResultSuccess + { + $project = $this->projectRepository->getProjectById($projectId); + $language = $project?->languages()->firstWhere('id', $languageId); + + if (\is_null($project) || \is_null($language)) { + return $this->errNotFound(__('Not Found')); + } + + if ($user->cannot('update', ProjectTranslation::class)) { + return $this->errFobidden(__('Access is denied')); + } + + try { + DB::transaction(function () use ($data, $project, $language) { + $this->translationModelSyncCommand->execute($project, $language, $data->getTranslations()); + }); + $this->clearCacheCommandHandler->byTag(CacheTag::ProjectTranslation); + } catch (\Throwable $e) { + report($e); + return $this->errService(__('Server Error')); + } + + return $this->ok(__('Translations successfully updated')); + } +} diff --git a/app/application/app/Services/ProjectLanguage/ModelSyncCommand.php b/app/application/app/Services/ProjectLanguage/ModelSyncCommand.php index 51816fb..869363a 100644 --- a/app/application/app/Services/ProjectLanguage/ModelSyncCommand.php +++ b/app/application/app/Services/ProjectLanguage/ModelSyncCommand.php @@ -23,6 +23,8 @@ public function execute(Project $project, Languages $languages): void 'title' => $language->getTitle(), 'sort' => $language->getSort(), 'is_default' => $language->isDefault(), + 'system_lang' => $language->getSystemLang(), + 'iso_code' => $language->getIsoCode(), ]; if ($language->getId() !== null) { diff --git a/app/application/app/Services/ProjectTranslation/ModelSyncCommand.php b/app/application/app/Services/ProjectTranslation/ModelSyncCommand.php new file mode 100644 index 0000000..fce52d9 --- /dev/null +++ b/app/application/app/Services/ProjectTranslation/ModelSyncCommand.php @@ -0,0 +1,60 @@ +projectTranslationRepository->getProjectTranslationsWithTrashed($project->id, $language->id)->all(); + $insert = []; + foreach ($update->getTranslations() as $translation) { + /** @var Translation $translation */ + /** @var ProjectTranslation $modelTranslation */ + $modelTranslation = $modelTranslations->firstWhere('code', $translation->getCode()); + + if ($modelTranslation === null && $translation->getText() === null) { + continue; + } + + if ($modelTranslation === null && $translation->getText() !== null) { + $insert[] = [ + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + 'project_id' => $project->id, + 'language_id' => $language->id, + 'code' => $translation->getCode(), + 'text' => $translation->getText(), + ]; + continue; + } + + if ($modelTranslation !== null && $translation->getText() === null) { + $modelTranslation->delete(); + continue; + } + + if ($modelTranslation->trashed()) { + $modelTranslation->deleted_at = null; + } + $modelTranslation->text = $translation->getText(); + $modelTranslation->save(); + } + + if (!empty($insert)) { + ProjectTranslation::query()->insert($insert); + } + } +} diff --git a/app/application/app/Services/Service.php b/app/application/app/Services/Service.php index 44adf5e..7d77602 100644 --- a/app/application/app/Services/Service.php +++ b/app/application/app/Services/Service.php @@ -3,7 +3,6 @@ namespace App\Services; use App\Models\Project; -use App\Models\ProjectLanguage; use App\ServiceResults\ServiceResultArray; use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultSuccess; @@ -11,7 +10,6 @@ use App\ServiceResults\StoreUpdateResult; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Cache; abstract class Service { @@ -60,9 +58,9 @@ final protected function result(array $data = []): ServiceResultArray return new ServiceResultArray(data: $data); } - final protected function resultSitePage(Project $project, ProjectLanguage $language, array $data, bool $isTranslation): PagePossibleWithoutTranslation + final protected function resultSitePage(Project $project, WebsiteTranslations $websiteTranslations, array $data, bool $isTranslation): PagePossibleWithoutTranslation { - return new PagePossibleWithoutTranslation($project, $language, $data, $isTranslation); + return new PagePossibleWithoutTranslation($project, $websiteTranslations, $data, $isTranslation); } final protected function error(int $code, string $message, array $errors = []): ServiceResultError diff --git a/app/application/app/Services/Site/ProjectService.php b/app/application/app/Services/Site/ProjectService.php index 8cfb9c1..d39ff20 100644 --- a/app/application/app/Services/Site/ProjectService.php +++ b/app/application/app/Services/Site/ProjectService.php @@ -6,19 +6,18 @@ use App\Models\User; use App\Dto\Builder\Project as ProjectBuilderDto; use App\Repositories\ProjectContentRepository; -use App\Repositories\ProjectLanguageRepository; use App\Repositories\ProjectLinkRepository; use App\Repositories\ProjectRepository; use App\ServiceResults\ServiceResultArray; use App\ServiceResults\ServiceResultError; use App\ServiceResults\Site\PagePossibleWithoutTranslation; use App\Services\Service; +use App\Services\WebsiteTranslations; final class ProjectService extends Service { public function __construct( private readonly ProjectRepository $projectRepository, - private readonly ProjectLanguageRepository $projectLanguageRepository, private readonly ProjectContentRepository $projectContentRepository, private readonly ProjectLinkRepository $projectLinkRepository, ) { } @@ -39,7 +38,7 @@ public function getProjects(?User $user, array $with): ServiceResultError | Serv ]); } - public function getAboutByProject(Project $project, ?string $languageCode, ?User $user = null): ServiceResultError | PagePossibleWithoutTranslation + public function getAboutByProject(Project $project, WebsiteTranslations $websiteTranslations, ?User $user = null): ServiceResultError | PagePossibleWithoutTranslation { if ( $project->is_public === false @@ -47,17 +46,13 @@ public function getAboutByProject(Project $project, ?string $languageCode, ?User ) { return $this->errFobidden(__('Access is denied')); } - $language = $this->projectLanguageRepository->getProjectLanguageByCodeOrDefault($project, $languageCode); - if (!$language) { - return $this->errNotFound(__('Language not found')); - } $data = [ 'project' => $project, - 'language' => $language, - 'content' => $this->projectContentRepository->getContentByLanguageId($project->id, $language->id), - 'links' => $this->projectLinkRepository->getLinksByProject($project, $language->id), + 'websiteTranslations' => $websiteTranslations, + 'content' => $this->projectContentRepository->getContentByLanguageId($project->id, $websiteTranslations->getLanguage()->id), + 'links' => $this->projectLinkRepository->getLinksByProject($project, $websiteTranslations->getLanguage()->id), ]; - return $this->resultSitePage($project, $language, $data, \is_null($data['content'])); + return $this->resultSitePage($project, $websiteTranslations, $data, \is_null($data['content'])); } } diff --git a/app/application/app/Services/WebsiteTranslations.php b/app/application/app/Services/WebsiteTranslations.php new file mode 100644 index 0000000..3cf633f --- /dev/null +++ b/app/application/app/Services/WebsiteTranslations.php @@ -0,0 +1,23 @@ +transactions[$text] ?? __($text); + } + + public function getLanguage(): ProjectLanguage + { + return $this->language; + } +} diff --git a/app/application/app/View/Components/Site/ChooseLanguage.php b/app/application/app/View/Components/Site/ChooseLanguage.php index 58e2647..17c78b5 100644 --- a/app/application/app/View/Components/Site/ChooseLanguage.php +++ b/app/application/app/View/Components/Site/ChooseLanguage.php @@ -2,8 +2,9 @@ namespace app\View\Components\Site; -use App\Models\ProjectLanguage; -use Illuminate\Support\Collection; +use App\Enums\CacheTag; +use App\Models\Project; +use App\Services\WebsiteTranslations; use Illuminate\Support\Str; use Illuminate\View\Component; use Illuminate\View\View; @@ -11,20 +12,25 @@ final class ChooseLanguage extends Component { public function __construct( - private readonly ProjectLanguage $language, - private readonly Collection $languages, + private readonly WebsiteTranslations $websiteTranslations, + private readonly Project $project, ) { } public function render(): View { - $link = Str::of( request()->getRequestUri() )->rtrim('/'); - if ($link->endsWith('/language/' . $this->language->code)) { - $link = $link->replace('/language/' . $this->language->code, '', false); + $link = Str::of( request()->url() )->rtrim('/'); + if ($link->endsWith('/language/' . $this->websiteTranslations->getLanguage()->code)) { + $link = $link->replace('/language/' . $this->websiteTranslations->getLanguage()->code, '', false); } + $seconds = 3600 * 12; + $languages = CacheTag::Project->getCache()->remember(self::class . $this->project->id, $seconds, function () { + return $this->project->languages; + }); + return view('components.site.choose-language', [ - 'language' => $this->language, - 'languages' => $this->languages, + 'websiteTranslations' => $this->websiteTranslations, + 'languages' => $languages, 'link' => (string) $link, ]); } diff --git a/app/application/app/View/Components/Site/Layout.php b/app/application/app/View/Components/Site/Layout.php index a0f13f5..2a99cb4 100644 --- a/app/application/app/View/Components/Site/Layout.php +++ b/app/application/app/View/Components/Site/Layout.php @@ -4,7 +4,7 @@ use App\Enums\StorageType; use App\Models\Project; -use App\Models\ProjectLanguage; +use App\Services\WebsiteTranslations; use Illuminate\View\Component; use Illuminate\View\View; @@ -12,7 +12,7 @@ final class Layout extends Component { public function __construct( private readonly Project $project, - private readonly ProjectLanguage $language, + private readonly WebsiteTranslations $websiteTranslations, ) { } public function render(): View @@ -20,7 +20,7 @@ public function render(): View return view('layout.site', [ 'project' => $this->project, 'logo' => $this->project->getStorageOne(StorageType::Logo), - 'language' => $this->language, + 'websiteTranslations' => $this->websiteTranslations, ]); } } diff --git a/app/application/app/View/Components/Volt/Forms/Languages.php b/app/application/app/View/Components/Volt/Forms/Languages.php index a4c3e41..f422d6f 100644 --- a/app/application/app/View/Components/Volt/Forms/Languages.php +++ b/app/application/app/View/Components/Volt/Forms/Languages.php @@ -29,12 +29,18 @@ private function getValue(): array $default = (int) $default; } foreach ($value['items'] as $index => $lang) { + $systemLang = null; + if (!empty($lang['system_lang'])) { + $systemLang = (int) $lang['system_lang']; + } $langs[$index] = [ - 'title' => $lang['title'] ?? '', - 'code' => $lang['code'] ?? '', - 'is_default' => ($index === $default), - 'sort' => $lang['sort'] ?? '', - 'id' => $lang['id'] ?? null, + 'title' => $lang['title'] ?? '', + 'code' => $lang['code'] ?? '', + 'is_default' => ($index === $default), + 'sort' => $lang['sort'] ?? '', + 'system_lang' => $systemLang, + 'iso_code' => $lang['iso_code'] ?? null, + 'id' => $lang['id'] ?? null, ]; } diff --git a/app/application/bootstrap/app.php b/app/application/bootstrap/app.php index f877115..1f526b3 100644 --- a/app/application/bootstrap/app.php +++ b/app/application/bootstrap/app.php @@ -11,11 +11,8 @@ commands: __DIR__.'/../routes/console.php', health: '/up', then: function () { - Route::middleware([\App\Http\Middleware\ProjectDomain::class])->group(base_path('routes/web-project.php')); - Route::middleware([ - \App\Http\Middleware\ProjectDomain::class, - \App\Http\Middleware\Project::class, - ]) + Route::middleware(['web', \App\Http\Middleware\ProjectDomainAndLanguage::class])->group(base_path('routes/web-project.php')); + Route::middleware(['web', \App\Http\Middleware\ProjectAndLanguage::class]) ->prefix('project/{project}') ->as('project.') ->group(base_path('routes/web-project.php')); diff --git a/app/application/database/migrations/2024_04_18_144547_add_system_language_to_project_languages_table.php b/app/application/database/migrations/2024_04_18_144547_add_system_language_to_project_languages_table.php new file mode 100644 index 0000000..46f4f29 --- /dev/null +++ b/app/application/database/migrations/2024_04_18_144547_add_system_language_to_project_languages_table.php @@ -0,0 +1,30 @@ +unsignedInteger('system_lang')->nullable()->after('sort'); + $table->string('iso_code', 30)->nullable()->after('system_lang'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('project_languages', function (Blueprint $table) { + $table->dropColumn('system_lang'); + $table->dropColumn('iso_code'); + }); + } +}; diff --git a/app/application/database/migrations/2024_04_19_084614_create_project_translations.php b/app/application/database/migrations/2024_04_19_084614_create_project_translations.php new file mode 100644 index 0000000..867deb3 --- /dev/null +++ b/app/application/database/migrations/2024_04_19_084614_create_project_translations.php @@ -0,0 +1,36 @@ +id(); + $table->string('code'); + $table->text('text'); + $table->unsignedBigInteger('project_id')->index(); + $table->foreign('project_id')->references('id')->on('projects'); + $table->unsignedBigInteger('language_id')->index(); + $table->foreign('language_id')->references('id')->on('project_languages'); + $table->timestamps(); + $table->softDeletes(); + $table->index(['project_id', 'language_id', 'deleted_at']); + $table->unique(['project_id', 'language_id', 'code']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('project_translations'); + } +}; diff --git a/app/application/lang/en.json b/app/application/lang/en.json index 1e550c9..8376977 100644 --- a/app/application/lang/en.json +++ b/app/application/lang/en.json @@ -255,5 +255,6 @@ "The link was successfully updated": "The link was successfully updated", "The link has been deleted": "The link has been deleted", "Language not found": "Language not found", - "Project not found": "Project not found" + "Project not found": "Project not found", + "Translations successfully updated": "Translations successfully updated" } diff --git a/app/application/lang/en/admin-sections.php b/app/application/lang/en/admin-sections.php index 946a4c3..a4283b1 100644 --- a/app/application/lang/en/admin-sections.php +++ b/app/application/lang/en/admin-sections.php @@ -11,4 +11,5 @@ 'Languages' => 'Languages', 'Last update' => 'Last update', 'Links project' => 'Links from the project', + 'Translations' => 'Translations', ]; diff --git a/app/application/lang/en/permissions.php b/app/application/lang/en/permissions.php index 88b02d2..8800e50 100644 --- a/app/application/lang/en/permissions.php +++ b/app/application/lang/en/permissions.php @@ -15,4 +15,5 @@ 'Project' => 'Projects', 'ProjectContent' => 'About the project', 'ProjectLink' => 'Links from the project', + 'ProjectTranslation' => 'Translations', ]; diff --git a/app/application/lang/en/validation.php b/app/application/lang/en/validation.php index 11fdad1..90940c1 100644 --- a/app/application/lang/en/validation.php +++ b/app/application/lang/en/validation.php @@ -281,11 +281,15 @@ 'is_public' => 'public', 'http_host' => 'hostname', 'sort' => 'sorting', + 'system_lang' => 'system language', + 'iso_code' => 'ISO code', 'is_default' => 'default', 'language-code' => 'language code', 'languages.items.*.code' => 'language code', 'languages.items.*.title' => 'language title', 'languages.items.*.sort' => 'language sorting', + 'languages.items.*.system_lang' => 'system language', + 'languages.items.*.iso_code' => 'ISO code', 'languages.items.*.id' => 'language ID', 'languages.default' => 'default language', 'language-default' => 'default language', @@ -294,5 +298,8 @@ 'logo.delete' => 'remove logo', 'link' => 'link', 'language_id' => 'language', + 'translations' => 'translations', + 'translations.*.code' => 'translations code', + 'translations.*.text' => 'translations', ], ]; diff --git a/app/application/lang/ru.json b/app/application/lang/ru.json index c513d72..2f1f239 100644 --- a/app/application/lang/ru.json +++ b/app/application/lang/ru.json @@ -255,5 +255,6 @@ "The link was successfully updated": "Ссылка успешно обновлена", "The link has been deleted": "Ссылка удалена", "Language not found": "Язык не найден", - "Project not found": "Проект не найден" + "Project not found": "Проект не найден", + "Translations successfully updated": "Переводы успешно обновлены" } diff --git a/app/application/lang/ru/admin-sections.php b/app/application/lang/ru/admin-sections.php index 5ec768a..d8751b7 100644 --- a/app/application/lang/ru/admin-sections.php +++ b/app/application/lang/ru/admin-sections.php @@ -11,4 +11,5 @@ 'Languages' => 'Языки', 'Last update' => 'Последнее обновление', 'Links project' => 'Ссылки от проекта', + 'Translations' => 'Переводы', ]; diff --git a/app/application/lang/ru/permissions.php b/app/application/lang/ru/permissions.php index dfe8cd7..03d89bf 100644 --- a/app/application/lang/ru/permissions.php +++ b/app/application/lang/ru/permissions.php @@ -15,4 +15,5 @@ 'Project' => 'Проекты', 'ProjectContent' => 'О проекте', 'ProjectLink' => 'Ссылки от проекта', + 'ProjectTranslation' => 'Переводы', ]; diff --git a/app/application/lang/ru/validation.php b/app/application/lang/ru/validation.php index 5f48b83..d6865b9 100644 --- a/app/application/lang/ru/validation.php +++ b/app/application/lang/ru/validation.php @@ -282,10 +282,14 @@ 'http_host' => 'имя хоста', 'sort' => 'сортировка', 'is_default' => 'по умолчанию', + 'system_lang' => 'системный язык', + 'iso_code' => 'ISO-код', 'language-code' => 'код языка', 'languages.items.*.code' => 'код языка', 'languages.items.*.title' => 'заголовок языка', 'languages.items.*.sort' => 'языковая сортировка', + 'languages.items.*.system_lang' => 'системный язык', + 'languages.items.*.iso_code' => 'ISO-код', 'languages.items.*.id' => 'ID языка', 'languages.default' => 'язык по умолчанию', 'language-default' => 'язык по умолчанию', @@ -294,5 +298,8 @@ 'logo.delete' => 'удалить логотип', 'link' => 'ссылка', 'language_id' => 'язык', + 'translations' => 'переводы', + 'translations.*.code' => 'код перевода', + 'translations.*.text' => 'переводы', ], ]; diff --git a/app/application/resources/site/scss/app.scss b/app/application/resources/site/scss/app.scss index e032aa3..0b3f177 100644 --- a/app/application/resources/site/scss/app.scss +++ b/app/application/resources/site/scss/app.scss @@ -49,7 +49,7 @@ body { } .header_logo { grid-area: logo; - max-height: 50px; + height: 50px; a { text-decoration: none; @@ -67,6 +67,7 @@ body { padding: 5px 10px; border: 1px solid #0a0e17; border-radius: 4px; + white-space: nowrap; } } #mobile-menu { @@ -231,12 +232,24 @@ body.mobile-menu-open { grid-template-areas: "logo language"; grid-template-columns: 1fr 200px; } + .header_logo { + width: 150px; + margin-left: 12px; + margin-top: 10px; + margin-bottom: 10px; + } #mobile-menu { display: none; } .main-container { flex-direction: row; } + #language { + margin-right: 10px; + } + .language__block { + margin-top: 6px; + } .language__button { padding: 7px 10px 7px 0; } diff --git a/app/application/resources/views/admin/projects/links/edit.blade.php b/app/application/resources/views/admin/projects/links/edit.blade.php index 4d2bc86..189a52a 100644 --- a/app/application/resources/views/admin/projects/links/edit.blade.php +++ b/app/application/resources/views/admin/projects/links/edit.blade.php @@ -1,7 +1,7 @@ @section('meta_title', __('admin-sections.Links project')) @section('h1', __('admin-sections.Project') . ': ' . $project->name) - @include('admin.projects._top') + @include('admin.projects.links._top')
diff --git a/app/application/resources/views/admin/projects/show.blade.php b/app/application/resources/views/admin/projects/show.blade.php index 59e8ebc..fe9ecbe 100644 --- a/app/application/resources/views/admin/projects/show.blade.php +++ b/app/application/resources/views/admin/projects/show.blade.php @@ -36,6 +36,18 @@ @endcan + @can('viewAny', \App\Models\ProjectTranslation::class) + + + + + {{ __('admin-sections.Translations') }} + + + + @endcan
diff --git a/app/application/resources/views/admin/projects/translations/edit.blade.php b/app/application/resources/views/admin/projects/translations/edit.blade.php new file mode 100644 index 0000000..f3504d3 --- /dev/null +++ b/app/application/resources/views/admin/projects/translations/edit.blade.php @@ -0,0 +1,42 @@ +@section('meta_title', __('admin-sections.Translations')) +@section('h1', __('admin-sections.Project') . ': ' . $project->name) + +
+
+
+
+

{{ __('admin-sections.Translations') }}

+
+
+ + + + + + + + + @foreach($translations as $index => $code) + + + + + @endforeach + +
{{ __('validation.attributes.text') }}{{ __('admin-sections.Translations') }}
+ {{ __($code) }} + + + +
+ @canany(['update'], \App\Models\ProjectTranslation::class) + @csrf + + @endcanany +
+
+
+
+
+
+
diff --git a/app/application/resources/views/admin/projects/translations/languages.blade.php b/app/application/resources/views/admin/projects/translations/languages.blade.php new file mode 100644 index 0000000..d14db7e --- /dev/null +++ b/app/application/resources/views/admin/projects/translations/languages.blade.php @@ -0,0 +1,36 @@ +@section('meta_title', __('admin-sections.Translations')) +@section('h1', __('admin-sections.Project') . ': ' . $project->name) + +
+
+
+
+

{{ __('admin-sections.Translations') }}

+
+ + + + + + + + @foreach($project->languages as $language) + + + + @endforeach + +
{{ __('admin-sections.Languages') }}
+ + + + + {{ $language->title }} + +
+
+
+
+
+
+
diff --git a/app/application/resources/views/components/site/choose-language.blade.php b/app/application/resources/views/components/site/choose-language.blade.php index 9c15d25..d8aa52d 100644 --- a/app/application/resources/views/components/site/choose-language.blade.php +++ b/app/application/resources/views/components/site/choose-language.blade.php @@ -1,10 +1,10 @@
- - +
diff --git a/app/application/resources/views/site/page-without-translation.blade.php b/app/application/resources/views/site/page-without-translation.blade.php index a405f3b..1e10a74 100644 --- a/app/application/resources/views/site/page-without-translation.blade.php +++ b/app/application/resources/views/site/page-without-translation.blade.php @@ -1,5 +1,5 @@ -@section('h1', __('site.Page without translation')) +@section('h1', $websiteTranslations->translate('site.Page without translation')) - + diff --git a/app/application/resources/views/site/projects/about.blade.php b/app/application/resources/views/site/projects/about.blade.php index ff8225f..5f4a118 100644 --- a/app/application/resources/views/site/projects/about.blade.php +++ b/app/application/resources/views/site/projects/about.blade.php @@ -1,6 +1,6 @@ -@section('meta_title', __('site.Project') . ': ' . $content->title) +@section('meta_title', $websiteTranslations->translate('site.Project') . ': ' . $content->title) @section('h1', $content->title) - + {!! $content->description !!} diff --git a/app/application/routes/web.php b/app/application/routes/web.php index 4ae5654..48d8468 100644 --- a/app/application/routes/web.php +++ b/app/application/routes/web.php @@ -27,6 +27,10 @@ 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/{language}', [\App\Http\Controllers\Admin\Projects\TranslationsController::class, 'edit'])->name('translations.edit')->where(['language' => '[0-9]+']); + Route::post('translations/{language}', [\App\Http\Controllers\Admin\Projects\TranslationsController::class, 'update'])->name('translations.update')->where(['language' => '[0-9]+']); + })->where(['project' => '[0-9]+']); Route::post('languages/new-language', [\App\Http\Controllers\Admin\LanguagesController::class, 'newLanguage'])->name('new-language');