Le pipeline de traduction
trans() cherche dans le catalogue, applique le formateur, gère le fallback — en 3 étapes.
Quand tu appelles $translator->trans('app.welcome'), Symfony exécute trois étapes dans cet ordre précis :
Symfony charge le catalogue de la locale demandée (ex.
fr) et cherche la clé dans le domaine indiqué (défaut : messages). Si la clé existe, il récupère la chaîne brute.
La chaîne brute passe dans le MessageFormatter. Il remplace les paramètres
%param%, évalue les pluriels Symfony Interval, ou délègue à l'IntlFormatter pour les domaines ICU (+intl-icu).
Si la clé est introuvable dans la locale demandée, Symfony remonte la chaîne de fallback :
fr_BE → fr → en. Si aucune locale ne possède la clé, trans() retourne la clé elle-même.
La signature de trans()
public function trans( string $id, // clé de traduction array $parameters = [], // ['%name%' => 'Alice'] ?string $domain = null, // null = 'messages' ?string $locale = null, // null = locale courante ): string
trans() tu obtiens la traduction dans une locale différente sans changer la locale globale de la requête. Utile pour envoyer un e-mail dans la langue de l'utilisatrice pendant que la session reste dans une autre langue.
Visualise le pipeline
Choisis une clé et une locale, observe les trois étapes du pipeline.
Fichiers de traduction — domaines et formats
domain.locale.format — la convention qui organise tes traductions en domaines et formats.
Symfony localise les fichiers de traduction selon une convention de nommage simple :
domain.locale.format Exemples : messages.fr.yaml messages.en.yaml validators.fr.xliff security.en.yaml checkout.fr.yaml # domaine personnalisé "checkout"
Le domaine regroupe des traductions par contexte fonctionnel.
messages est le domaine par défaut. Tu peux créer autant de domaines que tu veux : validators, security, emails, admin…
La locale suit le standard BCP 47 :
fr, en, fr_BE, en_US. Symfony supporte les deux formes (fr_FR et fr-FR), mais la convention Symfony utilise le tiret bas.
Formats disponibles
| Format | Loader | Usage recommandé |
|---|---|---|
yaml | YamlFileLoader | Développement, petits projets |
xliff | XliffFileLoader | Workflow de traduction professionnel (outil tiers) |
json | JsonFileLoader | Intégration front-end, partage avec JS |
php | PhpFileLoader | Traductions avec logique dynamique |
po / mo | PoFileLoader / MoFileLoader | Migration depuis gettext |
ini | IniFileLoader | Systèmes hérités |
csv | CsvFileLoader | Traductions gérées dans un tableur |
Structure d'un fichier YAML
# translations/messages.fr.yaml app: welcome: 'Bienvenue, %name% !' goodbye: 'Au revoir.' items_count: '{0} Aucun article|{1} Un article|]1,Inf] %count% articles'
Les clés peuvent être imbriquées avec des points ou via la hiérarchie YAML — Symfony les aplatit en app.welcome, app.goodbye, etc.
Structure d'un fichier XLIFF 2.0
<!-- translations/messages.fr.xliff --> <xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr"> <file id="messages"> <unit id="app.welcome"> <segment> <source>Welcome, %name%!</source> <target>Bienvenue, %name% !</target> </segment> </unit> </file> </xliff>
translation:extract et envoie-le aux traductrices.
Répertoire par défaut et chemins additionnels
# config/packages/translation.yaml framework: translator: default_path: '%kernel.project_dir%/translations' paths: - '%kernel.project_dir%/src/Translations'
Symfony charge tous les fichiers de tous les répertoires déclarés. En cas de clé en doublon, le dernier chargé l'emporte.
Resources/translations/. Symfony les charge automatiquement. Pour les surcharger, place un fichier du même nom dans le répertoire translations/ de ton projet.
Paramètres et substitution
Les paramètres %param% remplacent les placeholders — TranslatableMessage pour les objets.
La substitution de paramètres transforme une chaîne générique en message personnalisé. La convention Symfony utilise les délimiteurs %param%.
# messages.fr.yaml app.greeting: 'Bonjour, %name% ! Tu as %count% nouveau(x) message(s).'
// En PHP $translator->trans( 'app.greeting', ['%name%' => 'Alice', '%count%' => 3], );
Par convention, les paramètres sont entourés de
%. C'est une convention, pas une obligation technique — mais %count% a un rôle spécial pour la pluralisation.
Paramètres dans les templates Twig
{# Dans un template Twig #} {{ 'app.greeting'|trans({ '%name%': user.name, '%count%': messages|length }) }} {# Ou avec le tag trans #} {% trans with {'%name%': user.name} %}app.greeting{% endtrans %}
TranslatableMessage — les objets traduisibles
Quand tu veux passer un message traduisible comme valeur (par exemple dans une contrainte de validation, une notification, ou une réponse API), utilise TranslatableMessage :
use Symfony\Component\Translation\TranslatableMessage; // Création $msg = new TranslatableMessage( 'app.greeting', ['%name%' => 'Alice'], 'messages', // domaine, optionnel ); // Traduction explicite $string = $msg->trans($translator, 'fr'); // Twig traduit automatiquement les TranslatableInterface {{ message }}
TranslatableMessage implémente TranslatableInterface, qui expose une seule méthode : trans(TranslatorInterface $translator, ?string $locale): string. Tu peux créer tes propres objets traduisibles en implémentant cette interface — utile dans ta couche métier pour des objets qui portent un libellé (une devise, un statut, un type de document).
Paramètres globaux
Un paramètre utilisé partout (nom de l'appli, version…) peut être déclaré une seule fois sur le traducteur :
// Dans un EventSubscriber ou CompilerPass $translator->addGlobalParameter('%app_name%', 'Codex'); # messages.fr.yaml app.footer: '© %app_name% — Tous droits réservés'
%app_name% dans chaque appel trans(). Les paramètres locaux d'un appel trans() écrasent les paramètres globaux en cas de conflit.
% a une signification spéciale en YAML Symfony (paramètre de container). Entoure tes chaînes de guillemets simples ou doubles si elles contiennent des %.
Pluralisation — format Symfony Interval
[0] aucun|[1] un|]1,Inf] plusieurs — le format d'intervalles de Symfony pour les pluriels.
La pluralisation Symfony utilise un format d'intervalles séparés par des |. Ce format gère les cas spéciaux de manière explicite, sans dépendre de la locale.
Syntaxe des intervalles
# Intervalles mathématiques app.items: '[0] Aucun article|[1] Un article|]1,Inf] %count% articles' # Syntaxe simplifiée one/other app.comments: 'un commentaire|%count% commentaires' # Cas spéciaux app.users: '{0} Personne|{1} Une personne|[2,5] Quelques personnes|]5,Inf] %count% personnes'
[1,5] = de 1 à 5 inclus. ]1,5[ = de 2 à 4 (exclusif des deux côtés). ]1,Inf] = strictement supérieur à 1. {0} = exactement 0. Inf désigne l'infini.
Déclenchement de la pluralisation
La pluralisation est déclenchée par la présence de %count% dans les paramètres :
// Le paramètre spécial %count% active le moteur de pluralisation $translator->trans( 'app.items', ['%count%' => 3], ); // → "3 articles" $translator->trans( 'app.items', ['%count%' => 0], ); // → "Aucun article"
%count% dans les paramètres, le moteur de pluralisation n'est pas invoqué. Avec %count%, Symfony sélectionne automatiquement le segment d'intervalle correspondant.
Dans Twig
{# Filtre trans avec count #} {{ 'app.items'|trans({'%count%': items|length}) }} {# Tag transchoice (syntaxe alternative) #} {% trans with {'%count%': items|length} %}app.items{% endtrans %}
Ordre des segments
L'ordre des segments dans la chaîne n'a pas d'importance — Symfony évalue chaque intervalle de gauche à droite et retourne le premier qui correspond. Par convention, on les écrit du cas le plus restrictif au plus général.
| count | Résultat avec [0] Aucun|[1] Un|]1,Inf] %count% |
|---|---|
| 0 | Aucun |
| 1 | Un |
| 2 | 2 |
| 100 | 100 |
ICU MessageFormat — pluriels et sélections avancés
{count, plural, ...} et {gender, select, ...} — le format ICU pour les cas complexes.
Le format ICU (International Components for Unicode) est le standard de l'industrie pour l'internationalisation. Il gère nativement les règles grammaticales de toutes les langues, les sélections de genre, et les structures imbriquées.
Activer ICU pour un domaine
Il suffit d'ajouter le suffixe +intl-icu au nom du domaine dans le nom de fichier :
# Symfony Interval (standard) translations/messages.fr.yaml # ICU MessageFormat translations/messages+intl-icu.fr.yaml
messages+intl-icu est le domaine ICU correspondant à messages. Dans le code PHP et Twig, tu continues à appeler trans('app.welcome', [], 'messages') — Symfony choisit automatiquement le bon formateur selon le fichier disponible.
Pluriels ICU — plural
# translations/messages+intl-icu.fr.yaml app.items: '{count, plural, =0 {Aucun article} one {# article} other {# articles} }' app.items_en: '{count, plural, =0 {No items} one {# item} other {# items} }'
Le symbole # dans la valeur est remplacé par la valeur numérique du paramètre. Les catégories (zero, one, two, few, many, other) suivent les règles CLDR de chaque langue — other est le cas par défaut.
Sélections de genre — select
app.invite: '{gender, select, male {Il a accepté l''invitation} female {Elle a accepté l''invitation} other {La personne a accepté l''invitation} }'
l''invitation → "l'invitation".
Imbrication plural + select
app.notification: '{count, plural, =0 {Aucune notification} one {{gender, select, male {Il a} female {Elle a} other {La personne a}} une notification} other {{gender, select, male {Il a} female {Elle a} other {La personne a}} {count} notifications} }'
Appel PHP
// Les paramètres ICU n'ont pas de délimiteurs % $translator->trans( 'app.items', ['count' => 3], // pas de %count%, juste 'count' 'messages', ); // Genre $translator->trans( 'app.invite', ['gender' => 'female'], 'messages', );
%. count au lieu de %count%, gender au lieu de %gender%. Mélanger les deux formats dans le même fichier est une source de confusion — choisis l'un ou l'autre par domaine.
Simulateur — Symfony Interval vs ICU
Change la valeur du compteur pour voir la différence entre les deux formats.
MessageCatalogue et fallback
Le catalogue stocke les traductions d'une locale — le fallback enchaîne les catalogues pour les traductions manquantes.
Le MessageCatalogue est la structure de données qui stocke les traductions pour une seule locale. C'est lui que Symfony charge en mémoire lors du premier appel à trans().
L'interface du catalogue
use Symfony\Component\Translation\MessageCatalogue; // Création manuelle (utile pour les tests) $catalogue = new MessageCatalogue('fr', [ 'messages' => [ 'app.welcome' => 'Bienvenue !', ], ]); // Méthodes clés $catalogue->has('app.welcome'); // true (avec fallback) $catalogue->defines('app.welcome'); // true (SANS fallback) $catalogue->get('app.welcome'); // 'Bienvenue !' $catalogue->all('messages'); // ['app.welcome' => '...'] $catalogue->set('app.new', 'Nouveau !'); // ajoute/modifie
has() retourne true si la clé existe dans ce catalogue ou dans un catalogue de fallback. defines() retourne true uniquement si la clé est définie directement dans ce catalogue — sans remonter la chaîne.
Chaîne de fallback
La chaîne de fallback relie plusieurs catalogues par pointeurs. Quand une clé est introuvable dans un catalogue, Symfony suit la chaîne vers le catalogue suivant :
// addFallbackCatalogue() — chaîne de pointeurs (ne copie pas) $catalogueFrBe->addFallbackCatalogue($catalogueFr); $catalogueFr->addFallbackCatalogue($catalogueEn); // addCatalogue() — fusion directe dans le catalogue courant $catalogueFr->addCatalogue($catalogueFrExtra);
addFallbackCatalogue() crée une référence vers un autre catalogue : si une clé manque, Symfony consulte le catalogue suivant au moment de la traduction. addCatalogue() fusionne les messages d'un autre catalogue dans le courant — les clés existantes ne sont pas écrasées.
Configurer les locales de fallback
# config/packages/translation.yaml framework: translator: fallbacks: ['fr', 'en']
Ou en PHP (pour les tests unitaires) :
$translator->setFallbackLocales(['fr', 'en']);
en complet — c'est le filet de sécurité. Démarre les nouvelles traductions en en, puis ajoute les autres locales progressivement. Un message manquant en fr affichera la version anglaise plutôt que la clé brute.
Le catalogue dans les tests
use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Loader\ArrayLoader; // Traducteur léger pour les tests unitaires $translator = new Translator('fr'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', [ 'app.welcome' => 'Bienvenue !', ], 'fr'); $translator->trans('app.welcome'); // 'Bienvenue !'
LocaleSwitcher — changer de locale dynamiquement
runWithLocale() exécute du code dans une locale différente sans affecter la requête courante.
Le LocaleSwitcher (Symfony 6.4+) permet de changer temporairement la locale active sans toucher à la locale de la requête HTTP en cours. C'est l'outil idéal pour les e-mails, les exports, ou toute opération qui doit se dérouler dans une langue différente.
Imagines une admin francophone qui déclenche l'envoi d'un e-mail de confirmation à une utilisatrice anglophone. Si tu changes
$request->setLocale('en'), tu risques d'affecter toute la suite de la requête. LocaleSwitcher isole ce changement.
Interface du LocaleSwitcher
use Symfony\Component\Translation\LocaleSwitcher; // Lecture $switcher->getLocale(); // locale courante // Changement permanent (jusqu'au reset) $switcher->setLocale('en'); // Exécution dans une locale différente, puis retour automatique $result = $switcher->runWithLocale('en', function () use ($translator) { return $translator->trans('app.welcome'); }); // Réinitialisation à la locale par défaut du container $switcher->reset();
Exécute le callback dans la locale indiquée, puis restaure automatiquement la locale précédente — même si le callback lance une exception. C'est un pattern scoped change, comparable à
withLocale en Java ou au context manager Python.
Cas d'usage — e-mail dans la langue du destinataire
final class SendWelcomeEmailHandler { public function __construct( private readonly LocaleSwitcher $switcher, private readonly MailerInterface $mailer, private readonly Environment $twig, ) {} public function handle(Input $input): void { // L'e-mail est rendu dans la langue de l'utilisatrice $html = $this->switcher->runWithLocale( $input->locale, fn() => $this->twig->render('emails/welcome.html.twig', [ 'name' => $input->name, ]), ); // La session admin reste inchangée $this->mailer->send( (new Email()) ->to($input->email) ->html($html), ); } }
Injection dans les services
# config/services.yaml — autowiring automatique # LocaleSwitcher est disponible dès Symfony 6.4 App\MyService: arguments: $switcher: '@translation.locale_switcher'
runWithLocale() change aussi la locale de l'environnement Twig pendant l'exécution du callback. Les filtres |trans et les fonctions Twig utilisent automatiquement la locale du switcher.
setLocale() sans reset() peut affecter les requêtes suivantes si le processus est réutilisé par un OPcache chaud. Préfère runWithLocale() pour les changements temporaires.
Extraction — garder les fichiers de traduction à jour
translation:extract scanne le code et les templates pour trouver les clés manquantes.
Maintenir les fichiers de traduction à la main devient vite ingérable dans un projet qui grossit. La commande translation:extract automatise ce travail.
Fonctionnement
La commande scanne :
- Les templates Twig (
|trans, tag{% trans %}) - Le code PHP (appels à
TranslatorInterface::trans()etnew TranslatableMessage())
Elle compare les clés trouvées avec les fichiers de traduction existants et :
- Ajoute les clés manquantes avec une valeur vide (ou la clé elle-même)
- Ne supprime jamais les clés existantes (sauf avec l'option
--clean)
Commandes courantes
# Voir ce qui manque (sans toucher aux fichiers) php bin/console translation:extract fr --dir=src --output-dir=translations # Extraire vers XLIFF (pour envoi à une agence de traduction) php bin/console translation:extract fr --format=xliff --output-dir=translations # Extraire toutes les locales configurées php bin/console translation:extract --all --output-dir=translations # Nettoyer les clés orphelines (plus utilisées) php bin/console translation:extract fr --clean --output-dir=translations # Cibler un domaine précis php bin/console translation:extract fr --domain=validators --output-dir=translations
--dir : répertoire scanné pour trouver les clés (code source). --output-dir : répertoire où écrire les fichiers de traduction. Ces deux chemins peuvent être différents.
Workflow recommandé
- Tu ajoutes de nouvelles clés dans le code PHP/Twig.
- Tu lances
translation:extract fr— les nouvelles clés apparaissent dansmessages.fr.yamlavec une valeur vide. - Tu (ou une traductrice) remplis les valeurs vides.
- Tu lances
translation:extract fr --cleanpour supprimer les clés obsolètes.
Contraintes de l'extraction statique
trans('prefix.' . $variable)) ne sont pas détectées. Pour ces cas, déclare les clés dans un commentaire ou un fichier dédié, ou utilise des préfixes constants.
// Clé dynamique — non détectée par extract $translator->trans('status.' . $status); // Solution : déclarer explicitement les clés utilisées // translator-keys: status.active, status.inactive, status.pending $translator->trans('status.' . $status);
Intégration CI
# .github/workflows/ci.yml — vérifier qu'aucune clé ne manque - name: Check missing translations run: | php bin/console translation:extract fr --output-dir=var/trans-check diff translations/messages.fr.yaml var/trans-check/messages.fr.yaml
Configuration et injection
translation.yaml, enabled_locales, paths additionnels, injection de TranslatorInterface.
La configuration du composant Translation se fait dans config/packages/translation.yaml. Voici les options les plus importantes et leur impact.
Fichier de configuration complet
# config/packages/translation.yaml framework: default_locale: fr # locale si aucune n'est précisée translator: default_path: '%kernel.project_dir%/translations' paths: # répertoires additionnels - '%kernel.project_dir%/src/Translations' enabled_locales: ['en', 'fr', 'de'] # locales compilées en cache fallbacks: ['fr', 'en'] # ordre de fallback si clé absente default_domain: messages # domaine utilisé quand $domain=null logging: false # true = log les clés manquantes
Déclarer les locales actives permet à Symfony de ne compiler en cache que les fichiers de ces locales. Sans cette option, tous les fichiers de traductions sont chargés. C'est aussi la liste utilisée par
LocaleSwitcher pour valider les locales.
Injection de TranslatorInterface
use Symfony\Contracts\Translation\TranslatorInterface; final class NotificationService { public function __construct( private readonly TranslatorInterface $translator, ) {} public function buildMessage(string $userLocale): string { return $this->translator->trans( 'notification.ready', [], 'messages', $userLocale, ); } }
Symfony\Contracts\Translation\TranslatorInterface, jamais la classe concrète Translator. Cela respecte le principe de substitution et facilite les mocks dans les tests.
Locale dans les tests fonctionnels
// Dans un test fonctionnel Symfony $client->request('GET', '/fr/accueil'); // Récupérer le traducteur dans les tests unitaires $translator = static::getContainer()->get(TranslatorInterface::class); $translator->setLocale('en'); $result = $translator->trans('app.welcome');
Déboguer les traductions
# Lister toutes les traductions chargées pour une locale php bin/console debug:translation fr # Vérifier une clé précise php bin/console debug:translation fr --domain=messages # Voir l'état du cache php bin/console cache:clear --env=dev
| Commande | Usage |
|---|---|
debug:translation fr | Lister toutes les clés, leur statut (traduit / manquant / hérité) |
translation:extract fr | Synchroniser les fichiers de traduction avec le code |
cache:clear | Forcer le rechargement des catalogues en dev |
Synthèse — guide de choix
Quel format, quel domaine, Interval vs ICU — le guide de décision.
Tu connais maintenant tous les outils du composant Translation. Voici les critères pour choisir rapidement la bonne approche.
Format : Symfony Interval vs ICU
| Critère | Symfony Interval | ICU MessageFormat |
|---|---|---|
| Pluriels simples (FR/EN) | ✓ Suffisant | Possible |
| Pluriels complexes (RU/AR/PL) | ✗ Insuffisant | ✓ Nécessaire |
| Sélection de genre | ✗ Non supporté | ✓ select{} |
| Imbrication plural + select | ✗ Non supporté | ✓ Natif |
| Syntaxe | Simple, lisible | Plus verbeux |
| Dépendance | Aucune | Extension PHP intl |
| Paramètres | %count% | count (sans %) |
Choix du domaine
| Domaine | Contenu |
|---|---|
messages | Messages UI généraux, textes courants |
validators | Messages d'erreur de validation (Symfony l'utilise nativement) |
security | Messages d'authentification (login, accès refusé) |
emails | Contenu des e-mails transactionnels |
admin | Interface d'administration (séparer du front public) |
messages+intl-icu | Même domaine messages mais avec formateur ICU |
Arbre de décision
Récapitulatif des interfaces clés
| Interface / Classe | Rôle |
|---|---|
TranslatorInterface | Contrat d'injection — trans() et getLocale() |
MessageCatalogue | Stockage des traductions pour une locale |
TranslatableMessage | Objet traduisible passable comme valeur |
TranslatableInterface | Contrat pour les objets qui se traduisent eux-mêmes |
LocaleSwitcher | Changement de locale isolé (Symfony 6.4+) |
Points de vigilance
- Mélanger les paramètres
%count%(Interval) etcount(ICU) dans le même fichier. - Oublier de doubler les apostrophes dans les chaînes ICU (
l''invitation). - Construire des clés dynamiquement (
'status.' . $var) — elles ne sont pas extraites automatiquement.
- Déclarer
enabled_localesdanstranslation.yamldès le début. - Garder le catalogue
encomplet — c'est le filet de sécurité du fallback. - Choisir un format par domaine — ne pas mélanger Interval et ICU dans le même fichier.
- Automatiser
translation:extracten CI pour détecter les clés manquantes.