Rails i18n : le guide complet

Bien configurer l’internationalisation de Rails : fichiers de locale, interpolation, pluralisation, lazy lookup, fallbacks, et synchronisation des traductions depuis la CI.

Rails embarque un framework i18n compétent, et quasiment aucun avis sur la façon de l’utiliser. Ce guide couvre la configuration qui tient la route au-delà des premières centaines de chaînes.

Les bases

Les traductions vivent dans config/locales, indexées par locale :

yaml
# config/locales/en.yml
en:
  dashboard:
    title: "Dashboard"
    welcome: "Welcome back, %{name}"

Et sont récupérées avec t :

erb
<h1><%= t("dashboard.title") %></h1>
<p><%= t("dashboard.welcome", name: current_user.first_name) %></p>

Le piège du load path

Rails charge config/locales/*.{rb,yml}uniquement au premier niveau. Placez un fichier dans config/locales/admin/en.yml et il sera ignoré silencieusement. C’est la raison la plus fréquente pour laquelle une traduction qui existe pourtant de façon évidente semble manquante.

Corrigez cela dans config/application.rb :

ruby
config.i18n.load_path += Dir[Rails.root.join("config/locales/**/*.{rb,yml}")]

Faites-le tôt. Une fois que vous avez plus de quelques centaines de clés, vous voudrez des sous-dossiers, et découvrir cette règle à ce moment-là signifie déplacer des fichiers.

Une configuration qui vaut le coup

ruby
# config/application.rb
config.i18n.available_locales = %i[en fr de]
config.i18n.default_locale = :en
config.i18n.fallbacks = [:en]

available_locales combiné à enforce_available_locales — activé par défaut — signifie qu’une locale inattendue lève une exception plutôt que de ne rien afficher silencieusement. Gardez-le activé.

Levez une exception sur les traductions manquantes en développement et en test :

ruby
# config/environments/development.rb and test.rb
config.i18n.raise_on_missing_translations = true

C’est le réglage le plus rentable de ce guide. Sans lui, une clé manquante affiche un span translation missing jaune que personne ne remarque dans une langue qu’il ne lit pas. Avec lui, une clé manquante fait échouer le test qui affiche la vue.

En production, gardez les fallbacks actifs afin qu’un manque se replie sur la locale par défaut plutôt que d’afficher du balisage brut à un utilisateur.

Définir la locale

Autour de chaque requête, avec une remise à zéro ensuite :

ruby
class ApplicationController < ActionController::Base
  around_action :switch_locale

  private

  def switch_locale(&)
    I18n.with_locale(locale_from_request, &)
  end

  def locale_from_request
    params[:locale] ||
      current_user&.locale ||
      http_accept_language.compatible_language_from(I18n.available_locales) ||
      I18n.default_locale
  end
end

Utilisez I18n.with_locale plutôt que d’assigner I18n.locale =. L’assignation est locale au thread et persiste dans la requête suivante traitée par ce même thread, ce qui produit ce bug mémorable où un utilisateur voit parfois la langue d’un autre.

Interpolation

Rails utilise %{name} :

yaml
en:
  invitation: "%{inviter} invited you to %{project}"

Des placeholders nommés plutôt que positionnels, ce qui compte car l’ordre des mots diffère selon les langues — voir l’aide-mémoire des formats de placeholder.

Deux choses à savoir. Un argument d’interpolation manquant lève I18n::MissingInterpolationArgument, ce qui est le comportement souhaité. Et un % littéral dans une chaîne avec interpolation doit être échappé en %%.

Pour des valeurs formatées, utilisez %<name>s avec un spécificateur de format :

yaml
en:
  progress: "%<percent>.1f%% complete"

Pluralisation

Passez count et imbriquez les catégories :

yaml
en:
  messages:
    one: "1 message"
    other: "%{count} messages"
ruby
t("messages", count: 5)   # => "5 messages"

Le point critique : les catégories requises dépendent de la langue cible, pas de l’anglais. Le polonais a besoin de one, few, many, other ; l’arabe en a besoin de six. Un fichier polonais avec seulement one et other lèvera I18n::InvalidPluralizationData pour la plupart des nombres.

Rails n’intègre que les règles anglaises. Pour tout le reste, ajoutez la gem rails-i18n, qui embarque la pluralisation CLDR pour une longue liste de locales, ou le backend de pluralisation de la gem i18n. Voir les règles de pluriel par langue pour ce dont chaque langue a besoin.

Notez aussi que zero est prise en charge par Rails comme une clé de confort, vérifiée avant la catégorie CLDR quand count vaut 0 — utile pour formuler un état vide différemment, mais ce n’est pas une catégorie CLDR dans la plupart des langues.

Lazy lookup

Dans une vue, un point en tête se résout par rapport au chemin du template :

erb
<%# app/views/posts/index.html.erb %>
<h1><%= t(".title") %></h1>   <%# => posts.index.title %>

Cela garde les clés courtes et reflète votre structure. Le prix à payer, c’est que posts.index.title n’apparaît jamais littéralement dans le code, donc grep pour une clé trouvée dans un fichier de locale ne renvoie rien. Choisissez une convention et appliquez-la de façon cohérente — mélanger les deux est ce qui rend les clés vraiment difficiles à retrouver.

Le même mécanisme fonctionne dans les contrôleurs pour les messages flash, en se résolvant par rapport à controller.action.

Structurer les clés

Deux écoles, et le choix compte davantage que celle que vous choisissez :

Refléter la structure des vuesposts.index.title. S’associe naturellement au lazy lookup, les clés sont évidentes d’après leur emplacement, et déplacer une vue signifie déplacer ses clés.

Grouper par senserrors.card_declined, nav.settings. Encourage la réutilisation, survit aux refactorings, et exige un nommage réfléchi.

