Aller au contenu

Internationalisation backend (Spring Boot)

Le backend Spring Boot utilise des fichiers Java Properties standards pour ses traductions. Cette approche est native à Spring et bénéficie d'outils IDE matures (coloration, validation, go-to-definition). L'intégration avec le reste du système se fait via un service dédié I18nService qui encapsule le chargement et la résolution des clés.

Sources

backend/src/main/resources/messages_fr.properties, messages_en.properties, backend/src/main/java/com/perfshop/service/I18nService.java

Fichiers

Deux fichiers à la racine de src/main/resources/ :

backend/src/main/resources/
├── messages_fr.properties   (~35 KB, ~430 clés)
├── messages_en.properties   (~34 KB, ~430 clés)
├── application.yml
├── db/migration-fr/
└── i18n/
    ├── enigmes/
    └── logique/

Chaque fichier suit le format Java Properties classique :

# Messages d'erreur API
api.error.product_not_found=Produit introuvable
api.error.cart_empty=Votre panier est vide
api.error.payment_invalid=Les informations de paiement sont invalides

# Chaos métier
chaos.business.a1.description=TVA 19,6% au lieu de 20%
chaos.business.a1.log_detail=Application d'une TVA à 19,6% sur la commande {0}

# Licences
license.error.no_license=Aucune licence active sur cette instance PerfShop
license.error.feature_denied=Licence requise pour accéder à {0}
license.status.none_label=Aucune licence

Les valeurs peuvent contenir des placeholders numérotés au format {0}, {1}, etc. Ce sont les positions d'arguments de MessageFormat standard Java.

Conventions de nommage des clés

Les clés suivent une hiérarchie par points, en anglais, et suivent un schéma cohérent :

Préfixe Usage
api.error.* Messages d'erreur des endpoints REST
api.success.* Messages de succès
chaos.business.aN.* Descriptions et logs des anomalies métier A1 à A16
chaos.security.sN.* Descriptions et logs des failles OWASP S1 à S12
chaos.functional.fN.* Descriptions des exceptions F1 à F4
chaos.scripting.* Tokens Chaos Scripting
chaos.infra.* Chaos infrastructure (CPU, mémoire, GC…)
chaos.level* Labels des niveaux (Junior, Confirmé, Expert, Master)
scenario.n1-01.* Scénarios météo (name, description)
license.error.* Erreurs de validation de licence
license.status.* Labels du statut de licence
admin.service.* Messages de AdminUserService (validation, exceptions)
order.status.* Labels des statuts de commande
logique.* Catalogue des 25 questions logique pédagogique

Cette normalisation facilite les recherches : grep -r "chaos.business" messages_fr.properties trouve immédiatement toutes les clés liées au Chaos Métier.

Service I18nService

I18nService est un bean Spring qui charge les dictionnaires au démarrage et expose des méthodes de résolution.

Chargement

Au démarrage, I18nService lit la variable PERFSHOP_LANG (via @Value ou application.yml), détermine le nom du fichier messages_<lang>.properties correspondant et le charge en mémoire. Le français est chargé en premier comme dictionnaire de fallback :

@Service
public class I18nService {
    private final Properties current;   // langue active
    private final Properties fallback;  // toujours fr

    public I18nService(@Value("${perfshop.lang:fr}") String lang) {
        this.fallback = loadProperties("messages_fr.properties");
        this.current  = lang.equals("fr")
            ? this.fallback
            : loadProperties("messages_" + lang + ".properties");
    }
    // ...
}

Si la langue demandée n'a pas de fichier correspondant, I18nService logue un warning et utilise le dictionnaire français seul.

Méthodes exposées

Méthode Rôle
t(String key) Traduit une clé sans argument, retourne la clé elle-même si absente
t(String key, Object... args) Traduit avec substitution MessageFormat des {0}, {1}...

Exemples d'utilisation depuis un contrôleur ou un service :

return ResponseEntity.badRequest()
    .body(Map.of("error", i18n.t("api.error.cart_empty")));

return ResponseEntity.status(422)
    .body(Map.of("error", i18n.t("license.error.expired", info.expiresAt)));

throw new IllegalArgumentException(
    i18n.t("admin.service.email_exists", email));

Le résultat est une chaîne déjà traduite dans la langue active, prête à être renvoyée au client.

Lifecycle

