Mehrsprachigkeit der Oberfläche (Deutsch Standard, Englisch wählbar)

- Sprache je Benutzer (users.locale), Middleware SetLocale, Auswahl unter
  Einstellungen → Darstellung; angebotene Sprachen in config/app.php
- Übersetzungen über Laravel Lang (lang/de.json, lang/de/*.php), Seitentitel
  aus #[Title] werden übersetzt
- Eigene Texte und Korrekturen in lang/project/de.json, übernommen mit
  `artisan lang:apply-project` (überstehen `lang:update`)
- Tests: jede im Code verwendete Übersetzung ist vorhanden, eigene
  Übersetzungen sind übernommen, Sprachwahl und Standardsprache;
  SecurityTest prüft Texte sprachunabhängig über __()
- lang/ von Pint ausgenommen (erzeugte Dateien)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christoph
2026-10-01 09:52:20 +02:00
co-authored by Claude Opus 5.5
parent cdd2d5b676
commit db2d5d6228
32 changed files with 4108 additions and 18 deletions
@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
/**
* Überträgt die eigenen Übersetzungen aus lang/project/{locale}.json in lang/{locale}.json.
*
* lang/{locale}.json wird von Laravel Lang erzeugt; `lang:update` setzt dabei geänderte Paket-Texte
* zurück. Eigene Texte und Korrekturen stehen deshalb in lang/project/ und haben Vorrang.
*/
class ApplyProjectTranslations extends Command
{
protected $signature = 'lang:apply-project';
protected $description = 'Eigene Übersetzungen aus lang/project/ in lang/{locale}.json übernehmen';
public function handle(Filesystem $files): int
{
foreach ($files->glob(lang_path('project/*.json')) as $projectFile) {
$locale = pathinfo($projectFile, PATHINFO_FILENAME);
$target = lang_path("{$locale}.json");
$translations = $files->exists($target)
? json_decode($files->get($target), true, flags: JSON_THROW_ON_ERROR)
: [];
$project = json_decode($files->get($projectFile), true, flags: JSON_THROW_ON_ERROR);
$files->put($target, json_encode(
array_merge($translations, $project),
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
).PHP_EOL);
$this->components->info(count($project)." Übersetzungen nach lang/{$locale}.json übernommen.");
}
return self::SUCCESS;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
/**
* Setzt die Sprache der Oberfläche: die des angemeldeten Benutzers, sonst die Standardsprache.
*/
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
/** @var array<string, string> $available */
$available = config('app.available_locales');
$locale = $request->user()?->locale;
if (! is_string($locale) || ! array_key_exists($locale, $available)) {
$locale = config('app.locale');
}
App::setLocale($locale);
return $next($request);
}
}
+26 -1
View File
@@ -2,11 +2,36 @@
namespace App\Livewire\Settings;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Appearance settings')]
class Appearance extends Component
{
//
public string $locale = '';
/**
* Mount the component.
*/
public function mount(): void
{
$this->locale = App::getLocale();
}
/**
* Save the selected interface language and reload the page in that language.
*/
public function updatedLocale(): void
{
$this->validate([
'locale' => ['required', 'string', Rule::in(array_keys(config('app.available_locales')))],
]);
Auth::user()->update(['locale' => $this->locale]);
$this->redirectRoute('appearance.edit');
}
}
+2 -1
View File
@@ -20,6 +20,7 @@ use Spatie\Permission\Traits\HasRoles;
* @property int $id
* @property string $name
* @property string $email
* @property string|null $locale
* @property Carbon|null $email_verified_at
* @property string $password
* @property string|null $two_factor_secret
@@ -29,7 +30,7 @@ use Spatie\Permission\Traits\HasRoles;
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*/
#[Fillable(['name', 'email', 'password'])]
#[Fillable(['name', 'email', 'password', 'locale'])]
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
class User extends Authenticatable implements MustVerifyEmail, PasskeyUser
{