Licence CC BY-NC-SA

Traduire son site

Maintenant que votre site est opérationnel, il faut penser à monter en puissance et conquérir le reste du monde ! Pour cela, il va falloir apprendre quelques langues supplémentaires à votre site, car malheureusement tout le monde ne parle pas français. Ce chapitre a donc pour objectif de mettre en place un site multilingue. Cela passera par :

  • Traduire les messages qui apparaissent tels quels dans vos pages HTML ;
  • Placer un peu de contenu dynamique au milieu d'un texte ;
  • Afficher votre site dans différentes langues ;
  • Traduire du contenu d'entités.

Quand vous avez créé votre site, vous avez mis certains textes directement dans vos templates Twig. Seulement, quand il est question de traduire le site, la question se pose : « Comment faire pour que ce texte, actuellement en dur dans le template, change selon la langue de l'utilisateur ? » Eh bien, nous allons rendre ce texte dynamique. Comment ? Nous avons tout un chapitre pour y répondre.

Vous êtes prêts ? Allons-y !

Introduction à la traduction

Le principe

Si demain on vous demande de traduire un document du français vers une langue étrangère, de quoi auriez-vous besoin ? En plus de votre courage, il vous faut impérativement ces trois informations :

  • Ce qu'il faut traduire ;
  • Dans quelle langue ;
  • Ainsi qu'un dictionnaire si besoin.

Ensuite, la méthodologie est plutôt classique : on prend le texte original pour d'abord traduire les mots inconnus, puis on traduit les phrases en respectant la syntaxe de la langue cible.

En effet, quand nous commençons l'apprentissage d'une langue étrangère, nous cherchons le mot exact dans notre dictionnaire, sans imaginer qu'il s'agisse d'un adjectif accordé ou d'un verbe conjugué. On ne risque donc pas de trouver cette orthographe exacte. Symfony n'ayant pas d'intelligence artificielle, il va reproduire ce comportement systématiquement.

Ce qu'il en est avec Symfony2

Le service de traduction ne va donc pas s'embarrasser de considérations de syntaxe ou de grammaire ; c'est dû au fait qu'il ne s'agit pas d'un traducteur sémantique. Il n'analyse pas le contenu de la phrase — ni même ne regarde si ce qu'on lui fournit en est une. Il se chargera de traduire une chaîne de caractères d'une langue à l'autre, en la comparant avec un ensemble de possibilités. Notez bien cependant que la casse, tout comme les accents, sont importants. Symfony va vraiment chercher pour une correspondance exacte de la chaîne à traduire. C'est pourquoi on ne parle pas de dictionnaire, mais de catalogue.

Si d'un côté c'est plutôt rassurant car il ne peut pas faire d'erreur, l'autre côté implique évidemment que cette responsabilité vous incombe, car c'est vous qui allez écrire le catalogue pour Symfony ! Donc autant vous assurer que vous connaissez bien la langue dans laquelle vous allez traduire.

La langue source, c'est celle que vous avez déjà utilisée dans vos templates jusqu'à maintenant, donc probablement le français. Comme on l'a vu juste avant, Symfony n'allant chercher que la correspondance exacte, il n'y a pas réellement besoin de la spécifier. Quant à la langue cible, elle est en général demandée par l'utilisateur, parfois sciemment (quand il clique sur un lien qui traduit la page sur laquelle il se trouve), parfois implicitement (quand il suit un lien depuis un moteur de recherche, lien qui est dans sa langue), et parfois à son insu (les navigateurs envoient, dans l'en-tête des requêtes, la (les) locale(s) préférée(s) de l'utilisateur ; la locale utilisée sur un site est souvent stockée dans la session, liée à un cookie, qui voyage donc aussi à chaque requête).

On parle de locale pour désigner non seulement la langue de l'utilisateur, mais aussi d'autres paramètres régionaux, comme le format d'affichage de sommes d'argent (et donc la devise), de dates, etc. La locale contient un code de langue ainsi qu'une éventuelle indication du pays. Exemples : fr pour le français, fr_CH pour le français de Suisse, zh_Hant_TW pour le chinois traditionnel de Taïwan.

Les locales sont composées de codes de langue, au format ISO639-1, puis éventuellement d'un sous-tiret (_) et du code du pays au format ISO3166-2 Alpha-2.

Et s'il n'y a pas de traduction dans la locale demandée ?

Symfony possède un mécanisme qui permet d'afficher quelque chose par défaut. Imaginons que vous arriviez sur un site principalement anglophone géré par des Québecois, et ceux-ci, par égard aux Français, sont en train de préparer une version spéciale en « français de France ». Cependant, tout le site n'est pas encore traduit.

Vous demandez la traduction pour votre locale fr_FR du texte « site.devise » :

  1. Ce qui est déjà prévu pour la locale fr_FR vous sera retourné ;
  2. Ce qui n'est pas encore « traduit », mais existe en « français général » (locale fr) : c'est cette version qui sera envoyée ;
  3. Ce qui n'est pas du tout traduit en français, mais l'est en anglais, est affiché en anglais ;
  4. Ce qui ne possède aucune traduction est affiché tel quel, ici « site.devise ». Dans ce cas, quand c'est le texte original qui est affiché, c'est que vous avez oublié la traduction de ce terme.

Ainsi, il n'y aura jamais de vide là où vous avez spécifié du texte à traduire.

Prérequis

Avant de partir dans la traduction de son site, il faut vérifier que Symfony travaillera correctement avec les langues, et notamment celle qui est utilisée par défaut. Comme nous sommes sur un site francophone, je vais partir du principe que la langue par défaut de votre site est le français, et la locale fr.

Configuration

