Traduction et multilinguisme d'un site web : Gettext et poedit tutoriel et exemple
Par François Lasselin le jeudi, février 10 2011, 21:37 - Technologie - Lien permanent
Un site web en développement doit être (ou devenir) multilingue. Il faut donc déterminer une architecture permettant d'isoler le contenu linguistique du reste du code pour faciliter l'intégration des différentes langues. Un système de détection et de sélection de langues est également nécessaire. Il faut aussi trouver des traducteurs (mais pour ça, ce billet ne vous aidera pas ...) et surtout organiser le travail de traduction. Un traducteur n'est pas un intégrateur html ...
Une des solutions répandue et élégante est l'utilisation de Gettext. On détaille ici sa mise en œuvre.
Passer un site web du français à l'anglais et à d'autres langues européennes mais toujours dans l'alphabet latin (espagnol, allemand, italien,..) ou vers des langues 'mondiales' et dans des alphabets spécifiques (arabe, russe, chinois, japonais, ..) est une problématique qu'on peut résoudre de nombreuses façons.
Array, INI, CSV, Gettext, XML quel format choisir la technologie ?
Quelles sont les solutions possibles ?
- On peut bricoler un tableau (Array) avec la langue et le label comme clefs. Objectivement c'est la solution la plus performante car le tableau est déjà en PHP lors de son inclusion. Toutefois, il faut traduire l'array avec un traducteur qui tape du php (peu probable) ou générer le array à partir d'un autre format.
- Déjà plus simple, les fichiers CSV peuvent s'ouvrir/s'exporter à partir d'un tableur. Côté PHP la fonction fgetcsv() est native en php. Utilisée dans une boucle (la fonction lit le fichier ligne par ligne), elle parse le CSV et le charge potentiellement dans un tableau. Néanmoins, un tableur offre une grande liberté d'édition, il est facile de se tromper.
- le fichier INI ne nécessite pas de boucle en PHP. parse_ini_file() parse en une fois la totalité du fichier et retourne un array php. L’édition de fichier ini se fait dans un éditeur type bloc-note avec une syntaxe légère mais incontournable. Pas très friendly pour des non-informaticiens.
- XML (tbx, tmx, xliff, ...). Avec SimpleXML le XML est parsé puis chargé en mémoire. C'est tout de suite moins intéressant en terme de performance. Par ailleurs, pour un non-informaticien, le xml n'est pas un format compréhensible.
- Gettext...
GNU Gettext ?
Gettext est un projet GNU dédié à la traduction/multilinguisme.
- c'est un moyen facile de faire faire les traductions grâce à l'éditeur et au format de fichier
- c'est un standard utilisé par de nombreux framework (Zend dans Zend_translate) et projets PHP (Drupal, Wordpress, ..) et au delà car la techno n'est pas web à l'origine.
- c'est performant car géré par une extension compilée de PHP
- c'est performant car le format de fichier utilisé est binaire
- Ce n'est pas qu'une solution côté serveur, mais aussi une solution côté traducteur qui facilite l'industrialisation et la parallélisation de la traduction.
Gettext et le multithread: c'est grave docteur ?
La principale raison affichée pour ne pas utiliser GNU gettxt est un problème de collision en utilisation multithread. En effet, sur un serveur apache worker (multithread), quand plusieurs pages sont en train d'être générées pour des utilisateurs de langues différentes, la fonction setlocale() va s'appliquer à tous les thread d'un processus apache. Cela provoque le changement de langue de toutes le reste de l’exécution des autres pages. On arrive donc à des contenus incohérents. Ce problème n'est valable que pour le mod thread d'apache, et pas pour le mod prefork.
Sans rentrer dans une polémique sur l'avenir d'apache, ce problème n'est pas spécifique à gettext mais concerne l'écosystème php en général..
Pour de nombreuses raisons (dont les performances), PHP est majoritairement utilisé via mod_php donc via le mode Prefork. Donc, en dehors des cas particuliers d’exécutions multithread parce qu'il y a une exigence structurante: on peut utiliser gettext. Si malheureusement, vous utilisez php avec apache en mode worker, il est encore possible d'utiliser des implémentations gettext. Par exemple, le Zend Framework fournit un adaptateur avec Zend_Translate. Dans tous les cas, on peut utiliser les formats gettext sur tous les serveurs.
(remerciement à Benjamin)
Mettre en oeuvre gettext
(Télécharger cet exemple (php, pot, po, mo) sur la page des ressources)getext est présent dans de nombreux systèmes d'exploitations. L’exécution d'un phpinfo() permet de vérifier qu'il est bien activé. (Sur la dernière ubuntu c'est bien le cas par défaut). Getext fonctionne en associant une clef à chaque label. Le principe est le suivant sur le site on va remplacer les labels (genre "<h2>connectez-vous</h2>") par des appels à gettext avec un identifiant de la chaine. gettext renvoie alors le label du fichier de langue correspondant. Par convention , l'identifiant est le label dans la langue de référence (généralement, la langue commune à tout les traducteurs).
Dans un monde idéal, les chaînes de caractères à traduire n'apparaissent que dans les fichiers de la vue (voir design pattern MVC).
Dans un fichier phtml/template comment ça se passe ?
Dans la vue en utilisant gettext, on fait appel à la fonction gettext() auquel on passe en paramètre l'id de la chaine à afficher (à ce stade, on n'a pas encore créer cette chaine... mais il faut bien commercer par un bout). Exemple:
echo gettext("thank-you");
Ou, plus simplement en utilisant l'alias:
echo _("thank-you");
La
grande question métaphysique est de déterminer quelle langue utiliser
pour les id... sachant que si l'id n'est pas trouvé ou pas traduite,
l'id s'affiche à la place de la traduction.
Si on fait des mises en formes de texte ça marche aussi:
printf(_("Pi is something like %01.2f !"), pi());
Dans ce cas, le %01.2f fait partie de la clef.
On a donc un fichier phtml/template taggé pour gettext mais pas encore les contenus traduis. Pour générer les différents fichiers de langue, il faut recenser toutes les chaines pour les centraliser. On peut faire
find -name *.phtml > fichiersatraduire.txt
ou
find -name *.php > fichiersatraduire.txt
pour récupérer la liste des fichiers à
traduire
puis, pour recenser la liste des contenus à traduire et générer le template de traduction on utilise xgettext. Cette commande n'est pas spécifique à php. Elle est valable pour de nombreux langage. Attention, par défaut,elle prend en entré des fichiers ASCII. Donc, pas de caractères accentués.
xgettext -f fichiersatraduire.txt -o bloglasselin.pot
Le fichier .pot est un fichier de template. C'est à partir de ce fichier qu'on va générer les fichiers traduits ( 1 par langue).
Concrètement, le template est un modèle. Chaque traducteur génère son fichier de langue à partir de ce modèle.
Pour les chaines contenus dans les javascripts plusieurs possibilités: soit décliner les fichiers par langue, soit générer le fichier avec php. Les avis extérieurs sont les bienvenus...
Poedit
Sous ubuntu dans le gestionnaire de paquet rechercher et installer "poedit".
Sinon, pour les traducteurs plus vraisemblablement sous windows ou Mac, ça se télécharge là: http://www.poedit.net/download.php
On utilise poedit: Fichier->nouveau catalogue depuis un fichier POT. Immédiatement, Poedit va vous solliciter pour 2 choses: configurer la langue et enregistrer le fichier:
Poedit se connecte à des bases de données pour faire certaine traduction automatiquement (en se basant sur les id). Poedit est un éditeur simple. 2 colonnes: le label / la traduction dans la langue du traducteur.
Une fois la traduction terminée, on sauvegarde. A l'enregistrement,
poedit crée un fichier .po (fichier source) et aussi (même si il ne le dit pas) un fichier .mo (compilé). C'est ce fichier mo que l'on déploie sur le serveur. En accord avec la configuration définit dans le script PHP (voir configuration PHP plus bas), il faut mettre le fichier .mo dans le répertoire de l'arborescence suivante:
racine du site \
"nom du domaine"\
en_EN.utf8\
(si on a choisi l'anglais fr_FR pour le français) -> faire un
"local -a" sous linux pour voir la liste des locales disponibles.
LC_MESSAGES\
lefichier.mo
Attention,
les fichiers .mo sont mises en caches par Apache. Il faut redémarrer
apache après chaque compilation de fichier langue.
L'utilisation d'une variable de session
contenant la langue est nécessaire. La définition de la locale est à
isoler dans une classe PHP et systématiquement appelé à chaque requête.
Configuration PHP
Pour charger les fichiers de langues il faut définir la langue et le nom du fichier:<?phpCet exemple (php, pot, po, mo) est téléchargeable sur la page des ressources.
$lang='fr_FR.utf8';
$filename = 'default';
putenv("LC_ALL=$lang");
setlocale(LC_ALL, $lang);
bindtextdomain($filename, './locale');
bind_textdomain_codeset($filename, "UTF-8");
textdomain($filename);
?>
La discussion continue ailleurs
URL de rétrolien : http://blog.nalis.fr/index.php?trackback/104
Commentaires
Merci pour ces explications très concise et pourtant très simple.
Je me pose des questions sur l'utilisation de Gettext avec des données à traduire. Peut-être l'occasion de faire une 2eme page d'explication ?
Voilà mon problème :
Par exemple si en base, on a :
pomme, poire, banane
Comment vous y prendreriez vous pour traduire
"j'ai mangé une banane"
Le cas suivant ne sera pas bon :
$fruit = trouve_fruit(1) ;
sprintf ( _("j'ai mangé une %s"), _($fruit) );
car la variable $fruit ne sera pas connu ni remonté dans PoEdit.
Kéké de magdales
Bonjour kéké,
La traduction devrait avoir lieu avant que le résultat ne soit retourné pas trouve_fruit(). En fait, je pense que le fond du problème dans ce cas précis c'est que la fonction trouve_fruit() va chercher la chaine "banane" en base. C'est exact ?
Une possible solution serait de poser les différents labels dans une variable:
$label_fruit= array ("1" => _("banane"),
"2" => _("pomme"),
"3" => "html"
);
// $fruit = trouve_fruit(1) ; -> Du coup, on en a plus besoin ?
echo $label_fruit[1];
sprintf ( _("j'ai mangé une %s"), $label_fruit[$fruit] );
C'est pour traduire magdales.com ?
A propos de magdales, attention aux liens tout en bas sur le serveur de prod, leur URL est de type locale (127.0.0.1)
Bonjour ... désolé du temps de latence entre les réponses !
Depuis le 27 avril, plusieurs innovations ont vu le jour ... mais la traduction avance (en mon sens) trop doucement. Je suis un peu responsable de ce retard, ayant préféré chambouler le planning afin de mettre des évolutions pour les joueurs, plutôt que la traduction qui ne va chercher que de futurs hypothétiques joueurs. Car oui, il s'agit/agira de traduire tout le jeu.
J'ai corrigé les URL ... un oubli lors de la dernière MEP.
J'en profite pour répondre aux questions du maitre de ces lieux : François Lasselin.
Déjà, merci d'avoir bien voulu me répondre avec une solution technique envisageable ... mais pas dans mon cas.
En effet, je me base sur une base de donnée pour gagner en flexibilité, et je ne peux/veux pas faire un retour vers des labels en variables dans un fichier. Le fait est que mon jeu est Upgradable directement par les joueurs. Demain, on pourrait avoir des kiwis ou des physalis sans que j'ai eu une action à faire ...
Modifier mon fichier de variables contenant des array en passant par PHP ne me semble pas envisagable surtout sur des tables à fortes volumétries.
La solution Gettext ne me parrait pas la plus recommandé dans mon cas.
Cependant, j'ai mis en place d'autres solutions pour la traduction de données en BDD.
/* Attention, texte un peu dense à comprendre même pour les initiés. Sans offense si vous passez ce bloc de discutions */
Il s'agit d'une table de traduction avec une clé unique (appelons la id_trad), qui s'incrémente à chaque fois qu'une donnée rencontré n'est pas traduite.
Ainsi ma table imaginaire fruit contient maintenant 2 champs : nom, id_trad
A l'origine les variables id_trad ne sont pas renseignées (valeur à 0) ... pas de traduction existante en base.
Dans le jeu, banane doit être affiché ... ma fonction d'affichage va chercher la variable id_trad ... et détecte qu'elle vaut 0. Alerte, on créé une ligne dans la table id_trad : banane sans traduction avec comme valeur id_trad = A, et on modifie ma table fruit.
Comme il n'y a pas de traduction, on affiche "banane" ... mais on a gardé en mémoire qu'il y a un mot à traduire.
En papillonnant sur le net, un admin se penche sur la traduction du mot banane et le traduit en banana.
L'utilisateur suivant (et les autres) verront alors s'afficher correctement la traduction.
Il est bien évidement (dit-il après y avoir passé plusieurs nuits blanches) possible que l'administrateur soit aussi le premier utilisateur et qu'il cherche en base TOUTE les chaines à traduire avant qu'un joueur ne voient une chaîne manquante.
Par ailleurs, si le texte d'origine est en Français, que 100% a été traduit en Anglais, et 80% en Allemand et que vous êtes Allemand, la solution Gettext ne renvera du texte que en Français ou en Allemand selon la disponibilité des traductions.
Avec la méthode décrite plus haut, il est possible de choisir d'afficher le texte en Allemand, et si traduction indisponible, l'afficher en Anglais.
/* Vous pouvez relâcher votre attention */
Bref, ce système a l'avantage d'offrir une solution technique à mon problème, tout en gardant la solution sous forme de base de données.
En plus, depuis le mois d'avril, la partie moteur de traduction est en place ... Il ne me reste plus qu'à traper les chaines de caractères en dur dans le code (partie longue et il faut se coordonner avec les équipes) + la partie traduction en elle-même.
Bonne soirée à vous !
Kéké
Merci pour cet article :)
Merci pour cet article très intéressant! :)
Hi,
If you’re interested to localize web software, PC software, mobile software or any other type of software, I warmly recommend a new l10n tool that my team recently developed and will probably make your work a lot faster and easier: http://poeditor.com/
POEditor is intuitive and collaborative and has a lot of useful features to help your translations management process, which you can find enlisted on our website.
You can import from multiple localization file formats (like pot, po, xls, xlsx, strings, xml, resx, properties) or just use our REST API.
Feel free to try it out and recommend it to developers and everyone who might find it useful.
Si vous êtes intéressés de traduire logiciels pour Internet, pour PC, pour mobiles ou tout autre type de logiciels, je vous recommandons chaleureusement le nouvel instrument numérique ”l10n” que mon équipe a récemment créé – et qui a toutes les chances de rendre vos activités de bureau bien plus faciles et rapides.
Poeditor.com
POEditor est intuitif, basé sur travail en collaboration. Il comprend de nombreuses fonctions qui puissent vous soutenir lors du processus de gestion de traductions, que vous pourriez découvrir sur notre page Internet.
Vous pouvez importer depuis multiples types de fichiers de localisation (traduction) (pot, po, xls, xlsx, strings, xml, resx, properties) ou se servir directement de notre REST API.
N’hésitez pas de l’essayer et/ou le proposer aux développeurs ou, en général, a ceux qui en seraient intéressés.
Bonjour et merci pour cet excellent tuto.
J'ai une interrogation. Dans le cas d'un serveur mutualisé chez un des nombreux fournisseurs d'accès, autant dire que la solution de gettext est impossible puisqu’elle nécessite le redémarrage du serveur apache si j'ai bien tout compris.
Je doute que les fichiers de cache soit accessible sur ces serveurs mutualisés.
Y aurait-il une solution à ce problème ?
JJD
Pour rebondir sur la question de JJD, car je me suis posé la même à la lecture de l'article :
WordPress utilise les fichiers poo et la plupart des sites de ce CMS sont hébergés en mutualisé. Moi-même, je n'ai jamais eu besoin de redémarer apache après la modification d'un fichier via Poédit pour qu'il soit pris en compte sur du mutualisé.
Donc, il y a quelque chose qui m'échappe...
Edit : c'est bon j'ai trouvé. Quand l'extentiion n'est pas disponible sur le serveur on peut passer par une librairie. C'est ce que doit faire WordPress.
Lien vers la lib : http://www.gnu.org/software/gettext...