sequenceDiagram
  autonumber
  participant Env as .env
  participant Spring as Spring Boot
  participant I18n as I18nService
  participant Fs as Filesystem

  Env->>Spring: PERFSHOP_LANG=fr
  Spring->>I18n: @PostConstruct init
  I18n->>Fs: load messages_fr.properties (fallback)
  Fs-->>I18n: Properties fr (430 clés)
  I18n->>Fs: load messages_fr.properties (current)
  Note over I18n: Même fichier<br/>fallback = current
  I18n-->>Spring: I18nService prêt

  Note over Spring: Appels pendant l'exécution
  Spring->>I18n: t("api.error.cart_empty")
  I18n-->>Spring: "Votre panier est vide"

En mode anglais :

sequenceDiagram
  autonumber
  participant Env as .env
  participant Spring as Spring Boot
  participant I18n as I18nService
  participant Fs as Filesystem

  Env->>Spring: PERFSHOP_LANG=en
  Spring->>I18n: @PostConstruct init
  I18n->>Fs: load messages_fr.properties (fallback)
  Fs-->>I18n: Properties fr (430 clés)
  I18n->>Fs: load messages_en.properties (current)
  Fs-->>I18n: Properties en (430 clés)
  I18n-->>Spring: I18nService prêt

  Spring->>I18n: t("api.error.cart_empty")
  I18n-->>Spring: "Your cart is empty"
  Spring->>I18n: t("new.key.not.yet.translated")
  Note over I18n: Clé absente de EN
  I18n-->>Spring: (fallback FR) "Nouvelle clé..."

Intégration avec les exceptions métier

Plusieurs exceptions personnalisées (CartItemNotFoundException, CartAccessDeniedException, etc.) reçoivent un I18nService et construisent leur message au moment de la levée :

public class CartItemNotFoundException extends RuntimeException {
    public CartItemNotFoundException(I18nService i18n, Long itemId) {
        super(i18n.t("api.error.cart_item_not_found", itemId));
    }
}

Cette approche garantit qu'un message d'exception est toujours dans la langue active au moment où il est levé. L'exception peut ensuite remonter sans transformation jusqu'au @ExceptionHandler global qui le renvoie au client.

Symétrie FR/EN

Les deux fichiers doivent contenir exactement les mêmes clés. Au moment de cette rédaction, la symétrie est de 430 clés en FR / 430 clés en EN.

Pour vérifier la symétrie, un one-liner shell suffit :

diff <(grep -Eo '^[^#][^=]+' messages_fr.properties | sort) \
     <(grep -Eo '^[^#][^=]+' messages_en.properties | sort)

Ce diff doit être vide. Toute sortie signale une dérive à corriger.

Utilisation des placeholders

Les placeholders suivent la syntaxe MessageFormat standard :

api.error.product_out_of_stock=Le produit {0} n'est plus en stock
license.error.expired=La licence a expiré le {0}
admin.service.account_not_found=Compte admin introuvable pour l'id {0}

Un point à surveiller : MessageFormat traite les {...} spécialement si le nombre est suivi d'un type ({0,number}, {1,date}). Pour un simple remplacement textuel, laisser {0} tel quel suffit.

Les apostrophes françaises dans les valeurs sont une source classique de bugs MessageFormat — elles sont interprétées comme délimiteurs de quote et doivent être doublées ('') dans la valeur properties si la chaîne contient aussi des placeholders. Sans placeholders, pas de problème.

Noms de clé en anglais, valeurs dans la langue cible

Convention : les clés sont toujours en anglais, quel que soit le fichier. Seules les valeurs sont traduites :

# messages_fr.properties
api.error.cart_empty=Votre panier est vide

# messages_en.properties
api.error.cart_empty=Your cart is empty

# messages_es.properties (future)
api.error.cart_empty=Tu carrito está vacío

Cette convention facilite la maintenance et permet à un développeur non-francophone de comprendre la structure du fichier sans lire les valeurs.

Ajouter une clé

  1. Ajouter la clé en français dans messages_fr.properties
  2. Ajouter la même clé en anglais dans messages_en.properties
  3. Rebuild le backend (mvn clean package)
  4. Utiliser la clé dans le code Java via i18n.t("ma.nouvelle.cle")

Aucun autre fichier à toucher. Spring Boot recharge automatiquement les properties au redémarrage.

Ajouter une nouvelle langue

  1. Copier messages_fr.properties vers messages_<lang>.properties
  2. Traduire toutes les valeurs (laisser les clés telles quelles)
  3. Déployer avec PERFSHOP_LANG=<lang>

Aucune modification de code requise. I18nService charge automatiquement le nouveau fichier via sa convention de nommage.

Voir aussi