Page 1 sur 2

Techniques pour sites multilingues

Posté : 24 mars 2007, 02:24
par elvex
Bonjour,

Quelles techniques connaissez vous pour réaliser un site multilingue ?

Aucune des solutions que j'envisage ne me semble satisfaisante : elles conduisent toutes à un code trop lourd (toutes les langues dans la page) ou trop abstrait (tous les textes réunis dans un document source)

Quelles méthodes utilisez-vous ? variables, XML, bases de données ?
Dans quelles circonstances utiliser l'une ou l'autre ? (par exemple, selon que le texte seul est localisé, ou que le HTML/CSS, ou encore les images sont aussi susceptible de varier)
Quelles astuces utilisez-vous pour améliorer la lisibilité (dans l'indentation, le nommage des variables, l'architecture des fichiers, etc.)
Quels mécanismes d'automatisations sont possibles ?

Je précise que je recherche une solution "faite maison", et pas un CMS complet type SPIP.

Actuellement, j'utilise deux modèles :

1- Sélection du texte "en dur" dans la page

index.php
echo LANG=='fr' ? "Bonjour le monde" : "Hello world" ;
2- Enregistrement des textes localisés dans des fichiers séparés

fr.inc.php
$LB_helloworld = "Bonjour le monde" ;
en.inc.php
$LB_helloworld = "Hello world" ;
index.php
include LANG . '.inc.php' ;
echo $LB_helloworld ;
Je compte sur votre imagination :)

Merci !

Posté : 24 mars 2007, 12:55
par AB
Bonjour,

Ici un exemple de script qui utilise les cookies pour enregistrer le choix visiteur http://www.phpfrance.com/forums/voir_re ... php#183553 et qui affiche des pages différentes.

Si tu peux utiliser une base de donnée, ne t'en prive pas. Ce sera d'autant plus pratique pour un contenu CMS ou si tu as de nombreuses pages sur ton site. Le même exemple que ci-dessus adapté à une bdd :
//définition de la durée du cookie (1 an)
$durée_cook = "time()+ 365*3600*24";
//si le visiteur envoie la variable $_GET['lang'] pour choisir sa langue, on l'enregistre dans un cookie nommé 'lang'
isset($_GET['lang'])? setcookie("lang", $_GET['lang'], $durée_cook) : '' ; 

//si le cookie existe on le récupère sinon on donne la langue par défaut 
$lang = isset($_COOKIE['lang'])? $_COOKIE['lang'] : 'fr' ; 
//si $_GET['lang'] est envoyé on la récupère sinon on donne la valeur du cookie s'il existe sinon la valeur par défaut 
$lang = isset($_GET['lang'])? $_GET['lang'] : $lang ; 

//suivant la valeur de $lang on fait la correspondance avec le champ de la table dans la bdd. Si les champs de la table se nomment 'commentaire_fr',
//'commentaire_en' etc. cela donne :

switch ($lang) 
    { 
    case 'fr': $champ = 'commentaire_fr'; 
        break; 
    case 'en': $champ = 'commentaire_en'; 
        break; 
    case 'es': $champ = 'commentaire_es'; 
        break; 
    default : $champ = 'commentaire_fr'; 
    }
 
//puis la requête
$query = "SELECT $champ FROM table etc.
A la place de choisir des champs différents dans une même table tu peux choisir d'indiquer des tables différentes...

En fait tous les choix son possibles et fiables. Faut adapter suivant ses besoins :wink:

Posté : 24 mars 2007, 12:57
par naholyr
Pourquoi ne pas simplement utiliser gettext(), qui constitue le standard en la matière ?

1. Création de l'arborescence, par exemple

Code : Tout sélectionner

./locale |-- de | `-- LC_MESSAGES |-- en | `-- LC_MESSAGES `-- fr `-- LC_MESSAGES
2. Création des fichier .po (source) dans chaque dossier LC_MESSAGES. Ils ont pour nom le nom du «domaine de l'application» (façon d'identifier les tables de traduction), par exemple «MonAppli.po». Ces fichiers sont des paires de lignes de cette forme :

Code : Tout sélectionner

