Guide de localisation Laravel

Les fichiers de traduction Laravel, les deux styles de lookup, la pluralisation avec trans_choice, le middleware de locale, et la synchronisation des fichiers de langue PHP et JSON depuis la CI.

Laravel prend en charge deux systèmes de traduction côte à côte, ce qui est la première chose à comprendre et la source de la plupart des confusions.

Les deux systèmes

Les tableaux PHP à clés courtes, dans un répertoire par locale :

php
// lang/en/messages.php
return [
    "welcome" => "Welcome back, :name",
    "dashboard" => [
        "title" => "Dashboard",
    ],
];
php
__("messages.welcome", ["name" => $user->first_name]);
__("messages.dashboard.title");

Les fichiers JSON indexés par la chaîne source, un par locale :

json
// lang/fr.json
{
  "Welcome back, :name": "Bon retour, :name"
}
php
__("Welcome back, :name", ["name" => $user->first_name]);

Les deux fonctionnent. Le compromis :

Les clés courtes restent stables quand le texte anglais change, s’organisent proprement en espaces de noms, et sont le bon choix pour tout ce qui est réutilisé ou durable. Le coût est l’indirection — lire un template vous donne une clé, pas ce qu’elle dit.

Les clés JSON en chaîne se lisent très bien dans les templates et sont excellentes pour du prototypage. Leur défaut fatal apparaît la première fois que vous modifiez l’anglais : la clé est l’ancien anglais, donc passer de « Welcome back » à « Welcome back ! » orpheline silencieusement chaque traduction existante, dans chaque langue.

Pour tout ce qui vivra plus longtemps qu’un sprint, utilisez des clés courtes. Mélanger les deux dans un même projet est courant et sans problème — le JSON pour du texte marketing ponctuel, les clés courtes pour l’application.

Notez que l’emplacement a changé : lang/ à la racine du projet depuis Laravel 9, resources/lang/ avant cela.

Placeholders

Laravel utilise un deux-points en préfixe :

php
"welcome" => "Welcome back, :name",
"invitation" => ":inviter invited you to :project",

La casse du placeholder contrôle la casse de la valeur substituée — :Name produit John, :NAME produit JOHN. Astucieux, et bon à savoir, car cela signifie que :name et :Name ne sont pas interchangeables.

La syntaxe a un point sensible : un placeholder immédiatement suivi d’une lettre ou d’un underscore est ambigu, donc :name_suffix s’analyse comme un seul placeholder. Gardez un séparateur après les placeholders, ou renommez-les.

Pluralisation

php
// lang/en/messages.php
"messages" => "{0} No messages|{1} One message|[2,*] :count messages",
php
trans_choice("messages.messages", $count, ["count" => $count]);

Les formes sont séparées par des barres verticales, avec des valeurs exactes optionnelles entre accolades et des intervalles entre crochets. Sans intervalles explicites, Laravel retombe sur deux formes — singulier puis pluriel — sélectionnées par son propre pluraliseur.

C’est le point faible pour une localisation sérieuse. Le pluraliseur intégré de Laravel gère bien un modèle à deux formes, et mal les langues à quatre ou six formes. Le polonais, l’arabe, le russe et le gallois ont besoin d’une notation d’intervalle explicite par langue, écrite à la main, et une frontière mal placée produit un bug qui n’apparaît qu’à certains nombres précis.

Si vous prenez en charge ces langues, les options pratiques sont des intervalles explicites maintenus soigneusement par locale, ou le déplacement de ces messages spécifiques vers une bibliothèque ICU MessageFormat qui exprime les catégories de pluriel par leur nom et délègue les règles à CLDR. Voir les règles de pluriel par langue pour ce que chaque langue exige réellement.

Définir la locale

Depuis un middleware, enregistré tôt :

php
class SetLocale
{
    public function handle(Request $request, Closure $next)
    {
        $locale = $request->segment(1)
            ?? $request->user()?->locale
            ?? $request->getPreferredLanguage(config("app.supported_locales"));

        if (in_array($locale, config("app.supported_locales"), true)) {
            App::setLocale($locale);
        }

        return $next($request);
    }
}

Deux détails. Validez contre une liste blanche — App::setLocale() avec une entrée utilisateur non validée est une traversée de chemin qui n’attend qu’à se produire, puisque la locale devient un nom de répertoire. Et définissez un fallback_locale dans config/app.php pour qu’une clé manquante se dégrade vers votre langue par défaut plutôt que d’afficher la clé brute à un utilisateur.

Blade

blade
<h1>{{ __("messages.dashboard.title") }}</h1>
<p>{{ __("messages.welcome", ["name" => $user->first_name]) }}</p>

@lang("messages.dashboard.title")

{{ }} échappe la sortie, ce qui est ce que vous voulez. Si une traduction contient légitimement du markup, {!! !!} le rend brut — et c’est une décision à prendre délibérément, car cela signifie qu’un traducteur peut injecter du HTML dans votre page.

Le meilleur pattern pour une phrase contenant un lien est de garder le markup entièrement hors de la traduction, en passant les balises comme placeholders :

php
"terms" => "Read our :openterms of service:close before continuing.",

Peu élégant, et plus sûr que de mettre une balise anchor dans un fichier que les traducteurs éditent.

Messages de validation

Les messages de validation de Laravel sont traduisibles nativement dans lang/en/validation.php, y compris les surcharges par attribut :

php
"attributes" => [
    "email" => "email address",
],
"custom" => [
    "email" => [
        "required" => "We need an email address to send your receipt.",
    ],
],

À mettre en place tôt. Le faire après coup implique d’auditer chaque règle personnalisée et chaque message de l’application.