Pour savoir quelle est la langue sur le site (au cas où il ne serait pas possible d'afficher celle que le client souhaite), Symfony utilise un paramètre appelé locale, comme nous l'avons vu plus haut. La locale pour le français est fr, en minuscules, et il nous faut la définir comme locale par défaut dans le fichier app/config/parameters.yml. Ouvrez donc ce fichier et effectuez-y la manipulation mentionnée.

1
2
3
4
# app/config/parameters.yml

parameters:
    locale: fr # Mettez « fr » ici

Si ce paramètre n'est pas renseigné, le framework considérera qu'il travaille en anglais.

On va ensuite utiliser ce paramètre locale dans la configuration :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# app/config/config.yml

framework:
    # On définit la langue par défaut pour le service de traduction
    # Décommenter la ligne, et vérifier qu'elle est bien ainsi
    translator:      { fallback: %locale% }

# …

    # Vérifier cette ligne aussi, pour la langue par défaut de l'utilisateur
    # C'est celle qui sera utilisée si l'internaute ne demande rien
    default_locale: %locale%

(extrait)

Votre application sait maintenant que vous travaillez sur un site qui, à la base, est en français.

Mise en place d'une page de test

Pour la suite du chapitre, nous avons besoin d'une page sur laquelle réaliser nos tests. Je vous invite donc à créer la même que moi, afin qu'on s'y retrouve.

Tout d'abord voici la route, à rajouter au fichier de routes de l'application. On va la mettre dans le routing_dev.yml, car d'une part c'est une route de test (pas destinée à nos futurs visiteurs !), et d'autre part le fichier de routes de notre bundle est préfixé par /blog qu'on ne veut pas forcément ici. Voici donc la route en question :

1
2
3
4
5
# app/config/routing_dev.yml

SdzBlogBundle_traduction:
    pattern:  /traduction/{name}
    defaults: { _controller: SdzBlogBundle:Blog:traduction }

Pour que la route fonctionne, il nous faut aussi créer l'action qui lui correspond, dans le contrôleur Blog. Une fois de plus, je vous mets le code, rien de bien sorcier :

1
2
3
4
5
6
7
8
9
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

  public function traductionAction($name)
  {
    return $this->render('SdzBlogBundle:Blog:traduction.html.twig', array(
      'name' => $name
    ));
  }

Et comme indiqué dans le contrôleur, il nous faut la vue traduction.html.twig, la voici :

1
2
3
4
5
6
7
{# src/Sdz/BlogBundle/Resources/views/Blog/traduction.html.twig #}

<html>
  <body>
    Hello {{ name }}!
  </body>
</html>

C'est bon, on va pouvoir mettre la main à la pâte !

Bonjour le monde

Actuellement, quand vous accédez à /traduction/winzou, la page qui s'affiche ne contient que « Hello winzou! », et ce quelle que soit la langue. Nous allons faire en sorte qu'en français nous ayons « Bonjour winzou! », c'est-à-dire que le « Hello » soit traduit en « Bonjour ».

Dire à Symfony « Traduis-moi cela »

La traduction est possible dans Symfony à deux endroits : dans les contrôleurs et dans la vue. Cette dernière option est la plus conseillée, car c'est dans les vues que se situe l'affichage et donc bien souvent le texte à traduire.

Le filtre Twig {{ ''|trans }}

Un filtre est, dans le langage Twig, une fonction destinée à formater/modifier une valeur. C'est donc tout à fait adapté à la traduction de texte, car modifier le texte pour qu'il soit dans une autre langue est une transformation comme une autre !

Plus précisément dans notre cas, c'est le filtre trans que l'on va utiliser. La syntaxe est la suivante : {{ 'ma chaîne'|trans }} ou encore {{ ma_variable|trans }}. Ce filtre est prévu pour s'appliquer sur des variables ou des courtes chaînes, voici un exemple dans un contexte :

1
2
3
4
5
<div>
  <p>{{ message|trans }}</p>
  <button>{{ 'cancel'|trans }}</button>
  <button>{{ 'validate'|trans }}</button>
</div>

La balise de bloc Twig {% trans %}

Une autre possibilité de traduction depuis la vue consiste à encadrer tous les textes dans des blocs {% trans %} … {% endtrans %}. Ce bloc permet de traduire du texte brut, mais attention il est impossible d'y mettre autre chose que du texte. Balises HTML, code Twig, etc. sont interdits ici. Une des utilisations les plus parlantes est pour les conditions générales d'utilisation d'un site, où il y a de gros paragraphes avec du texte brut, voici un exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<p>
  {% trans %}Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur
  quam nisi, sollicitudin ut rhoncus semper, viverra in augue. Suspendisse
  potenti. Fusce sit amet eros tortor. Class aptent taciti sociosqu ad litora
  torquent per conubia nostra, per inceptos himenaeos. Ut arcu justo, tempus sit
  amet condimentum vel, rhoncus et ipsum. Mauris nec dui nec purus euismod
  imperdiet. Cum sociis natoque penatibus et magnis dis parturient montes,
  nascetur ridiculus mus. Mauris ultricies euismod dolor, at hendrerit nulla
  placerat et. Aenean tincidunt enim quam. Aliquam cursus lobortis odio, et
  commodo diam pulvinar ut. Nunc a odio lorem, in euismod eros. Donec viverra
  rutrum ipsum quis consectetur. Etiam cursus aliquam sem eget gravida. Sed id
  metus nulla. Cras sit amet magna quam, sed consectetur odio. Vestibulum feugiat
  justo at orci luctus cursus.{% endtrans %}
</p>
<p>
  {% trans %}Vestibulum sollicitudin euismod tellus sed rhoncus. Pellentesque
  habitant morbi tristique senectus et netus et malesuada fames ac turpis
  egestas. Duis mattis feugiat varius. Aenean sed rutrum purus. Nam eget libero
  lorem, ut varius purus. Etiam nec nulla vitae lacus varius fermentum. Mauris
  hendrerit, enim nec posuere tempus, diam nisi porttitor lacus, at placerat
  elit nulla in urna. In id nisi sapien.{% endtrans %}
</p>

D'accord, l'exemple n'est pas vraiment bon, mais cela illustre l'utilisation. On a de gros pavés de texte, et je vous laisse regarder et réfléchir à ce que cela aurait représenté avec la solution précédente du filtre. ;)

Le service translator

Parfois, vous devrez malgré tout réaliser quelques traductions depuis le contrôleur, dans le cas d'inscriptions dans un fichier de log, par exemple. Dans ce cas il faut faire appel au service translator, qui est le service de traduction que la balise et le filtre Twig utilisent en réalité. Son utilisation directe est très aisée, voyez par vous-mêmes :

1
2
3
4
5
6
7
8
<?php
// Depuis un contrôleur

// On récupère le service translator
$translator = $this->get('translator');

// Pour traduire dans la locale de l'utilisateur :
$texteTraduit = $translator->trans('Mon message à inscrire dans les logs');

Notre vue

Bon, et on fait comment, finalement, pour traduire notre « Hello » en « Bonjour » ?

C'est vrai, revenons-en à nos moutons. Adaptons donc le code de notre vue en rajoutant le filtre trans de Twig pour qu'il traduise notre « Hello » :

1
2
3
4
5
6
7
{# src/Sdz/BlogBundle/Resources/views/Blog/traduction.html.twig #}

<html>
  <body>
    {{ 'Hello'|trans }} {{ name }}!
  </body>
</html>

Accédez à nouveau à /traduction/winzou via l'environnement de développement.

Eh, mais… c'est encore « Hello » qui s'affiche ! Pourtant on s'est bien mis en français, non ?

C'est exact ! Mais rappelez-vous la méthode pour faire une traduction. Il faut savoir quoi traduire, ça c'est OK, il faut savoir dans quelle langue le traduire, ça c'est OK, la locale de l'utilisateur est automatiquement définie par Symfony2. Il nous manque donc… le dictionnaire !

En effet, on n'a pas encore renseigné Symfony sur comment dire « Hello » en français. Les fichiers qui vont le lui dire s'appellent des catalogues, nous y venons.

Le catalogue

Vous l'aurez compris, le catalogue est l'endroit où l'on va associer la chaîne à traduire avec sa version en langue étrangère. Si vous avez créé votre bundle grâce à la commande generate:bundle, alors Symfony2 vous a créé automatiquement un catalogue exemple, c'est le fichier enregistré dans le dossier Resources/translations du bundle, et qui contiendra par la suite les paires de type 'Ma chaîne en français' <=> 'My string in English'.

Les formats de catalogue

Les exemples sont pour traduire de l'anglais au français, et non l'inverse.

Le format XLIFF

Symfony recommande le XLIFF, une application du XML. C'est pourquoi, dans les bundles générés avec la ligne de commande, vous trouvez ce fichier Resources/translations/messages.fr.xlf qui contient l'exemple suivant que je commente :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- src/Sdz/BlogBundle/Resources/translations/messages.fr.xlf -->

<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en" datatype="plaintext" original="file.ext">
    <body>
      <trans-unit id="1">
        <!-- La chaîne source, à traduire. C'est celle que l'on utilisera :
        {% trans %}Symfony2 is great{% endtrans %}
        ou
        {{ 'Symfony2 is great'|trans }}
        ou
        $translator->trans('Symfony2 is great')) -->
        <source>Symfony2 is great</source>

        <!-- La chaîne cible, traduction de la chaîne ci-dessus -->
        <target>J'aime Symfony2</target>
      </trans-unit>
    </body>
  </file>
</xliff>

L'avantage du XLIFF est qu'il s'agit d'un format officiel, dont vous pouvez faire valider la structure en plus de la syntaxe. De plus, du fait du support natif de XML par PHP, il est facile de le modifier via PHP, donc de créer une interface dans votre site pour modifier les traductions. En revanche, le désavantage du XLIFF est celui du XML de façon générale : c'est très verbeux, c'est-à-dire que pour traduire une seule ligne il nous en faut vingt.

Le format YAML

Voici l'équivalent YAML du fichier messages.fr.xlf précédent :

1
2
3
4
# src/Sdz/BlogBundle/Resources/translations/messages.fr.yml

# La syntaxe est : « la chaîne source: la chaîne cible »
Symfony2 is great: J'aime Symfony2

Vous allez me dire que vu sa simplicité on se demande pourquoi le XLIFF existe. Je suis d'accord avec vous, pour l'utilisation simple que nous allons faire, le YAML est meilleur, car on distingue bien mieux les chaînes à traduire. Si c'est votre choix également, pensez à supprimer le fichier messages.fr.xlf afin qu'il n'interfère pas. Enfin, gardez en tête le format XLIFF si vous souhaitez manipuler vos traductions en PHP, car c'est plus délicat en YAML.

Attention également lorsque vous avez des deux-points ( « : » ) dans votre chaîne source, vu la syntaxe utilisée dans le fichier YAML vous vous doutez qu'il ne va pas apprécier. Il faut dans ce cas encadrer la chaîne source par des guillemets, comme ceci :

1
"Phone number:": Numéro de téléphone :

Notez que vous pouvez ne pas encadrer la chaîne cible avec les guillemets, bien que celle-ci contienne également les deux-points. Cela est dû à la syntaxe du YAML lui-même : la fin de la chaîne cible est le retour à la ligne, donc impossible à confondre avec les deux-points. ;)

