move into sonardesk
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?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]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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' => 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']],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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,19 @@
|
||||
<?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,78 @@
|
||||
<?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,44 @@
|
||||
<?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,53 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Localization\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Localization\Models\Locale;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class SetLocale
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$sessionKey = (string) config('localization.session_key', 'locale');
|
||||
$fallback = (string) config('localization.default_locale', config('app.locale', 'en'));
|
||||
|
||||
$code = $request->route('locale');
|
||||
|
||||
if (! $code && $request->hasSession()) {
|
||||
$code = $request->session()->get($sessionKey);
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('locales')) {
|
||||
app()->setLocale(is_string($code) && $code !== '' ? $code : $fallback);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (! $code) {
|
||||
$code = Locale::query()
|
||||
->where('is_default', true)
|
||||
->value('code') ?: $fallback;
|
||||
}
|
||||
|
||||
$isActive = Locale::query()
|
||||
->where('code', $code)
|
||||
->where('is_active', true)
|
||||
->exists();
|
||||
|
||||
if ($isActive) {
|
||||
app()->setLocale((string) $code);
|
||||
|
||||
if ($request->hasSession()) {
|
||||
$request->session()->put($sessionKey, $code);
|
||||
}
|
||||
} else {
|
||||
app()->setLocale($fallback);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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,15 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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,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 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,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(LocalizationServiceProvider::class));
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
Reference in New Issue
Block a user