Ein Laravel mehrsprachig System aufzubauen wirkt auf den ersten Blick wie das bloße Übersetzen einiger Texte, doch in einem echten Projekt verteilt sich die Arbeit auf drei getrennte Ebenen: Oberflächentexte (Sprachdateien), die Sprachverwaltung in URLs (Locale-Routing) und das Übersetzen der dynamischen Inhalte, die in deiner Datenbank liegen. Dieser Leitfaden behandelt alle drei von A bis Z, mit funktionierendem Code. Der Ansatz beruht auf der realen Architektur, die ich auf meinem eigenen Portfolio (aslain.dev) verwende, wo ich fünf Sprachen verwalte: TR/EN/FR/NL/DE.
Locale-Grundlagen: app.locale und Fallback
In Laravel wird die aktive Sprache mit App::getLocale() gelesen und mit App::setLocale('en') gesetzt. Zwei Einstellungen sind entscheidend, beide in config/app.php:
'locale' => 'de',
'fallback_locale' => 'en',
fallback_locale legt fest, auf welche Sprache zurückgegriffen wird, wenn ein Übersetzungsschlüssel in der gewählten Sprache fehlt. Es ist die am häufigsten vergessene, aber rettendste Einstellung in einem mehrsprachigen Projekt: Ist ein Text noch nicht ins Französische übersetzt, sieht der Nutzer die englische Version statt eines leeren Bildschirms. Um deine unterstützten Sprachen an einer Stelle zu verwalten, führst du ein einfaches Array:
// config/app.php oder eine eigene Config-Datei
'available_locales' => ['tr', 'en', 'fr', 'nl', 'de'],
Sprachdateien: die Oberflächentexte
Für statische Oberflächentexte (Menüs, Button-Beschriftungen, Formularüberschriften) verwendest du Laravels eingebaute Übersetzungsdateien. Es gibt zwei Formate. Das erste sind PHP-Array-Dateien; lang/de/messages.php sieht so aus:
<?php
return [
'welcome' => 'Willkommen, :name',
'nav' => [
'projects' => 'Projekte',
'contact' => 'Kontakt',
],
];
In Blade rufst du es mit __('messages.welcome', ['name' => $user->name]) auf; die Punktnotation erreicht verschachtelte Schlüssel. Das zweite Format sind JSON-Übersetzungsdateien: In lang/fr.json verwendest du den Quellsatz selbst als Schlüssel. Das ist praktisch für kurze Oberflächentexte:
{
"Save changes": "Enregistrer les modifications",
"Projects": "Projets"
}
Der Aufruf bleibt __('Save changes'). Laravel prüft zuerst die JSON-Datei, dann die PHP-Arrays. Für Pluralformen verwendest du trans_choice(): trans_choice('messages.apples', $count) mit durch Pipes getrennten Regeln in der Datei, etwa 'apples' => '{0} keine Äpfel|{1} ein Apfel|[2,*] :count Äpfel'.
Locale-Routing: die Sprache erkennen und setzen
Der sauberste Weg, die Sprache bei jeder Anfrage zu setzen, ist eine Middleware. Du kannst die Präferenz des Nutzers zuerst aus der Session lesen, sonst aus dem Accept-Language-Header:
php artisan make:middleware SetLocale
public function handle(Request $request, Closure $next)
{
$supported = config('app.available_locales');
$locale = $request->session()->get('locale')
?? $request->getPreferredLanguage($supported)
?? config('app.fallback_locale');
if (in_array($locale, $supported, true)) {
App::setLocale($locale);
}
return $next($request);
}
getPreferredLanguage() gleicht den Accept-Language-Header des Browsers mit den von dir unterstützten Sprachen ab, was zuverlässiger ist als manuelles Parsen. In Laravel 11/12 registrierst du die Middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [\App\Http\Middleware\SetLocale::class]);
})
Möchtest du die Sprache in der URL (z. B. /en/projects), fasst du deine Routen in einer Präfix-Gruppe zusammen. Das ist wertvoll für SEO, denn jede Sprache erhält ihre eigene indexierbare Adresse:
Route::prefix('{locale}')
->whereIn('locale', ['tr','en','fr','nl','de'])
->group(function () {
Route::get('/projects', [ProjectController::class, 'index']);
});
Ein Sprachumschalter schreibt die Präferenz dann in die Session und kehrt zurück:
Route::get('/lang/{locale}', function (string $locale) {
if (in_array($locale, config('app.available_locales'), true)) {
session()->put('locale', $locale);
}
return back();
});
Datenbankinhalte übersetzen
Die eigentliche Herausforderung sind nicht die Sprachdateien, sondern die dynamischen Inhalte, die ein Nutzer über das Admin-Panel eingibt: Projekttitel, Leistungsbeschreibungen, Blogbeiträge. Hier gibt es zwei solide Ansätze. Der erste besteht darin, jede übersetzbare Spalte als JSON zu speichern. In der Migration machst du die Spalte zu json und castest sie im Model:
// Migration
$table->json('title');
// Model
protected $casts = ['title' => 'array'];
Die Zeile enthält {"tr":"Başlık","en":"Title","fr":"Titre"}. Beim Lesen gibt eine kleine Hilfsmethode die gewählte Sprache zurück und greift auf Englisch zurück, dann auf die erste verfügbare Sprache:
public function tr(string $field, ?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
$values = $this->{$field} ?? [];
return $values[$locale]
?? $values['en']
?? (reset($values) ?: '');
}
Diese Methode ist schnell und benötigt keine zusätzliche Tabelle; genau das verwende ich auf aslain.dev. Der zweite Ansatz ist das Paket spatie/laravel-translatable. Es standardisiert dieselbe JSON-Spalten-Idee und löst $project->title automatisch in die aktive Sprache auf. Die Installation ist einfach:
composer require spatie/laravel-translatable
Du fügst dem Model den HasTranslations-Trait hinzu und deklarierst public array $translatable = ['title', 'body'];. Bei großen Projekten mit vielen Sprachen oder Bedarf an Übersetzungsverwaltung erleichtert das Paket die Wartung; bei kleinen Projekten reicht dein eigener Trait.
Performance und häufige Fallstricke
Einige praktische Punkte halten das System stabil:
- Caching: Führe in der Produktion
php artisan config:cacheundroute:cacheaus. Routen mit Closures lassen sich nicht cachen; verschiebe sie in einen Controller. - Vernachlässige den Fallback nicht: Ein einziger fehlender Schlüssel erzeugt einen leeren Titel. Definiere auch innerhalb deines JSON-Inhalts stets eine Rückfallkette.
- hreflang: Nutzt du die Sprache in der URL, füge dem
<head>für jede Sprache den alternativen Link hinzu; Google liefert dann die richtige Sprache an den richtigen Nutzer. - Datum und Zahlen: Mit Carbon liefert
$date->locale(app()->getLocale())->translatedFormat('d F Y')lokalisierte Monatsnamen; übersetze das Format nicht von Hand.
Häufige Fragen
Sollte ich eine JSON-Spalte oder eine separate Übersetzungstabelle verwenden?
Ist die Anzahl der Sprachen fest und überschaubar (etwa 5), ist eine JSON-Spalte der einfachste und schnellste Weg; alle Sprachen kommen in einer einzigen Abfrage zurück. Brauchst du viele Sprachen, die Möglichkeit, Sprachen getrennt abzufragen, oder die Verfolgung des Übersetzungsstatus, ist eine eigene translations-Tabelle flexibler.
Sollte ich die Sprache über ein URL-Präfix oder die Session verwalten?
Ist SEO wichtig, bevorzuge ein URL-Präfix (/en/...): Jede Sprache erhält ihre eigene indexierbare Adresse. Für ein rein persönliches Panel oder eine Single-Page-App genügt eine session-/cookie-basierte Auswahl und bleibt einfacher.
Wofür ist fallback_locale da?
Kann ein Übersetzungsschlüssel in der gewählten Sprache nicht gefunden werden, greift Laravel automatisch auf die fallback_locale-Sprache zurück. Das verhindert, dass unübersetzter Text leer erscheint; sie wird meist auf Englisch gesetzt.
Planst du ein mehrsprachiges Laravel-Projekt? Ob Aufbau von Grund auf oder Migration einer bestehenden Seite auf mehrere Sprachen — ich helfe dir, eine saubere, wartbare Architektur aufzubauen. Nimm Kontakt mit mir auf.