Le format PHP

Moins utilisé, ce format est néanmoins possible. La syntaxe est celle d'un simple tableau PHP, comme ceci :

1
2
3
4
5
6
<?php
// src/Sdz/BlogBundle/Resources/translations/messages.fr.php

return array(
    'Symfony2 is great' => 'J\'aime Symfony2',
);

La mise en cache du catalogue

Quelque soit le format que vous choisissez pour vos catalogues, ceux-ci seront mis en cache pour une utilisation plus rapide dans l'environnement de production. En effet, ne l'oubliez pas, si Symfony2 a la gentillesse de regénérer le cache du catalogue à chaque exécution dans l'environnement de développement, il ne le fait pas en production. Cela signifie que vous devez vider manuellement le cache pour que vos changements s'appliquent en production. Pensez-y avant de chercher des heures pourquoi votre modification n'apparaît pas.

Vous pouvez choisir le format qui vous convient le mieux, mais dans la suite du cours je vais continuer avec le format YAML, qui possède quelques autres avantages intéressants dont je parle plus loin.

Notre traduction

Maintenant qu'on a vu comment s'organisait le catalogue, on va l'adapter à nos besoins. Créez le fichier messages.fr.yml nécessaire pour traduire notre « Hello » en français. Vous devriez arriver au résultat suivant sans trop de problèmes :

1
2
3
4
# src/Sdz/BlogBundle/Resources/translations/messages.fr.yml

# On veut traduire « Hello » en « Bonjour »
Hello: Bonjour

Pour tester, videz votre cache et rechargez la page /traduction/winzou. Ça marche ! Vous lisez désormais « Bonjour winzou! ». Et si vous changez le paramètre dans app/config/parameters.yml pour que la locale par défaut soit à nouveau l'anglais, vous avez à nouveau « Hello winzou! ».

Chaque fois que vous créez un nouveau fichier de traduction, il vous faut rafraîchir votre cache avec la commande cache:clear, que vous soyez dans l'environnement de production ou de développement. En effet, si le mode dev permet de prendre en compte les modifications du catalogue sans vider le cache, ce n'est pas le cas pour la création d'un fichier de catalogue !

Si vous avez une erreur du type « Input is not proper UTF-8, indicate encoding ! », n'oubliez pas de bien encoder tous vos fichiers en UTF-8 sans BOM.

Ajouter un nouveau message à traduire

On se doute bien que l'on n'aura pas qu'une seule chaîne à traduire sur tout un site. Il va falloir que tout ce qu'on souhaite voir s'afficher dans la langue de l'utilisateur soit renseigné dans le catalogue. Pour chaque nouvelle chaîne, on ajoute une unité de traduction :

1
2
3
4
# src/Sdz/BlogBundle/Resources/translations/messages.fr.yml

Hello: Bonjour
Bye: Au revoir

Ainsi, à chaque fois que vous avez une nouvelle traduction à ajouter, vous ajoutez une ligne.

Extraire les chaînes sources d'un site existant

