Personnellement, l’anglais ne me pose pas de souci. J’ai été professeur d’anglais pendant presque 10 ans, et je lis et consomme tellement de contenu en anglais, que parfois je ne me rends même pas compte que l’interface WordPress de certains de mes clients est moitié française, moitié anglaise.

Selon leurs besoins, ces clients utilisent des extensions qui malheureusement ne sont pas traduites en français. Qu’elles ne soient pas traduites peut arriver. Peut-être que la team n’avait pas les ressources pour le faire correctement, ou avait simplement la flemme. Mais le souci, c’est quand elles ne sont pas traduisibles ! Pourtant, rendre son code traduisible n’est pas ultra compliqué.

“Internationaliser” son thème (ou extension) signifie simplement “le rendre traduisible”, et WordPress, tout magique comme peut l’être ce CMS, nous offre tous les outils nécessaire pour ce faire !

__() et _e() : les fonctions de base

Dans votre code, promettez-moi de ne jamais écrire ça:

register_post_type( 'monCPT', array(
    'label' => 'Mon CPT',
    ...
) );

Mais préférez simplement :

register_post_type( 'monCPT', array(
    'label' => __( 'Mon CPT', 'montextdomain' ),
    ...
) );

Dans le premier cas, l’intitulé du type de contenu personnalisé est codé en dur et non-traduisible, dans le deuxième, il l’est. C’est tout simple.

La fonction __() prends deux paramètres: une chaîne $string, et une autre chaîne $textdomain, et retourne la traduction de la chaîne de caractère donnée $string. S’il n’y a pas de traduction disponible, la chaîne originale est retournée.

La fonction _e() prends les deux mêmes paramètres, mais affiche le résultat au lieu de simplement le retourner. Faire _e( 'Bonjour', 'textdomain' ) ou echo __( 'Bonjour', 'textdomain' ) est parfaitement équivalent.

Le paramètre $textdomain est une clé indiquant dans quel groupe de traductions chercher celle correspondant à la chaîne en question. Simplement parce que la même chaîne peut être utilisée par plusieurs extensions, mais peut se traduire différemment dans chaque extension. Par exemple le mot “item” isolé peut se traduire par “produit” (“cart item”) ou élément “menu item”, ou même “item”. Le text domain permet de lever cette ambiguïté en catégorisant en quelque sorte vos traductions.

Dans 99,9% des cas, un seul text domain est utilisé par extension ou thème. En général, le text domain est exactement la même chaîne que le slug de votre extension ou thème. Si vous voulez utiliser un text domain différent, il faut le déclarer dans l’entête de votre extension ou thème. Pour plus de clarté, même si ce n’est pas nécessaire, je le déclare toujours. Pour mon thème Kawi, le text domain est kawi, simplement.

/**
 * Theme Name: Kawi
 * Text Domain: kawi
 * Domain Path: /languages
 */

Le Domain Path est le chemin vers les traductions, si vous les incluez dans votre thème ou extension. Comme le text domain, languages/ est la valeur par défaut, que vous pouvez donc omettre.

Traduire des chaines plus complexes

Dans un bon 50% des cas, les fonctions __() et _e() sont suffisantes. Maintenant, il faut traiter les autres cas, qui sont au final tout aussi courants !

Par exemple, comment faire si vous avez besoin de traduire “Il y a 35 commentaires sur cet article”, sachant que 35 est une variable ?

$string = __( 'There is ', 'textdomain' ) . $commentsnumber . __( ' comments on this article.', 'textdomain' );
echo $string;

Ouille ! c’est moche ! Et imaginez le traducteur, qui dans son outil va trouver un “There is” isolé, sans contexte, et un ” comments on this article.” dans le même cas (avec un espace devant) ! En plus, si c’est un pluriel cela devrait être “There are”, même si j’entends pas mal de “There is” + pluriel de la bouche d’anglophones. Mais bon, c’est une autre histoire.

Non, on ne concatène pas de chaînes comme ça. On utilise printf() et sprintf(). Ce ne sont pas des fonctions de traduction à proprement parler, mais elles sont très utiles, puisqu’elles permettent d’insérer des placeholders dans une chaîne de caractère pour pouvoir les remplacer par des variables.

Cela donne donc :

printf( __( 'There is %d comments on this article.', 'textdomain' ), $commentsnumber );

On avance. On a effectivement une seule phrase à traduire, et pas deux morceaux sans aucun sens. Dans la chaîne à traduire, %d sera remplacé par la valeur de $commentsnumber.

Avec sprintf() et printf() vous pouvez aussi utiliser plusieurs placeholders. Dans ce cas, il est vivement recommandé de les numéroter, car on ne sait jamais, différentes langues utilisent des ordres des mots différents, donc certains placeholders pourraient être inversés. Par exemple:

