This commit is contained in:
Mark Gregory
2026-07-24 11:18:03 +01:00
parent 9825a0ab0e
commit d8d39d5810
24 changed files with 329 additions and 8 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
<?php
namespace Modules\Blog\Models;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow;
final class Blog extends Model { use HasWorkflow; protected $table='blog_posts'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'blog_post_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow; use Modules\Localization\Concerns\HasTranslations;
final class Blog extends Model { use HasTranslations, HasWorkflow; protected $table='blog_posts'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'blog_post_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
@@ -0,0 +1,27 @@
<?php
namespace Modules\Localization\Concerns;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Modules\Localization\Models\Locale;
use Modules\Localization\Models\LocalizedContent;
trait HasTranslations
{
public function translations(): MorphMany
{
return $this->morphMany(LocalizedContent::class, 'translatable');
}
public function translated(string $field, ?string $locale = null, mixed $fallback = null): mixed
{
$locale ??= app()->getLocale();
$translation = $this->translations->first(fn (LocalizedContent $item): bool => $item->field === $field && $item->locale?->code === $locale);
if ($translation && filled($translation->value)) { return $translation->value; }
return $fallback ?? $this->getAttribute($field);
}
public function setTranslation(string $field, string $localeCode, ?string $value, ?int $userId = null): LocalizedContent
{
$locale = Locale::query()->where('code', $localeCode)->firstOrFail();
return $this->translations()->updateOrCreate(
['locale_id' => $locale->id, 'field' => $field],
['value' => $value, 'updated_by' => $userId]
);
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
return [
'default_locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'session_key' => 'platform_locale',
'supported_subjects' => [
'pages' => ['model' => Modules\Pages\Models\Page::class, 'label' => 'Pages', 'fields' => ['title', 'summary', 'content', 'meta_title', 'meta_description']],
'blog' => ['model' => Modules\Blog\Models\Blog::class, 'label' => 'Blog', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']],
'news' => ['model' => Modules\News\Models\News::class, 'label' => 'News', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']],
],
];
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('locales', function (Blueprint $table): void {
$table->id();
$table->string('code', 20)->unique();
$table->string('name', 100);
$table->string('native_name', 100);
$table->string('regional_code', 20)->nullable();
$table->string('direction', 3)->default('ltr');
$table->string('date_format', 40)->default('d/m/Y');
$table->string('time_format', 40)->default('H:i');
$table->string('timezone', 80)->default('UTC');
$table->string('currency_code', 3)->nullable();
$table->string('currency_symbol', 10)->nullable();
$table->unsignedInteger('position')->default(0);
$table->boolean('is_default')->default(false);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('localized_contents', function (Blueprint $table): void {
$table->id();
$table->morphs('translatable');
$table->foreignId('locale_id')->constrained()->cascadeOnDelete();
$table->string('field', 100);
$table->longText('value')->nullable();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['translatable_type', 'translatable_id', 'locale_id', 'field'], 'localized_content_unique');
$table->index(['locale_id', 'field']);
});
}
public function down(): void
{
Schema::dropIfExists('localized_contents');
Schema::dropIfExists('locales');
}
};
@@ -0,0 +1,16 @@
<?php
namespace Modules\Localization\Database\Seeders;
use Illuminate\Database\Seeder;
use Modules\Localization\Models\Locale;
final class LocaleSeeder extends Seeder
{
public function run(): void
{
Locale::query()->updateOrCreate(['code' => 'en'], [
'name' => 'English', 'native_name' => 'English', 'regional_code' => 'en-GB',
'direction' => 'ltr', 'date_format' => 'd/m/Y', 'time_format' => 'H:i',
'timezone' => config('app.timezone', 'UTC'), 'currency_code' => 'GBP',
'currency_symbol' => '£', 'position' => 0, 'is_default' => true, 'is_active' => true,
]);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Modules\Localization\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Modules\Localization\Models\Locale;
use Modules\Localization\Services\LocaleManager;
final class LocaleController extends Controller
{
public function index(): View
{
abort_unless(auth()->user()->can('localization.manage'), 403);
$subjects = collect(config('localization.supported_subjects'))->map(function (array $config): array {
$model = $config['model'];
return $config + ['records' => $model::query()->latest()->limit(8)->get()];
});
return view('localization::admin.index', ['locales' => Locale::query()->orderBy('position')->get(), 'subjects' => $subjects]);
}
public function switch(string $locale, LocaleManager $manager): RedirectResponse
{
$manager->activate($locale);
return back();
}
public function store(Request $request): RedirectResponse
{
abort_unless($request->user()->can('localization.manage'), 403);
$data = $request->validate($this->rules());
if (($data['is_default'] ?? false) === true) { Locale::query()->update(['is_default' => false]); }
Locale::query()->create($data);
return back()->with('success', 'Locale created.');
}
public function update(Request $request, Locale $locale): RedirectResponse
{
abort_unless($request->user()->can('localization.manage'), 403);
$data = $request->validate($this->rules($locale));
if (($data['is_default'] ?? false) === true) { Locale::query()->whereKeyNot($locale->id)->update(['is_default' => false]); }
$locale->update($data);
return back()->with('success', 'Locale updated.');
}
public function destroy(Request $request, Locale $locale): RedirectResponse
{
abort_unless($request->user()->can('localization.manage'), 403);
abort_if($locale->is_default, 422, 'The default locale cannot be deleted.');
$locale->delete();
return back()->with('success', 'Locale deleted.');
}
private function rules(?Locale $locale = null): array
{
return [
'code' => ['required','string','max:20','unique:locales,code,'.($locale?->id ?? 'NULL')],
'name' => ['required','string','max:100'], 'native_name' => ['required','string','max:100'],
'regional_code' => ['nullable','string','max:20'], 'direction' => ['required','in:ltr,rtl'],
'date_format' => ['required','string','max:40'], 'time_format' => ['required','string','max:40'],
'timezone' => ['required','timezone'], 'currency_code' => ['nullable','string','size:3'],
'currency_symbol' => ['nullable','string','max:10'], 'position' => ['nullable','integer','min:0'],
'is_default' => ['sometimes','boolean'], 'is_active' => ['sometimes','boolean'],
];
}
}
@@ -0,0 +1,36 @@
<?php
namespace Modules\Localization\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Modules\Localization\Models\Locale;
final class TranslationController extends Controller
{
public function edit(string $subject, int $id): View
{
abort_unless(auth()->user()->can('localization.translate'), 403);
[$record, $config] = $this->resolve($subject, $id);
$record->load('translations.locale');
return view('localization::admin.translate', ['record' => $record, 'subject' => $subject, 'config' => $config, 'locales' => Locale::query()->active()->get()]);
}
public function update(Request $request, string $subject, int $id): RedirectResponse
{
abort_unless($request->user()->can('localization.translate'), 403);
[$record, $config] = $this->resolve($subject, $id);
$data = $request->validate(['translations' => ['array'], 'translations.*' => ['array']]);
foreach ($data['translations'] ?? [] as $localeCode => $fields) {
foreach ($config['fields'] as $field) {
$record->setTranslation($field, $localeCode, $fields[$field] ?? null, $request->user()->id);
}
}
return back()->with('success', 'Translations saved.');
}
private function resolve(string $subject, int $id): array
{
$config = config("localization.supported_subjects.$subject");
abort_unless(is_array($config), 404);
$record = $config['model']::query()->findOrFail($id);
return [$record, $config];
}
}
@@ -0,0 +1,19 @@
<?php
namespace Modules\Localization\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Modules\Localization\Models\Locale;
use Symfony\Component\HttpFoundation\Response;
final class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$code = $request->route('locale') ?: $request->session()->get(config('localization.session_key'));
if (! $code) { $code = Locale::query()->where('is_default', true)->value('code') ?: config('localization.default_locale'); }
if (Locale::query()->where('code', $code)->where('is_active', true)->exists()) {
app()->setLocale($code);
$request->session()->put(config('localization.session_key'), $code);
}
return $next($request);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Modules\Localization\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Locale extends Model
{
protected $fillable = ['code','name','native_name','regional_code','direction','date_format','time_format','timezone','currency_code','currency_symbol','position','is_default','is_active'];
protected function casts(): array { return ['is_default' => 'boolean', 'is_active' => 'boolean']; }
public function translations(): HasMany { return $this->hasMany(LocalizedContent::class); }
public function scopeActive(Builder $query): Builder { return $query->where('is_active', true)->orderBy('position')->orderBy('name'); }
}
@@ -0,0 +1,13 @@
<?php
namespace Modules\Localization\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
final class LocalizedContent extends Model
{
protected $fillable = ['locale_id','field','value','updated_by'];
public function translatable(): MorphTo { return $this->morphTo(); }
public function locale(): BelongsTo { return $this->belongsTo(Locale::class); }
public function editor(): BelongsTo { return $this->belongsTo(User::class, 'updated_by'); }
}
@@ -0,0 +1,18 @@
<?php
namespace Modules\Localization\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Localization\Services\LocaleManager;
final class LocalizationServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(module_path('Localization', 'Config/config.php'), 'localization');
$this->app->singleton(LocaleManager::class);
$this->app->register(RouteServiceProvider::class);
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path('Localization', 'Database/Migrations'));
$this->loadViewsFrom(module_path('Localization', 'Resources/views'), 'localization');
}
}
@@ -0,0 +1,12 @@
<?php
namespace Modules\Localization\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
Route::middleware('web')->group(module_path('Localization', 'Routes/web.php'));
}
}
@@ -0,0 +1 @@
<x-layouts.app :title="__('Localisation')"><main class="p-6 space-y-6"><div><flux:heading size="xl">Localisation</flux:heading><flux:text>Languages, regional formats and translated content.</flux:text></div><div class="grid gap-6 xl:grid-cols-3"><section class="xl:col-span-2 space-y-4"><flux:heading>Locales</flux:heading>@foreach($locales as $locale)<form method="POST" action="{{ route('administration.localization.locales.update',$locale) }}" class="rounded-xl border p-4 grid gap-3 md:grid-cols-4">@csrf @method('PUT')<flux:input name="code" label="Code" :value="$locale->code"/><flux:input name="name" label="Name" :value="$locale->name"/><flux:input name="native_name" label="Native name" :value="$locale->native_name"/><flux:input name="regional_code" label="Regional code" :value="$locale->regional_code"/><flux:select name="direction" label="Direction"><option value="ltr" @selected($locale->direction==='ltr')>LTR</option><option value="rtl" @selected($locale->direction==='rtl')>RTL</option></flux:select><flux:input name="timezone" label="Timezone" :value="$locale->timezone"/><flux:input name="currency_code" label="Currency" :value="$locale->currency_code"/><flux:input name="currency_symbol" label="Symbol" :value="$locale->currency_symbol"/><input type="hidden" name="date_format" value="{{ $locale->date_format }}"><input type="hidden" name="time_format" value="{{ $locale->time_format }}"><input type="hidden" name="position" value="{{ $locale->position }}"><label class="flex items-center gap-2"><input type="hidden" name="is_active" value="0"><input type="checkbox" name="is_active" value="1" @checked($locale->is_active)> Active</label><label class="flex items-center gap-2"><input type="hidden" name="is_default" value="0"><input type="checkbox" name="is_default" value="1" @checked($locale->is_default)> Default</label><div class="md:col-span-2 flex justify-end"><flux:button type="submit" variant="primary">Save</flux:button></div></form>@endforeach</section><section class="rounded-xl border p-5"><flux:heading>Add locale</flux:heading><form method="POST" action="{{ route('administration.localization.locales.store') }}" class="mt-4 space-y-3">@csrf<flux:input name="code" label="Code" placeholder="fr" required/><flux:input name="name" label="Name" placeholder="French" required/><flux:input name="native_name" label="Native name" placeholder="Français" required/><flux:input name="regional_code" label="Regional code" placeholder="fr-FR"/><flux:select name="direction" label="Direction"><option value="ltr">LTR</option><option value="rtl">RTL</option></flux:select><flux:input name="timezone" label="Timezone" value="UTC" required/><flux:input name="currency_code" label="Currency code"/><flux:input name="currency_symbol" label="Currency symbol"/><input type="hidden" name="date_format" value="d/m/Y"><input type="hidden" name="time_format" value="H:i"><input type="hidden" name="position" value="0"><input type="hidden" name="is_active" value="1"><flux:button type="submit" variant="primary">Add locale</flux:button></form></section></div><section class="space-y-4"><flux:heading>Translate content</flux:heading><div class="grid gap-4 lg:grid-cols-3">@foreach($subjects as $key=>$subject)<div class="rounded-xl border p-4"><div class="font-semibold">{{ $subject['label'] }}</div><div class="mt-3 space-y-2">@foreach($subject['records'] as $record)<a class="block rounded-lg border px-3 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800" href="{{ route('administration.localization.translations.edit',[$key,$record->id]) }}">{{ $record->title ?? ('#'.$record->id) }}</a>@endforeach</div></div>@endforeach</div></section></main></x-layouts.app>
@@ -0,0 +1 @@
<x-layouts.app :title="__('Translate content')"><main class="p-6 space-y-6"><div><flux:heading size="xl">Translate: {{ $record->title ?? ('#'.$record->id) }}</flux:heading><flux:text>{{ $config['label'] }} · source language values are shown for reference.</flux:text></div><form method="POST" action="{{ route('administration.localization.translations.update',[$subject,$record->id]) }}" class="space-y-6">@csrf @method('PUT')@foreach($locales as $locale)<section class="rounded-xl border p-5 space-y-4"><div class="flex items-center justify-between"><flux:heading>{{ $locale->native_name }}</flux:heading><span class="text-sm text-zinc-500">{{ $locale->code }}</span></div>@foreach($config['fields'] as $field)@php($existing=$record->translations->first(fn($item)=>$item->locale_id===$locale->id&&$item->field===$field)?->value)<div><div class="mb-1 text-sm font-medium">{{ str($field)->headline() }}</div><div class="mb-2 text-xs text-zinc-500">Source: {{ str(strip_tags((string)$record->getAttribute($field)))->limit(180) }}</div>@if(in_array($field,['content','summary','excerpt','meta_description']))<flux:textarea name="translations[{{ $locale->code }}][{{ $field }}]" rows="6">{{ $existing }}</flux:textarea>@else<flux:input name="translations[{{ $locale->code }}][{{ $field }}]" :value="$existing"/>@endif</div>@endforeach</section>@endforeach<div class="flex justify-end"><flux:button type="submit" variant="primary">Save translations</flux:button></div></form></main></x-layouts.app>
+13
View File
@@ -0,0 +1,13 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Localization\Http\Controllers\LocaleController;
use Modules\Localization\Http\Controllers\TranslationController;
Route::get('/locale/{locale}', [LocaleController::class, 'switch'])->name('localization.switch');
Route::middleware(['auth','verified'])->prefix('administration/localization')->name('administration.localization.')->group(function (): void {
Route::get('/', [LocaleController::class, 'index'])->name('index');
Route::post('/locales', [LocaleController::class, 'store'])->name('locales.store');
Route::put('/locales/{locale}', [LocaleController::class, 'update'])->name('locales.update');
Route::delete('/locales/{locale}', [LocaleController::class, 'destroy'])->name('locales.destroy');
Route::get('/translate/{subject}/{id}', [TranslationController::class, 'edit'])->name('translations.edit');
Route::put('/translate/{subject}/{id}', [TranslationController::class, 'update'])->name('translations.update');
});
@@ -0,0 +1,16 @@
<?php
namespace Modules\Localization\Services;
use Illuminate\Support\Collection;
use Modules\Localization\Models\Locale;
final class LocaleManager
{
public function active(): Collection { return Locale::query()->active()->get(); }
public function default(): ?Locale { return Locale::query()->where('is_default', true)->first() ?? Locale::query()->active()->first(); }
public function activate(string $code): Locale
{
$locale = Locale::query()->active()->where('code', $code)->firstOrFail();
app()->setLocale($locale->code);
session()->put(config('localization.session_key'), $locale->code);
return $locale;
}
}
@@ -0,0 +1,10 @@
<?php
namespace Modules\Localization\Tests\Feature;
use Tests\TestCase;
final class ModuleBootTest extends TestCase
{
public function test_localization_module_is_registered(): void
{
$this->assertTrue(app()->providerIsLoaded(\Modules\Localization\Providers\LocalizationServiceProvider::class));
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "Localization",
"alias": "localization",
"description": "Locales, translated content, regional settings and language switching.",
"keywords": ["localization", "translation", "locale", "language"],
"priority": 36,
"providers": ["Modules\\Localization\\Providers\\LocalizationServiceProvider"],
"files": []
}
+2 -2
View File
@@ -1,4 +1,4 @@
<?php
namespace Modules\News\Models;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow;
final class News extends Model { use HasWorkflow; protected $table='news_articles'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'news_article_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow; use Modules\Localization\Concerns\HasTranslations;
final class News extends Model { use HasTranslations, HasWorkflow; protected $table='news_articles'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'news_article_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
+2 -1
View File
@@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Modules\Workflow\Concerns\HasWorkflow;
use Modules\Localization\Concerns\HasTranslations;
final class Page extends Model
{
use HasFactory, HasWorkflow, SoftDeletes;
use HasFactory, HasTranslations, HasWorkflow, SoftDeletes;
protected $table = 'pages';
protected $fillable = ['title', 'slug', 'summary', 'content', 'status', 'published_at', 'is_home'];
@@ -53,6 +53,8 @@ final class RolePermissionSeeder extends Seeder
'workflow.view',
'workflow.manage',
'workflow.review',
'localization.manage',
'localization.translate',
];
foreach ($permissions as $permission) {
+1 -1
View File
@@ -12,7 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$middleware->web(append: [\Modules\Localization\Http\Middleware\SetLocale::class]);
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
+2 -1
View File
@@ -31,5 +31,6 @@
"Events": true,
"Careers": true,
"Locations": true,
"Workflow": true
"Workflow": true,
"Localization": true
}
@@ -34,7 +34,7 @@
<flux:sidebar.group :heading="__('Platform')" class="grid"><flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:sidebar.item><flux:sidebar.item icon="squares-2x2" :href="route('administration.dashboard')" :current="request()->routeIs('administration.dashboard')" wire:navigate>{{ __('Administration') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Workflow')" class="grid"><flux:sidebar.item icon="arrows-right-left" :href="route('administration.workflows.index')" :current="request()->routeIs('administration.workflows.*')" wire:navigate>{{ __('Review Queue') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Management')" class="grid"><flux:sidebar.item icon="users" :href="route('administration.users.index')" :current="request()->routeIs('administration.users.*')" wire:navigate>{{ __('Users') }}</flux:sidebar.item><flux:sidebar.item icon="shield-check" :href="route('administration.roles.index')" :current="request()->routeIs('administration.roles.*')" wire:navigate>{{ __('Roles & Permissions') }}</flux:sidebar.item><flux:sidebar.item icon="photo" :href="route('administration.media.index')" :current="request()->routeIs('administration.media.*')" wire:navigate>{{ __('Media') }}</flux:sidebar.item><flux:sidebar.item icon="magnifying-glass" :href="route('administration.search.index')" :current="request()->routeIs('administration.search.*')" wire:navigate>{{ __('Search') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Configuration')" class="grid"><flux:sidebar.item icon="globe-alt" :href="route('administration.seo.index')" :current="request()->routeIs('administration.seo.*')" wire:navigate>{{ __('SEO') }}</flux:sidebar.item><flux:sidebar.item icon="cog-6-tooth" :href="route('administration.settings.index')" :current="request()->routeIs('administration.settings.*')" wire:navigate>{{ __('Settings') }}</flux:sidebar.item><flux:sidebar.item icon="cube" :href="route('administration.modules.index')" :current="request()->routeIs('administration.modules.*')" wire:navigate>{{ __('Modules') }}</flux:sidebar.item><flux:sidebar.item icon="clipboard-document-list" :href="route('administration.audit.index')" :current="request()->routeIs('administration.audit.*')" wire:navigate>{{ __('Audit Log') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Configuration')" class="grid"><flux:sidebar.item icon="language" :href="route('administration.localization.index')" :current="request()->routeIs('administration.localization.*')" wire:navigate>{{ __('Localisation') }}</flux:sidebar.item><flux:sidebar.item icon="globe-alt" :href="route('administration.seo.index')" :current="request()->routeIs('administration.seo.*')" wire:navigate>{{ __('SEO') }}</flux:sidebar.item><flux:sidebar.item icon="cog-6-tooth" :href="route('administration.settings.index')" :current="request()->routeIs('administration.settings.*')" wire:navigate>{{ __('Settings') }}</flux:sidebar.item><flux:sidebar.item icon="cube" :href="route('administration.modules.index')" :current="request()->routeIs('administration.modules.*')" wire:navigate>{{ __('Modules') }}</flux:sidebar.item><flux:sidebar.item icon="clipboard-document-list" :href="route('administration.audit.index')" :current="request()->routeIs('administration.audit.*')" wire:navigate>{{ __('Audit Log') }}</flux:sidebar.item></flux:sidebar.group>
</flux:sidebar.nav><flux:spacer /><x-stylist-theme-common::desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" /></flux:sidebar>
<flux:header class="lg:hidden"><flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" /><flux:spacer /><flux:dropdown position="top" align="end"><flux:profile :initials="auth()->user()->initials()" icon-trailing="chevron-down" /><flux:menu><flux:menu.radio.group><div class="p-0 text-sm font-normal"><div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm"><flux:avatar :name="auth()->user()->name" :initials="auth()->user()->initials()" /><div class="grid flex-1 text-start text-sm leading-tight"><flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading><flux:text class="truncate">{{ auth()->user()->email }}</flux:text></div></div></div></flux:menu.radio.group><flux:menu.separator /><flux:menu.radio.group><flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item></flux:menu.radio.group><flux:menu.separator /><form method="POST" action="{{ route('logout') }}" class="w-full">@csrf<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full cursor-pointer" data-test="logout-button">{{ __('Log out') }}</flux:menu.item></form></flux:menu></flux:dropdown></flux:header>
{{ $slot }}