msgid "Identifiant de la chaîne à traduire" msgstr "Valeur de la chaîne traduite"
En identifiant on utilise systématiquement la chaîne non traduite tout simplement, exemple :

Code : Tout sélectionner

msgid "Hello %s !" msgstr "Bonjour %s !" msgid "Bye bye :)" msgstr "Au revoir :)"
3. Compilation des fichiers .po en .mo. C'est ce qui donne au système sa performance ;)
Pour chaque fichier il suffit d'utiliser msgfmt :

Code : Tout sélectionner

$ msgfmt -o ./locale/fr/LC_MESSAGES/MonAppli.mo ./locale/fr/LC_MESSAGES/MonAppli.po
J'utilise personnellement cette commande qui va me compiler tous les fichiers .po d'un dossier (récursivement) avec le même nom en .mo :

Code : Tout sélectionner

$ find ./locale -name *.po | while read f; do msgfmt -o $(echo $f | sed 's/po$/mo/') $f && echo "OK $f" || echo "FAIL $f"; done
4. Utilisation. On va simplement reprendre l'exemple de la doc :)
<?php

// Choix de la langue
setlocale(LC_ALL, 'fr');

// Spécifie la localisation des tables de traduction (le dossier contenant l'arborescence) pour le domaine indiqué
bindtextdomain("MonAppli", "./locale");

// Choix du domaine
textdomain("MonAppli");

// La traduction est cherché dans ./locale/fr/LC_MESSAGES/MonAppli.mo

// Affichage d'un message de test
printf(gettext("Hello %s !") . "\n", getenv('USER'));

// Or use the alias _() for gettext()
echo _("Bye bye :)") . "\n";

?>


