Construire un système Laravel multilingue ressemble au premier abord à la simple traduction de quelques chaînes, mais dans un vrai projet le travail se répartit sur trois couches distinctes : le texte d'interface (fichiers de langue), la gestion de la langue dans les URL (routage de locale) et la traduction du contenu dynamique stocké en base de données. Ce guide couvre les trois de bout en bout, avec du code fonctionnel. L'approche repose sur l'architecture réelle que j'utilise sur mon propre portfolio (aslain.dev), où je gère cinq langues : TR/EN/FR/NL/DE.
Les bases de la locale : app.locale et fallback
Dans Laravel, la langue active se lit avec App::getLocale() et se définit avec App::setLocale('en'). Deux réglages comptent, tous deux dans config/app.php :
'locale' => 'fr',
'fallback_locale' => 'en',
fallback_locale définit la langue de repli lorsqu'une clé de traduction est absente dans la langue sélectionnée. C'est le réglage le plus souvent oublié mais le plus salvateur d'un projet multilingue : si une chaîne n'est pas encore traduite en français, l'utilisateur voit la version anglaise plutôt qu'un écran vide. Pour gérer vos langues prises en charge depuis un seul endroit, conservez un tableau simple :
// config/app.php ou un fichier de config dédié
'available_locales' => ['tr', 'en', 'fr', 'nl', 'de'],
Fichiers de langue : le texte d'interface
Pour le texte d'interface statique (menus, libellés de boutons, titres de formulaires), utilisez les fichiers de traduction intégrés de Laravel. Il existe deux formats. Le premier est constitué de fichiers tableau PHP ; lang/fr/messages.php ressemble à ceci :
<?php
return [
'welcome' => 'Bienvenue, :name',
'nav' => [
'projects' => 'Projets',
'contact' => 'Contact',
],
];
Dans Blade, vous l'appelez avec __('messages.welcome', ['name' => $user->name]) ; la notation par points atteint les clés imbriquées. Le second format est constitué de fichiers JSON : dans lang/fr.json, vous utilisez la phrase source elle-même comme clé. C'est pratique pour les courtes chaînes d'interface :
{
"Save changes": "Enregistrer les modifications",
"Projects": "Projets"
}
L'appel reste __('Save changes'). Laravel vérifie d'abord le fichier JSON, puis les tableaux PHP. Pour les pluriels, utilisez trans_choice() : trans_choice('messages.apples', $count) avec des règles séparées par des barres verticales dans le fichier, comme 'apples' => '{0} aucune pomme|{1} une pomme|[2,*] :count pommes'.
Routage de locale : détecter et définir la langue
La manière la plus propre de définir la langue à chaque requête est un middleware. Vous pouvez lire la préférence de l'utilisateur d'abord depuis la session, puis depuis l'en-tête Accept-Language :
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() compare l'en-tête Accept-Language du navigateur aux langues que vous prenez en charge, ce qui est plus fiable que de l'analyser à la main. Dans Laravel 11/12, vous enregistrez le middleware dans bootstrap/app.php :
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [\App\Http\Middleware\SetLocale::class]);
})
Si vous voulez la langue dans l'URL (par ex. /en/projects), encadrez vos routes dans un groupe avec préfixe. C'est précieux pour le SEO car chaque langue obtient sa propre adresse indexable :
Route::prefix('{locale}')
->whereIn('locale', ['tr','en','fr','nl','de'])
->group(function () {
Route::get('/projects', [ProjectController::class, 'index']);
});
Un sélecteur de langue écrit alors la préférence dans la session et revient en arrière :
Route::get('/lang/{locale}', function (string $locale) {
if (in_array($locale, config('app.available_locales'), true)) {
session()->put('locale', $locale);
}
return back();
});
Traduire le contenu de la base de données
Le vrai défi n'est pas les fichiers de langue mais le contenu dynamique qu'un utilisateur saisit via le panneau d'administration : titres de projets, descriptions de services, articles de blog. Deux approches solides existent ici. La première consiste à stocker chaque colonne traduisible en JSON. Dans la migration, vous déclarez la colonne en json et la castez dans le modèle :
// migration
$table->json('title');
// model
protected $casts = ['title' => 'array'];
La ligne contient {"tr":"Başlık","en":"Title","fr":"Titre"}. À la lecture, une petite méthode utilitaire renvoie la langue sélectionnée et se replie sur l'anglais, puis sur la première langue disponible :
public function tr(string $field, ?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
$values = $this->{$field} ?? [];
return $values[$locale]
?? $values['en']
?? (reset($values) ?: '');
}
Cette méthode est rapide et ne nécessite aucune table supplémentaire ; c'est exactement ce que j'utilise sur aslain.dev. La seconde approche est le paquet spatie/laravel-translatable. Il standardise la même idée de colonne JSON et résout $project->title vers la langue active automatiquement. L'installation est simple :
composer require spatie/laravel-translatable
Vous ajoutez le trait HasTranslations au modèle et déclarez public array $translatable = ['title', 'body'];. Pour de grands projets avec de nombreuses langues ou des besoins de gestion de traduction, le paquet facilite la maintenance ; pour de petits projets, votre propre trait suffit.
Performance et pièges courants
Quelques points pratiques gardent le système robuste :
- Mise en cache : En production, exécutez
php artisan config:cacheetroute:cache. Les routes contenant des closures ne peuvent pas être mises en cache ; déplacez-les vers un contrôleur. - Ne négligez pas le fallback : Une seule clé manquante produit un titre vide. Définissez toujours une chaîne de repli à l'intérieur de votre contenu JSON également.
- hreflang : Si vous utilisez la langue dans l'URL, ajoutez le lien alternatif de chaque langue dans le
<head>; Google sert alors la bonne langue au bon utilisateur. - Dates et nombres : Avec Carbon,
$date->locale(app()->getLocale())->translatedFormat('d F Y')donne les noms de mois localisés ; ne traduisez pas le format à la main.
Questions fréquentes
Dois-je utiliser une colonne JSON ou une table de traductions séparée ?
Si le nombre de langues est fixe et raisonnable (disons 5), une colonne JSON est la voie la plus simple et la plus rapide ; toutes les langues reviennent en une seule requête. Si vous avez besoin de nombreuses langues, de la possibilité d'interroger les langues séparément, ou de suivre l'état de traduction, une table translations dédiée est plus flexible.
Dois-je gérer la langue via un préfixe d'URL ou via la session ?
Si le SEO compte, préférez un préfixe d'URL (/en/...) : chaque langue obtient sa propre adresse indexable. Pour un panneau purement personnel ou une application en une seule page, un choix basé sur la session/le cookie suffit et reste plus simple.
À quoi sert fallback_locale ?
Si une clé de traduction est introuvable dans la langue sélectionnée, Laravel se replie automatiquement sur la langue fallback_locale. Cela évite que le texte non traduit apparaisse vide ; elle est généralement réglée sur l'anglais.
Vous planifiez un projet Laravel multilingue ? Qu'il s'agisse d'une installation à partir de zéro ou de la migration d'un site existant vers plusieurs langues, je peux vous aider à bâtir une architecture propre et maintenable. Contactez-moi.