En pratique, la plupart des applications finissent par combiner les deux : des clés structurelles pour le texte spécifique à une page, et un espace de noms partagé pour tout ce qui est réutilisé. Ce qui compte, c’est qu’une chaîne partagée vive à un seul endroit — la même phrase dupliquée sous trois clés, ce sont trois traductions à payer et trois occasions de diverger.

Modèles et attributs

Rails recherche automatiquement les noms de modèles et d’attributs :

yaml
en:
  activerecord:
    models:
      user:
        one: "User"
        other: "Users"
    attributes:
      user:
        email: "Email address"
    errors:
      models:
        user:
          attributes:
            email:
              blank: "is required"

C’est ce qui rend les messages d’erreur de validation traduisibles sans toucher au modèle. Cela vaut le coup de le mettre en place tôt, car le faire après coup implique d’auditer chaque message d’erreur personnalisé.

Vues, mailers et tout le reste

Les chaînes que les développeurs oublient, dans l’ordre où elles sont habituellement oubliées :

  • Sujets et corps des mailers. Définissez la locale à partir de la préférence du destinataire, pas de la requête en cours — un job en arrière-plan qui envoie une notification n’a pas de locale de requête.
  • Jobs en arrière-plan. Même problème. Passez la locale explicitement dans les arguments du job.
  • Exports PDF et CSV. Souvent construits séparément et jamais internationalisés.
  • Messages d’erreur d’API JSON, s’ils sont un jour exposés à un utilisateur.
  • Données de seed et libellés d’enum. t("statuses.#{status}") plutôt que status.humanize.

Synchroniser les traductions

Dès que la traduction se fait hors du dépôt, il faut que les fichiers circulent automatiquement, sinon ils dérivent. Avec le CLI de WebTranslateIt :

bash
wti push      # envoie config/locales/en.yml
wti pull      # récupère les locales traduites
wti status    # niveau d’avancement par langue
wti diff      # ce qu’un push changerait, sans le faire

Intégrez-le à la CI : poussez les chaînes source à chaque merge sur votre branche désignée, récupérez les traductions chaque nuit dans une pull request. wti diff en tant que vérification de pull request est la pièce qui vaut le coup d’ajouter tôt — elle révèle que « ce refactoring supprime 40 segments traduits » pendant que le changement est encore en relecture.

Un fichier .wti à la racine du dépôt contient le token du projet et le mapping des fichiers, si bien que la configuration voyage avec le code. Voir les flux de travail de localisation basés sur Git pour la stratégie de branches qui évite que les push se marchent dessus.

Garder les fichiers honnêtes

Deux problèmes s’accumulent silencieusement : des clés référencées dans le code qui n’existent dans aucun fichier de locale, et des clés qui dorment dans les fichiers de locale sans que rien ne les référence plus. Les deux sont invisibles jusqu’à ce que quelqu’un tombe sur la manquante en production.

La gem i18n-tasks trouve les deux :

bash
i18n-tasks missing      # référencées dans le code, absentes des fichiers de locale
i18n-tasks unused       # présentes dans les fichiers de locale, référencées nulle part
i18n-tasks normalize    # trie les clés de façon cohérente pour garder des diffs lisibles
i18n-tasks health       # tout en une fois

Ajoutez i18n-tasks missing à la CI et une clé manquante devient un build en échec plutôt qu’un ticket support. normalize vaut aussi le coup d’être lancé — sans un ordre de clés cohérent, chaque fichier de locale régénéré produit un diff qui touche chaque ligne, ce qui rend les pull requests de traduction impossibles à relire.

La seule réserve, c’est que les clés dynamiques — t("statuses.#{status}") — semblent inutilisées pour une analyse statique. i18n-tasks permet de déclarer ces motifs dans sa config afin qu’ils ne soient plus signalés.

Une checklist

  • config.i18n.load_path élargi pour inclure les sous-dossiers.
  • raise_on_missing_translations activé en développement et en test.
  • Fallbacks configurés pour la production.
  • I18n.with_locale dans un around_action, jamais une simple assignation.
  • rails-i18n ajoutée si vous prenez en charge une locale autre que l’anglais.
  • Catégories de pluriel générées pour chaque langue cible, pas copiées depuis l’anglais.
  • Mailers et jobs qui reçoivent une locale explicite.
  • Synchronisation automatisée dans les deux sens.

Frequently asked questions

Où placer les fichiers de locale Rails ?
Dans config/locales. Par défaut, Rails ne charge que config/locales/*.{rb,yml} — les fichiers dans des sous-dossiers sont ignorés à moins de les ajouter à config.i18n.load_path, ce qui est la raison la plus fréquente pour laquelle une traduction semble manquante.
Comment pluraliser avec Rails i18n ?
Passez une option count et imbriquez les catégories de pluriel sous la clé. Rails sélectionne la catégorie selon les règles de pluralisation de la locale, si bien qu’une clé en polonais a besoin de one, few, many et other, contrairement à une clé en anglais qui ne nécessite que one et other.
Qu’est-ce que le lazy lookup dans Rails ?
Dans une vue, t('.title') avec un point en tête se résout par rapport au chemin du template — dans app/views/posts/index.html.erb, cela recherche posts.index.title. Cela garde les clés courtes et reflète la structure de vos vues, au prix d’une difficulté accrue à retrouver les clés par recherche textuelle.
Comment gérer les traductions manquantes dans Rails ?
Configurez des fallbacks pour qu’une clé manquante se replie sur la locale par défaut plutôt que d’afficher un span translation-missing. En développement et en test, levez plutôt une exception sur les traductions manquantes, afin que le manque échoue bruyamment là où quelqu’un le verra.

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.