Mais moi j'ai déjà un site tout prêt en français, je dois trouver toutes les chaînes sources et les copier à la main ? :'(

Justement, non. Vous allez devoir ajouter les balises et/ou filtres Twig de traduction dans vos vues, ça, c'est inévitable. Mais une fois que ce sera fait, les concepteurs de Symfony ont pensé aux personnes dans votre cas, et ont développé un outil permettant d'extraire toutes les chaînes entre balises {% trans %} et celles avec le filtre |trans.

Cet outil se présente sous forme d'une commande, il s'agit de translation:update. Sa version complète est la suivante : translation:update [--prefix[="..."]] [--output-format[="..."]] [--dump-messages] [--force] locale bundle. Cette commande va lire toutes les vues du bundle spécifié, et compilera toutes les chaînes sources dans un catalogue. Vous n'aurez plus qu'à définir les chaînes cibles.

Si cela paraît être la commande miracle, elle ne fonctionne cependant pas pour extraire les chaînes sources des contrôleurs, donc utilisées avec <?php $this->get('translator')->trans(/* ... */), ni sur le contenu des variables traduites avec {{ maVariable|trans }} (car il ne connaît pas la valeur de la variable maVariable !). Et bien entendu, n'oubliez pas de balise/filtre Twig de traduction !

La commande translation:update est du même genre que doctrine:schema:update, dans le sens où il vous faut choisir de donner (en plus des deux paramètres obligatoires) soit --dump-messages, soit --force pour qu'elle fasse réellement quelque chose. Cela permet de vérifier le résultat avant qu'elle n'écrive effectivement dans le catalogue.

  • La première option --dump-messages affiche tous les messages dans la console, plus ou moins tels qu'ils seront dans un catalogue en YAML (c'est pour cela que c'est le format de sortie par défaut). Elle tient compte des messages déjà traduits, donc pas de souci que votre précédent travail soit écrasé. Cela vous permet aussi de passer d'un format à l'autre si vous aviez commencé le travail en réutilisant le fichier messages.fr.xlf du bundle par exemple.
  • La seconde option --force effectue la mise à jour des catalogues, tout en conservant une sauvegarde des versions précédentes.

Par défaut, les extractions sont de type chaîne source: __chaîne source. En effet, Symfony ne peut deviner comment traduire la chaîne source, il la remet donc comme chaîne cible, mais en la préfixant avec __. Avec l'option --prefix="...", vous pouvez changer la partie __ par ce que vous voulez.

Il est temps de passer à la pratique, exécutons cette commande pour extraire les messages de notre bundle, et regardez ce qu'il en sort :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
C:\wamp\www\Symfony> php app/console translation:update --dump-messages --force fr SdzBlogBundle
Generating "fr" translation files for "SdzBlogBundle"
Parsing templates
Loading translation files

Displaying messages for domain messages:

Hello: Bonjour
Bye: 'Au revoir'
'Symfony2 is great': 'J''aime Symfony2'

Writing files

Comme je vous l'avais mentionné, les chaînes cibles sont identiques aux chaînes sources mais avec un préfixe. À l'aide de votre éditeur préféré, cherchez les occurrences de __ (ou du préfixe que vous avez défini vous-mêmes) dans vos catalogues pour les mettre en surbrillance. Vous ciblerez ainsi très rapidement les nouveautés, c'est-à-dire ce que vous devez traduire !

Lorsque la commande affiche Displaying messages for domain messages, elle se contente d'afficher tous les messages du domaine, après son travail d'extraction. Les « Hello » et « Bye » viennent de notre fichier YAML, et l'autre ligne du fichier XLIFF déjà existant. D'ailleurs, le générateur a remis cette valeur dans le fichier YAML (format par défaut), vous pouvez donc supprimer le fichier XLIFF sereinement (si vous avez choisi le YAML bien entendu). Vous noterez d'ailleurs que cette valeur a été échappée, même si ici ce n'est pas utile, vous pouvez supprimer toutes les quotes sans problèmes.

Enfin, vous noterez que les chaînes cibles ne sont pas préfixées, car nous les avions déjà traduites !

Symfony s'occupe automatiquement des éventuels doublons, vous ne devriez normalement plus en avoir après avoir utilisé la commande translations:update avec l'option --force.

Traduire dans une nouvelle langue

Pour conquérir le monde, ce qui reste notre but à tous, c'est bien de parler anglais, mais il ne faut pas s'arrêter là ! On souhaite maintenant traduire les messages également en allemand. Il faut alors tout simplement créer le catalogue adéquat, mais on peut se simplifier la vie : dupliquez le fichier messages.fr.yml et nommez la copie messages.de.yml. Ensuite, vous n'avez plus qu'à y modifier les chaînes cibles :

1
2
3
# src/Sdz/BlogBundle/Resources/translations/messages.de.yml

Hello: Guten Tag

Vous pouvez dès à présent tester le bon fonctionnement de l'allemand en changeant le paramètre dans app/config/parameters.yml, après avoir vidé votre cache bien entendu (nous avons ajouté un fichier dans le catalogue).

Je vous conseille vivement de bien préparer — si ce n'est de terminer — les catalogues pour une locale, puis de les dupliquer pour les autres, et non de travailler sur toutes les locales à la fois. Vous éviterez ainsi les casse-têtes de traductions incomplètes (manque de chaînes sources surtout). La traduction est un processus qui doit se faire tout à la fin de la réalisation d'un site.

Récupérer la locale de l'utilisateur

Déterminer la locale

Jusqu'à présent, pour changer la locale de notre site nous avons modifié à la main le fichier de configuration. Bien entendu, ce n'est pas une solution viable, vous n'allez pas rester derrière votre PC à changer la locale dès qu'un internaute étranger arrive sur votre site !

La question se pose donc de savoir comment adapter la locale à l'utilisateur qui navigue sur votre site. Souvenez-vous, j'ai précisé un peu plus haut qu'il y avait plusieurs moyens de connaître la locale de l'utilisateur. Je vous les rappelle ici :

  1. L'utilisateur clique sur un lien qui traduit la page sur laquelle il se trouve ;
  2. L'utilisateur envoie ses préférences dans les en-têtes des requêtes ;
  3. Les paramètres par défaut.

L'ordre correspond à la priorité observée par Symfony.

La plupart des sites multilingues affichent la locale dans l'URL (exemple : http://www.site.com/fr). La locale ne dépend donc pas de l'utilisateur, mais de l'adresse à laquelle il se trouve.

Routing et locale

Vous l'avez peut-être déjà compris, pour que la locale apparaisse dans les URL, il va falloir ajouter un paramètre dans nos URL.

Le paramètre d'URL

Nous l'avons vu dans le chapitre sur le routeur : certains paramètres de route bénéficient d'un traitement spécial de la part de Symfony2. Et devinez quoi ? Il y en a un prévu pour récupérer la locale ! Il s'agit du paramètre _locale, qui a déjà été mentionné dans le chapitre sur le routeur.

Mais les paramètres, on doit les traiter dans les contrôleurs, normalement, non ? Donc on va devoir modifier toutes nos actions, en plus des routes ?

Justement non, c'est en cela que le paramètre _locale (et d'autres) sont spéciaux. En effet, Symfony sait quoi faire avec ces paramètres, vous n'avez donc pas à les récupérer dans vos actions — sauf si le traitement diffère sensiblement en fonction de ce paramètre — ni le mettre vous-mêmes en session, ni quoi que ce soit.

Voici ce que cela donne sur notre route de test :

1
2
3
4
5
# app/config/routing_dev.yml

SdzBlogBundle_traduction:
    pattern:  /{_locale}/traduction/{name}
    defaults: { _controller: SdzBlogBundle:Blog:traduction }

Les paramètres de route sont différents des variables que vous pouvez passer en GET. Ainsi, l'URL /traduction/winzou?_locale=ur ne traduira pas votre page (en ourdou dans le cas présent), car ici _locale n'est pas un paramètre de route.

Vous pouvez déjà tester en essayant /fr/traduction/winzou ou /de/traduction/winzou, le contenu de la page est bien traduit.

Attention par contre, si vous allez sur /en/traduction/winzou, vous avez toujours « Bonjour ». En effet, pour l'instant on n'a pas de catalogue pour l'anglais. Alors effectivement le « Hello » inscrit dans la vue est déjà en anglais, mais ça, Symfony ne le sait pas ! Car on aurait pu mettre n'importe quoi à la place de « Hello », comme on le verra un peu plus loin. Bref, comme il n'y a pas de catalogue correspondant à la langue demandée, Symfony affiche la page dans la langue fallback (langue de repli en français), définie dans le fichier de configuration config.yml, qui est dans notre cas le français. ;)

Mais cela veut dire qu'on va éditer tous nos fichiers de routing et prendre chaque route une à une ?

Non, rassurez-vous ! Il y a au moins un moyen plus rapide de faire cela, qu'on a aussi vu dans le chapitre sur les routes… il s'agit de l'utilisation d'un préfixe ! Voyez par vous-mêmes comment rajouter le paramètre _locale sur tout notre blog :

1
2
3
4
5
# app/config/routing.yml

SdzBlogBundle:
    resource:  "@SdzBlogBundle/Resources/config/routing.yml"
    prefix:    /{_locale}/blog # Ici, on ajoute {_locale} au préfixe !

Vous pouvez désormais demander vos pages de blog en différentes langues selon l'URL : /fr/blog, /en/blog, etc. Bien entendu, pour que ce soit parfaitement opérationnel, vous devez généraliser l'utilisation du filtre ou de la balise Twig trans, et traduire les textes dans les catalogues correspondants.

Il y a actuellement un problème avec les routes de FOSUserBundle quand on utilise cette solution de préfixer depuis app/config/routing.yml. La route fos_user_security, quand elle est préfixée, n'est plus liée à l'action. Ainsi, pour les routes de FOSUserBundle, il vaut mieux dupliquer le fichier de routing du bundle dans votre UserBundle et y préfixer les routes qui en ont besoin. Vous n'avez plus qu'à importer le routing de votre UserBundle à la place de celui de FOSUserBundle, et vous n'avez pas besoin de mettre la partie du préfixe /{_locale} à l'importation.

