Traduction WordPress : Vous faites fausse route !

Traduction WordPress : Vous faites fausse route !

Je voudrais vous parler de la façon dont on développe son plugin ou thème dans le but de le rendre traduisible.

Y penser c’est déjà bien, mais il y a beaucoup d’erreurs à éviter, en voici une liste non exhaustive.

Rappels

Il existe en PHP une fonction _() qui est un alias de gettext(). Gettext est un outil unix qui gère la traduction de chaîne, pour faire simple.

Dans WordPress, cet équivalent plus simple à utiliser est __(). WordPress a créé d’autres fonctions utilisant gettext() afin de simplifier encore plus la vie des développeurs.

Par exemple, au lieu de faire echo __() il existe _e(). Aussi dans les fonctions de sécurité connues comme esc_attr(), il existe esc_attr__() et esc_attr_e().

La documentation de chacune de ces fonctions se trouve en bas de la page de celle de la fonction __() sur le codex.

Dans le reste de l’article je vais dire « plugin » mais cela fonctionne aussi pour un thème.

Erreurs  à éviter

Bande de français !

La première erreur est une erreur française. Nous sommes français, nous développons pour nos clients français ou pour notre communauté française, ou alors nous n’estimons pas possible que d’autres personnes hors France utilisent notre plugin.

C’est bien sûr faux, si votre plugin est sur le repository officiel, alors il sera téléchargé par le monde entier.

Cette erreur est donc tout simplement de NE PAS traduire son plugin et de tout mettre en dur :

$foo = "Merci Julio et à bientôt !";

Il faut donc penser de suite, dès les premières lignes de code à permettre l’internationalisation de votre code. La fonction __() fera très bien l’affaire ici :

$foo = __( "Merci Julio et à bientôt !" );

Domaine

En me corrigeant, j’en vient déjà à la 2ème erreur, je n’ai pas renseigné le dernier paramètre qui est le text domain.

Ce text domain est une chaine qui va servir à regrouper les traductions de mon plugin et ainsi ne pas les mélanger avec le core de WordPress ou d’autres plugins.

C’est une paramètre optionnel MAIS obligatoire dans le cas où c’est votre phrase ou mot que vous souhaitez traduire.

Je vais utiliser dans cet article le text domain « plugin-domain« .

$foo = __( "Merci Julio et à bientôt !", 'plugin-domain' );

Domaine²

Avez-vous dans l’entête de votre plugin indiqué le text domain justement ?

Voici un exemple :

/*
* Plugin Name: Mon plugin
* Author: Julio
* Text Domain: plugin-domain
*/

Cette valeur doit être identique au nom du dossier de votre plugin, autrement dit, le slug de votre plugin.

Elle va permettre à WordPress de pouvoir mettre à jour les fichiers de langue de votre plugin sans que ayez besoin de faire une nouvelle version.

Merci !

Et déjà la 3ème erreur, j’ai laissé mon texte en français.

Le fait de laisser son texte en français dans du code prêt à traduire signifie que si je suis sur une installation sans langue, je serais donc en en_US (anglais US), SAUF mon plugin en français !

Déroutant non ? Ha non, pas pour vous qui avez votre installation en français, vous ne vous êtes aperçu de rien, forcément.

Il faut TOUJOURS développer en anglais, même les noms de vos variables d’ailleurs, évitez $champ et utilisez $field par exemple.

$foo = __( "Thank you Julio, see you soon!", 'plugin-domain' );

$name

Je suis content, enfin réussi, mais ça me chagrine de devoir mettre le nom de l’utilisateur à remercier dans mon code, je voudrai le récupérer d’une variable, voici ce qui est (mal) fait :

$name = $GLOBALS['current_user']->user_name;
$foo = __( "Thank you $name, see you soon!", 'plugin-domain' );

Super non ? NON !?

Et non, car vous essayez de donner à gettext le mot $name à traduire. Si dans votre site cela fonctionne, c’est parce qu’ici le code PHP est évalué et la variable remplacée par sa valeur.

