Comment bien organiser vos traductions dans Symfony

Share Button

Read the English version

Nous travaillons actuellement sur un gros projet internationnalisé, et en conséquence, nous avons énormément de traductions dans notre application. Ces traductions sont réparties dans plusieurs bundles (7 pour l’instant) et sont utilisées dans toute l’application. Au début du développement, nous n’avions pas d’organisation spécifique pour les traductions et nous les mettions dans n’importe quel bundle (celui sur lequel nous étions en train de travailler).

Mais, un beau jour ensoleillé, j’ai ouvert un fichier de traductions et j’ai réalisé à quel point c’était devenu horrible.
Nous avons donc commencé à réfléchir à une manière simple et efficace de garder ces fichiers de traductions bien organisés. Ah, on utilise yaml, donc si vous utilisez xliff, ou n’importe quoi d’autre, ce billet ne vous aidera peut être pas. Mais peut être que si.

Comment organiser vos traductions?

Ce que nous voulions :

  • - savoir exactement où mettre une nouvelle clé de traduction
  • - savoir dans quel fichier (et quel bundle) regarder pour retrouver une clé de traduction données (depuis un template)
  • - simple à maintenir
  • - avoir une idée du message final simplement regardant la clé
  • - separer la logique des différents bundles
  • - trouver quelque chose que nous pourrons réutiliser dans d’autres projets

Les fichiers que nous utilisons

Pour le moment, nous n’utilisons que 4 fichiers (pour chaque bundle), mais il est tout à fait possible que nous ayons besoin d’en ajouter de nouveaux ultérieurement. Dans chaque nom de fichier, XX représente la locale (en, fr, es, de, …) :

  • admin.XX.yml : Nous avons une partie administration personnalisée (non publique). Chaque message de la partie administration doit se trouver dans ce fichier.
  • flashes.XX.yml : Nous avons plein de messages flash, donc nous avons décidé de les mettre dans un fichier séparé. Si vous n’avez pas beaucoup de messages flash, vous pouvez surement les mettre sous la
    clé flash de votre fichier messages.XX.yml (jetez un oeil à la section suivante pour en savoir plus sur les clés).
  • messages.XX.yml : Il s’agit du fichier de traduction par défaut pour les messages classiques.
  • validators.XX.yml : Il s’agit du fichier de traduction par défaut pour les messages de validation.

Explications sur les clés

Après beaucoup de café (moulu à la main), nous avons identifié des clés “statiques” que nous utiliserons. En d’autres termes, chaque clé de traduction doit se trouver sous l’une (ou plus) de ces clés statiques. Vous comprendrez mieux ensuite avec un exemple concret. En attendant, voici les clés que nous utilisons :