Il manque cependant un garde-fou à notre solution : avec une locale ainsi apparente, un petit malin peut très bien changer à la main la locale dans une URL, et arriver sur un site en traduction que vous ne pensiez pas être accessible. Veillez donc à limiter les locales disponibles en ajoutant les requirements pour ce paramètre. De fait, le routing final devrait ressembler à cela :

1
2
3
4
5
6
7
# app/config/routing.yml

SdzBlogBundle:
    resource:  "@SdzBlogBundle/Resources/config/routing.yml"
    prefix:    /{_locale}/blog
    requirements:
        _locale: en|fr # les locales disponibles, séparées par des pipes « | »

Soyons clairs : si vous avez des routes qui contiennent la locale, vous n'avez rien d'autre à faire. Ni manipuler la session, ni l'objet request, ni quoi que ce soit d'autre. Symfony s'en charge.

Les paramètres par défaut

Ces paramètres sont prévus pour éviter que l'internaute ne trouve ni aucun contenu, ni un contenu incompréhensible. Mais ils sont aussi définis pour que Symfony puisse fonctionner. Nous avons vu l'ordre de priorité dans les possibilités de passer la locale au framework. En fait, Symfony ne se base que sur la session, mais la remplit selon cet ordre de priorité. Seulement, il y a toujours un moment où l'internaute arrive pour la toute première fois sur notre site (ou une nouvelle fois après avoir entièrement nettoyé son cache navigateur, ce qui, pour le serveur, revient au même). Du coup, il faut bien des paramètres par défaut.

Au début de ce chapitre, nous avons vérifié et changé quelques paramètres dans app/config/config.yml. Reprenons le code de ce fichier un peu plus en détail, afin de mieux comprendre à quoi il sert :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# app/config/config.yml

framework:
    # On définit la langue par défaut pour le service de traduction
    # Ce qui n'est pas disponible dans la locale de l'utilisateur
    # sera affiché dans celle spécifiée ici
    translator:      { fallback: %locale% }

    # …

    # On initialise la locale de requête, celle par défaut pour
    # l'internaute arrivant pour la toute première fois sur votre site
    default_locale: %locale%

Organiser vos catalogues

Quand il y a beaucoup de traductions, les fichiers deviennent vite difficiles à manipuler. Il faut parcourir beaucoup de lignes pour retrouver là où l'on souhaite faire une modification, et c'est contraire au mode de vie des informaticiens qui veut qu'on puisse se retrouver rapidement dans du code ou dans des fichiers. Je vais donc vous proposer des solutions pour alléger vos catalogues.

Utiliser des mots-clés plutôt que du texte comme chaînes sources

Voilà une solution intéressante, à vous de choisir de l'adopter pour toutes vos traductions ou juste les chaînes très longues. L'idée est d'utiliser, au lieu du texte dans la langue source, des mots-clés.

Plutôt qu'un long discours, je vous propose un petit exemple. Prenons une page statique avec pas mal de texte, ce qui implique beaucoup de texte dans le catalogue, par exemple :

1
2
3
# Dans un catalogue

Le site où on apprend tout... à partir de zéro !: The website where you learn it all... from scratch!

L'entrée du catalogue est composée de deux longues phrases, ce n'est pas terrible. Et je ne vous parle pas de son utilisation dans les vues :

