ZF2 – internationalisation – traduction multilingue – Zend\I18n\Translator

Mise en place d’un site multilingue dans ZF2 avec Zend\I18n\Translator

Dans l’objectif de toucher une plus grande audience en s’assurant que l’information est cohérente pour  le plus d’utilisateurs possible, il est logique de développer un site multilingue.

par un site multilingue, on sous-entend la prise en charge de la localisation et de l’internationalisation:

  • i18n : internationalisation : implémentations des fonctionalités d’internationalisation dans l’application
    • $this->translate(‘hello’)
    • $date = DateTime()
  • l10n : localisation : adaptation de l’application à une région/culture etc…
    • résultat1 : ‘hello’?, ‘bonjour’? , autre?
    • résultat2 : 14/02/2013 ? ou 02/14/2013 ? autre ?  // DateTime()->setTimezone(new DateTimezone(“europe/rome”)); // au besoin

Une bonne prise en charge de la traduction du contenu dans une langue utilisateur sera fera en prenant en compte les spécificités du langage comme l’utilisation des pluriels et/ou la traduction selon le contexte ainsi que la prise en compte adéquate des formats d’affichage de données comme les dates ou les nombres qui diffèrent selon la région .Ainsi un article multilingue pourra prendre en compte des format différents de dates (US:02/14/13 et UK:14/02/13 ,etc… ), des unités mesures différentes (métriques, US), des format de nombres en utilisant séparateur de décimales ou de milliers différents (‘,’ ou ‘.’ ) etc…

Pour l’internationalisation, ZF2 intègre l’utilisation de Zend\I18n\Traslator dans plusieurs composants du framework. (validateurs de chaines alphanumériques, aides de vues, filtres de nombres/chaines alphanumériques/dates. etc…)

Dans ce tuto, nous traiterons de la traduction du contenu (par la suite, selon mes priorité, j’ajouterai l’adaptation des nombres et des dates selon la locale, ainsi que l’utilisation de la traduction dans les formulaires et dans les validateurs).

Zend\I18n\Translator permet de gérer les pluriels (dons les formes varient selon les languages). il permet par ailleur d’utiliser des domaines de traduction (ou contexte). ce qui nous permettra, par exemple, de traduire le mot anglais ‘home’ en français par ‘domicile’ dans un domaine ‘user-profile’, et en ‘Accueil’ dans un domaine ‘web’ (on utilisera pour ça un fichier par domaine). les données à traduire se trouverons dans des fichiers de traduction contenant le mappage de chaque chaine à traduire, en tant qu’identifiants, avec une chaine de traduction. Plusieurs format sont utilisables: (Gettex, TMX, XLIFF, phparray, etc… ) et, bien sûr, chacun à ses avantages et ses inconvénients.

N.B. : Assurez-vous que l’extention php intl est activée pour l’utilisation des composants i18n

Terminologie :

  • Locale : elle est représentée par une chaine composée de 2 caractères pour le langage (obligatoire), suivi, éventuellement, d’un code région (2 charactère ). on peut y ajouter facultativement l’encodage de caractère utilisé. (quelques exemples : fr-FR.UTF-8 , en_US , fr )
  • l’encodage de caractères ou ‘Character encoding’ : un nom représentant l’ensemble de caractères utilisés (ou ‘character set’ : Unicode, Latin, etc…). exemples UTF-8, ASCII, etc….
  • Internationalisation (i18n)
  • Localisation (L10n)

Composition d’une locale : ex fr-MA.utf8 (anciennement fr_MA.utf-8 )

  • fr : langue de la locale
  • MA : région de la locale
  • utf-8 : encodage de la locale

la locale du système est utilisée par défaut. on la change via : \Locale::setDefault()

utilisation d’un translator:

le chargement de la traduction peut se faire selon plusieurs méthodes: 1 fichier par domaine et locale, 1fichier par domaine, sources distantes par domaine, etc…

pour utiliser Zend\i18n\Translator hors d’une application ZF2, il suffit d’instancier une classe Translator et de lui ajouter des fichiers de traduction. plusieurs formats existent (dans ce tuto on se contente des format phparray et gettext).