Hors gettext n’est pas du php mais un outil unix qui n’évaluera pas cette variable !

Il existe des outils et des cas où cette façon de faire ne fonctionnera pas, il ne faut donc PAS mettre de variable de cette façon.

CONcat

Tentons une correction :

$name = $GLOBALS['current_user']->user_name;
$foo = __( "Thank you", 'plugin-domain' ) . $name . __( ", see you soon!", 'plugin-domain' );

Mieux ? MMmnnnon, pas encore. La concaténation de la variable fonctionne, certes, dans votre site, tout est parfait, très bien.

MAIS dans certaines langues, il est fort possible que le nom de la personne se positionne en premier dans la phrase, l’ordre des mots change parfois.

La concaténation me force ici à dire « merci » avant le nom.

Et ne pensez pas à me dire « et si on traduisait le Thank you en vide ??? » car toutes les autres occurrences du mot « Thank you » seraient alors vides aussi.

Il nous faut utiliser une fonction de formatage de texte comme sprintf() ou printf().

$name = $GLOBALS['current_user']->user_name;
$foo = sprintf __( "Thank you %s, see you soon!", 'plugin-domain' ), $name );

Cot cot cot

Me voilà enfin propre avec ma chaîne ! Pardon ? Pas encore ??

Vous utilisez des guillemets, les « double quotes ». Alors, oui ça fonctionne là, mais … ne prenez pas cette habitude car vous risquez de vous faire avoir, puisqu’une variable PHP contenue dans une chaîne entre guillemets sera évaluée.

Modifions un peu notre exemple :

$num_tickets = get_transient( 'num_tickets' ); // Exemple 6
$num_users = get_transient( 'num_users' ); // Exemple 3
$foo = sprintf( __( "%d tickets left and %d users.", 'plugin-domain' ), $num_tickets, $num_users );

Ici on reste bon, le contenu de $foo est :

6 tickets left and 3 users.

Mais imaginons qu’en français nous préférons mettre en avant les utilisateurs et ensuite les tickets, il me suffirait de traduire la chaîne en :

%d utilisateurs et %d tickets restants.

Ce qui me donne sur mon site :

6 utilisateurs et 3 tickets restants.

Aïe … les quantités ne sont pas bonnes ! Je ne peut PAS intervertir mes %d à remplacer !

Ouf, sprintf() permet de le faire :

$num_tickets = get_transient( 'num_tickets' ); // Exemple 6

$num_users = get_transient( 'num_users' ); // Exemple 3

$foo = sprintf( __( "%1$d tickets left and %2$d users.", 'plugin-domain' ), $num_tickets, $num_users );

On voit bien maintenant que le premier paramètre et le second sont ordonnés, je peux traduire en :

%2$d utilisateurs et %1$d tickets restants.

Ce qui me donne sur mon site :

%2 utilisateurs et %1 tickets restants.

Quoi ?? Mais oui regardez, la variable $d de la chaîne %1$d a été évaluée et comme elle n’existe pas, elle vaut NULL.

Voilà, vous avez compris pourquoi il FAUT utiliser uniquement les apostrophes, la single quote.

Corrigeons :

$num_tickets = get_transient( 'num_tickets' ); // Exemple 6
$num_users = get_transient( 'num_users' ); // Exemple 3
$foo = sprintf( __( '%1$d tickets left and %2$d users.', 'plugin-domain' ), $num_tickets, $num_users );

Ce qui me donne sur mon site :

3 utilisateurs et 6 tickets restants.

Parfait.

Chariot

Je trouve que mes lignes sont trop longues, je vais les couper, je peux ?

$foo = sprintf( __( '%1$d tickets left and ' .
'%2$d users.', 'plugin-domain' ), $num_tickets, $num_users );

Non !

$foo = sprintf( __( '%1$d tickets left ' . 'and %2$d users.', 'plugin-domain' ), $num_tickets, $num_users );

Non plus ! Aucune concaténation dans vos chaînes. Cela peut fonctionner dans POEdit par exemple, mais pas forcément ailleurs, dans des logiciels qui vous sont inconnus.

