← Tous les cours / Symfony Translation Cours
00 — LE PIPELINE

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 :

1. Catalogue
2. Formateur
3. Fallback
Étape 1 — Recherche dans le catalogue
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.
Étape 2 — Application du formateur
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).
Étape 3 — Fallback
Si la clé est introuvable dans la locale demandée, Symfony remonte la chaîne de fallback : fr_BEfren. 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
💡 La locale peut être forcée : en passant un quatrième argument à 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.

01 — FICHIERS

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"
Domaine
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
Locale
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

FormatLoaderUsage recommandé
yamlYamlFileLoaderDéveloppement, petits projets
xliffXliffFileLoaderWorkflow de traduction professionnel (outil tiers)
jsonJsonFileLoaderIntégration front-end, partage avec JS
phpPhpFileLoaderTraductions avec logique dynamique
po / moPoFileLoader / MoFileLoaderMigration depuis gettext
iniIniFileLoaderSystèmes hérités
csvCsvFileLoaderTraductions 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>
💡 XLIFF pour les traductions professionnelles : c'est le format standard des outils CAT (Computer-Assisted Translation) comme Transifex, Phrase ou Lokalise. Génère XLIFF avec 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.

⚠️ Convention Bundle : les bundles peuvent embarquer leurs propres traductions dans Resources/translations/. Symfony les charge automatiquement. Pour les surcharger, place un fichier du même nom dans le répertoire translations/ de ton projet.
02 — PARAMÈTRES

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],
);
Format des paramètres
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 }}
TranslatableInterface
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'
💡 addGlobalParameter() depuis Symfony 6.3 : évite de passer %app_name% dans chaque appel trans(). Les paramètres locaux d'un appel trans() écrasent les paramètres globaux en cas de conflit.
⚠️ Attention aux % dans YAML : le caractère % 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 %.
03 — PLURIELS SYMFONY

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'
Syntaxe d'intervalle
[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% est le déclencheur : sans %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.

countRésultat avec [0] Aucun|[1] Un|]1,Inf] %count%
0Aucun
1Un
22
100100
⚠️ Limites du format Symfony : les intervalles ne connaissent pas les règles grammaticales de chaque langue. En russe, en arabe, ou en polonais, les pluriels ont plusieurs formes selon les dizaines et les unités. Pour ces langues, utilise le format ICU (section suivante).
04 — PLURIELS ICU

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
Domaine ICU
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}
}'
💡 Échapper les apostrophes : en format ICU, l'apostrophe est un caractère d'échappement. Pour afficher une apostrophe littérale, double-la : 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',
);
⚠️ Paramètres ICU sans % : contrairement au format Symfony, les paramètres ICU s'écrivent sans %. 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.

1
Format Symfony Interval
Format ICU
05 — CATALOGUES

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() vs defines()
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 :

fr_BE
fr
en
// 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 vs addCatalogue
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']);
💡 Bonne pratique : garde toujours ton catalogue 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 !'
06 — LOCALESWITCHER

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.

Problème résolu
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();
runWithLocale()
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'
💡 Compatibilité avec Twig : 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.
⚠️ Thread safety : en PHP-FPM (une requête = un processus), 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.
07 — EXTRACTION

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 :

Elle compare les clés trouvées avec les fichiers de traduction existants et :

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 vs --output-dir
--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é

Coder
extract
Traduire
Vérifier
  1. Tu ajoutes de nouvelles clés dans le code PHP/Twig.
  2. Tu lances translation:extract fr — les nouvelles clés apparaissent dans messages.fr.yaml avec une valeur vide.
  3. Tu (ou une traductrice) remplis les valeurs vides.
  4. Tu lances translation:extract fr --clean pour supprimer les clés obsolètes.

Contraintes de l'extraction statique

⚠️ L'extraction est statique : elle analyse le code source, pas l'exécution. Les clés construites dynamiquement (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
08 — CONFIGURATION

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
enabled_locales
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,
        );
    }
}
💡 TranslatorInterface, pas Translator : injecte toujours l'interface 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
CommandeUsage
debug:translation frLister toutes les clés, leur statut (traduit / manquant / hérité)
translation:extract frSynchroniser les fichiers de traduction avec le code
cache:clearForcer le rechargement des catalogues en dev
09 — SYNTHÈSE

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èreSymfony IntervalICU MessageFormat
Pluriels simples (FR/EN)✓ SuffisantPossible
Pluriels complexes (RU/AR/PL)✗ Insuffisant✓ Nécessaire
Sélection de genre✗ Non supporté✓ select{}
Imbrication plural + select✗ Non supporté✓ Natif
SyntaxeSimple, lisiblePlus verbeux
DépendanceAucuneExtension PHP intl
Paramètres%count%count (sans %)

Choix du domaine

DomaineContenu
messagesMessages UI généraux, textes courants
validatorsMessages d'erreur de validation (Symfony l'utilise nativement)
securityMessages d'authentification (login, accès refusé)
emailsContenu des e-mails transactionnels
adminInterface d'administration (séparer du front public)
messages+intl-icuMême domaine messages mais avec formateur ICU

Arbre de décision

Pluriel simple (FR/EN) ?
→ oui
Interval [0]|[1]|]1,Inf]
Langue slave / arabe / autre ?
→ oui
ICU {count, plural, ...}
Genre grammatical ?
→ oui
ICU {gender, select, ...}
Changer de locale ponctuellement ?
→ oui
LocaleSwitcher::runWithLocale()

Récapitulatif des interfaces clés

Interface / ClasseRôle
TranslatorInterfaceContrat d'injection — trans() et getLocale()
MessageCatalogueStockage des traductions pour une locale
TranslatableMessageObjet traduisible passable comme valeur
TranslatableInterfaceContrat pour les objets qui se traduisent eux-mêmes
LocaleSwitcherChangement de locale isolé (Symfony 6.4+)

Points de vigilance

⚠️ Trois pièges fréquents :
  • Mélanger les paramètres %count% (Interval) et count (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.
💡 Checklist de démarrage :
  • Déclarer enabled_locales dans translation.yaml dès le début.
  • Garder le catalogue en complet — 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:extract en CI pour détecter les clés manquantes.