Ce qu’on oublie

  • Les notifications par mail. Le mail en file d’attente s’exécute sans requête, donc la locale doit être passée explicitement. Notification::locale() et le contrat HasLocalePreference sur votre modèle utilisateur gèrent tous les deux ce cas.
  • Les jobs en file d’attente en général. Même problème, même correction.
  • La génération de PDF et d’exports.
  • Les réponses d’erreur d’API, si un utilisateur les voit un jour.
  • Les libellés d’enum et de statut, là où Str::title($status) fait le travail qu’une traduction devrait faire.

Synchroniser les traductions

Les fichiers de tableaux PHP de Laravel sont un format pris en charge, donc ils se synchronisent directement :

bash
wti push      # lang/en/*.php et lang/en.json envoyés
wti pull      # fichiers traduits reçus en retour
wti diff      # ce que changerait un push

WebTranslateIt analyse nativement le format de tableau PHP, y compris les commentaires magiques pour des notes par chaîne aux traducteurs — c’est le bon endroit pour indiquer que :name ne contient qu’un prénom, ou qu’une chaîne a une limite de longueur. Voir le contexte visuel pour les traducteurs pour comprendre pourquoi cela compte plus qu’il n’y paraît.

Push au merge sur votre branche désignée, pull chaque nuit dans une pull request ; les flux de travail de localisation basés sur Git couvre la stratégie de branche.

Localiser les URLs et les modèles

Les segments de route. Laravel n’a pas de routage traduit intégré, donc le pattern habituel est un groupe avec préfixe de locale :

php
Route::prefix("{locale}")
    ->whereIn("locale", config("app.supported_locales"))
    ->middleware(SetLocale::class)
    ->group(function () {
        Route::get("dashboard", DashboardController::class)->name("dashboard");
    });

Puis liez la locale par défaut pour que route("dashboard") n’ait pas besoin qu’on la lui passe à chaque fois :

php
URL::defaults(["locale" => app()->getLocale()]);

Sans cette valeur par défaut, chaque appel à route() dans chaque vue a besoin du paramètre, et celui que vous oubliez lève une exception en production.

Le contenu de modèle traduisible est un problème différent des chaînes d’interface — des données saisies par l’utilisateur plutôt que du texte de développeur, donc cela relève de la base de données plutôt que des fichiers de langue. Les deux formes courantes sont une colonne JSON par attribut traduisible, ou une table de traductions séparée avec une ligne par locale. Les colonnes JSON sont plus simples et s’interrogent mal ; une table de traductions demande plus de travail mais permet d’indexer et de rechercher par langue. Des packages existent pour les deux ; la décision à prendre en premier est de savoir si vous avez besoin de rechercher dans le contenu traduit, car cette réponse tranche la question.

Ne mettez pas de contenu généré par l’utilisateur dans des fichiers de langue. Ce n’est pas du texte traduisible, ça change à l’exécution, et ça entrera en conflit avec votre synchronisation.

Tests

php
public function test_dashboard_renders_in_french(): void
{
    App::setLocale("fr");

    $this->get("/fr/dashboard")->assertOk();
}

Vérifiez le comportement plutôt que les chaînes traduites, pour qu’une révision de texte ne casse pas la suite.

La vérification à automatiser est la complétude : chaque clé présente dans lang/en présente dans chaque autre locale, aucune clé laissée comme sa propre traduction, et chaque chaîne trans_choice portant le nombre de formes que sa langue exige. Le fait que le pluraliseur de Laravel échoue silencieusement pour les langues à plus de deux formes fait de ce dernier point l’assertion la plus utile de l’ensemble.

Erreurs courantes

  • Des clés JSON en chaîne pour du texte qui sera modifié, orphelinant chaque traduction au premier changement de formulation.
  • App::setLocale() sur une entrée non validée.
  • Se fier au pluraliseur par défaut pour des langues à plus de deux formes.
  • Pas de fallback_locale, si bien qu’une clé manquante s’affiche comme messages.dashboard.title à un utilisateur.
  • Du markup à l’intérieur de chaînes de traduction, rendu avec {!! !!}.
  • Oublier la locale sur les mails et jobs en file d’attente.

Frequently asked questions

Où sont stockés les fichiers de langue de Laravel ?
Dans lang/ à la racine du projet pour Laravel 9 et ultérieur, et resources/lang/ avant cela. Les fichiers PHP à clés courtes vivent dans un sous-répertoire par locale tel que lang/fr/messages.php, tandis que les fichiers de traduction JSON se trouvent directement dans lang/ sous la forme lang/fr.json.
Quelle est la différence entre __() et trans_choice() ?
__() récupère une traduction unique. trans_choice() sélectionne entre des formes de pluriel en fonction d’un compte, en utilisant les formes séparées par des barres verticales et la notation d’intervalle optionnelle dans la chaîne de traduction.
Dois-je utiliser des clés courtes ou la chaîne complète comme clé ?
Des clés courtes pour tout ce qui est réutilisé ou susceptible de changer, car la clé reste stable quand le texte est modifié. Les clés JSON en chaîne complète sont pratiques pour du texte ponctuel mais deviennent obsolètes dès que la formulation anglaise est révisée, puisque la clé elle-même est l’ancienne formulation.
Comment définir la locale par requête dans Laravel ?
Appelez App : :setLocale() depuis un middleware enregistré tôt dans la pile, en résolvant la locale à partir du segment d’URL, de la préférence de l’utilisateur authentifié ou de l’en-tête Accept-Language. La définir dans un contrôleur est trop tardif pour tout ce qui est résolu plus tôt dans la requête.

Keep reading

Translate your app without the spreadsheet round-trip

WebTranslateIt reads the file formats and placeholder syntax described on this page, validates them as translators work, and syncs the results straight back into your repository.