voici un fichier de traduction (format ‘phparray’)

utilisation :

on peut aussi utiliser une fabrique pour créer un translator:

on peut aussi indiquer au translator un format de nommage des fichiers de traduction qu’il pourra utiliser (si présents). en fonction de la langue de traduction un fichier respectant le paterne spécifié (avec le nom de la locale dans le nom) sera utilisé dynamiquement (on pourra ajouter des fichiers de traduction sans modifier le code)

on peut dans ce cas disposer de dossiers ayant comme nom la locale ( fr, de, en, etc ), dans lequels on place un fichier de traduction nommé “message.php”. (on pourrai utiliser un pattern : ‘language/%s.mo’ si l’on veut des fichiers gettext dans un même répertoire et qui se nomment fr.php, en_US.php, etc… )

utilisation du translator dans ZF2:

Avant d’attaquer la suite, sachez qu’il est possible de paramétrer la traduction pour chaque module. il est recommandé d’utiliser un contexte par module (par ex :le nom du module). si vous partagez votre module vous connaissez au moins un avantage de faire comme ceci. l’ajout de contextes supplémentaires et les optimisations sont facile à faire par la suite, si besoin.

pour commencer, une classe fabrique est déclarée dans le service Manager. elle créer le translator en récupérant la clé de configuration ‘translator’.

ci-dessous le paramétrage par défaut dans le skeleton (dans module/Application/config/module.config.php ) :

pour info :

  • la locale par défaut est : en_US // c’est ici que l’on met la langue par défaut du site
  • la méthode de traduction utilisée dans le skeleton : gettex
  • méthode de chargement : basé sur le nom du fichier, respectant le pattern spécifié (‘pattern’ => ‘%s.mo’) et se trouvant dans le dossier ‘language’ du module courant (c.a.d. module/Application/language ). le fichier s’appellera par exemple : fr_FR.mo, ou en_US.mo

avant de montrer comment créer des catalogues de traduction avec POEdit, nous testons la traduction du site dans plusieur langues.

commençons par modifier la langue par défaut du site pour utiliser le français par défaut (ou autre):

En rafraichissant la page d’accueil du skeleton après la modification de la locale,  on se rend compte que le skeleton est déjà traduit en français (en plus d’une douzaine de langues).

le domaine de traduction ajouté ici sert à éviter les interférences avec les traductions des autres modules. Si jamais ils traduisent une même chaine différement. Dans ce cas le translator prendra le premier qu’il trouve. l’ordre de chargment des modules a un impacte sur l’ordre de recherche d’une traduction et le traslator utilise le même ordre que celui du chargement des modules.

rappelez-vous que le Translator renvoie la première traduction qu’il trouve et que si on veut traduire une chaine dans un domaine de traduction, on le spécifie au moment de la traduction.

P.I. un domaine de traduction peut servir de contexte de traduction, et on peut créer des fichiers de traduction qui ne sont pas attachés à un/des module(s).

la traduction d’une chaine de caractère se fait via le translator comme ceci :

$translator->translate($message, $textDomain, $locale);

et dans une vue en utilisant l’aide de vue $this->translate(‘chaine à traduire’ /*, $domaine, $locale */)

Pour l’exemple qui suit on traduit une chaine présente dans les catalogue de traduction du skeleton (dans le dossier module/Application/language). vous remarquerez que le skeleton est traduit dans plusieurs langues (14 aujourd’hui).

Traduction d’une chaine dans la locale par defaut à l’intérieur d’une vue :

Traduction d’une chaine en spécifiant un domaine :

Traduction d’une chaine dans une autre locale à partir d’une vue :

dans le cas où on veut utiliser un même domaine dans le reste du code :

création et compilation d’un catalogue de traduction gettext:

gettex est le system de traduction utilisé par défaut dans les systèmes unix. il utilise des fichiers binaire (extention .mo) pour traduire des chaines (c’est ainsi que fonctionnent plusieurs commandes unix en multilingue). dans le cadre d’une application php, une extension php du même nom est utilisée et Gettex est un système de traduction performant, très répandu et fiable.

nous utiliserons ici gettext, et pour traduire nos sites, nous avons besoin de générer des catalogues de traductions (avec poedit que l’on utilise plus bas). les catalogues de traductions sont générés en scannant les sources à la recherche des fonctions translate(‘qqch‘), (ou _(‘qqch‘) ) pour en extraire les chaines à traduire . des fichiers générés auront l’extention .po et contiendrons les chaines de caractère à traduire

la structure d’un fichier po ressemble à ceci :

(le # marque le début d’un commentaire)

voici quelques exemples d’entrées (les commentaires sont facultatifs mais pratiques pour connaitre d’où vient la chaine) :

pour plus d’information sur le format gettext: GNU gettext

pour être plus performant, gettext utilise des fichiers binaires (.mo) qui sont générés à partir des fichiers texte (.po) (un fichier *.mo est une compilation d’un fichier *.po).

Compilation des fichiers textes (po) en fichiers binaires (mo)

(plus loins vous verrez comment scanner  vos source avec poedit pour en extraire les chaines à traduire et compiler avec celui-ci)

il est possible d’utiliser plusieurs outils pour compiler des fichiers ‘*.po’ en ‘*.mo’. on peut par exemple le faire en ligne de commande comme ceci si on est sur unix (mac/linux/solaris/BSD/etc…)

On peut aussi compiler en utilisant un script php (ce qui peut être très utile pour générer des traductions à partir des entrées utilisateur, par exemple, dans un CMS/wiki, ou tout simplement si on n’est pas sur un environnement unix). pour ce faire, nous avons la possibilité d’utiliser php.mo que l’on peut cloner via git : https://github.com/josscrowcroft/php.mo

voici un ensemble d’outils très utiles dans le cas où il y a besoin de manipuler/convertir des traductions vers ou à partir d’autres formats en plus de PO ( Java .properties, OpenOffice, Mozilla, XLIFF, TMX, TBX, CSV, Qt .ts), :
Pootle, Virtaal & The Translate Toolkit : http://sourceforge.net/projects/translate/

génération d’un catalogue de traduction gettext à partir de l’application:

pour la suite du tuto nous utilisons Poedit (disponible pour windows, linux, macOS, xBSD) : http://sourceforge.net/projects/poedit/

Dans le jargon POEdit on appelle les fichiers .po et sa compilation .mo un catalogue.

l’entête du fichier PO contiendra des méta-données générées à partir de la configuration renseignée au premier démarrage de poedit (ou modifié par la suite via le menu poedit/préférences sur OSX ou Edition/Préférences sur zindozs).

pour pouvoir générer un catalogue de traduction à partir d’un projet existant, nous devons spécifier à poedit quelles sont les fichiers à scanner pour y trouver des chaines de caractère à traduire. (on prendra l’habitude de scanner un module à la foi)

Paramétrer un analyser pour scanner les fichiers php et phtml

Dans le cas d’un projet ZF2, les templates ‘*.phtml’ sont à inclure en plus des fichiers ‘*.php’, et nous spécifions le language de programmation (‘PHP’).

poedit-pref-analysers

poedit-pref-analyser-php

Paramétrer un analyser pour scanner les fichiers twig

Dans ZF2, il est possible d’utiliser d’autres moteurs de templates (smarty, twig, etc)

Dans le cas d’utilisation du moteur de template twig (utilisé dans symphony) , après l”installation d’un module de prise en charge (http://modules.zendframework.com/?query=twig), des fichiers ‘file.twig’ seront utilisés pour les vues, (à la place des fichiers ‘file.phtml’)

documentation twig : http://twig.sensiolabs.org/documentation

les fichiers twig ayant une syntaxe différente des fichiers php, il faut ajouter un nouvel extracteur dans poedit :
Twig-Gettext-Extractor : https://github.com/umpirsky/Twig-Gettext-Extractor

on adapte le paramètre commande de l’analyser (ou parser command):

au besoin de l’utiliser en ligne de commande, on s’y prend comme ceci:

poedit-pref-analyser-twig

Création d’un catalogue :

Après avoir paramétré le(s) analyser(s), il nous faut scanner les sources de notre application

Là où on aura utilisé translate(‘…’), poedit en extrait la chaine, pour la mettre dans le catalogue

voici comment faire :
1 – Fichier/Nouveau Catalogue
poedit-catalogue-1-properties
la forme plurielle utilise la même syntaxe que le C: Pluriel Gettext : http://translate.sourceforge.net/wiki/l10n/pluralforms#f  ( rappelez-vous que certaines langues peuvent utilisez une forme plurielle différente, et dans ce cas il vous faut entrer la forme de la langue qui sera traduite )

2 – dans les chemins des source, nous indiquons un chemin absolu, puis le dossier local ‘.’, en cliquant à chaque foi sur le bouton Nouvel Element:

poedit-catalogue-2-paths

3 – on supprime les fonctions gettext et gettext_noop car on ne s’en sert pas. on ajoute translate. poedit va scanner les sources à la recherche de ces fonctions dans les fichier *.php et *.phtml spécifiés précédement

(on garde ‘_’ , car on va s’en servir pour comme hack afin de détecter les label de nos formulaires. par ailleurs l’aide de vue _() est utilisée dans ZF1 comme raccourci de l’aide de vue translate ainsi que dans d’autres frameworks PHP )
poedit-catalogue-3-functions

4 – on se place dans le dossier languages du module (shift+cmd+g sur OSX pour aller dans un dossier system), et nomme le fichier selon le pattern de nommage paramétré dans le module ( %s.mo : fr_FR.po )

poedit-catalogue-4
5 – choix de la langue (su poedit ne la determine pas)
poedit-catalogue-5
6 – validation
poedit-catalogue-6

vous remarquez que j’ai scanné le skeleton :
7 – on peut maintenant traduire ce qui n’a pas été traduit
poedit-catalogue-7

il ne reste à vérifier que le contenu du site est correctement traduit (les erreur humaines sont possibles)

Détection des label dans les formulaires

Poedit ne detecte pas toutes les chaines dans le code. voyez l’exemple ci-dessous:

pour ce faire, comme ZF2 n’utilise pas la fonction _(‘chaine à traduire‘), on va s”en servire pour un petit hack

Dans notre code il nous suffira de l’utiliser là où on a besoin de détecter une chaine à traduire

on aurai pu utiliser une autre fonction, mais il aurai fallu la déclarer dans poedit, et surtout s’assurer qu’elle ne sera pas utilisée dans le code (en plus des modules dans vendor )

paramétrage de l’internationalisation des validateurs :

les messages d’erreurs des validateurs ont été traduit par la communauté ZF2 et se trouvent dans le dossier ressources du package du framework (récupérables à partir de : https://github.com/zendframework/zf2.git )

utilisation du translator dans un controller :

ou encore :

Ajout de la sélection automatique de la langue en fonction du navigateur de l’utilisateur

adaptez selon les besoins

(suite à venir)

.

Did you like this? Share it:

6 Responses to ZF2 – internationalisation – traduction multilingue – Zend\I18n\Translator

  1. Merci pour ton super article.
    J’ai une question concernant la traduction des multioptions des elements de formulaire de type multicheckbox/radio/select.
    Comment gérer cela?

  2. il me semble qu’il sont traduits automatiquement, mais j’ai pas encore eut l’occasion de travailler avec les multicheck box

    par contre pour la detection des tes chaines, tu peut déclarer cette fonction dans ton index.php

    function _($str) {
    return $str;
    }

    tu remarquera qu’elle ne fait que renvoyer ce qu’on lui donne. par contre elle te servira pour poedit car _ fait partie des mots clés déclarés ( point no 3 ci dessus )

    donc partout où tu aura _(‘quelquechose à traduire’), poedit l’ajoutera au catalogue de traduction

    (je reposte un autre commentaire pour te confirmer pour les multicheckbox)

  3. J’ai une autre question , comment peut on gérer une traduction multi sites?
    J’ai des fichiers de traductions .ini avec des sections:

    common_fr.ini:

    [monsite1]
    cle1 = valeur1
    ...
    [monsite2]
    cle1 = valeur2
    ...
    
    'translator' => array(
    		'translation_file_patterns' => array(
    			array(
    				'type' => 'Mix\I18n\Translator\Loader\PhpIni',
    				'base_dir' => __DIR__ . '/../language',
    				'pattern' => 'common_%s.ini',
    				'text_domain' => 'candidat'
    			)
    		),
    ),
    

    Actuellement je fais un truc pourri qui utilise le registre pour stocker le nom de domaine courant.

     array(
    	'Mix\I18n\Translator\Loader\PhpIni' => 'Mix\I18n\Translator\Loader\PhpIniFactory'
    ),
    

    Mais ca ne fonctionne pas car ca l’enregistre dans le ServiceManager et non dans le PluginManager d’apres ce que j’ai compris.

    Quel est la solution a ce problème ?

  4. un loader utilisant les fichiers ini a été ajouté il y a quelques mois dans ZF2 : https://github.com/zendframework/zf2/blob/master/library/Zend/I18n/Translator/Loader/Ini.php

    tu remarquera que le loader récupère le contenu du bloc [‘translation’] si il y en a, sinon le contenu tel quel et donc tu ne peut pas utiliser directement tes fichiers comme ils sont formattés . donc au besoin tu peut t’en inspirer pour créer ton loader (la syntaxe des fichiers ini est un peut différente de celle que tu utilise
    par ex en modifiant ces lignes pour récupérer des bloc du tu passe le nom en paramètre: https://github.com/zendframework/zf2/blob/master/library/Zend/I18n/Translator/Loader/Ini.php#L44

    par ailleurs, je croix que qu’un fichier ini avec un bloc par site n’est pas forcément la meilleur idée (surtout si tu a plusieurs sites).

    dans le tuto ci-dessus, on scanne chaque module pour générer sa traduction (on peut bien sûr scanner toute l’appli cation mais la portabilité des traductions de nos modules risque d’en être dégradée).

    pour centraliser les traductions de plusieurs sites, tu a besoin de coder des outils adaptés à tes besoins.
    Par exemple : centraliser tes traduction dans une base à partir de laquelle tu génère des fichiers pour chaque site ou si tu a juste deux ou trois sites extraire la traduction, après chaque mise à jour, tu extrai les données de chaque site via un script.
    par contre, dans cette démarche tu te retrouve point de départ, car à chaque modification dans le code on re-scanne avec poedit, on traduit les nouvelles chaines détectées, et on sauvegarde le catalogue.

    D’autre outils peuvent te rendre la tâche facile : http://sourceforge.net/projects/translate/ (site: http://translatehouse.org/products.html)

  5. David says:

    Zf1 gérait les sections des fichiers ini alors que Zf2 gere uniquement les fichiers ini mais pas les sections.
    Historiquement je trouvais plus facile de créer 1 seul fichier de traduction avec 1 section pour chacun de mes 8 sites.
    C’est la raison pour laquelle j’ai créé mon propre loader Mix\I18n\Translator\Loader\PhpIni mais impossible de parvenir à lui injecter mon objet “Site”.
    Donc a moins qu’il y ait une evolution de prévue pour pouvoir créer un custom LoaderFactory, je dois trouver une autre solution.
    Effectivement je pourrais utiliser la base de données mais zend a prevu un systeme pour pouvoir mettre en cache facilement les fichiers de traductions, or ce ne sera pas le cas si je les mets en base.
    J’utilise déjà Gedmo des DoctrineExtensions qui me permets de traduire les pays d’une table peut etre devrais je le généraliser aux autres fichiers de traductions.
    Bref, je trouve que zf2 ne permets pas facilement de gérer le multi sites.

Open Close