printf( __( 'You have %d subscribers and %d lists', 'textdomain' ), $subscribers, $lists );

printf() remplace les placeholders dans l’ordre dans lequel il les trouve dans ses paramètres. Donc si par hasard, un traducteur inversait les éléments dans la chaînes ( “Vous avez %d listes et %d abonnés” ) les données seraient inversées !

printf( __( 'You have %1$d subscribers and %2$d lists', 'textdomain' ), $subscribers, $lists );

Voilà qui est mieux. On peut maintenant traduire en faisant une référence plus explicite à chaque placeholder. Par exemple, la traduction “Vous avez %2$d listes et %1$d abonnés” fonctionnera correctement et affichera les bonnes données.

C’est bien beau tout ça, mais on a toujours pas résolu le souci singulier/pluriel !

_n() à la rescousse !

_n() est (un peu) plus complexe. Elle prend quatre paramètres : _n( string $single, string $plural, int $number, string $domain ), et permet d’aller chercher le singulier ou le pluriel d’une traduction, en fonction de $number.

Ce qui veut dire qu’on peut traduire notre phrase deux fois (une forme singulier et une forme pluriel) et aller chercher la bonne en fonction du nombre de commentaires comme ceci:

printf( 
    _n( 'There is %d comment on this article.', 'There are %d comments on this article.', $commentsnumber, 'textdomain' ), 
    $commentsnumber 
);

HTML et traductions

Pour les liens ou tout autre élément HTML, on a un souci. Si votre phrase contient un lien (une balise <a>) dont le texte ancre va changer en fonction de la langue, alors il vaut mieux laisser la balise dans la chaîne à traduire.

printf(
    __( 'For more help, visit <a href="%s">our website</a> and submit a support request.', 'textdomain' ),
    'https://example.com'
);

En utilisant printf() et un placeholder pour l’url, on s’assure que le traducteur ne peut pas modifier l’adresse du lien. Il pourrait cependant enlever la balise <a> complète. Mais, on est un peu coincés, là ! Donc on a pas vraiment le choix et on laisse le lien dans la chaîne à traduire. On peut aussi laisser les balises de formatage comme <em>, <strong>, ou <span>, utilisé pour donner une classe CSS.

Par contre, pour tout ce qui est balise de structure, comme <div>, <section> ainsi que les titres, on va autant que possible garder les balises hors de la chaîne à traduire. Donc on va éviter :

<?php _e( '<h1>Voici un titre !</h1>', 'textdomain' ); ?>

Et préférer :

<h1><?php _e( 'Voici un titre !', 'textdomain' ); ?></h1>

Ici pas de risque que le traducteur ne modifie accidentellement la balise. Il n’a à s’occuper que du texte.

L’idée générale est qu’il faut éviter au maximum de découper les phrases / unités sémantiques complètes, et la chaîne à traduire doit contenir le moins d’HTML possible. Juste du texte.

Aidez le traducteur avec un peu de contexte : _x()

_x() fonctionne comme __(), mais prends un troisième paramètre : _x( string $text, string $context, string $domain ). Le paramètre $context est simplement une indication pour les traducteurs indiquant le contexte dans lequel apparaît la chaîne à traduire, pour éviter toute ambiguïté, car même au sein d’une même extension, certaines chaîne isolées peuvent apparaître plusieurs fois et se traduire différemment.

Aussi, il peut être difficile de traduire un mot isolé, genre “Add”, ou “Item” ou “Complete”. Un peu de contexte aide beaucoup !

Appelez la sécurité !

Une règle de sécurité assez basique est d’échapper les chaînes affichées sur le devant du site. Que veux dire échapper ? Prenons un exemple (simple et un peu bidon).

Vous avez un formulaire de contact, qui après soumission affiche un résumé de votre demande. Du type “Merci ! Voici un résumé de votre message : “.

Imaginez qu’un petit malin entre “<script>alert('TOTO');</script>” dans le champ “nom”. Si le contenu du champ n’est pas échappé lors de l’affichage du résumé sur l’écran suivant, vous aurez une alerte JavaScript !

Ici, l’exemple est complètement bidon, mais le but est de vous montrer que le JS est effectivement interprété et exécuté lors de l’affichage de la valeur brute du champ ! Une alerte, c’est bénin, mais ce petit malin a effectivement exécuté du JS sur votre site, et aurait pu faire bien pire !

On ne va pas parler de ce qu’il se passe en backend, mais le souci est le même. Il faut traiter la donnée et s’assurer qu’elle soit propre et bien ce qui est attendu.

Sur le devant du site, il faut échapper la chaîne, c’est-à-dire faire en sorte que le texte s’affiche sans que l’HTML de votre site ne soit modifié. Et encore une fois, WordPress a plein d’outil pour nous aider !