Dans les fichiers messages.XX.yml etadmin.XX.yml

  • action : Chaque libellé qui décrit une action (editer, supprimer, voir, ajouter un item, retour à la homepage, …
  • description : Chaque meta-description (balise meta). Cette clé devrait se trouver sous la clé meta.
  • form : Tout se qui se rapporte aux formulaires. Cette clé contiendra au moins les clés label, legend etplaceholder.
  • label : Chaque libellé de champ de formulaire. Cette clé doit se trouver sous la clé form.
  • legend : Chaque légende de fieldset. Cette clé doit se trouver sous la clé form.
  • message : C’est la clé par défaut. Elle contient les textes que vous n’avez pas pu placer ailleurs.
  • meta : Cette clé ne doit contenir que les clés title et/ou description
  • placeholder : Les valeurs par défaut (attribut html5 placeholder) des différents chamsp input/textarea.
  • title : Cette clé peut contenir soit des textes de titre (correspondant aux balises h1, h2, …) ou les titres de page (balise “title”).
  • warn : Chaque message d’exception (“cette page n’existe pas”, …) ou de confirmation (“Etes-vous sûr?”, …).
  • word : Cette clé contient tout les mots individuels (et, ou , jour, …)

Dans le fichier flashes.XX.yml

  • flash : Chaque clé doit être placée sous cette clé.
  • error : Chaque message d’erreur flash.
  • notice : Chaque message de notification flash.
  • success : Chaque message de succès flash.

Dans le fichier validators.XX.yml

Les clés “statiques” sont les noms de contraintes. Allez à la section suivante pour voir un exemple concret en action.

Exemples concrets

Imaginez une application Forum. Voici quelques objets possibles pour réaliser cette application : Category, Question, Answer, User et Comment.

Exemple de ce à quoi le fichier messages.fr.yml ressemblerait

# Obtao\ForumBundle\Resources\translations\messages.fr.yml
forum: #la première clé est toujours le nom du bundle
  action: #souvent les libellés ded liens ou des boutons de votre application
    lastPosts: Voir les dernières réponses postées
    newAnswer: Ajouter une réponse
    newQuestion: Poster une question
  category: # un objet peut (et devrait) toujours être une clé
    title: # Les titres relatifs à l'objet "category"
      new: Créer une catégorie
      show: Billets de la catégorie "%name%"
    warn: # messages d'alertes relatifs à l'objet "category"
      forbidden: Vous n'avez pas accès à cette catégorie
      notFound: Cette catégorie n'existe pas
  comment:
    action:
      add: Ajouter une nouveau commentaire
    word:
      comment: commentaire|commentaires
  meta:
    description: # textes à afficher dans les balises meta
      main: Ce forum parle de tout. Créez vous un compte
      members: Notre communauté est riche et hétérogène. Créez un compte et rejoignez-nous.
    title: # Les textes à afficher dans les balises title
      main: Bienvenue sur notre forum
      members: La communauté du forum
  question: 
    form: # voici les messages relatifs au formulaire "question" (probablement la classe QuestionType)
      help: Choisissez la catégorie qui décrit le mieux votre question
      label:
        category: Catégorie # le libellé de la propriété "category" de l'objet "question"
        createdAt: Créé le # pour la propriété "created_at"
        question: Question # ...
        user: Ajouté par
      placeholder:
        question: Entrez votre question ici
    warn:
      notFound: La question n'existe pas
  title: # titres (habituellement affichés dans les balises h1, h2, ... ou dans un menu)
    newQuestion: Poster une nouvelle question
    signin: Créer un compte
    welcome: Bvienvenue sur notre forum
  word:
    and: et
    by: par
    the: le

Comme vous le voyez, en plus des clés “statiques”, nous utilisons les noms d’objets pour garder les choses bien organisées. Si vous suivez ces conventions, vous retrouverez facilement une clé de traduction depuis un template et inversement. Par exemple, si vous trouvez la clé ‘forum.answer.action.edit’ dans un template, vous pouvez facilement comprendre que le message final serait quelque chose comme “Modifier la réponse” ou “Editer ma réponse”, et que vous pouvez la trouver dans le fichier Obtao/ForumBundle/Resources/translations/messages.XX.yml.
Vous pouvez remarquer 2 choses dans l’exemple ci-dessus :
- même si les traductions sont les mêmes, nous créons deux clés : forum.action.newQuestion et forum.title.newQuestion. Peut être avez-vous déjà compris la différence : la première sera probablement un lien, par exemple :

<a href="{{ path('forum_question_new')}}">{{ 'forum.action.newQuestion'|trans }}</a>

alors que la seconde sera le titre de la page vers laquelle le lien précédent vous mènera, par exemple :
<h1>{{ 'forum.action.newQuestion'|trans }}</h1>

Et c’est une bonne idée, si vous décidez de changer l’une de ces traductions plus tard. Par exemple “Ajouter un nouveau billet” pour le lien et “Créer un billet” dans le titre.

La seconde chose, un peu moins important, est que j’ai écris les traductions de la clé word en minuscules. Comme ce sont des mots uniques (si vous avez respecté les conventions), ils peuvent être affichés au début ou au milieu d’une phrase. Regardez l’exemple ci-dessous pour bien comprendre :

{# dans un template Twig #}

{# première possibilité #}
{{ 'forum.word.the'|trans|capitalize }} {{ question.date }}, {{ 'forum.word.by'|trans }} {{ question.user.name }}
{# affichera "Le 05/31/2013 par Gregquat" #}

{# seconde possibilité #}
{{ 'forum.word.by'|capitalize|trans }} {{ question.user.name }}, {{ 'forum.word.the'|trans }} {{ question.date }}
{# affichera "Par Gregquat, le 05/31/2013" #}

Nous conservons la flexibilité que nous voulions avoir.

Exemple de ce à quoi le fichier flashes.fr.yml ressemblerait

Voici un exemple du fichier contenant la traduction des messages flash.

# Obtao\ForumBundle\Resources\translations\flashes.fr.yml
forum: # encore une fois, la première clé est le nom du bundle
  flash: # la seconde clé doit toujours être "flash", et seulement celle là
    error:
      answer: # Les messages d'erreur flash liés à l'objet "Answer"
        alreadyExist: Vous ne pouvez pas ajouter de réponse puisque vous avez déjà posté aujourd'hui
        cannotDelete: Vous ne pouvez pas supprimer cette réponse
      # Je mets toujours un message générique
      general: Désolé, une erreur est survenue
   notice:
     haveAnswers: Quelqu'un a répondu à votre question %name%
     profileNotComplete: Votre profile n'est pas complet, vous devriez remplir tous les champs
   success:
     account:
       created: Votre compte a été créé avec succès
       complete: Votre profil est complet, beau travail!
     answer:
       created: Votre réponse a été créée
       edited: Vos changements ont été sauvegardés
       deleted: Votre réponse a été supprimée
     # encore un message générique
     general: L'action a été réalisée avec succès
     question:
       posted: Votre question a été postée avec succès
       removed: Votre question a été supprimée avec succès

Encore une fois, si vous voyez le message “forum.flash.success.answer.created” dans un Contrôleur (puisqu’il s’agit d’un message flash, vous l’utiliserez souvent dans un Contrôleur), vous comprenez immédiatement que la traduction sera quelque chose comme “Votre réponse a été créée avec succès”, et que c’est relatif au ForumBundle.

Exemple de ce à quoi le fichier validators.fr.yml ressemblerait

Voici un exemple pour les messages de validation de formulaire.

# Obtao\ForumBundle\Resources\translations\validators.fr.yml
forum: # encore une fois, la première clé est le nom du bundle
  notBlank: Ce champ est obligatoire # message par défaut pour la contrainte "notBlank"
  emailInvalid: Ce n'est pas une adresse email valide # un autre message par défaut
  question: # un nom d'objet
    notBlank: Veuillez saisir une question
    tooShort: La question doit faire au moins 20 caractères.
    ...

Comme vous pouvez le voir dans l’exemple ci-dessus, je définis des messages par défaut (normalement, je les mettrais plutôt dans un CoreBundle, ou dans un fichier plus “commun”, mais je les ai mis dans le fichier du ForumBundle pour cet exemple). Vous pouvez également noter que j’utilise les noms de contrainte comme clé, et parfois j’utilise des noms d’objets pour définir des messages spécifiques à ces objets.

Conclusion

Ce billet doit être compris comme une proposition pour conserver vos messages de traduction bien organisés. Evidemment, vous n’êtes pas obligé d’utiliser la même organisation que moi, mais je l’utilise dans mes projets et elle a fait ses preuves.
Donc, mon seul conseil est de l’essayer et de vous faire votre propre opinion. Peut être que vous l’aimerez.
Et si vous utilisez déjà votre propre système d’organisation, je suis très intéressé.

Share Button

5 thoughts on “Comment bien organiser vos traductions dans Symfony

  1. Globalement c’est intéressant, mais je te le dis tout de go, quand tu devras gérer du RTL tu vas regretter `forum.word.the`. De plus il ne faut pas oublier que l’i18n c’est plus (bien plus !) que de la traduction, il y aussi les histoires de formatage (date / monnaie) !

    En fait cette technique ne donne que l’apparence de la flexibilité, car elle n’est flexible que dans un scope (très) restreint (le merveilleux monde de l’alphabet latin, et encore) :)

    • Effectivement, ce billet n’aborde que l’aspect “traduction de textes” et il faut réfléchir avant où on les met. Mon exemple n’est peut être pas le meilleur, et pour l’instant on ne supporte pas de langues problèmatiques à ce niveau.
      Pour les dates, on utilise SonataIntlBundle qui est assez bien foutu. La monnaie, on est encore en train de se prendre la tête avec mais on a un système de traduction/conversion maison (voir le post Currency Change rates update on Symfony2 using openexchangerates.org API, pas encore traduit). Enfin, pour le reste, on a pas encore eu la “chance” d’avoir de plus grosses problématiques. Mais ça viendra!

      Merci de ton retour. D’ailleurs, si tu peux nous exposer ta méthode, ça m’intéresse.

  2. Petite question, je débute dans symfony et j’aimerais bien organiser de cette façon mes traductions (je les organisé d’une façon équivalente sur d’autres frameworks)

    Mais je trouve pas comment associé le validator avec les fichiers de traductions.

    Par exemple, pour que toutes les contraintes “notBlank” par défaut prennent la traduction forum.notBlank du fichier validators.fr.yml

    Comment as-tu fais ?

    Merci :)

    • Salut,

      En fait les traductions par défaut de Symfony n’utilisent pas de clé et les messages sont “en dur” (va voir vendor/symfony/symfony/src/Symfony/Component/Validator/Constraints/NotBlank.php par exemple)

      Du coup, tu as 2 façons de faire ce que tu veux (si j’ai bien compris) :

      1) La “bonne” (plus sûre mais plus de boulot) : spécifier dans chaque contrainte le message que tu veux :

      // ...
      
      /**
       * @ORM\Entity()
       * @ORM\Table()
       */
      class Entity
      {
          /**
          * @ORM\Column(type="string", length=150)
          *
          * @Assert\NotBlank(message="obtao.notBlank")
          * @Assert\Length(min=2,minMessage="obtao.length.min"))
          */
          protected $title;
      
          // ...
      

      + : Tu peux spécifier les messages que tu veux et tu peux les surcharger si besoin (par obtao.entity.title.notBlank par exemple)
      - : Tu dois spécifier les messages dans chaque contrainte de chaque entité

      2) La “moins bonne” (plus facile mais plus risquée) : te baser sur les messages de Symfony

      Il te suffit de surcharger les messages de base de Symfony dans ton validators.fr.yml :

      # dans Obtao/AcmeBundle/Resources/translations/validators.fr.yml
      
      'This value should not be blank.': 'Merci de remplir cette valeur'
      'This file is not a valid image.': 'Il faut impérativement choisir une image'
      'This value should be {{ limit }} or more.': 'La valeur minimale est {{ limit }}'
      # ...
      

      + : Il suffit de faire le travail une fois. Ensuite, inutile de spécifier les messages dans les contraintes.
      - : Il faut surcharger tous les messages de Symfony. De plus si Symfony change un message, la surcharge ne fonctionnera plus.

      Il y a peut être une troisième solution qui consisterait à surcharger le système de validation mais je pense que c’est un peu too much pour un besoin qui est simple à la base (en plus je suis pas certain à 100% que ce soit possible).

      Personnellement, j’utilise la solution 1 (j’utilise une clé du genre “obtao.general.notBlank”)

      En espérant avoir répondu. Bon courage et merci!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Protected by WP Anti Spam