Avantages
  • La localisation d'une appli non localisée au départ est très simple (cf. mon Post-Scriptum).
  • On utilise la méthode standard GNU, ce qui signifie qu'un éventuel portage ne posera jamais le moindre problème.
  • On utilise la richesse de gettext, pour en avoir une idée jeter un oeil par exemple à la fonction ngettext(), comment gérer aussi simplement les pluriels avec une autre méthode ? ;)
  • On a une performance accrue (utilisation de la RAM, et rapidité de la traduction amoindries).
  • L'utilisation est vraiment simplissime avec la fonction _()
  • La définition de la langue de travail repose sur setlocale(), ce qui localise toute l'application et certaines fonctions comme date() en profiteront du même coup.
  • On peut gérer plusieurs encodages (des pages en UTF-8, d'autres en ISO-quelquechose, etc...) avec la génération de fichiers .po au bon encodage, et de la fonction bind_textdomain_codeset()
Inconvénients
  • Nécessite de compiler les fichier *.po avant de les envoyer sur le serveur.
  • Nécessite la présence du module gettext dans la version de PHP (pas vraiment une difficulté puisque c'est activé par défaut)
Localisation d'une appli non prévue au départ

- Ajouter au début du script les appels à setlocale() (pour changer la langue), bindtextdomain() (pour préciser l'emplacement de l'arborescence pour le domaine) et textdomain() (pour préciser le domaine).
- Encadrer les chaînes à localiser avec _()
- Utiliser le programme xgettext qui va générer un modèle de fichier .po (il va détecter les chaînes localisées). Par exemple la commande appliquée à notre script

Code : Tout sélectionner

xgettext MonAppli.php --from-code=UTF-8
Va donner le fichier messages.po

Code : Tout sélectionner

# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-03-24 11:51+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <[email protected]>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: appli.php:15 #, php-format msgid "Hello %s !" msgstr "" #: appli.php:18 msgid "Bye bye :)" msgstr ""
- Dupliquer ce fichier dans l'arborescence de ./locale
- Traduire
- Compiler (avec la commande que j'ai donnée qui va tout compiler récursivement).
- L'application est intégralement traduire :)


[edit=1]Ajout des liens[/edit]

Posté : 24 mars 2007, 13:45
par AB
Pourquoi ne pas simplement utiliser gettext(), qui constitue le standard en la matière ?
N'y aurait-il pas le mot "simplement" en trop dans ta phrase? :lol:

Voilà elvex servi avec des exemples de code du plus simple au plus pro.

Posté : 24 mars 2007, 15:07
par naholyr
Pourquoi ne pas simplement utiliser gettext(), qui constitue le standard en la matière ?
N'y aurait-il pas le mot "simplement" en trop dans ta phrase? :lol:
Non parce qu'à part la phase de compilation, c'est vraiment aussi simple à mettre en place que toutes les autres méthodes. Et à l'usage il est plus simple à maintenir.

De plus c'est la méthode standard pour toute application localisée dans le monde GNU. Toutes les applications passent par gettext pour gérer le multilangue, alors tant qu'à faire autant appliquer les concepts qui marchent :)

Puis pour un traducteur, il est plus simple d'éditer un fichier .po qu'un fichier .php. Une erreur (de syntaxe par exemple) dans le fichier .po n'amènera pas au moindre plantage de l'application.

De plus, étant la méthode standard, c'est un format connu et reconnu et tu peux facilement faire appel à la communauté des traducteurs dans le libre, qui sont habitués à travailler avec ce format.

En prime, connaîs-tu une autre méthode qui permette aussi facilement de gérer les pluriels irréguliers et les encodages (parmi d'autres fonctionnalités) sans avoir à réinventer la roue ?

Posté : 25 mars 2007, 11:07
par Ripat
+1 pour gettext pour toutes les raisons évoquées ci-dessus.

Son utilisation est plus simple qu'il n'y paraît au premier abord. Les sources des localisations peuvent se compiler en local, sur le serveur de développement, pour ensuite télécharger les binaires sur le serveur de production. Même si c'est un mutualisé.

Il m'arrive toutefois à devoir parfois redémarrer le serveur apache pour des raisons que je ne me suis pas encore expliquées.

Ne pas croire les tests de performances trouvés sur le net, plus particulièrement celui-ci. Mes propres tests m'ont montré le contraire. Il est vrai que j'ai testé sur de gros fichiers po et une page qui faisait +- 70 appels à gettext().

Je les ai refaits pour illustrer ce post intéressant. L'alternative à gettext() est un include d'un tableau indicé du type:
$str = array ("Hello" => "Bonjour", .... , "Bye" => "Au revoir");
Résultats:

Code : Tout sélectionner

Méthode: ab -n 1000 "url_test.php" 70 appels sur un tableau/po de 6000 éléments gettext 1.5 s. array 38.0 s. 70 appels sur un tableau/po de 1400 éléments gettext 1.7 s. array 9.2 s.
Autre avantage de gettext: cette méthode ne semble pas influencée par la taille de la table de localisation. Certainement parce-qu'elle est indexée lors de la compilation alors que l'accès au tableau PHP est toujours séquentiel. Je n'ai pas trouvé comment tester l'utilisation mémoire avec ab. Si je trouve, j'éditerai ce post.

Posté : 25 mars 2007, 22:18
par Invité
Bonjour,

Loin de moi de dire qu'utiliser la méthode gettext() n'est pas la solution qui apporte le plus de fonctionnalités pour toutes les raisons que naholyr et Ripat ont cités.

Mais je ne suis pas persuadé que cela s'impose absolument dans tous les cas. Par exemple je fais des petits sites administrables (CMS) qui utilisent une bdd. Dès lors rien de plus simple que d'utiliser par exemple un champ pour chaque langue pour les parties administrables. Le reste est géré directement en php, et dans mon cas cela ne fait que quelques éléments.
Pour ce genre de site, je vois mal la nécessité ou l'utilité d'utiliser gettext(). Surtout qu'ils sont hébergés sur des serveurs mutualisés et si comme dit Ripat il faut parfois redémarrer le serveur apache...

Alors dites-moi si je me trompe, la méthode gettext() n'est-elle pas surdimensionnée pour des petits sites?

Posté : 26 mars 2007, 01:44
par naholyr
Comme toutes les méthodes «pro» : l'utilisation d'un framework, le respect de standards, la mise en place d'un dépôt SVN, le commentaire du code, etc...

La question est de savoirsi on travaille vraiment sur un petit projet, car ne pas utiliser ces méthodes le condamne souvent d'office à rester petit ;)

Posté : 26 mars 2007, 09:22
par Expreg
Comme toutes les méthodes «pro» : l'utilisation d'un framework, le respect de standards, la mise en place d'un dépôt SVN, le commentaire du code, etc...

La question est de savoirsi on travaille vraiment sur un petit projet, car ne pas utiliser ces méthodes le condamne souvent d'office à rester petit ;)
Je suis d'accord et pas d'accord en même temps. :wink:
Exemple, j'ai un client (spécifique ciblé uniquement labo pharma) qui à un site de 6 pages simples de présentation avec guère plus de 5 ou 6 lignes de texte par page, le tout en 5 langues.

Une $var en cookie et de simples fichiers txt ont suffit pour gérer les langues.
L'evolution du site est inexistante car les explications sont tellement ciblées que seuls les gens concernés par cette technologie ont un pouvoir de compréhension.

Donc voilà !
Maintenant il est clair qu'il faut savoir que "l'outil" gettext() existe et est pratique.

Posté : 26 mars 2007, 09:58
par Megadeth
Salut,

Perso, pour un site multilingues, je "switche" une variable en GET et selon sa valeur je fais tel ou tel include/require. Ca fait pas mal de fichiers, ca nécessite un peu de rigueur mais ca marche très bien au final :wink:

Mega
:)

Posté : 26 mars 2007, 11:45
par naholyr
Exemple, j'ai un client (spécifique ciblé uniquement labo pharma) qui à un site de 6 pages simples de présentation avec guère plus de 5 ou 6 lignes de texte par page, le tout en 5 langues.

Une $var en cookie et de simples fichiers txt ont suffit pour gérer les langues.
L'evolution du site est inexistante car les explications sont tellement ciblées que seuls les gens concernés par cette technologie ont un pouvoir de compréhension.
L'exemple est bon et évidemment que dans ce cas utiliser gettext() donne l'impression d'utiliser un marteau pour écraser une mouche. Mais ici on n'est pas dans le cadre de la traduction d'une application, mais dans le cadre de l'écriture d'un document. Ce n'est pas pareil ;)

gettext (et toutes les méthodes d'internationalisation, même basée sur des include) est plus dans son domaine dans les petits textes. C'est idéal pour la traduction de l'interface utilisateur : messages d'erreur, courts textes, textes des boutons, etc...

Pour le contenu en lui-même il ne doit pas être stocké dans un fichier PO. Déjà je me vois mal inclure du HTML dans un fichier PO, ça ressemblerait à n'importe quoi !
Par contre rien n'empêche de gérer la source de données dans le fichier PO (je fais ça sur un site, je trouve ça très beau comme méthode) :
include gettext('/contenu/aide/index.html');
Une fois le contenu traduit en allemand je le mets dans /contenu/aide/index.de.html (par exemple) et je traduis simplement "/contenu/aide/index.html" en "/contenu/aide/index.de.html" dans de.po :)

Pourquoi ne pas alors utiliser include "/contenu/aide/index.$lang.html" ?
- Parce que le fichier n'existe pas forcément pour la langue, du coup mon include est systématiquement encadre d'un if().
- Parce qu'utiliser une variable nécessite de vérifier son contenu avant (si jamais j'ai une faille de sécurité et que $lang est corrompue, je cours un risque) et ça alourdit encore le code.
- Parce que je peux très bien vouloir par exemple aller chercher certaines traductions de page vers un autre document (par exemple une page générique d'avertissement "da allemande version of your dokument pas là" :lol: )

Pourquoi ne pas mettre le texte dans le fichier PO ?
- Parce qu'il faudra certainement inclure du HTML dans la traduction, ou pire hacher tout le texte et traduire chaque morceau.
- Parce qu'un fichier de traduction est par définition statique, alors que le contenu est dynamique.

J'ai tranché la question avec ce couplage des deux méthodes, que je trouve élégant.

Je dirais qu'il faut bien situer les différents problèmes dans la localisation d'une application, et je le découperais comme ça :
  • La définition de la langue : ça c'est très classique, et je n'en ai même pas parlé. On vérifie si un cookie ou un $_GET['lang'] est défini. Si oui on utilise ça, sinon on extrait le code langue de l'entête envoyé par le navigateur pour trouver la langue par défaut, et on met en place le cookie. Classique, éprouvé, on ne reviendra pas dessus.
  • La traduction de l'interface (partie statique) : là on a 2 grandes méthodes. La méthode base de données me paraît complètement absurde, faire une requête pour aller chercher l'intitulé du bouton "Envoyer" me semble démesuré, mais bon il faut y refléchir pourquoi pas. La méthode par fichier se découpe en deux "sous-méthodes" : gettext et include. On ne reviendra pas sur les avantages/inconvénients des deux méthodes on en a à peu près fait le tour je pense.
  • La traduction du contenu (partie dynamique) : là pareil on a la méthode base de données ou fichiers. Mais la méthode par fichier PO me paraît inadaptée, vu le caractère fortement dynamique du contenu d'un site. Utiliser des fichiers PO pour le contenu rendrait impossible la mise en place d'un CMS. On pourra cependant utiliser de la traduction de contenu statique pour définir la source de données (nom de la table ou chemin du fichier contenu le texte localisé).
Je sens poindre la rédaction d'une FAQ ou d'un tuto au vu de ce topic ;)