La fonction esc_html() va échapper une chaîne de caractère en s’assurant que l’HTML n’est pas interprété. On l’utilise donc pour échapper et afficher les chaînes qui vont se trouver dans de l’HTML, comme les titres, les paragraphes, etc… En échappant le champ nom du formulaire de l’exemple précédent, on va avoir :

Nom: <script>alert('TOTO');</script>
Email : ...

Mais le JS ne sera pas exécuté ! Ouf !

Pour les chaînes censées apparaître dans des attributs HTML (comme les value des <input> par exemple), vous devez utilise esc_attr(). Pour les urls, vous devez utiliser esc_url(). Il y en a d’autres, mais ce sont les plus courantes et les plus utiles.

Par contre, attention quand il y a de l’HTML dans la chaîne à traduire, comme :

printf(
    __( 'For more help, visit <a href="%s">our website</a> and submit a support request.', 'textdomain' ),
    'https://example.com'
);

Ici, si on échappe la chaîne, comme ceci:

printf(
    esc_html( __( 'For more help, visit <a href="%s">our website</a> and submit a support request.', 'textdomain' ) ),
    'https://example.com'
);

On aura pas le lien, car la balise <a> va être encodée de façon à s’afficher normalement :

For more help, visit <a href="https://example.com">our website</a> and submit a support request.

Donc dans ce cas, on doit malheureusement laisser passer, sans échapper. Par contre, si l’url est une variable, il faut l’échapper :

printf(
    __( 'For more help, visit <a href="%s">our website</a> and submit a support request.', 'textdomain' ),
    esc_url( $url )
);

Maintenant, vous vous demandez surement “Pourquoi j’échapperais des chaînes de textes traduisibles ? Pourquoi un traducteur s’amuserait-il à ajouter de l’HTML dans ses traductions pour casser les sites sur lesquels sa traduction est installée ?”

C’est vrai. Ils n’ont pas vraiment de raison de faire ça. Mais si je présente ces fonctions, c’est pour que vous soyez conscient qu’elles existent et sont indispensables, mais aussi parce qu’il vaut mieux se méfier des données dans les placeholders ! Surtout si ce sont des données entrées par l’utilisateur ! Mieux vaut prévenir que guérir !

Combos de fonctions

Ce qui est sympa avec ces fonctions de traductions et d’échappement, c’est qu’on a souvent besoin d’en utiliser plusieurs imbriqués les unes dans les autres. Et pour ce faire, WordPress mets à notre disposition des combos de fonctions, deux en uns, ou plus ! Par exemple, on a déjà vu _e(), qui est un combo echo + __() !

Mais on a aussi :

  • _ex() = _e() + _x()
  • _nx() = _n() + _x()
  • esc_html__() = esc_html() + __()
  • esc_html_e() = esc_html() + _e()
  • esc_html_x() = esc_html() + _x()
  • esc_attr__() = esc_attr() + __()
  • esc_attr_e() = esc_attr() + _e()
  • esc_attr_x() = esc_attr() + _x()

A vous de jouer maintenant !

Cet article était assez long, car il présente beaucoup de choses, mais au final, internationaliser son thème ou son extension n’est pas si compliqué ! WordPress mets plein d’outils à notre disposition pour rendre nos développements traduisibles, et nous devons le faire.

Retenez simplement ces quelques règles :

  • JAMAIS de chaînes de caractères hardcodées. On utilise TOUJOURS une fonction de traduction comme __() ou _e() au minimum.
  • Autant que possible, on ne coupe pas d’unité sémantiques (phrases ou paragraphes). On utilise printf() et sprintf() avec des placeholders. Attention aux formes singulier/pluriel.
  • On échappe toute chaîne/donnée pouvant être considérée comme non-sécurisée quand on les affiche, en utilisant esc_html(), esc_attr() et ses potes. Ce qui est sécurisé ou pas pourrais faire l’objet d’un article/débat entier. Une donnée venant de la base de données pourrait potentiellement ne pas être sécurisée. Ne jamais faire confiance à vos utilisateurs et toujours être prudent avec leurs entrées !

Si vous retenez et appliquez ces règles, vous serez sur la bonne voie ! Dans un prochain article, on verra comment créer un fichier de traduction, le charger, et effectivement utiliser tout ce qui est mis en place grâce aux fonctions présentées dans celui-ci.

Enjoy !

PS: Si vous avez aimé ce article et êtes intéressé pour apprendre à développer pour WordPress en utilisant les meilleures pratiques, jetez-un oeil à WPCookBook ! C’est un vrai livre de recettes WordPress pour apprendre à développer pour WordPress proprement et exploiter au maximum tous les outils et APIs mis à notre disposition. Inscrivez-vous pour être notifié de sa publication !