Added the ability to dynamically translate on the project website.
This commit is contained in:
parent
a648ba3db9
commit
491249c8d8
@ -3,6 +3,7 @@
|
||||
namespace App\Dto\Service\Admin\Project;
|
||||
|
||||
use App\Dto\Service\Dto;
|
||||
use App\Enums\Lang;
|
||||
|
||||
final readonly class Language extends Dto
|
||||
{
|
||||
@ -11,7 +12,9 @@ public function __construct(
|
||||
private string $code,
|
||||
private int $sort,
|
||||
private bool $isDefault,
|
||||
private ?int $id,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Dto\Service\Admin\Project\Translation;
|
||||
|
||||
use App\Dto\Service\Dto;
|
||||
|
||||
final readonly class Translation extends Dto
|
||||
{
|
||||
public function __construct(
|
||||
private string $code,
|
||||
private ?string $text
|
||||
) { }
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function getText(): ?string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Dto\Service\Admin\Project\Translation;
|
||||
|
||||
use App\Exceptions\Dto\Admin\Project\Transaction\TranslationsException;
|
||||
|
||||
final class Translations
|
||||
{
|
||||
private array $translations = [];
|
||||
|
||||
public function addTranslation(string $code, ?string $text): void
|
||||
{
|
||||
if (!in_array($code, self::getTranslationCodes())) {
|
||||
throw new TranslationsException('Translation code "' . $code . '" not available');
|
||||
}
|
||||
$this->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',
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Dto\Service\Admin\Project\Translation;
|
||||
|
||||
use App\Dto\Service\Dto;
|
||||
|
||||
final readonly class Update extends Dto
|
||||
{
|
||||
public function __construct(
|
||||
private Translations $translations,
|
||||
) { }
|
||||
|
||||
public function getTranslations(): Translations
|
||||
{
|
||||
return $this->translations;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
enum CacheTag: string
|
||||
{
|
||||
case Project = 'project';
|
||||
case ProjectTranslation = 'project_translation';
|
||||
|
||||
public function getCache(): TaggedCache
|
||||
{
|
||||
|
@ -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()
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Dto\Admin\Project\Transaction;
|
||||
|
||||
final class TranslationsException extends \Exception
|
||||
{
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin\Projects;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Projects\Translations\UpdateRequest;
|
||||
use App\Services\Admin\Project\TranslationService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
final class TranslationsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TranslationService $translationService,
|
||||
) { }
|
||||
|
||||
public function languages(int $projectId, Request $request): View
|
||||
{
|
||||
$user = $request->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());
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ protected function viewPageWithoutTranslation(PagePossibleWithoutTranslation $re
|
||||
{
|
||||
return \view('site.page-without-translation', [
|
||||
'project' => $result->getProject(),
|
||||
'language' => $result->getLanguage(),
|
||||
'websiteTranslations' => $result->getWebsiteTranslations(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
$project = $request->get('project');
|
||||
$websiteTranslations = $request->get('websiteTranslations');
|
||||
|
||||
if (\is_null($request->project)) {
|
||||
if (!\is_null($language)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
43
app/application/app/Http/Middleware/ProjectLanguage.php
Normal file
43
app/application/app/Http/Middleware/ProjectLanguage.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\CacheTag;
|
||||
use App\Models\Project;
|
||||
use App\Repositories\ProjectLanguageRepository;
|
||||
use App\Repositories\ProjectRepository;
|
||||
use App\Repositories\ProjectTranslationRepository;
|
||||
use App\Services\WebsiteTranslations;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
abstract class ProjectLanguage
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly ProjectRepository $projectRepository,
|
||||
private readonly ProjectLanguageRepository $projectLanguageRepository,
|
||||
private readonly ProjectTranslationRepository $projectTranslationRepository
|
||||
) { }
|
||||
|
||||
protected function getWebsiteTranslations(Project $project, ?string $languageCode): ?WebsiteTranslations
|
||||
{
|
||||
$seconds = 3600 * 3;
|
||||
$language = CacheTag::Project->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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -0,0 +1,38 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Admin\Projects\Translations;
|
||||
|
||||
use App\Contracts\FormRequestDto;
|
||||
use App\Dto\Service\Admin\Project\Translation\Translation;
|
||||
use App\Dto\Service\Admin\Project\Translation\Translations;
|
||||
use App\Dto\Service\Admin\Project\Translation\Update;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
|
||||
final class UpdateRequest extends FormRequest implements FormRequestDto
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'translations' => ['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);
|
||||
}
|
||||
}
|
@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -45,6 +49,25 @@ protected function casts(): array
|
||||
return [
|
||||
'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();
|
||||
}
|
||||
}
|
||||
|
26
app/application/app/Models/ProjectTranslation.php
Normal file
26
app/application/app/Models/ProjectTranslation.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
final class ProjectTranslation extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'project_translations';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'language_id',
|
||||
'text',
|
||||
'code',
|
||||
];
|
||||
}
|
24
app/application/app/Policies/ProjectTranslationPolicy.php
Normal file
24
app/application/app/Policies/ProjectTranslationPolicy.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\ProjectTranslation;
|
||||
use App\Models\User;
|
||||
|
||||
final readonly class ProjectTranslationPolicy extends Policy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->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');
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\ProjectTranslation;
|
||||
use App\Services\Search\CreateSearchInstanceCommand;
|
||||
use App\Services\Search\Search;
|
||||
|
||||
final readonly class ProjectTranslationRepository
|
||||
{
|
||||
public function __construct(
|
||||
private CreateSearchInstanceCommand $createSearchInstanceCommand,
|
||||
) { }
|
||||
public function getProjectTranslations(int $projectId, int $languageId): Search
|
||||
{
|
||||
$query = ProjectTranslation::query()->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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Admin\Project;
|
||||
|
||||
use App\Dto\Service\Admin\Project\Translation\Translations;
|
||||
use App\Dto\Service\Admin\Project\Translation\Update;
|
||||
use App\Enums\CacheTag;
|
||||
use App\Models\ProjectTranslation;
|
||||
use App\Models\User;
|
||||
use App\Repositories\ProjectRepository;
|
||||
use App\Repositories\ProjectTranslationRepository;
|
||||
use App\ServiceResults\ServiceResultArray;
|
||||
use App\ServiceResults\ServiceResultError;
|
||||
use App\ServiceResults\ServiceResultSuccess;
|
||||
use App\Services\ClearCacheCommandHandler;
|
||||
use App\Services\ProjectTranslation\ModelSyncCommand;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class TranslationService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProjectRepository $projectRepository,
|
||||
private readonly ProjectTranslationRepository $projectTranslationRepository,
|
||||
private readonly ClearCacheCommandHandler $clearCacheCommandHandler,
|
||||
private readonly ModelSyncCommand $translationModelSyncCommand,
|
||||
) { }
|
||||
|
||||
public function languages(int $projectId, User $user): ServiceResultError | ServiceResultArray
|
||||
{
|
||||
$project = $this->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'));
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -0,0 +1,60 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\ProjectTranslation;
|
||||
|
||||
use App\Dto\Service\Admin\Project\Translation\Translation;
|
||||
use App\Dto\Service\Admin\Project\Translation\Translations;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectLanguage;
|
||||
use App\Models\ProjectTranslation;
|
||||
use App\Repositories\ProjectTranslationRepository;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
final readonly class ModelSyncCommand
|
||||
{
|
||||
public function __construct(
|
||||
private ProjectTranslationRepository $projectTranslationRepository,
|
||||
) { }
|
||||
|
||||
public function execute(Project $project, ProjectLanguage $language, Translations $update): void
|
||||
{
|
||||
$modelTranslations = $this->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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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']));
|
||||
}
|
||||
}
|
||||
|
23
app/application/app/Services/WebsiteTranslations.php
Normal file
23
app/application/app/Services/WebsiteTranslations.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ProjectLanguage;
|
||||
|
||||
final readonly class WebsiteTranslations
|
||||
{
|
||||
public function __construct(
|
||||
private ProjectLanguage $language,
|
||||
private array $transactions,
|
||||
) { }
|
||||
|
||||
public function translate(string $text): string
|
||||
{
|
||||
return $this->transactions[$text] ?? __($text);
|
||||
}
|
||||
|
||||
public function getLanguage(): ProjectLanguage
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -29,11 +29,17 @@ 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'] ?? '',
|
||||
'system_lang' => $systemLang,
|
||||
'iso_code' => $lang['iso_code'] ?? null,
|
||||
'id' => $lang['id'] ?? null,
|
||||
];
|
||||
}
|
||||
|
@ -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'));
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?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('project_languages', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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_translations', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
@ -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"
|
||||
}
|
||||
|
@ -11,4 +11,5 @@
|
||||
'Languages' => 'Languages',
|
||||
'Last update' => 'Last update',
|
||||
'Links project' => 'Links from the project',
|
||||
'Translations' => 'Translations',
|
||||
];
|
||||
|
@ -15,4 +15,5 @@
|
||||
'Project' => 'Projects',
|
||||
'ProjectContent' => 'About the project',
|
||||
'ProjectLink' => 'Links from the project',
|
||||
'ProjectTranslation' => 'Translations',
|
||||
];
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
|
@ -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": "Переводы успешно обновлены"
|
||||
}
|
||||
|
@ -11,4 +11,5 @@
|
||||
'Languages' => 'Языки',
|
||||
'Last update' => 'Последнее обновление',
|
||||
'Links project' => 'Ссылки от проекта',
|
||||
'Translations' => 'Переводы',
|
||||
];
|
||||
|
@ -15,4 +15,5 @@
|
||||
'Project' => 'Проекты',
|
||||
'ProjectContent' => 'О проекте',
|
||||
'ProjectLink' => 'Ссылки от проекта',
|
||||
'ProjectTranslation' => 'Переводы',
|
||||
];
|
||||
|
@ -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' => 'переводы',
|
||||
],
|
||||
];
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
@section('meta_title', __('admin-sections.Links project'))
|
||||
@section('h1', __('admin-sections.Project') . ': ' . $project->name)
|
||||
<x-admin.layout>
|
||||
@include('admin.projects._top')
|
||||
@include('admin.projects.links._top')
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card border-0 shadow components-section">
|
||||
|
@ -36,6 +36,18 @@
|
||||
</td>
|
||||
</tr>
|
||||
@endcan
|
||||
@can('viewAny', \App\Models\ProjectTranslation::class)
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ route('admin.projects.translations.languages', ['project' => $project->id]) }}" class="fw-bold">
|
||||
<svg width="16" height="16" data-slot="icon" class="align-text-top" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"></path>
|
||||
</svg>
|
||||
{{ __('admin-sections.Translations') }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endcan
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -0,0 +1,42 @@
|
||||
@section('meta_title', __('admin-sections.Translations'))
|
||||
@section('h1', __('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">
|
||||
<h3 id="category" class="mb-4">{{ __('admin-sections.Translations') }}</h3>
|
||||
<div class="table-responsive">
|
||||
<form method="post" action="{{ route('admin.projects.translations.update', ['project' => $project->id, 'language' => $language->id]) }}">
|
||||
<table class="table table-centered table-nowrap mb-0 rounded">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th class="border-0" style="width: 250px;">{{ __('validation.attributes.text') }}</th>
|
||||
<th class="border-0">{{ __('admin-sections.Translations') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($translations as $index => $code)
|
||||
<tr>
|
||||
<td>
|
||||
{{ __($code) }}
|
||||
</td>
|
||||
<td>
|
||||
<x-volt.forms.input-type-hidden :name="'translations[' . $index . '][code]'" :value="$code" />
|
||||
<x-volt.forms.input title="" :name="'translations[' . $index . '][text]'" :value="$projectTranslations[$code] ?? null" />
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@canany(['update'], \App\Models\ProjectTranslation::class)
|
||||
@csrf
|
||||
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
|
||||
@endcanany
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.layout>
|
@ -0,0 +1,36 @@
|
||||
@section('meta_title', __('admin-sections.Translations'))
|
||||
@section('h1', __('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">
|
||||
<h3 id="category" class="mb-4">{{ __('admin-sections.Translations') }}</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-centered table-nowrap mb-0 rounded">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th class="border-0">{{ __('admin-sections.Languages') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($project->languages as $language)
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ route('admin.projects.translations.edit', ['project' => $project->id, 'language' => $language->id]) }}" class="fw-bold">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{{ $language->title }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.layout>
|
@ -1,10 +1,10 @@
|
||||
<div id="language">
|
||||
<div class="language__block">
|
||||
<button class="language__button" type="button" aria-label="{{ __('site.Choose language') }}">
|
||||
<button class="language__button" type="button" aria-label="{{ $websiteTranslations->translate('site.Choose language') }}">
|
||||
<svg width="16" height="16" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"></path>
|
||||
</svg>
|
||||
{{ $language->title }}
|
||||
{{ $websiteTranslations->getLanguage()->title }}
|
||||
<svg width="16" height="16" class="language__button__str" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"></path>
|
||||
</svg>
|
||||
|
@ -1,9 +1,11 @@
|
||||
<div class="mb-4">
|
||||
@if(!empty($title) || !empty($example))
|
||||
<label for="form-input-{{ $requestName }}">{{ $title }}
|
||||
@if(!empty($example))
|
||||
<span class="label__example">({{ __('example:') }} {!! $example !!})</span>
|
||||
@endif
|
||||
</label>
|
||||
@endif
|
||||
<input id="form-input-{{ $requestName }}" class="form-control @error($requestName) is-invalid @enderror" name="{{ $name }}" type="{{ $type }}" @if($type !== 'password') value="{{ $value }}" @endif {{ $attributes }}>
|
||||
@error($requestName)
|
||||
<span class="invalid-feedback">{{ $message }}</span>
|
||||
|
@ -6,8 +6,10 @@
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th class="border-0 rounded-start">{{ __('validation.attributes.title') }}</th>
|
||||
<th class="border-0">{{ __('validation.attributes.code') }}</th>
|
||||
<th class="border-0">{{ __('validation.attributes.sort') }}</th>
|
||||
<th class="border-0" style="width: 200px;">{{ __('validation.attributes.code') }}</th>
|
||||
<th class="border-0" style="width: 140px;">{{ __('validation.attributes.sort') }}</th>
|
||||
<th class="border-0" style="width: 120px;">{{ __('validation.attributes.iso_code') }}</th>
|
||||
<th class="border-0">{{ __('validation.attributes.system_lang') }}</th>
|
||||
<th class="border-0">{{ __('validation.attributes.is_default') }}</th>
|
||||
<th class="border-0 rounded-end"></th>
|
||||
</tr>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<input class="form-control @error(\App\Helpers\Helpers::formatAttributeNameToRequestName($name)) is-invalid @enderror" name="{{ $name }}" type="{{ $type }}" value="{{ $value }}" required>
|
||||
<input class="form-control @error(\App\Helpers\Helpers::formatAttributeNameToRequestName($name)) is-invalid @enderror" name="{{ $name }}" type="{{ $type }}" value="{{ $value }}" @if($required) required @endif>
|
||||
@error(\App\Helpers\Helpers::formatAttributeNameToRequestName($name))
|
||||
<span class="invalid-feedback">{{ $message }}</span>
|
||||
@enderror
|
||||
|
@ -6,6 +6,7 @@
|
||||
'value' => $lang['title'],
|
||||
'name' => $name . '[items][' .$index . '][title]',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
])
|
||||
</td>
|
||||
<td>
|
||||
@ -13,6 +14,7 @@
|
||||
'value' => $lang['code'],
|
||||
'name' => $name . '[items][' . $index . '][code]',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
])
|
||||
</td>
|
||||
<td>
|
||||
@ -20,6 +22,21 @@
|
||||
'value' => $lang['sort'],
|
||||
'name' => $name . '[items][' . $index . '][sort]',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
])
|
||||
</td>
|
||||
<td>
|
||||
@include('components.volt.forms.languages.input', [
|
||||
'value' => $lang['iso_code'] ?? null,
|
||||
'name' => $name . '[items][' . $index . '][iso_code]',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
])
|
||||
</td>
|
||||
<td>
|
||||
@include('components.volt.forms.languages.system_lang', [
|
||||
'value' => $lang['system_lang'] ?? null,
|
||||
'name' => $name . '[items][' . $index . '][system_lang]',
|
||||
])
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
|
@ -0,0 +1,9 @@
|
||||
<select class="form-select @error(\App\Helpers\Helpers::formatAttributeNameToRequestName($name)) is-invalid @enderror" name="{{ $name }}">
|
||||
<option value=""></option>
|
||||
@foreach(\App\Enums\Lang::cases() as $lang)
|
||||
<option @if($lang->value === $value) selected @endif value="{{ $lang->value }}">{{ $lang->getTitle() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error(\App\Helpers\Helpers::formatAttributeNameToRequestName($name))
|
||||
<span class="invalid-feedback">{{ $message }}</span>
|
||||
@enderror
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ $language->code }}">
|
||||
<html lang="{{ $websiteTranslations->getLanguage()->attribute_lang }}">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
@ -14,7 +14,7 @@
|
||||
<div class="wrapper">
|
||||
<header class="header">
|
||||
<div class="header_logo">
|
||||
<a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $language) }}">
|
||||
<a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $websiteTranslations->getLanguage()) }}">
|
||||
@if($logo)
|
||||
<img src="{{ $logo->url }}" alt="{{ $project->name }}">
|
||||
@else
|
||||
@ -22,7 +22,7 @@
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
<button id="mobile-menu" type="button" aria-label="{{ __('site.Menu') }}">
|
||||
<button id="mobile-menu" type="button" aria-label="{{ $websiteTranslations->translate('site.Menu') }}">
|
||||
<svg class="open" width="45" height="45" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
|
||||
</svg>
|
||||
@ -30,13 +30,13 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<x-site.choose-language :language="$language" :languages="$project->languages" />
|
||||
<x-site.choose-language :websiteTranslations="$websiteTranslations" :project="$project" />
|
||||
</header>
|
||||
<div class="main-container">
|
||||
<nav id="menu">
|
||||
<div class="menu__title">{{ __('site.Menu') }}</div>
|
||||
<ul>
|
||||
<li><a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $language) }}" @class(['active' => request()->route()->named(['home', 'language.home'])])>{{ __('site.About project') }}</a></li>
|
||||
<li><a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['home', 'language.home', 'project.home', 'project.language.home'])])>{{ $websiteTranslations->translate('site.About project') }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="section-container">
|
||||
@ -45,7 +45,7 @@
|
||||
{{ $slot }}
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<a href="https://git.kor-elf.net/kor-elf/my-projects-website" target="_blank">{{ __('site.Powered by service') }}</a>
|
||||
<a href="https://git.kor-elf.net/kor-elf/my-projects-website" target="_blank">{{ $websiteTranslations->translate('site.Powered by service') }}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
@section('h1', __('site.Page without translation'))
|
||||
@section('h1', $websiteTranslations->translate('site.Page without translation'))
|
||||
|
||||
<x-site.layout :project="$project" :language="$language">
|
||||
<x-site.layout :project="$project" :websiteTranslations="$websiteTranslations">
|
||||
|
||||
</x-site.layout>
|
||||
|
@ -1,6 +1,6 @@
|
||||
@section('meta_title', __('site.Project') . ': ' . $content->title)
|
||||
@section('meta_title', $websiteTranslations->translate('site.Project') . ': ' . $content->title)
|
||||
@section('h1', $content->title)
|
||||
|
||||
<x-site.layout :project="$project" :language="$language">
|
||||
<x-site.layout :project="$project" :websiteTranslations="$websiteTranslations">
|
||||
{!! $content->description !!}
|
||||
</x-site.layout>
|
||||
|
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user