style updates

This commit is contained in:
Mark Gregory
2026-07-24 14:59:44 +01:00
parent 24ab771cd3
commit dd23205d0b
482 changed files with 7173 additions and 1435 deletions
@@ -1,24 +1,33 @@
<?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; }
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]
+8 -3
View File
@@ -1,11 +1,16 @@
<?php
use Modules\Blog\Models\Blog;
use Modules\News\Models\News;
use Modules\Pages\Models\Page;
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']],
'pages' => ['model' => Page::class, 'label' => 'Pages', 'fields' => ['title', 'summary', 'content', 'meta_title', 'meta_description']],
'blog' => ['model' => Blog::class, 'label' => 'Blog', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']],
'news' => ['model' => News::class, 'label' => 'News', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']],
],
];
@@ -1,7 +1,9 @@
<?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
@@ -35,6 +37,7 @@ return new class extends Migration
$table->index(['locale_id', 'field']);
});
}
public function down(): void
{
Schema::dropIfExists('localized_contents');
@@ -1,7 +1,10 @@
<?php
namespace Modules\Localization\Database\Seeders;
use Illuminate\Database\Seeder;
use Modules\Localization\Models\Locale;
final class LocaleSeeder extends Seeder
{
public function run(): void
@@ -1,11 +1,14 @@
<?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
@@ -13,48 +16,63 @@ final class LocaleController extends Controller
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]); }
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]); }
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'],
'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'],
];
}
}
@@ -1,10 +1,13 @@
<?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
@@ -12,8 +15,10 @@ final class TranslationController extends Controller
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);
@@ -24,13 +29,16 @@ final class TranslationController extends Controller
$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];
}
}
+19 -4
View File
@@ -1,12 +1,27 @@
<?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'); }
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');
}
}
@@ -1,13 +1,28 @@
<?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'); }
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');
}
}
@@ -1,7 +1,10 @@
<?php
namespace Modules\Localization\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Localization\Services\LocaleManager;
final class LocalizationServiceProvider extends ServiceProvider
{
public function register(): void
@@ -10,6 +13,7 @@ final class LocalizationServiceProvider extends ServiceProvider
$this->app->singleton(LocaleManager::class);
$this->app->register(RouteServiceProvider::class);
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path('Localization', 'Database/Migrations'));
@@ -1,7 +1,10 @@
<?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
+3 -1
View File
@@ -1,9 +1,11 @@
<?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::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');
@@ -1,16 +1,28 @@
<?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 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;
}
}
@@ -1,10 +1,14 @@
<?php
namespace Modules\Localization\Tests\Feature;
use Modules\Localization\Providers\LocalizationServiceProvider;
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));
$this->assertTrue(app()->providerIsLoaded(LocalizationServiceProvider::class));
}
}