diff --git a/app/Console/Commands/UpdateTranslationsCommand.php b/app/Console/Commands/UpdateTranslationsCommand.php index dd104766..e5d11469 100644 --- a/app/Console/Commands/UpdateTranslationsCommand.php +++ b/app/Console/Commands/UpdateTranslationsCommand.php @@ -4,11 +4,11 @@ namespace App\Console\Commands; +use App\Models\Language; use App\Models\LanguageLine; use Illuminate\Console\Command; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; -use Symfony\Component\Finder\SplFileInfo; use Throwable; class UpdateTranslationsCommand extends Command @@ -18,7 +18,10 @@ class UpdateTranslationsCommand extends Command * * @var string */ - protected $signature = 'wf:translations {--force : Overwrite existing translations}'; + protected $signature = 'wf:translations + {--force : Remove existing translations before running} + {--locale=* : Locale to load} + '; /** * The console command description. @@ -41,27 +44,36 @@ class UpdateTranslationsCommand extends Command */ public function handle() { - collect(File::files(lang_path())) - ->each(function (SplFileInfo $file) { - $language = $file->getFilenameWithoutExtension(); + $locales = collect($this->option('locale')); + + Language::all() + ->filter(function (Language $language) use ($locales) { + return $locales->isEmpty() || $locales->contains($language->code); + }) + ->each(function (Language $language) { + $file = lang_path($language->code . '.json'); + + if (! file_exists($file)) { + return; + } try { $lines = json_decode( - File::get($file->getPathname()), + File::get($file), associative: true, flags: \JSON_THROW_ON_ERROR ); } catch (Throwable $th) { - $this->warn("Could not fetch the contents of {$file->getFilename()}. Skipping..."); + $this->warn("Could not fetch the contents of {$file}. Skipping..."); $lines = []; } collect($lines) ->each(function ($line, $key) use ($language) { - $this->lines[$key][$language] = $line; + $this->lines[$key][$language->code] = $line; }); - $this->info("[$language] Fetched " . \count($lines) . ' translations.'); + $this->info("[$language->code] Fetched " . \count($lines) . ' translations.'); }); LanguageLine::withoutEvents( @@ -85,7 +97,9 @@ protected function updateEverything(): void ]) ->all(); - LanguageLine::upsert($values, ['group', 'key'], ['text']); + LanguageLine::truncate(); + + LanguageLine::insert($values); $this->info('Replaced all database translations.'); } @@ -94,24 +108,26 @@ protected function updateMissing(): void { $existingLines = LanguageLine::query() ->where('group', '*') - ->pluck('key'); + ->get(); $newLines = collect(); foreach ($this->lines as $key => $text) { - if ($existingLines->contains($key)) { + $existingLine = $existingLines->firstWhere('key', $key); + + if (! \is_null($existingLine) && \array_key_exists($key, $existingLine->text)) { continue; } $newLines->push([ 'group' => '*', 'key' => $key, - 'text' => json_encode($text), + 'text' => json_encode(array_merge($existingLine->text, $text)), ]); } if ($newLines->isNotEmpty()) { - LanguageLine::insert($newLines->all()); + LanguageLine::upsert($newLines->all(), ['group', 'key'], ['text']); $this->info("Added {$newLines->count()} missing database translations."); } else { diff --git a/app/Exceptions/LanguageNotInISO639.php b/app/Exceptions/LanguageNotInISO639.php new file mode 100644 index 00000000..72dd66c2 --- /dev/null +++ b/app/Exceptions/LanguageNotInISO639.php @@ -0,0 +1,15 @@ +getLocale(), + default_locale(), ]; foreach ($locales as $locale) { diff --git a/app/Http/Controllers/Admin/LanguageController.php b/app/Http/Controllers/Admin/LanguageController.php index 69ba0af4..a853825a 100644 --- a/app/Http/Controllers/Admin/LanguageController.php +++ b/app/Http/Controllers/Admin/LanguageController.php @@ -4,14 +4,17 @@ namespace App\Http\Controllers\Admin; +use App\Console\Commands\UpdateTranslationsCommand; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\LanguageRequest; use App\Http\Resources\Collections\LanguageCollection; use App\Http\Resources\LanguageResource; use App\Models\Language; use App\Models\LanguageLine; +use App\Services\ISO_639_1; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\Artisan; use Inertia\Inertia; use Inertia\Response; @@ -19,7 +22,7 @@ class LanguageController extends Controller { public function lines(?string $locale = null): JsonResponse { - $locale = locales()->has($locale) ? $locale : config('app.fallback_locale'); + $locale = locales()->has($locale) ? $locale : default_locale(); return response()->json( LanguageLine::getTranslationsForGroup($locale, '*') @@ -39,7 +42,9 @@ public function index(): Response public function create(): Response { return Inertia::render('Languages/Edit', [ - 'source' => LanguageLine::getTranslationsForGroup(config('app.fallback_locale'), '*'), + 'source' => LanguageLine::getTranslationsForGroup(default_locale(), '*'), + 'languages' => ISO_639_1::getCombinedLanguageOptions() + ->reject(fn ($_, string $code) => locales()->has($code)), ])->model(Language::class); } @@ -47,17 +52,38 @@ public function store(LanguageRequest $request): RedirectResponse { $attributes = $request->validated(); + // Not accepting user input during language creation + unset($attributes['lines']); + $language = Language::create($attributes); + Artisan::call(UpdateTranslationsCommand::class, [ + '--locale' => $language->code, + ]); + return redirect()->route('admin.languages.edit', $language) ->with('success', __('language.event.created')); } + public function restore(): RedirectResponse + { + $this->authorize('create', Language::class); + + Artisan::call(UpdateTranslationsCommand::class, [ + '--force' => true, + ]); + + return redirect()->route('admin.languages.index') + ->with('success', __('language.event.restored')); + } + public function edit(Language $language): Response { return Inertia::render('Languages/Edit', [ 'resource' => LanguageResource::make($language), - 'source' => LanguageLine::getTranslationsForGroup(config('app.fallback_locale'), '*'), + 'source' => LanguageLine::getTranslationsForGroup(default_locale(), '*'), + 'languages' => ISO_639_1::getCombinedLanguageOptions() + ->reject(fn ($_, string $code) => locales()->has($code) || $code === $language->code), ])->model(Language::class); } diff --git a/app/Http/Requests/Admin/LanguageRequest.php b/app/Http/Requests/Admin/LanguageRequest.php index ef070105..e06c6c81 100644 --- a/app/Http/Requests/Admin/LanguageRequest.php +++ b/app/Http/Requests/Admin/LanguageRequest.php @@ -4,6 +4,7 @@ namespace App\Http\Requests\Admin; +use App\Services\ISO_639_1; use Illuminate\Foundation\Http\FormRequest as BaseRequest; use Illuminate\Validation\Rule; @@ -17,11 +18,12 @@ class LanguageRequest extends BaseRequest public function rules(): array { return [ - 'code' => ['required', 'size:2', - Rule::unique('languages', 'code')->ignore($this->code, 'code'), - + 'code' => [ + 'required', + 'size:2', + Rule::unique('languages', 'code')->ignore($this->language), + Rule::in(ISO_639_1::getLanguageCodes()), ], - 'name' => ['required', 'string'], 'enabled' => ['boolean'], 'lines' => ['array'], ]; diff --git a/app/Http/Resources/LanguageResource.php b/app/Http/Resources/LanguageResource.php index 7a0049e6..0dc894b2 100644 --- a/app/Http/Resources/LanguageResource.php +++ b/app/Http/Resources/LanguageResource.php @@ -16,6 +16,7 @@ protected function default(Request $request): array 'code' => $this->code, 'name' => $this->name, 'enabled' => $this->enabled, + 'direction' => $this->direction, 'lines' => LanguageLine::getTranslationsForGroup($this->code, '*'), ]; } diff --git a/app/Models/Language.php b/app/Models/Language.php index 643ce9ce..69748661 100644 --- a/app/Models/Language.php +++ b/app/Models/Language.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Services\ISO_639_1; use App\Traits\ClearsResponseCache; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -35,7 +36,6 @@ class Language extends Model protected $fillable = [ 'code', - 'name', 'enabled', 'lines', ]; @@ -66,4 +66,19 @@ public function scopeWhereEnabled(Builder $query): Builder { return $query->where('enabled', true); } + + public function getNameAttribute(): string + { + return ISO_639_1::getCombinedLanguageName($this->code); + } + + public function getNativeNameAttribute(): string + { + return ISO_639_1::getNativeLanguageName($this->code); + } + + public function getDirectionAttribute(): string + { + return ISO_639_1::getLanguageDirection($this->code); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 4bf3e1f2..ead7614f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -97,7 +97,7 @@ protected static function booted(): void */ public function preferredLocale(): string { - return $this->locale ?? config('app.fallback_locale'); + return $this->locale ?? default_locale(); } public function hasSetPassword(): bool diff --git a/app/Providers/LanguageServiceProvider.php b/app/Providers/LanguageServiceProvider.php index 97b2272e..779df581 100644 --- a/app/Providers/LanguageServiceProvider.php +++ b/app/Providers/LanguageServiceProvider.php @@ -19,7 +19,12 @@ public function register() $this->app->singleton('languages', function () { return Language::all() ->mapWithKeys(fn (Language $language) => [ - $language->code => $language->only(['name', 'enabled']), + $language->code => $language->only([ + 'name', + 'nativeName', + 'enabled', + 'direction', + ]), ]); }); } diff --git a/app/Services/ISO_639_1.php b/app/Services/ISO_639_1.php new file mode 100644 index 00000000..1f098d9e --- /dev/null +++ b/app/Services/ISO_639_1.php @@ -0,0 +1,1033 @@ + [ + 'name' => 'Afar', + 'nativeName' => 'Afaraf', + 'direction' => 'ltr', + ], + 'ab' => [ + 'name' => 'Abkhaz', + 'nativeName' => 'аҧсуа бызшәа, аҧсшәа', + 'direction' => 'ltr', + ], + 'ae' => [ + 'name' => 'Avestan', + 'nativeName' => 'avesta', + 'direction' => 'ltr', + ], + 'af' => [ + 'name' => 'Afrikaans', + 'nativeName' => 'Afrikaans', + 'direction' => 'ltr', + ], + 'ak' => [ + 'name' => 'Akan', + 'nativeName' => 'Akan', + 'direction' => 'ltr', + ], + 'am' => [ + 'name' => 'Amharic', + 'nativeName' => 'አማርኛ', + 'direction' => 'ltr', + ], + 'an' => [ + 'name' => 'Aragonese', + 'nativeName' => 'aragonés', + 'direction' => 'ltr', + ], + 'ar' => [ + 'name' => 'Arabic', + 'nativeName' => 'العربية', + 'direction' => 'rtl', + ], + 'as' => [ + 'name' => 'Assamese', + 'nativeName' => 'অসমীয়া', + 'direction' => 'ltr', + ], + 'av' => [ + 'name' => 'Avaric', + 'nativeName' => 'авар мацӀ, магӀарул мацӀ', + 'direction' => 'ltr', + ], + 'ay' => [ + 'name' => 'Aymara', + 'nativeName' => 'aymar aru', + 'direction' => 'ltr', + ], + 'az' => [ + 'name' => 'Azerbaijani', + 'nativeName' => 'azərbaycan dili', + 'direction' => 'ltr', + ], + 'ba' => [ + 'name' => 'Bashkir', + 'nativeName' => 'башҡорт теле', + 'direction' => 'ltr', + ], + 'be' => [ + 'name' => 'Belarusian', + 'nativeName' => 'беларуская мова', + 'direction' => 'ltr', + ], + 'bg' => [ + 'name' => 'Bulgarian', + 'nativeName' => 'български език', + 'direction' => 'ltr', + ], + 'bh' => [ + 'name' => 'Bihari', + 'nativeName' => 'भोजपुरी', + 'direction' => 'ltr', + ], + 'bi' => [ + 'name' => 'Bislama', + 'nativeName' => 'Bislama', + 'direction' => 'ltr', + ], + 'bm' => [ + 'name' => 'Bambara', + 'nativeName' => 'bamanankan', + 'direction' => 'ltr', + ], + 'bn' => [ + 'name' => 'Bengali, Bangla', + 'nativeName' => 'বাংলা', + 'direction' => 'ltr', + ], + 'bo' => [ + '639-2/B' => 'tib', + 'name' => 'Tibetan Standard, Tibetan, Central', + 'nativeName' => 'བོད་ཡིག', + 'direction' => 'ltr', + ], + 'br' => [ + 'name' => 'Breton', + 'nativeName' => 'brezhoneg', + 'direction' => 'ltr', + ], + 'bs' => [ + 'name' => 'Bosnian', + 'nativeName' => 'bosanski jezik', + 'direction' => 'ltr', + ], + 'ca' => [ + 'name' => 'Catalan', + 'nativeName' => 'català', + 'direction' => 'ltr', + ], + 'ce' => [ + 'name' => 'Chechen', + 'nativeName' => 'нохчийн мотт', + 'direction' => 'ltr', + ], + 'ch' => [ + 'name' => 'Chamorro', + 'nativeName' => 'Chamoru', + 'direction' => 'ltr', + ], + 'co' => [ + 'name' => 'Corsican', + 'nativeName' => 'corsu, lingua corsa', + 'direction' => 'ltr', + ], + 'cr' => [ + 'name' => 'Cree', + 'nativeName' => 'ᓀᐦᐃᔭᐍᐏᐣ', + 'direction' => 'ltr', + ], + 'cs' => [ + '639-2/B' => 'cze', + 'name' => 'Czech', + 'nativeName' => 'čeština, český jazyk', + 'direction' => 'ltr', + ], + 'cu' => [ + 'name' => 'Old Church Slavonic, Church Slavonic, Old Bulgarian', + 'nativeName' => 'ѩзыкъ словѣньскъ', + 'direction' => 'ltr', + ], + 'cv' => [ + 'name' => 'Chuvash', + 'nativeName' => 'чӑваш чӗлхи', + 'direction' => 'ltr', + ], + 'cy' => [ + '639-2/B' => 'wel', + 'name' => 'Welsh', + 'nativeName' => 'Cymraeg', + 'direction' => 'ltr', + ], + 'da' => [ + 'name' => 'Danish', + 'nativeName' => 'dansk', + 'direction' => 'ltr', + ], + 'de' => [ + '639-2/B' => 'ger', + 'name' => 'German', + 'nativeName' => 'Deutsch', + 'direction' => 'ltr', + ], + 'dv' => [ + 'name' => 'Divehi, Dhivehi, Maldivian', + 'nativeName' => 'ދިވެހި', + 'direction' => 'rtl', + ], + 'dz' => [ + 'name' => 'Dzongkha', + 'nativeName' => 'རྫོང་ཁ', + 'direction' => 'ltr', + ], + 'ee' => [ + 'name' => 'Ewe', + 'nativeName' => 'Eʋegbe', + 'direction' => 'ltr', + ], + 'el' => [ + '639-2/B' => 'gre', + 'name' => 'Greek (modern)', + 'nativeName' => 'ελληνικά', + 'direction' => 'ltr', + ], + 'en' => [ + 'name' => 'English', + 'nativeName' => 'English', + 'direction' => 'ltr', + ], + 'eo' => [ + 'name' => 'Esperanto', + 'nativeName' => 'Esperanto', + 'direction' => 'ltr', + ], + 'es' => [ + 'name' => 'Spanish', + 'nativeName' => 'Español', + 'direction' => 'ltr', + ], + 'et' => [ + 'name' => 'Estonian', + 'nativeName' => 'eesti, eesti keel', + 'direction' => 'ltr', + ], + 'eu' => [ + '639-2/B' => 'baq', + 'name' => 'Basque', + 'nativeName' => 'euskara, euskera', + 'direction' => 'ltr', + ], + 'fa' => [ + '639-2/B' => 'per', + 'name' => 'Persian (Farsi)', + 'nativeName' => 'فارسی', + 'direction' => 'rtl', + ], + 'ff' => [ + 'name' => 'Fula, Fulah, Pulaar, Pular', + 'nativeName' => 'Fulfulde, Pulaar, Pular', + 'direction' => 'ltr', + ], + 'fi' => [ + 'name' => 'Finnish', + 'nativeName' => 'suomi, suomen kieli', + 'direction' => 'ltr', + ], + 'fj' => [ + 'name' => 'Fijian', + 'nativeName' => 'vosa Vakaviti', + 'direction' => 'ltr', + ], + 'fo' => [ + 'name' => 'Faroese', + 'nativeName' => 'føroyskt', + 'direction' => 'ltr', + ], + 'fr' => [ + '639-2/B' => 'fre', + 'name' => 'French', + 'nativeName' => 'français, langue française', + 'direction' => 'ltr', + ], + 'fy' => [ + 'name' => 'Western Frisian', + 'nativeName' => 'Frysk', + 'direction' => 'ltr', + ], + 'ga' => [ + 'name' => 'Irish', + 'nativeName' => 'Gaeilge', + 'direction' => 'ltr', + ], + 'gd' => [ + 'name' => 'Scottish Gaelic, Gaelic', + 'nativeName' => 'Gàidhlig', + 'direction' => 'ltr', + ], + 'gl' => [ + 'name' => 'Galician', + 'nativeName' => 'galego', + 'direction' => 'ltr', + ], + 'gn' => [ + 'name' => 'Guaraní', + 'nativeName' => "Avañe'ẽ", + 'direction' => 'ltr', + ], + 'gu' => [ + 'name' => 'Gujarati', + 'nativeName' => 'ગુજરાતી', + 'direction' => 'ltr', + ], + 'gv' => [ + 'name' => 'Manx', + 'nativeName' => 'Gaelg, Gailck', + 'direction' => 'ltr', + ], + 'ha' => [ + 'name' => 'Hausa', + 'nativeName' => '(Hausa) هَوُسَ', + 'direction' => 'rtl', + ], + 'he' => [ + 'name' => 'Hebrew', + 'nativeName' => 'עברית', + 'direction' => 'rtl', + ], + 'hi' => [ + 'name' => 'Hindi', + 'nativeName' => 'हिन्दी, हिंदी', + 'direction' => 'ltr', + ], + 'ho' => [ + 'name' => 'Hiri Motu', + 'nativeName' => 'Hiri Motu', + 'direction' => 'ltr', + ], + 'hr' => [ + 'name' => 'Croatian', + 'nativeName' => 'hrvatski jezik', + 'direction' => 'ltr', + ], + 'ht' => [ + 'name' => 'Haitian, Haitian Creole', + 'nativeName' => 'Kreyòl ayisyen', + 'direction' => 'ltr', + ], + 'hu' => [ + 'name' => 'Hungarian', + 'nativeName' => 'magyar', + 'direction' => 'ltr', + ], + 'hy' => [ + '639-2/B' => 'arm', + 'name' => 'Armenian', + 'nativeName' => 'Հայերեն', + 'direction' => 'ltr', + ], + 'hz' => [ + 'name' => 'Herero', + 'nativeName' => 'Otjiherero', + 'direction' => 'ltr', + ], + 'ia' => [ + 'name' => 'Interlingua', + 'nativeName' => 'Interlingua', + 'direction' => 'ltr', + ], + 'id' => [ + 'name' => 'Indonesian', + 'nativeName' => 'Bahasa Indonesia', + 'direction' => 'ltr', + ], + 'ie' => [ + 'name' => 'Interlingue', + 'nativeName' => 'Originally called Occidental; then Interlingue after WWII', + 'direction' => 'ltr', + ], + 'ig' => [ + 'name' => 'Igbo', + 'nativeName' => 'Asụsụ Igbo', + 'direction' => 'ltr', + ], + 'ii' => [ + 'name' => 'Nuosu', + 'nativeName' => 'ꆈꌠ꒿ Nuosuhxop', + 'direction' => 'ltr', + ], + 'ik' => [ + 'name' => 'Inupiaq', + 'nativeName' => 'Iñupiaq, Iñupiatun', + 'direction' => 'ltr', + ], + 'io' => [ + 'name' => 'Ido', + 'nativeName' => 'Ido', + 'direction' => 'ltr', + ], + 'is' => [ + '639-2/B' => 'ice', + 'name' => 'Icelandic', + 'nativeName' => 'Íslenska', + 'direction' => 'ltr', + ], + 'it' => [ + 'name' => 'Italian', + 'nativeName' => 'Italiano', + 'direction' => 'ltr', + ], + 'iu' => [ + 'name' => 'Inuktitut', + 'nativeName' => 'ᐃᓄᒃᑎᑐᑦ', + 'direction' => 'ltr', + ], + 'ja' => [ + 'name' => 'Japanese', + 'nativeName' => '日本語 (にほんご)', + 'direction' => 'ltr', + ], + 'jv' => [ + 'name' => 'Javanese', + 'nativeName' => 'ꦧꦱꦗꦮ, Basa Jawa', + 'direction' => 'ltr', + ], + 'ka' => [ + '639-2/B' => 'geo', + 'name' => 'Georgian', + 'nativeName' => 'ქართული', + 'direction' => 'ltr', + ], + 'kg' => [ + 'name' => 'Kongo', + 'nativeName' => 'Kikongo', + 'direction' => 'ltr', + ], + 'ki' => [ + 'name' => 'Kikuyu, Gikuyu', + 'nativeName' => 'Gĩkũyũ', + 'direction' => 'ltr', + ], + 'kj' => [ + 'name' => 'Kwanyama, Kuanyama', + 'nativeName' => 'Kuanyama', + 'direction' => 'ltr', + ], + 'kk' => [ + 'name' => 'Kazakh', + 'nativeName' => 'қазақ тілі', + 'direction' => 'ltr', + ], + 'kl' => [ + 'name' => 'Kalaallisut, Greenlandic', + 'nativeName' => 'kalaallisut, kalaallit oqaasii', + 'direction' => 'ltr', + ], + 'km' => [ + 'name' => 'Khmer', + 'nativeName' => 'ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ', + 'direction' => 'ltr', + ], + 'kn' => [ + 'name' => 'Kannada', + 'nativeName' => 'ಕನ್ನಡ', + 'direction' => 'ltr', + ], + 'ko' => [ + 'name' => 'Korean', + 'nativeName' => '한국어', + 'direction' => 'ltr', + ], + 'kr' => [ + 'name' => 'Kanuri', + 'nativeName' => 'Kanuri', + 'direction' => 'ltr', + ], + 'ks' => [ + 'name' => 'Kashmiri', + 'nativeName' => 'कश्मीरी, كشميري‎', + 'direction' => 'rtl', + ], + 'ku' => [ + 'name' => 'Kurdish', + 'nativeName' => 'Kurdî, كوردی‎', + 'direction' => 'rtl', + ], + 'kv' => [ + 'name' => 'Komi', + 'nativeName' => 'коми кыв', + 'direction' => 'ltr', + ], + 'kw' => [ + 'name' => 'Cornish', + 'nativeName' => 'Kernewek', + 'direction' => 'ltr', + ], + 'ky' => [ + 'name' => 'Kyrgyz', + 'nativeName' => 'Кыргызча, Кыргыз тили', + 'direction' => 'ltr', + ], + 'la' => [ + 'name' => 'Latin', + 'nativeName' => 'latine, lingua latina', + 'direction' => 'ltr', + ], + 'lb' => [ + 'name' => 'Luxembourgish, Letzeburgesch', + 'nativeName' => 'Lëtzebuergesch', + 'direction' => 'ltr', + ], + 'lg' => [ + 'name' => 'Ganda', + 'nativeName' => 'Luganda', + 'direction' => 'ltr', + ], + 'li' => [ + 'name' => 'Limburgish, Limburgan, Limburger', + 'nativeName' => 'Limburgs', + 'direction' => 'ltr', + ], + 'ln' => [ + 'name' => 'Lingala', + 'nativeName' => 'Lingála', + 'direction' => 'ltr', + ], + 'lo' => [ + 'name' => 'Lao', + 'nativeName' => 'ພາສາລາວ', + 'direction' => 'ltr', + ], + 'lt' => [ + 'name' => 'Lithuanian', + 'nativeName' => 'lietuvių kalba', + 'direction' => 'ltr', + ], + 'lu' => [ + 'name' => 'Luba-Katanga', + 'nativeName' => 'Tshiluba', + 'direction' => 'ltr', + ], + 'lv' => [ + 'name' => 'Latvian', + 'nativeName' => 'latviešu valoda', + 'direction' => 'ltr', + ], + 'mg' => [ + 'name' => 'Malagasy', + 'nativeName' => 'fiteny malagasy', + 'direction' => 'ltr', + ], + 'mh' => [ + 'name' => 'Marshallese', + 'nativeName' => 'Kajin M̧ajeļ', + 'direction' => 'ltr', + ], + 'mi' => [ + '639-2/B' => 'mao', + 'name' => 'Māori', + 'nativeName' => 'te reo Māori', + 'direction' => 'ltr', + ], + 'mk' => [ + '639-2/B' => 'mac', + 'name' => 'Macedonian', + 'nativeName' => 'македонски јазик', + 'direction' => 'ltr', + ], + 'ml' => [ + 'name' => 'Malayalam', + 'nativeName' => 'മലയാളം', + 'direction' => 'ltr', + ], + 'mn' => [ + 'name' => 'Mongolian', + 'nativeName' => 'Монгол хэл', + 'direction' => 'ltr', + ], + 'mr' => [ + 'name' => 'Marathi (Marāṭhī)', + 'nativeName' => 'मराठी', + 'direction' => 'ltr', + ], + 'ms' => [ + '639-2/B' => 'may', + 'name' => 'Malay', + 'nativeName' => 'bahasa Melayu, بهاس ملايو‎', + 'direction' => 'ltr', + ], + 'mt' => [ + 'name' => 'Maltese', + 'nativeName' => 'Malti', + 'direction' => 'ltr', + ], + 'my' => [ + '639-2/B' => 'bur', + 'name' => 'Burmese', + 'nativeName' => 'ဗမာစာ', + 'direction' => 'ltr', + ], + 'na' => [ + 'name' => 'Nauruan', + 'nativeName' => 'Dorerin Naoero', + 'direction' => 'ltr', + ], + 'nb' => [ + 'name' => 'Norwegian Bokmål', + 'nativeName' => 'Norsk bokmål', + 'direction' => 'ltr', + ], + 'nd' => [ + 'name' => 'Northern Ndebele', + 'nativeName' => 'isiNdebele', + 'direction' => 'ltr', + ], + 'ne' => [ + 'name' => 'Nepali', + 'nativeName' => 'नेपाली', + 'direction' => 'ltr', + ], + 'ng' => [ + 'name' => 'Ndonga', + 'nativeName' => 'Owambo', + 'direction' => 'ltr', + ], + 'nl' => [ + '639-2/B' => 'dut', + 'name' => 'Dutch', + 'nativeName' => 'Nederlands, Vlaams', + 'direction' => 'ltr', + ], + 'nn' => [ + 'name' => 'Norwegian Nynorsk', + 'nativeName' => 'Norsk nynorsk', + 'direction' => 'ltr', + ], + 'no' => [ + 'name' => 'Norwegian', + 'nativeName' => 'Norsk', + 'direction' => 'ltr', + ], + 'nr' => [ + 'name' => 'Southern Ndebele', + 'nativeName' => 'isiNdebele', + 'direction' => 'ltr', + ], + 'nv' => [ + 'name' => 'Navajo, Navaho', + 'nativeName' => 'Diné bizaad', + 'direction' => 'ltr', + ], + 'ny' => [ + 'name' => 'Chichewa, Chewa, Nyanja', + 'nativeName' => 'chiCheŵa, chinyanja', + 'direction' => 'ltr', + ], + 'oc' => [ + 'name' => 'Occitan', + 'nativeName' => "occitan, lenga d'òc", + 'direction' => 'ltr', + ], + 'oj' => [ + 'name' => 'Ojibwe, Ojibwa', + 'nativeName' => 'ᐊᓂᔑᓈᐯᒧᐎᓐ', + 'direction' => 'ltr', + ], + 'om' => [ + 'name' => 'Oromo', + 'nativeName' => 'Afaan Oromoo', + 'direction' => 'ltr', + ], + 'or' => [ + 'name' => 'Oriya', + 'nativeName' => 'ଓଡ଼ିଆ', + 'direction' => 'ltr', + ], + 'os' => [ + 'name' => 'Ossetian, Ossetic', + 'nativeName' => 'ирон æвзаг', + 'direction' => 'ltr', + ], + 'pa' => [ + 'name' => '(Eastern) Punjabi', + 'nativeName' => 'ਪੰਜਾਬੀ', + 'direction' => 'ltr', + ], + 'pi' => [ + 'name' => 'Pāli', + 'nativeName' => 'पाऴि', + 'direction' => 'ltr', + ], + 'pl' => [ + 'name' => 'Polish', + 'nativeName' => 'język polski, polszczyzna', + 'direction' => 'ltr', + ], + 'ps' => [ + 'name' => 'Pashto', + 'nativeName' => 'پښتو', + 'direction' => 'rtl', + ], + 'pt' => [ + 'name' => 'Portuguese', + 'nativeName' => 'Português', + 'direction' => 'ltr', + ], + 'qu' => [ + 'name' => 'Quechua', + 'nativeName' => 'Runa Simi, Kichwa', + 'direction' => 'ltr', + ], + 'rm' => [ + 'name' => 'Romansh', + 'nativeName' => 'rumantsch grischun', + 'direction' => 'ltr', + ], + 'rn' => [ + 'name' => 'Kirundi', + 'nativeName' => 'Ikirundi', + 'direction' => 'ltr', + ], + 'ro' => [ + '639-2/B' => 'rum', + 'name' => 'Romanian', + 'nativeName' => 'Română', + 'direction' => 'ltr', + ], + 'ru' => [ + 'name' => 'Russian', + 'nativeName' => 'Русский', + 'direction' => 'ltr', + ], + 'rw' => [ + 'name' => 'Kinyarwanda', + 'nativeName' => 'Ikinyarwanda', + 'direction' => 'ltr', + ], + 'sa' => [ + 'name' => 'Sanskrit (Saṁskṛta)', + 'nativeName' => 'संस्कृतम्', + 'direction' => 'ltr', + ], + 'sc' => [ + 'name' => 'Sardinian', + 'nativeName' => 'sardu', + 'direction' => 'ltr', + ], + 'sd' => [ + 'name' => 'Sindhi', + 'nativeName' => 'सिन्धी, سنڌي، سندھی‎', + 'direction' => 'ltr', + ], + 'se' => [ + 'name' => 'Northern Sami', + 'nativeName' => 'Davvisámegiella', + 'direction' => 'ltr', + ], + 'sg' => [ + 'name' => 'Sango', + 'nativeName' => 'yângâ tî sängö', + 'direction' => 'ltr', + ], + 'si' => [ + 'name' => 'Sinhalese, Sinhala', + 'nativeName' => 'සිංහල', + 'direction' => 'ltr', + ], + 'sk' => [ + '639-2/B' => 'slo', + 'name' => 'Slovak', + 'nativeName' => 'slovenčina, slovenský jazyk', + 'direction' => 'ltr', + ], + 'sl' => [ + 'name' => 'Slovene', + 'nativeName' => 'slovenski jezik, slovenščina', + 'direction' => 'ltr', + ], + 'sm' => [ + 'name' => 'Samoan', + 'nativeName' => "gagana fa'a Samoa", + 'direction' => 'ltr', + ], + 'sn' => [ + 'name' => 'Shona', + 'nativeName' => 'chiShona', + 'direction' => 'ltr', + ], + 'so' => [ + 'name' => 'Somali', + 'nativeName' => 'Soomaaliga, af Soomaali', + 'direction' => 'ltr', + ], + 'sq' => [ + '639-2/B' => 'alb', + 'name' => 'Albanian', + 'nativeName' => 'Shqip', + 'direction' => 'ltr', + ], + 'sr' => [ + 'name' => 'Serbian', + 'nativeName' => 'српски језик', + 'direction' => 'ltr', + ], + 'ss' => [ + 'name' => 'Swati', + 'nativeName' => 'SiSwati', + 'direction' => 'ltr', + ], + 'st' => [ + 'name' => 'Southern Sotho', + 'nativeName' => 'Sesotho', + 'direction' => 'ltr', + ], + 'su' => [ + 'name' => 'Sundanese', + 'nativeName' => 'Basa Sunda', + 'direction' => 'ltr', + ], + 'sv' => [ + 'name' => 'Swedish', + 'nativeName' => 'svenska', + 'direction' => 'ltr', + ], + 'sw' => [ + 'name' => 'Swahili', + 'nativeName' => 'Kiswahili', + 'direction' => 'ltr', + ], + 'ta' => [ + 'name' => 'Tamil', + 'nativeName' => 'தமிழ்', + 'direction' => 'ltr', + ], + 'te' => [ + 'name' => 'Telugu', + 'nativeName' => 'తెలుగు', + 'direction' => 'ltr', + ], + 'tg' => [ + 'name' => 'Tajik', + 'nativeName' => 'тоҷикӣ, toçikī, تاجیکی‎', + 'direction' => 'ltr', + ], + 'th' => [ + 'name' => 'Thai', + 'nativeName' => 'ไทย', + 'direction' => 'ltr', + ], + 'ti' => [ + 'name' => 'Tigrinya', + 'nativeName' => 'ትግርኛ', + 'direction' => 'ltr', + ], + 'tk' => [ + 'name' => 'Turkmen', + 'nativeName' => 'Türkmen, Түркмен', + 'direction' => 'ltr', + ], + 'tl' => [ + 'name' => 'Tagalog', + 'nativeName' => 'Wikang Tagalog', + 'direction' => 'ltr', + ], + 'tn' => [ + 'name' => 'Tswana', + 'nativeName' => 'Setswana', + 'direction' => 'ltr', + ], + 'to' => [ + 'name' => 'Tonga (Tonga Islands)', + 'nativeName' => 'faka Tonga', + 'direction' => 'ltr', + ], + 'tr' => [ + 'name' => 'Turkish', + 'nativeName' => 'Türkçe', + 'direction' => 'ltr', + ], + 'ts' => [ + 'name' => 'Tsonga', + 'nativeName' => 'Xitsonga', + 'direction' => 'ltr', + ], + 'tt' => [ + 'name' => 'Tatar', + 'nativeName' => 'татар теле, tatar tele', + 'direction' => 'ltr', + ], + 'tw' => [ + 'name' => 'Twi', + 'nativeName' => 'Twi', + 'direction' => 'ltr', + ], + 'ty' => [ + 'name' => 'Tahitian', + 'nativeName' => 'Reo Tahiti', + 'direction' => 'ltr', + ], + 'ug' => [ + 'name' => 'Uyghur', + 'nativeName' => 'ئۇيغۇرچە‎, Uyghurche', + 'direction' => 'ltr', + ], + 'uk' => [ + 'name' => 'Ukrainian', + 'nativeName' => 'Українська', + 'direction' => 'ltr', + ], + 'ur' => [ + 'name' => 'Urdu', + 'nativeName' => 'اردو', + 'direction' => 'rtl', + ], + 'uz' => [ + 'name' => 'Uzbek', + 'nativeName' => 'Oʻzbek, Ўзбек, أۇزبېك‎', + 'direction' => 'ltr', + ], + 've' => [ + 'name' => 'Venda', + 'nativeName' => 'Tshivenḓa', + 'direction' => 'ltr', + ], + 'vi' => [ + 'name' => 'Vietnamese', + 'nativeName' => 'Tiếng Việt', + 'direction' => 'ltr', + ], + 'vo' => [ + 'name' => 'Volapük', + 'nativeName' => 'Volapük', + 'direction' => 'ltr', + ], + 'wa' => [ + 'name' => 'Walloon', + 'nativeName' => 'walon', + 'direction' => 'ltr', + ], + 'wo' => [ + 'name' => 'Wolof', + 'nativeName' => 'Wollof', + 'direction' => 'ltr', + ], + 'xh' => [ + 'name' => 'Xhosa', + 'nativeName' => 'isiXhosa', + 'direction' => 'ltr', + ], + 'yi' => [ + 'name' => 'Yiddish', + 'nativeName' => 'ייִדיש', + 'direction' => 'rtl', + ], + 'yo' => [ + 'name' => 'Yoruba', + 'nativeName' => 'Yorùbá', + 'direction' => 'ltr', + ], + 'za' => [ + 'name' => 'Zhuang, Chuang', + 'nativeName' => 'Saɯ cueŋƅ, Saw cuengh', + 'direction' => 'ltr', + ], + 'zh' => [ + '639-2/B' => 'chi', + 'name' => 'Chinese', + 'nativeName' => '中文 (Zhōngwén), 汉语, 漢語', + 'direction' => 'ltr', + ], + 'zu' => [ + 'name' => 'Zulu', + 'nativeName' => 'isiZulu', + 'direction' => 'ltr', + ], + ]; + + public static function getLanguages(): array + { + return self::LANGUAGES; + } + + public static function getLanguage(string $code): array + { + $code = Str::of($code) + ->lower() + ->trim() + ->value(); + + if (! \array_key_exists($code, self::LANGUAGES)) { + throw new LanguageNotInISO639($code); + } + + return self::LANGUAGES[$code]; + } + + public static function getLanguageName(string $code): string + { + return self::getLanguage($code)['name']; + } + + public static function getNativeLanguageName(string $code): string + { + return self::getLanguage($code)['nativeName']; + } + + public static function getCombinedLanguageName(string $code): string + { + $language = self::getLanguage($code); + + return $language['name'] . ' (' . $language['nativeName'] . ')'; + } + + public static function getLanguageCodes(): array + { + return array_keys(self::LANGUAGES); + } + + public static function getLanguageOptions(): Collection + { + return collect(self::LANGUAGES) + ->map(fn ($language, $code) => [ + 'code' => $code, + 'name' => $language['name'], + ]) + ->values(); + } + + public static function getNativeLanguageOptions(): Collection + { + return collect(self::LANGUAGES) + ->map(fn ($language, $code) => [ + 'code' => $code, + 'name' => $language['nativeName'], + 'direction' => $language['direction'], + ]) + ->values(); + } + + public static function getCombinedLanguageOptions(): Collection + { + return collect(self::LANGUAGES) + ->map(fn ($language, $code) => [ + 'code' => $code, + 'name' => $language['name'] . ' (' . $language['nativeName'] . ')', + 'direction' => $language['direction'], + ]) + ->values(); + } + + public static function getLanguageDirection(string $code): string + { + return self::getLanguage($code)['direction']; + } +} diff --git a/app/Services/TranslatableFormRequestRules.php b/app/Services/TranslatableFormRequestRules.php index 0236d1ac..55741cb2 100644 --- a/app/Services/TranslatableFormRequestRules.php +++ b/app/Services/TranslatableFormRequestRules.php @@ -26,7 +26,7 @@ public static function make(string $model, array $input): array } locales()->each(function (array $config, string $locale) use ($rules, $key, $rule) { - if (! $locale !== config('app.fallback_locale')) { + if (! $locale !== default_locale()) { $rule = collect($rule) ->map(fn (string $rule) => Str::startsWith($rule, 'required') ? 'nullable' : $rule) ->all(); diff --git a/app/helpers.php b/app/helpers.php index ca183495..c63f16c5 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Services\Features; +use App\Services\ISO_639_1; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; @@ -13,7 +14,7 @@ /** * Return the available locales. * - * @return \Illuminate\Support\Collection + * @return Collection */ function locales(): Collection { @@ -21,11 +22,23 @@ function locales(): Collection } } +if (! function_exists('text_direction')) { + /** + * Return the current text direction. + * + * @return string + */ + function text_direction(): string + { + return ISO_639_1::getLanguageDirection(app()->getLocale()); + } +} + if (! function_exists('active_locales')) { /** * Return the currently enabled locales. * - * @return \Illuminate\Support\Collection + * @return Collection */ function active_locales(): Collection { @@ -78,7 +91,7 @@ function localized_input(mixed $input, ?string $locale = null): mixed $locale ??= app()->getLocale(); if (! array_key_exists($locale, $input) || $input[$locale] === '') { - $locale = config('app.fallback_locale'); + $locale = default_locale(); } return $input[$locale] ?? null; @@ -108,7 +121,7 @@ function settings(array|string|null $key = null): mixed function localized_settings(?string $key = null): mixed { return settings($key . '.' . app()->getLocale()) - ?? settings($key . '.' . config('app.fallback_locale')); + ?? settings($key . '.' . default_locale()); } } diff --git a/database/migrations/2024_02_14_201150_add_iso_639-1_support_to_languages_table.php b/database/migrations/2024_02_14_201150_add_iso_639-1_support_to_languages_table.php new file mode 100644 index 00000000..c14d7689 --- /dev/null +++ b/database/migrations/2024_02_14_201150_add_iso_639-1_support_to_languages_table.php @@ -0,0 +1,31 @@ +dropColumn('name'); + }); + + Language::all() + ->each(function (Language $language) { + $language->update([ + 'code' => Str::lower($language->code), + ]); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 5ad40857..77902f2c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -33,12 +33,10 @@ public function run() Language::insert([ [ 'code' => 'ro', - 'name' => 'Română', 'enabled' => true, ], [ 'code' => 'en', - 'name' => 'English', 'enabled' => false, ], ]); diff --git a/resources/js/components/Blocks/BlockItem.vue b/resources/js/components/Blocks/BlockItem.vue index ea050dcb..0019d0e6 100644 --- a/resources/js/components/Blocks/BlockItem.vue +++ b/resources/js/components/Blocks/BlockItem.vue @@ -10,7 +10,7 @@ -
+