1
2
3
4
5
{# Dans une vue #}

{% trans %}Le site où on apprend tout... à partir de zéro !{% trans %}
{# ou #}
{{ 'Le site où on apprend tout... à partir de zéro !'|trans }}

Passons maintenant à l'utilisation d'un mot-clé, vous allez tout de suite comprendre. Voici d'abord le catalogue :

1
2
3
# Dans un catalogue

site.devise: The website where you learn it all... from scratch!

Vous voyez déjà à quel point c'est plus léger !

Bien entendu, si le catalogue est léger, il n'y a rien de magique : vous devez dans ce cas utiliser deux catalogues. L'un pour l'anglais et l'autre pour le français !

Mais l'avantage se situe surtout dans les vues, où un mot-clé est plus synthétique qu'une longue phrase, utile pour ne pas se perdre au milieu du code HTML de votre vue. Voyez vous-mêmes :

1
2
3
4
5
{# Dans une vue #}

{% trans %}site.devise{% trans %}
{# ou #}
{{ 'site.devise'|trans }}

Vous voyez : quelques mots-clés bien choisis pour résumer une phrase, séparés par des points, et vous avez déjà gagné en clarté dans vos vues et catalogues ! Cela est utilisable avec n'importe quel format de catalogue. N'hésitez pas à vous en servir copieusement.

Un des avantages également est de voir très rapidement une chaîne non traduite : au lieu du joli texte en français, vous aurez un « xxx.yyy » au milieu de votre page. Cela saute mieux aux yeux, et évite les oublis !

Enfin, un mot sur la création de deux catalogues au lieu d'un seul. C'est en réalité plutôt une bonne chose, car cela permet non seulement de séparer le texte de tout le code HTML, mais cela permet aussi de mutualiser ! En effet, si vous vous servez d'un mot ou d'une phrase de façon récurrente sur votre site (la devise par exemple), celui-ci ne sera stocké qu'à un seul endroit, dans votre catalogue. Vous pourrez alors le modifier à un unique endroit, et les répercussions s'appliqueront partout sur votre site.

Nicher les traductions

C'est une possibilité qui découle de l'utilisation des mots-clés.

Cette possibilité n'est disponible que dans les catalogues au format YAML.

Si vous optez pour l'utilisation des mots-clés, ce que je vous conseille, vous arriverez très certainement à un résultat de ce genre :

1
2
3
4
5
6
# Dans un catalogue

article.edit.title: Édition d'un article
article.edit.submit_button: Valider
article.show.edit_button: Éditer l'article
article.show.create_button: Créer un nouvel article

Ce qui était très clair avec une seule ligne, le devient déjà moins lorsqu'il y en a quatre, alors imaginez avec plus !

En bons développeurs avisés, vous avez tout de suite repéré les redondances, et votre envie de les factoriser est grande. Sachez que vous n'êtes pas seuls, les développeurs du YAML ont pensé à tout, voici comment optimiser votre catalogue :

1
2
3
4
5
6
7
8
9
# Dans un catalogue

article:
    edit:
        title:         Édition d'un article
        submit_button: Valider
    show:
        edit_button:   Éditer l'article
        create_button: Créer un nouvel article

Quand Symfony va lire cette portion de YAML, il va remplacer chaque séquence « deux points − retour à la ligne − indentation » par un simple point, devenant ainsi l'équivalent de ce que vous aviez précédemment. Très pratique !

Côté utilisation, dans les vues ou avec le service translator, rien ne change. Vous utilisez toujours {{ 'article.edit.title'|trans }} par exemple.

Sachez que c'est une fonctionnalité du YAML, et non du service de traduction de Symfony2. Vous pouvez voir cette utilisation dans votre fichier de configuration app/config/config.yml par exemple !

Pour en revenir à l'organisation du catalogue avec ces mots-clés, je vous propose de toujours respecter une structure de ce genre :

1
2
3
4
5
sdz:              # Le namespace racine que vous utilisez
    blog:         # Le nom du bundle, sans la partie Bundle
        article:  # Le nom de l'entité ou de la section
            list: # Les différents messages, pages et/ou actions
            new:  # Etc.

Permettre le retour à la ligne au milieu des chaînes cibles

Certains éditeurs ne gèrent pas le retour à la ligne automatique, et du coup, ce ne sont pas les chaînes sources trop longues qui posent problème, mais les chaînes cibles. Le parseur YAML fourni avec Symfony supporte une syntaxe intéressante qui permet d'éviter d'avoir à faire défiler horizontalement le contenu des catalogues.

Difficile d'expliquer cela sans un exemple, prenons la charte du Site du Zéro :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Dans un catalogue

charte:
    titre: Mentions légales
    donnee:
        # Le chevron « > » en début de chaîne indique que la chaîne cible est sur
        # plusieurs lignes, mais les retours à la ligne ne seront pas présents
        # dans le code HTML, car ils seront remplacés par des espaces.
        # L'indentation doit être faite sur tout le paragraphe.
        debut: >
            Le Site du Zéro recueille des informations (login, e-mail) lors de
            votre enregistrement en tant que membre du site. Lors de votre
            connexion au site, un fichier "log" stocke les actions effectuées
            par votre ordinateur (via son adresse IP) au serveur.

        # La pipe « | » permet la même chose, mais les retours à la ligne seront
        # présents dans le code HTML, et non remplacés par des espaces.
        # Vous pouvez utiliser nl2br() sur une telle chaîne, cela permet
        # d'avoir le code comme présenté ci-dessous (l'indendation en moins).
        fin: |
            Lorsque que vous vous connectez en tant que membre du Site du Zéro et
            que vous cochez la case correspondante, un cookie est envoyé à votre
            ordinateur afin qu'il se souvienne de votre login et de votre mot de
            passe. Ceci vous est proposé uniquement afin d'automatiser la
            procédure de connexion, et n'est en aucun cas utilisé par Simple IT à
            d'autres fins.

Avec la pipe et le chevron, vous pouvez donc faire tenir votre catalogue sur 80 caractères de large, ou tout autre nombre qui vous convient.

Utiliser des listes

Encore une possibilité du language YAML qui peut s'avérer pratique dans le cas de catalogues !

Reprenons l'exemple précédent de la charte pour en faire une liste. En effet, on rencontre souvent une série de paragraphes, dont certains seront supprimés, d'autres ajoutés, et il faut pouvoir le faire assez rapidement. Si vous n'utilisez pas de liste, et que vous supprimez la partie 2 sur 3, ou que vous ajoutez un nouveau paragraphe entre deux autres… vous devez soit adapter votre vue, soit renuméroter les parties et paragraphes. Bref, ce n'est clairement pas idéal.

Heureusement, il y a un moyen d'éviter cela en YAML, et voici comment :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Dans un catalogue

charte:
    titre: Mentions légales
    donnee:
        # les éléments de liste sont précédés d'un tiret en YAML
        - >
            Le Site du Zéro recueille des informations (login, e-mail) lors de
            votre enregistrement en tant que membre du site. Lors de votre
            connexion au site, un fichier "log" stocke les actions effectuées
            par votre ordinateur (via son adresse IP) au serveur.
        - |
            Lorsque que vous vous connectez en tant que membre du Site du Zéro et
            que vous cochez la case correspondante, un cookie est envoyé à votre
            ordinateur afin qu'il se souvienne de votre login et de votre mot de
            passe. Ceci vous est proposé uniquement afin d'automatiser la
            procédure de connexion, et n'est en aucun cas utilisé par Simple IT à
            d'autres fins.
        - Merci de votre attention.

On va pouvoir utiliser cela dans une boucle for ?

C'est justement l'idée, oui ! On peut utiliser une structure qui va générer une partie de votre page de conditions générales d'utilisation en bouclant sur les valeurs du catalogue, bien vu ! Cela va donner quelque chose comme cela :

1
2
3
4
5
{# Dans une vue #}

{% for i in 0..2 %}
    <p>{{ ('charte.donnee.' ~ i )|trans }}</p>
{% endfor %}

La notation 0..2 est une syntaxe Twig pour générer une séquence linéaire. Le nombre avant les deux points (..) est le début, celui après est la fin.

Donc quand vous ajoutez un paragraphe, vous l'insérez à la bonne place dans le catalogue, sans vous préoccuper de son numéro. Vous n'avez qu'à incrémenter la fin de la séquence. De même si vous supprimez un paragraphe, vous n'avez qu'à décrémenter la limite de la séquence.

Comme pour PHP, les tableaux récupérés depuis le YAML commencent à 0 et non 1.

Utiliser les domaines

Si vous avez commencé à bien remplir votre fichier messages.fr.yml, vous pouvez vous rendre compte qu'il grossit assez vite. Et surtout, qu'il peut y avoir des conflits entre les noms des chaînes sources si vous ne faites pas assez attention.

En fait, il est intéressant de répartir et regrouper les traductions par domaine. Le domaine par défaut est messages, c'est pourquoi nous utilisons depuis le début le fichier messages.XX.XXX. Un domaine correspond donc à un fichier.

Vous pouvez donc créer autant de fichiers/domaines que vous voulez, la première partie représentant le nom du domaine de traduction que vous devrez utiliser.

Mais comment définir le domaine à utiliser pour telle ou telle traduction ?

C'est un argument à donner à la balise, au filtre ou à la fonction trans, tout simplement :

  • Balise : {% trans from 'domaine' %}chaîne{% endtrans %}.
  • Filtre : {{ 'chaîne'|trans({}, 'domaine' }}.
  • Service : <?php $translator->trans('chaîne', array(), 'domaine').

C'est pour cette raison qu'il faut utiliser les domaines avec parcimonie. En effet, si vous décidez d'utiliser un domaine différent de celui par défaut (messages), alors il vous faudra le préciser dans chaque utilisation de trans ! Attention donc à ne pas créer 50 domaines inutilement, le choix doit avoir un intérêt.

Domaines et bundles

Quelle est la différence entre un domaine et son bundle ? Est-ce qu'on peut avoir les mêmes domaines dans des bundles différents ?

Autant de questions qui, je le sais, vous taraudent l'esprit. En fait, c'est plutôt simple : les domaines n'ont rien à voir avec les bundles. Voilà, c'est dit.

Du coup, cela veut dire que vous pouvez tout à fait avoir un domaine « A » dans un bundle, et ce même domaine « A » dans un autre bundle. Le contenu de ces deux bouts de catalogue vont s'additionner pour former le catalogue complet du domaine « A ». C'est ce que nous faisons déjà avec le domaine « messages » en fait ! Une vue du bundle « A » pourra alors utiliser une traduction définie dans le bundle « B », et inversement, à condition que le domaine soit le même.

Et si plusieurs fichiers d'un même domaine définissent la même chaîne source, alors c'est le fichier qui est chargé en dernier qui l'emporte (il écrase la valeur définie par les précédents). L'ordre de chargement des fichiers du catalogue est le même que celui de l'instanciation des bundles dans le Kernel. Il faut donc vérifier tout cela dans votre fichier app/AppKernel.php.

Un domaine spécial : validators

Vous avez peut-être essayé de traduire les messages que vous affichez lors d'erreurs à la soumission de formulaires, et avez remarqué que vous ne pouviez pas les traduire comme tout le reste.

Pourtant, les messages d'erreur fournis par le framework étaient traduits, eux !

Oui, mais c'est parce que Symfony2 n'utilise pas le domaine « messages » pour traduire les messages d'erreur des formulaires. Le framework est prévu pour travailler avec le domaine « validators » dans ce contexte des messages d'erreur. Il vous suffit alors de placer vos traductions dans ce domaine (dans le fichier validators.fr.yml par exemple), et ce dans le bundle de votre choix comme nous venons de le voir.

Nous reviendrons sur ce domaine spécial un peu plus loin.

Traductions dépendantes de variables

La traduction d'un texte n'est pas quelque chose d'automatique. En effet, toutes les langues ne se ressemblent pas, et il peut y avoir des différences qui ont des conséquences importantes sur notre façon de gérer les traductions.

Prenons deux exemples qui vont vous faire comprendre tout de suite :

  • En français, le point d'exclamation est précédé d'une espace, alors qu'en anglais non. Du coup, le « Hello {{ name }}! » que l'on a dans notre vue n'est pas bon, car sa traduction devient « Bonjour {{ name }}! », sans espace avant le point d'exclamation. La traduction n'est pas correcte !
  • En anglais comme en français, mettre au pluriel un nom ne se limite pas toujours à rajouter un « s » à la fin. Comment faire un if dans notre vue pour prendre cela en compte ?

Le composant de traduction a tout prévu, ne vous inquiétez pas et regardons cela tout de suite. ;)

Les placeholders

Pour mettre mon espace devant le point d'exclamation français, est-ce que je dois ajouter dans le catalogue la traduction de «!» en « !» ?

Bien tenté, mais il y a heureusement une meilleure solution !

La solution apportée par Symfony est relativement simple : on va utiliser un placeholder, sorte de paramètre dans une chaîne cible. Cela va nous permettre de régler ce problème d'espacement. Rajoutez ceci dans vos catalogues français et anglais :

1
2
3
# src/Sdz/BlogBundle/Resources/translations/messages.fr.yml

hello: Bonjour %name% !

Et

1
2
3
# src/Sdz/BlogBundle/Resources/translations/messages.en.yml

hello: Hello %name%!

Nous avons mis un placeholder nommé %name% dans chacune des traductions anglaise et française. La valeur de ce placeholder sera spécifiée lors du rendu de la vue, ce qui permet de traduire la phrase complète. Cela évite de découper les traductions avec une partie avant la variable et une partie après la variable, et heureusement lorsque vous avez plusieurs variables dans une même phrase !

Bien entendu il faut adapter un peu notre vue, voici comment passer la valeur du placeholder de la vue au traducteur :

1
2
3
{# src/Sdz/BlogBundle/Resources/views/Blog/traduction.html.twig #}

{{ 'hello'|trans({'%name%': name}) }}

Le premier paramètre donné ici au filtre trans est un tableau, dont les index sont les placeholders avec les caractères % qui le délimitent, et les valeurs, celles par lesquelles le placeholder sera remplacé dans la chaîne cible. Nous venons de dire à Symfony que « quand tu traduis la chaîne source "hello", tu vas remplacer %name% qui se trouve dans la chaîne cible par le contenu de la variable name », qui contient ici le nom de l'utilisateur.

Un placeholder doit être encadré par des % dans les vues, alors que ce n'est pas réellement nécessaire pour le service. Mais par convention, et pour mieux les voir dans les chaînes cibles lors de l'ajout d'une nouvelle langue par exemple, mieux vaut les utiliser partout. Du coup, ces caractères % doivent être présents dans l'index du tableau des placeholders donné au filtre.

Testez donc l'affichage de cette page en français, puis en anglais. Le point d’exclamation est bien précédé d'une espace en français, mais pas en anglais, et le nom d'utilisateur s'affiche toujours !

Parce qu'on n'utilise pas toujours le filtre, voici les syntaxes pour toutes les possibilités d'utilisation :

  • Balise : {% trans with {'%name%': name} %}hello{% endtrans %}.
  • Filtre : {{ 'hello'|trans({'%name%': name}) }}.
  • Service : <?php $translator->trans('hello', array('%name%' => $name)).

Et dans le cas où le paramètre a une valeur fixe dans telle vue, vous pouvez bien évidemment utiliser du texte brut à la place du nom de la variable name, comme ceci :

1
{{ 'hello'|trans({'%name%': 'moi-même'}) }}

Les placeholders dans le domaine validators

Les messages d'erreur de formulaires, qui sont donc dans le domaine validators, peuvent contenir des nombres, principalement quand on spécifie des contraintes de longueur. Ces nombres, il faut bien les afficher à l'utilisateur. Pour cela, vous allez me dire qu'il faut utiliser les placeholders.

Raté ! Ce n'est pas du tout comme cela qu'il faut faire dans ce cas. Rassurez-vous, ce n'est que l'exception qui confirme la règle.

Donc dans le cas des messages d'erreur générés par le composant Validator, et uniquement dans ce cas, il ne faut pas utiliser les placeholders, mais une syntaxe propre à la validation. Cette syntaxe est la même que celle de Twig en fait : {{ limit }}.

Prenons le cas où vous avez utilisé la contrainte Length, vous avez envie de mentionner le nombre limite de caractères (que ce soit le maximum ou le minimum) et le nombre de caractères entrés par l'utilisateur. Ces valeurs sont fournies par le service de validation, dans les variables limit et value respectivement. Ce n'est donc pas %limit% qu'il faut utiliser dans votre traduction, mais {{ limit }}, comme ceci :

1
2
3
4
5
6
# src/Sdz/BlogBundle/Resources/translations/validators.fr.yml

password:
    length:
        short: "Vous avez entré {{ value }} caractères. Or, le mot de passe ne peut en comporter moins de {{ limit }}"
        long:  "Vous avez entré {{ value }} caractères. Or, le mot de passe ne peut en comporter plus de {{ limit }}"

Notez les guillemets autour des chaînes cibles. Ils sont aussi à mettre obligatoirement — encore un détail qui vaut une séance chez le coiffeur.

La raison de cette exception est que le validateur n'envoie pas les valeurs de ces variables au traducteur, il les garde pour lui et fait la substitution après le retour de la chaîne traduite par le traducteur. Pensez-y !

Gestion des pluriels

On va maintenant essayer d'afficher (et y réussir !) le nombre d'articles correspondant à une catégorie sur votre blog, sous la forme « Il y a (nombre) articles ». Il peut y en avoir un seul ou plusieurs, et comme on veut faire les choses bien, il faut que cela affiche « Il y a 1 article » et « Il y a (plus d'un) articles », avec le « s » qui apparaît quand le nombre d'articles dépasse 1.

Si vous deviez le faire tout de suite, vous feriez sûrement un petit test dans la vue pour choisir quelle chaîne traduire, dans ce style-là :

1
2
3
4
5
6
7
8
{# Dans une vue #}
{# Attention, ceci est un mauvais exemple, à ne pas utiliser ! #}

{% if nombre <= 1 %}
  {{ 'article.nombre.singulier'|trans({'%count%': nombre}) }}
{% else %}
  {{ 'article.nombre.pluriel'|trans({'%count%': nombre}) }}
{% endif %}

Avec le catalogue associé :

1
2
3
4
5
6
7
# src/Sdz/BlogBundle/Resources/translations/messages.fr.yml
# Attention, ceci est un mauvais exemple, à ne pas utiliser !

article:
    nombre:
        singulier: Il y a %count% article
        pluriel:   Il y a %count% articles

Eh bien, votre intention est louable, mais une fois de plus, les concepteurs de Twig et de Symfony ont déjà réfléchi à cela et ont tout prévu ! La nouvelle balise/filtre/fonction à utiliser s'appelle transchoice, et elle s'utilise avec en argument le nombre sur lequel faire la condition, voyez par vous-mêmes :

Le filtre :

1
{{ 'article.nombre'|transchoice(nombre) }}

La balise :

1
{% transchoice nombre %}article.nombre{% endtranschoice %}

Le service :

1
2
3
<?php

$translator->transchoice($nombre, 'article.nombre');

Le catalogue, quant à lui, contient donc les deux syntaxes dans une même chaîne source. Voici la syntaxe particulière à adopter :

1
2
3
4
# src/Sdz/BlogBundle/Resources/translations/messages.fr.yml

article:
    nombre: "Il y a %count% article|]1,+Inf]Il y a %count% articles"

Avec cette syntaxe, Symfony pourra savoir que la première partie est pour 0 ou 1 article, et la seconde pour 2 ou plus. Je ne m'attarderai pas dessus, la documentation officielle est là si vous voulez absolument plus d'informations.

Notez que le placeholder %count% est automatiquement remplacé par le paramètre donné à la nouvelle fonction transchoice pour qu'elle détermine la chaîne cible à utiliser. Il n'est donc pas nécessaire de passer manuellement le tableau de placeholders comme on a pu le faire précédemment. Par contre, le nom du placeholder est obligatoirement %count%, il vous faut donc l'utiliser dans la chaîne cible.

Afficher des dates au format local

J'affiche souvent des dates, et j'aimerais avoir les noms des jours/mois, mais comment les traduire ?

Si vous n'avez pas les extensions ICU et intl installées et activées sur votre serveur, la lecture de ce paragraphe ne vous servira à rien. Vérifiez si votre serveur de production possède ces extensions en accédant au config.php disponible dans le répertoire /web. Si vous avez une recommandation qui vous parle d'intl, vous devez installer et/ou activer l'extension si vous avez un serveur dédié, et tenter de discuter avec l'hébergeur si vous êtes sur un serveur mutualisé.

Pour afficher les dates sous la forme « vendredi 11 janvier 2013 », vous avez sûrement déjà utilisé le code {{ date|date('l j F Y') }}. Malheureusement, l'objet Date de PHP n'est pas très bon en langues… et quelque soit votre locale, les noms de jours et de mois sont en anglais. D'ailleurs, ils le sont même sur la page de la documentation.

Je vous rassure tout de suite : il est bien possible de traduire ces dates ! Dans nos vues Twig, il va falloir pour cela utiliser le filtre localizeddate à la place de juste date. Son utilisation est la suivante :

1
{{ date|localizeddate(dateFormat, timeFormat, locale) }}

Les paramètres qu'on lui passe sont les suivants :

  1. dateFormat : le format pour la date ;
  2. timeFormat : le format pour l'heure ;
  3. locale : la locale dans laquelle afficher la date formatée. Pas besoin de la spécifier, elle est fournie dans le contexte.

Mais pourquoi séparer les formats de date et d'heure ?

Voilà, c'était trop beau pour être vrai, on ne peut pas utiliser la syntaxe habituelle pour le format de date/heure (du moins, pas encore). À la place, on a le choix entre quatre formats : full, long, medium et short, pour l'heure comme pour la date, correspondant aux affichages donnés dans le tableau suivant. Il n'est pas possible de les modifier, mais il est en revanche possible de les combiner (donc avoir la date long et l'heure short, par exemple). À défaut de pouvoir faire exactement comme vous voulez, vous avez au moins les mois et les jours traduits correctement, et dans un format tout de même convenable. ;)

Format

Date

Heure

full

jeudi 15 novembre 2012

14:22:15 Heure normale de l’Europe centrale

long

15 novembre 2012

14:22:15 HNEC

medium

15 nov. 2012

14:22:15

short

15/11/12

14:22

none

(rien)

(rien)

Table:Formats français de date et heure avec {{ date|localizeddate(dateFormat, timeFormat, 'fr_FR') }}

Notez que si la locale est simplement fr, une virgule s'ajoute après le nom du jour (donnant « jeudi, 15 novembre 2012 ») ainsi qu'un « h » après la chaîne « H:m:s » (« 14:22:15 h Heure normale de l’Europe centrale ») pour le format full, et la date au format short aura pour séparateurs des points au lieu de slashes. On n'utilise que très rarement les formats full et long pour l'heure. :D

J'ai précisé ici en dur la locale, mais dans votre code ne la mettez pas : elle est automatiquement définie à la locale courante. Votre utilisation sera ainsi aisée :

1
Aujourd'hui nous sommes le {{ 'now'|localizeddate('full', 'none') }} et il est {{ 'now'|localizeddate('none', 'short') }}

Et si vous l'exécutez en même temps que j'écris ces lignes (ce qui me paraît impossible…), vous obtiendrez :

Aujourd'hui nous sommes le lundi 14 janvier 2013 et il est 20:02

Résultat

Attention, si vous rencontrez l'erreur suivante : « The filter "localizeddate" does not exist in ... », c'est que vous n'avez pas encore activé l'extension Twig qui fournit ce filtre. Pour cela, rajoutez simplement cette définition de service dans votre fichier de configuration :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# app/config/config.yml

# …

# Activation de l'extension Twig intl
services:
    twig.extension.intl:
       class: Twig_Extensions_Extension_Intl
       tags:
           - { name: twig.extension }

Pour information, les autres extensions Twig peuvent se trouver à l'adresse suivante : https://github.com/fabpot/Twig-extensions/tree/master/lib/Twig/Extensions/Extension.

Pour conclure

Voici pour terminer un petit récapitulatif des différentes syntaxes complètes, sachant que la plupart des arguments sont facultatifs.

Les balises :

1
2
3
4
5
{# Texte simple #}
{% trans with {'%placeholder%': placeholderValue} from 'domaine' into locale %}maChaîne{% endtrans %}

{# Texte avec gestion de pluriels #}
{% transchoice count with {'%placeholder%': placeholderValue} from 'domaine' into locale %}maChaîne{% endtranschoice %}

Les filtres :

1
2
3
4
5
{# Texte simple #}
{{ 'maChaîne'|trans({'%placeholder%': placeholderValue}, 'domaine', locale) }}

{# Texte avec gestion de pluriels #}
{{ 'maChaîne'|transchoice (count,  {'%placeholder%': placeholderValue}, 'domaine', locale) }}

Les méthodes du service :

1
2
3
4
5
6
7
8
<?php
$translator = $this->get('translator'); // depuis un contrôleur

// Texte simple
$translator->trans('maChaîne',  array('%placeholder%' => $placeholderValue) , 'domaine', $locale);

// Texte avec gestion de pluriels
$translator->transchoice($count, 'maChaîne',  array('%placeholder%' => $placeholderValue) , 'domaine', $locale)

Service

Vous savez maintenant comment créer les traductions dans les différentes langues que vous souhaitez gérer sur votre site !


En résumé

  • La méthodologie d'une traduction est la suivante :
    • Détermination du texte à traduire : cela se fait grâce à la balise et au filtre Twig, ou directement grâce au service translator ;
    • Détermination de la langue cible : cela s'obtient avec la locale, que Symfony2 définit soit à partir de l'URL, soit à partir des préférences de l'internaute.
  • Traduction à l'aide d'un dictionnaire : cela correspond aux catalogues dans Symfony ;
  • Il existe plusieurs formats possibles pour les catalogues, le YAML étant le plus simple ;
  • Il existe différentes méthodes pour bien organiser ses catalogues, pensez-y !
  • Il est possible de faire varier les traductions en fonction de paramètres et/ou de pluriels.