Posté : 26 mars 2007, 11:58
par Hubert Roksor
Tiens, c'est pas bête ce "include gettext()" :)
Pour le reste... tl;dr :oops:

Posté : 26 mars 2007, 12:05
par Expreg
Je sens poindre la rédaction d'une FAQ ou d'un tuto au vu de ce topic ;)
Yapuka ! :wink:

Posté : 26 mars 2007, 14:41
par AB
Je dirais qu'il faut bien situer les différents problèmes dans la localisation d'une application, et je le découperais comme ça :
  • La définition de la langue : ça c'est très classique, et je n'en ai même pas parlé. On vérifie si un cookie ou un $_GET['lang'] est défini. Si oui on utilise ça, sinon on extrait le code langue de l'entête envoyé par le navigateur pour trouver la langue par défaut, et on met en place le cookie. Classique, éprouvé, on ne reviendra pas dessus.
  • La traduction de l'interface (partie statique) : là on a 2 grandes méthodes. La méthode base de données me paraît complètement absurde, faire une requête pour aller chercher l'intitulé du bouton "Envoyer" me semble démesuré, mais bon il faut y refléchir pourquoi pas. La méthode par fichier se découpe en deux "sous-méthodes" : gettext et include. On ne reviendra pas sur les avantages/inconvénients des deux méthodes on en a à peu près fait le tour je pense.
  • La traduction du contenu (partie dynamique) : là pareil on a la méthode base de données ou fichiers. Mais la méthode par fichier PO me paraît inadaptée, vu le caractère fortement dynamique du contenu d'un site. Utiliser des fichiers PO pour le contenu rendrait impossible la mise en place d'un CMS. On pourra cependant utiliser de la traduction de contenu statique pour définir la source de données (nom de la table ou chemin du fichier contenu le texte localisé).
Je sens poindre la rédaction d'une FAQ ou d'un tuto au vu de ce topic ;)
Merci de ces précisions.

Effectivement quand je donnais plus haut l'exemple avec une BDD, je pensais à du contenu CMS (j'ai parlé également en tant qu'invité car je m'étais fait déconnecter).

Pour la traduction de l'interface, il ne m'est pas venu à l'idée d'utiliser une bdd. Etant donné que mes sites font peu de pages et qu'il y a de nombreux éléments communs inclus faciles à gérer (pseudo frames etc) , il ne reste plus que les éléments différents de chaque page à traiter et cela ne fait pas un gros travail (bien qu'un peu fastidieux).
J'entrevois maintenant tout l'intérêt d'un gettext pour les gros sites :D

Posté : 26 mars 2007, 15:04
par naholyr
N'oubliez pas que gettext n'est qu'une technique strictement équivalent au fichier d'include par langue, avec certes moults avantages en plus ;) mais qui se place au même niveau.

Tiens j'ai trouvé une doc très sympa : http://www.mandragor.org/tutoriels/gettext/0