Een Laravel meertalig systeem bouwen lijkt op het eerste gezicht slechts het vertalen van een paar teksten, maar in een echt project verspreidt het werk zich over drie aparte lagen: interfaceteksten (taalbestanden), taalbeheer in URL's (locale-routing) en het vertalen van de dynamische content die in je database staat. Deze gids behandelt alle drie van begin tot eind, met werkende code. De aanpak is gebaseerd op de echte architectuur die ik gebruik op mijn eigen portfolio (aslain.dev), waar ik vijf talen beheer: TR/EN/FR/NL/DE.
Locale-basis: app.locale en fallback
In Laravel wordt de actieve taal gelezen met App::getLocale() en ingesteld met App::setLocale('en'). Twee instellingen zijn belangrijk, beide in config/app.php:
'locale' => 'nl',
'fallback_locale' => 'en',
fallback_locale bepaalt naar welke taal wordt teruggevallen wanneer een vertaalsleutel ontbreekt in de geselecteerde taal. Het is de meest vergeten maar meest reddende instelling in een meertalig project: als een tekst nog niet naar het Frans is vertaald, ziet de gebruiker de Engelse versie in plaats van een leeg scherm. Om je ondersteunde talen vanuit één plek te beheren, houd je een eenvoudige array bij:
// config/app.php of een eigen config-bestand
'available_locales' => ['tr', 'en', 'fr', 'nl', 'de'],
Taalbestanden: interfaceteksten
Voor statische interfaceteksten (menu's, knoplabels, formulierkoppen) gebruik je Laravels ingebouwde vertaalbestanden. Er zijn twee formaten. Het eerste zijn PHP-arraybestanden; lang/nl/messages.php ziet er zo uit:
<?php
return [
'welcome' => 'Welkom, :name',
'nav' => [
'projects' => 'Projecten',
'contact' => 'Contact',
],
];
In Blade roep je het aan met __('messages.welcome', ['name' => $user->name]); puntnotatie bereikt geneste sleutels. Het tweede formaat zijn JSON-vertaalbestanden: in lang/fr.json gebruik je de bronzin zelf als sleutel. Dit is handig voor korte interfaceteksten:
{
"Save changes": "Enregistrer les modifications",
"Projects": "Projets"
}
De aanroep is opnieuw __('Save changes'). Laravel kijkt eerst in het JSON-bestand, daarna in de PHP-arrays. Voor meervoudsvormen gebruik je trans_choice(): trans_choice('messages.apples', $count) met door pipes gescheiden regels in het bestand, zoals 'apples' => '{0} geen appels|{1} één appel|[2,*] :count appels'.
Locale-routing: de taal detecteren en instellen
De schoonste manier om de taal bij elke request in te stellen is een middleware. Je kunt de voorkeur van de gebruiker eerst uit de sessie lezen, en anders uit de 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() matcht de Accept-Language-header van de browser met de talen die je ondersteunt, wat betrouwbaarder is dan deze zelf te parsen. In Laravel 11/12 registreer je de middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [\App\Http\Middleware\SetLocale::class]);
})
Wil je de taal in de URL (bijv. /en/projects), plaats je routes dan in een prefix-groep. Dit is waardevol voor SEO omdat elke taal zijn eigen indexeerbare adres krijgt:
Route::prefix('{locale}')
->whereIn('locale', ['tr','en','fr','nl','de'])
->group(function () {
Route::get('/projects', [ProjectController::class, 'index']);
});
Een taalwisselaar schrijft dan de voorkeur naar de sessie en keert terug:
Route::get('/lang/{locale}', function (string $locale) {
if (in_array($locale, config('app.available_locales'), true)) {
session()->put('locale', $locale);
}
return back();
});
Database-content vertalen
De echte uitdaging zijn niet de taalbestanden maar de dynamische content die een gebruiker invoert via het adminpaneel: projecttitels, dienstbeschrijvingen, blogposts. Hier zijn twee solide benaderingen. De eerste is elke vertaalbare kolom opslaan als JSON. In de migratie maak je de kolom json en cast je deze in het model:
// migratie
$table->json('title');
// model
protected $casts = ['title' => 'array'];
De rij bevat {"tr":"Başlık","en":"Title","fr":"Titre"}. Bij het lezen geeft een kleine hulpmethode de geselecteerde taal terug en valt terug op Engels, daarna op de eerste beschikbare taal:
public function tr(string $field, ?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
$values = $this->{$field} ?? [];
return $values[$locale]
?? $values['en']
?? (reset($values) ?: '');
}
Deze methode is snel en heeft geen extra tabel nodig; dit is precies wat ik gebruik op aslain.dev. De tweede benadering is het pakket spatie/laravel-translatable. Het standaardiseert hetzelfde JSON-kolom-idee en lost $project->title automatisch op naar de actieve taal. De installatie is eenvoudig:
composer require spatie/laravel-translatable
Je voegt de HasTranslations-trait toe aan het model en declareert public array $translatable = ['title', 'body'];. Voor grote projecten met veel talen of behoefte aan vertaalbeheer vergemakkelijkt het pakket het onderhoud; voor kleine projecten is je eigen trait voldoende.
Prestaties en veelvoorkomende valkuilen
Een paar praktische punten houden het systeem solide:
- Caching: Voer in productie
php artisan config:cacheenroute:cacheuit. Routes met closures kunnen niet gecachet worden; verplaats ze naar een controller. - Sla de fallback niet over: Eén ontbrekende sleutel levert een lege titel op. Definieer ook binnen je JSON-content altijd een terugvalketen.
- hreflang: Gebruik je de taal in de URL, voeg dan voor elke taal de alternatieve link toe aan de
<head>; Google levert dan de juiste taal aan de juiste gebruiker. - Datums en getallen: Met Carbon geeft
$date->locale(app()->getLocale())->translatedFormat('d F Y')gelokaliseerde maandnamen; vertaal het formaat niet handmatig.
Veelgestelde vragen
Moet ik een JSON-kolom of een aparte vertaaltabel gebruiken?
Als het aantal talen vast en redelijk is (zeg, 5), is een JSON-kolom de eenvoudigste en snelste weg; alle talen komen terug in één query. Heb je veel talen nodig, de mogelijkheid om talen apart te bevragen, of het bijhouden van vertaalstatus, dan is een aparte translations-tabel flexibeler.
Moet ik de taal beheren via URL-prefix of via de sessie?
Als SEO belangrijk is, kies dan voor een URL-prefix (/en/...): elke taal krijgt zijn eigen indexeerbare adres. Voor een puur persoonlijk paneel of een single-page-app is een sessie/cookie-gebaseerde keuze voldoende en eenvoudiger.
Wat doet fallback_locale?
Als een vertaalsleutel niet wordt gevonden in de geselecteerde taal, valt Laravel automatisch terug op de fallback_locale-taal. Dit voorkomt dat onvertaalde tekst leeg verschijnt; deze wordt meestal op Engels gezet.
Plan je een meertalig Laravel-project? Of het nu een opzet vanaf nul is of het migreren van een bestaande site naar meerdere talen, ik kan je helpen een schone, onderhoudbare architectuur te bouwen. Neem contact met me op.