Si vous avez de longues phrases et que vous ne souhaitez pas avoir des lignes de code de plus de 255 caractères, COUPEZ vos lignes :

$num_tickets = get_transient( 'num_tickets' ); // Exemple 1
$foo = sprintf( __( '%1$d tickets left and ', 'plugin-domain' ), $num_tickets );
$num_users = get_transient( 'num_users' ); // Exemple 1
$foo .= sprintf( __( '%2$d users.', 'plugin-domain' ), $num_users, $num_users );

$foo a été concaténée avec .= et le nom de la fonction __() + la chaîne à traduire + le domaine sont sur la même ligne de code, parfait.

Plural form

Ho mais attendez ! Je vois que ces deux quantités valent 1, alors que j’avais mis tickets et users au pluriel, c’est moche !

Ok j’ai une idée !

$foo = __( '%d ticket(s) left.', 'plugin-domain' );

Mieux, mais pas très pro. Encore ?

$num_tickets = get_transient( 'num_tickets' ); // Exemple 1
if ( $num_tickets > 1 ) { // _n()
  $foo = __( '%d tickets left.', 'plugin-domain' );
} else {
  $foo = __( '%d ticket left.', 'plugin-domain' );
}

Ça fonctionne. En français. Car par exemple en anglais le 0 est pluriel, hors dans ce cas ci leur 0 sera singulier.

La bonne façon de faire est d’utiliser la fonction _n(), tu essaies ?

$foo = _n( '1 ticket left.', '%d tickets left.', $num_tickets, 'plugin-domain' );

Ça fonctionne, en français … car en français notre seul singulier est « 1 », mais dans d’autres langues il commence à « 5 ». Il faut donc toujours utiliser%d ou %s.

$foo = _n( '%d ticket left.', '%d tickets left.', $num_tickets, 'plugin-domain' );
  • 1er paramètre = singulier
  • 2ème paramètre = pluriel
  • 3ème paramètre = nombre pour la comparaison
  • 4ème paramètre = text domain

J’ai préféré utiliser %s plutôt que %d alors que le nombre de ticket sera bel et bien un entier MAIS avec une chaîne de caractère je permets à ce chiffre de contenir des virgules et espaces pour les séparateurs de milliers.

Haaa voilà ! Enfin !

Chaque langue ou presque a un pluriel personnel, voici la liste de ces règles :

http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html?id=l10n/pluralforms

Add New

Passons à un autre exemple, je veux pouvoir traduire « Add New« , je fais donc :

$foo = __( 'Add New', 'plugin-domain' );

Cela s’utilise donc dans le menu de mon custom post type. Mais comment traduire ça ?

« Ajouter un nouveau » ? « Ajouter une nouvelle » ? « Ajouter des nouveaux » ? « Ajouter des nouvelles » ? Aïe, impossible ni de connaitre le genre ni le nombre de cette chaîne.

Il faut lui ajouter un contexte afin de savoir quels genre et nombre choisir. Cela se fait avec _x() :

$foo = _x( 'Add New', 'car', 'plugin-domain' );

Je code donc en anglais et j’utilise le 3ème paramètre pour renseigner le contexte. Le dernier paramètre est toujours le text domain.

En français je peux alors traduire par :

"Ajouter une voiture" ou "Ajouter une nouvelle"

Si j’ai besoin de cette même traduction plus tard, il me suffit de modifier le contexte :

$foo = _x( 'Add New', 'book', 'plugin-domain' );

En français je peux alors traduire par :

"Ajouter un livre" ou "Ajouter un nouveau"

Login

Bon, j’ai compris, et ça c’est bon ?

$foo = __( 'Login', 'plugin-domain' );

Oui et non. C’est bon mais WordPress connait déjà cette chaîne. Le text domain de WordPress est « default« , ne rien mettre en text domain revient à mettre cette valeur.

$foo = __( 'Login'  );

Le fait d’ajouter votre propre text domain vous force a créer une traduction.

