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_configuration_langue.png
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.

poedit_traduction.png
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:
<?php
$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);
?>
Cet exemple (php, pot, po, mo) est téléchargeable sur la page des ressources.