Vous pouvez, selon moi, utiliser les chaînes courtes sans domaine afin de bénéficier des traductions de WordPress, mais si la chaîne est longue, vous risquez de voir un jour cette traduction modifiée, au risque que son contexte change pour vous.

gisa

Autre exemple venant du core :

$draft_saved_date_format = __( 'g:i:s a' );

Ceci est correct, mais je ne comprends pas forcément comment traduire ça. Il s’agit en fait d’un format de date/time.

Dans votre code PHP, commentez alors cette ligne :

/* translators: draft saved date format, see http://php.net/date */
$draft_saved_date_format = __( 'g:i:s a' );

Par contre, dans vos plugins, si vous avez besoin d’un format de date ou d’heure, ne recréez pas vos données, lisez les options :

$date_format = get_option( 'date_format' );

$time_format = get_option( 'time_format' );

Point important rappelé par Nicolas Maillard, il faut utiliser la fonction date_i18n() et non pas date() pour afficher une date dans la langue du site, sinon c’est la langue du serveur qui sera affichée !!

date( get_option( 'date_format' ) );
// Affiche : January, 26th 2015 sur mon site en français
date_i18n( get_option( 'date_format' ) );
// Affiche : 26 Janvier 2015 sur mon site en français

DOM

Nous avons vu pas mal de chaînes assez simples, qu’en est-il du code HTML ?

Prenons un nouvel exemple :

$bar = 'awesome';
$foo = sprintf( __('<h3>You are %s!</h3>', 'plugin-domain'), $bar );

Cela fonctionne, parfait. MAIS vous laissez la possibilité aux autres langues de mettre des balises H1, ou même ne rien mettre ? Pas logique selon moi. Exportez le contenu HTML important :

$bar = 'awesome';
$foo = '<h3>' . sprintf( __('You are %s!', 'plugin-domain'), $bar ) . '</h3>';

Maintenant les balises H3 sont forcées ! Même choses pour les styles et class, évitez de laissez ça en traduction. Par contre s’il s’agit de balises plus simple comme I, B, EM, STRONG etc, vous pouvez, selon moi, laisser passer.

Dernier cas : les liens !

$foo = sprintf( __('<a href="%s">This link.</a>', 'plugin-domain'), 'http://example.com' );

Avec ce cas je ne permet pas aux autres langues de modifier mon lien. C’est bien par exemple pour vos crédits ou des URLs qui ne changeront pas.

Par contre s’il s’agit de vidéos tutos youtube, de documentation, ces liens sont susceptibles d’exister dans d’autres langues, laissez alors les liens dans la traduction. Il se pourrait même que le lien soit supprimé car, n’existant qu’en français, la personne ai besoin de le supprimer.

D’un autre côté, selon les cas, vous pouvez aussi forcer les liens en les sortants de la traduction comme on a fait avec les H3.

SCRIPT

Si vous souhaitez afficher du JavaScript dans votre code, il est possible que vous fassiez ça :

<script>
alert( '<?php _e( 'Some string to translate', 'plugin-domain' ); ?>' );
</script>

Mais pour du contenu plus lourd, je vous recommande de créer un .js.

Malheureusement un .js ne peut pas être traduit directement via le code PHP.

Néanmoins la fonction wp_localize_script() permet de pallier à ce problème.

Exemple provenant du codex :

<?php

// Register the script first.
wp_register_script( 'some_handle', 'path/to/myscript.js' );

// Now we can localize the script with our data.
$translation_array = array( 'some_string' => __( 'Some string to translate', 'plugin-domain' ), 'a_value' => '10' );
wp_localize_script( 'some_handle', 'object_name', $translation_array );

// The script can be enqueued now or later.
wp_enqueue_script( 'some_handle' );

Vous pouvez maintenant récupérer dans votre script :

<script>
alert( object_name.some_string) ; // alerts 'Some string to translate'
</script>

RN

Il existe quelques chaînes un peu spéciales que gettext n’aime pas trop :

$foo = __( ' ', 'plugin-domain' );

La tentative de traduction d’un espace sans contexte n’est pas valide, utilisez plutôt _x().

$foo = _x( ' ', 'context', 'plugin-domain' );

Et la tentative de traduction de retour chariot n’est pas non plus appréciée :

$foo = __( '\r', 'plugin-domain' );
Utilisez la nouvelle ligne (new ligne) :
$foo = __( '\n', 'plugin-domain' );

GENERATEWP

Petit point spécial pour parler du site generatewp.com qui vous permet entre autre de générer facilement le code des custom posts types.

Le soucis est que le code fourni mets la priorité de ton hook sur 0 :

add_action( 'init', 'custom_post_type', 0 );

Et habituellement les plugins et thèmes ajoutent le chargement de la traduction aussi dans init :

add_action( 'init', 'myplugin_init' );
function myplugin_init() {
  $plugin_dir = basename( dirname( __FILE__ ) ) . '/lang';
  load_plugin_textdomain( 'plugin-domain', false, $plugin_dir );
}

La priorité par défaut étant 10, les traductions du CPT sont chargées à vide avant que le fichier de traduction ne le soit, et aucun traduction ne fonctionne pour votre CPT …

Vous avez appris quelque chose ? Tant mieux, c’est le but !

Lire la suite

Vous aimez ? Partagez !

Abonnement gratuit à 0€


9 thoughts on “Traduction WordPress : Vous faites fausse route !”

  • 1
    Mickaël Gris on 24 janvier 2015 Répondre
    Merci pour le partage !

    La conférence était encore une fois très intéressante.
    Des rappels aux fondamentaux, ça ne fait jamais de mal :-) .

  • 2
    @developpeuseWeb on 24 janvier 2015 Répondre
    Merci beaucoup ! L’article est très clair et agréable à lire, avec des exemples concrets, c’est top !
  • 3
    Nicolas on 25 janvier 2015 Répondre
    Super et très précis notamment les contextes.

    Par contre sur mac l’apostrophe est une simple quote, et ça pose régulièrement problème justement sur les chaines traduites.

  • 4
    Nicolas on 26 janvier 2015 Répondre
    Bonjour Julio,

    Tu aurais pu faire référence à la fonction date_i18n() dans ton chapitre « gisa » car le format de date n’est pas suffisant s’il y a du texte (jour, mois). Un simple date() ne les traduira pas.

    _x(‘Nicolas.’, ‘ABSOLUTE Web’, ‘boiteaweb’)

    • 5
      Julio Potier on 26 janvier 2015
      ouiiii article mis à jour, merci Nicolas !!
  • 6
    Alex on 28 janvier 2015 Répondre
    Salut Julio,

    Merci pour ce récap Julio :)

    Je profite de cet article pour partager un article qui vient de me sortir de la mouise : http://www.cssigniter.com/ignite/wordpress-poedit-translation-secrets/

    On y apprend notamment qu’il faut ajouter _n:1,2 au lieu de _n dans PoEdit pour bien avoir les deux chaines à traduire. Ca marche aussi avec _x et les autres fonctions où il y a plusieurs chaînes.

    A+ les amis

  • 7
    Anouar Fourti on 9 février 2015 Répondre
    Super! C’est exactement ce dont j’avais la flemme d’approfondir dans mon tuto de traduction de thème/plugin WP, mais qu’on retrouve en beaucoup plus complet ici. Du coup, j’ai ajouté ton lien pour compléter mon tuto.
  • 8
    Antione Burel on 9 novembre 2015 Répondre
    Everything is very open with a clear clarification of the challenges. It was really informative. Your website is very helpful. Thank you for sharing!
  • 9
    Ricki on 26 octobre 2016 Répondre
    Pour traduire des fichiers .po et .pot, vous pouvez utiliser aussi un outil de traduction en-ligne comme https://poeditor.com/

Laisser un commentaire

Avant de parler, merci de lire la charte des commentaires.

Utiliser le tag [php][/php] pour ajouter du code ou utilisez un service comme pastebin.com.
Cibler un commentateur avec un "@", merci à Mention Comments Authors !