Pages

Date 5 avril 2014

Jobeet ZF2 - Jour 11 - Internationalisation et régionalisation

Après une assez longue absence, je me remet petit a petit à mes tutoriaux ZF2.
Je vous propose aujourd'hui d'aborder l'internationalisation (i18n) et la régionalisation (i10n).


Extrait de Wikipédia:
L'internationalisation est le processus de conception d'un logiciel afin qu'il puisse être adapté aux différentes langues et régions sans modifications techniques.
La régionalisation est le processus d'adaptation des logiciels à une région spécifique ou à une langue en y ajoutant des éléments spécifiques locaux et la traduction du texte.

Nous allons, dans un premier temps, nous intéresser au module d'Administration. Vous devriez ensuite être capable de traduire le module Front vous-même plus tard.
Commencez par récupérer le code du jour 10 où j'ai traduit toutes les chaines de caractères en anglais.

Le composant Translator

C'est grâce à ce composant que nous allons pouvoir gérer la traduction de l'administration et du site.
Translator peut utiliser 4 formats pour les fichiers de traduction:
  • Tableau PHP
  • Gettext
  • Tmx
  • Xliff
Pour ce tutoriel, nous utiliserons le format "tableau PHP".

Commençons par déclarer notre Translator. Pour cela, allons modifier le fichier module.config.php du module Admin:
[...]
    'service_manager' => array(
        'factories' => array(
            'admin_navigation' => 'ZfcAdmin\Navigation\Service\AdminNavigationFactory',
            'translator' => 'Zend\Mvc\Service\TranslatorServiceFactory',
        ),
    ),
    'translator' => array(
        array(
            'locale' => 'fr_FR',
            'translation_file_patterns' => array(
                'type'     => 'phparray',
                'base_dir' => __DIR__ . '/../language',
                'pattern'  => '%s.php',
            ),
        ),
    ),

Nous venons d'indiquer que nous souhaitons utiliser le Translator dans notre application (via une fabrique fournit par ZF2, que nous déclarons dans le service manager).
Ensuite, nous configurons notre Translator et nous indiquons:

  • locale: nous utiliserons la locale fr_FR par défaut,
  • type: les fichiers de traductions seront au format PHP (un tableau)
  • base_dir: les fichiers seront dans module/Admin/language
  • pattern: les fichiers s'appeleront NomDeLaLocale.php (ex: fr_FR.php)
Vous aurez remarqué que nous configurons le Translator via un tableau de tableau. Je vous expliquerais pourquoi un peu plus bas.
Créons maintenant le fichier fr_FR.php dans un répertoire "language" dans notre module Admin et inséré le code suivant:
<?php
    return array();
Ce fichier contiendra nos traductions sous la forme:
"Chaine 1 à traduire" => "Chaine 1 traduite en fr_FR",
"Chaine 2 à traduire" => "Chaine 2 traduite en fr_FR",

Pour traduire nos chaines de texte, ZF2 fournit une aide de vue translate(). Traduisons la page d'accueil de l'administration. Ouvrez la vue module/Admin/view/zfc-admin/admin/index.phtml:
<div class="container">
    <h2><?php echo $this->translate("Homepage");?></h2>
    <div class="row">
        <div class="span3">
            <a href="<?php echo $this->url('zfcadmin/category');?>"><i class="fam-cog"></i> <?php echo $this->translate("Categories");?></a>
        </div>
        <div class="span9">
            <?php echo $this->translate("Categories management");?>
        </div>
    </div>
    <hr />
    <div class="row">
        <div class="span3">
            <a href="<?php echo $this->url('zfcadmin/job');?>"><i class="fam-page"></i> <?php echo $this->translate("Jobs");?></a>
        </div>
        <div class="span9">
            <?php echo $this->translate("Jobs management");?>
        </div>
    </div>
    <hr />
    <div class="row">
        <div class="span3">
            <a href="#"><i class="fam-user"></i> <?php echo $this->translate("Users");?></a>
        </div>
        <div class="span9">
            <?php echo $this->translate("Users management");?>
        </div>
    </div>
</div>

Rajoutez ensuite les traductions suivantes dans le fichier language/fr_FR.php:
<?php
return array(
    "Homepage" => "Accueil",
    "Categories" => "Catégories",
    "Jobs"       => "Emplois",
    "Users"      => "Utilisateurs",
    "Categories management" => "Gestion des catégories",
    "Jobs management" => "Gestion des annonces",
    "Users management" => "Gestion des utilisateurs",
);
Dans votre navigateur, vous devriez avoir ça:

Il manque encore la traduction de la chaine "You are here" dans le fil d'ariane. Editez le fichier module/Admin/view/layout/adminBreadcrumb.phtml et ajoutez l'aide de vue translate():
<?php
    $container = $this->navigation()->breadcrumbs();
?>
    <ul class="breadcrumb">
        <li><?php echo $this->translate("You are here");?> :</li>
        <?php foreach($this->pages as $page): ?>
            <?php if( ! $page->isActive()): ?>
                <li>
                    <a href="<?php echo $page->getHref() ?>"><?php echo $page->getLabel() ?></a>
                    <span class="divider"> &gt; </span>
                </li>
            <?php else: ?>
                <li class="active">
                    <?php if($container->getLinkLast()): ?>
                        <a href="<?php echo $page->getHref() ?>">
                    <?php endif ?>
                    <?php echo $page->getLabel() ?>
                    <?php if($container->getLinkLast()): ?>
                        </a>
                    <?php endif ?>
                </li>
            <?php endif ?>
        <?php endforeach ?>
    </ul>
Ajoutez aussi la traduction dans le fichier fr_FR.php:
[...]
"You are here" => "Vous êtes ici",
[...]

Faites de même pour toutes les vues utilisées par l'administration:
  • utilisez l'aide de vue
  • ajoutez les traductions
Pour les formulaires, cela va être très simple. Si nous avons configuré un Translator, ZF2 va automatiquement l'utiliser dans les aides de vue des formulaires. Ajoutez juste les traductions des libellés des champs, et votre formulaire sera automatiquement traduit. Testez cela avec le formulaire de création de categorie. Pour cela, ajoutez les traductions suivante de votre fichier fr_FR.php:
[...]
"Name" => "Nom de la catégorie",
"Add" => "Ajouter",
Allez sur la page du formulaire d'ajout (http://votedomaine/admin/category/page/1/category/add). Le formulaire est traduit (pour traduire le reste, tel que le titre, allez traduire la vue associée):

Pour traduire les messages d'erreur, ne vous embêtez pas à traduire vous-même tous les messages: l'équipe de Zend l'a fait pour nous! Attention, ce n'est valable que pour les messages d'erreur des validateurs standards. Si vous créez vos propres validateurs, il faudra évidemment gérer vous-même les traductions!

Pour cette partie, j'avais cherché il y a quelques mois (sans résultat) et j'ai trouvé la solution en travaillant sur un projet ZF2 dans le cadre de mon boulot.
Le répertoire contenant le framework contient un répertoire "resources", contenant lui-même plusieurs répertoires de langues. Ces répertoires contiennent un fichier nommé "Zend_Validate.php" où l'on trouve toutes les traductions des messages d'erreurs de validateurs.

Nous allons indiquer à notre Translator que nous voulons utiliser ce fichier de traduction, en plus de notre fichier de traduction fr_FR.php. C'est pour cela que précédemment, nous avions un tableau de tableau: il est possible de charger plusieurs fichiers, dans des répertoires différents.
Modifions le fichier module.config.php et ajoutons la traduction des message d'erreur.
[...]
'translator' => array(
    'locale' => 'fr_FR',
    'translation_file_patterns' => array(
        array(
            'type'     => 'phparray',
            'base_dir' => __DIR__ . '/../language',
            'pattern'  => '%s.php',
        ),
        array(
            'type' => 'phpArray',
            'base_dir' => './vendor/zendframework/zendframework/resources/languages/',
            'pattern'  => 'fr/Zend_Validate.php',
        ),
    ),
),
Pour les messages d'erreur, je n'ai pas trouvé comment utilisé la locale correctement. Du coup, je précise directement que je souhaite utiliser la traduction fr...
Une autre solution serait de copié le fichier /vendor/zendframework/zendframework/resources/languages/fr/Zend_Validate.php dans notre répertoire language...

Voilà, vous savez utiliser le Translator. Traduisez le reste de l'administration ou récupérer le code source avec l'administration complètement traduit ici.

Traduction des urls (les routes)

C'est un des points qui m'a longtemps posé problème. J'avais vu lors d'une mise à jour de ZF2 (la version 2.2.1 il me semble) que l'on pouvait traduire les routes, mais sans trouver d'information dans la documentation officielle...(Si vous avez un lien, je suis preneur). A l'aide de mes amis Google et StackOverflow, j'ai fini par trouver des exemples de code que je n'ai pas réussi a faire fonctionner. J'ai finalement fini par trouver une solution en mixant différents éléments trouvés au hasard de mes recherches.

Sachez d'abord que seules les routes de type Segment peuvent être traduites (pour les autres types de routes, ce sera à vous de passer les différents paramètres traduits).
Voyons comment cela fonctionne pour les routes de type Segment.

D'abord, il faut indiquer à nos routes, via la configuration de celles-ci, qu'elles sont traduisibles. Nous allons traduire les urls du module Admin.

Editez le fichier module.config.pĥp du module Admin et ajoutez la ligne
'router' => array(
    'router_class' => 'Zend\Mvc\Router\Http\TranslatorAwareTreeRouteStack', // La ligne magique !!
    'routes' => array(
        'zfcadmin' => array(
            [...]
        ),
    ),
),

Il faut ensuite indiquer  à ZF2 quel objet translator utilisé et l'injecter dans l'objet Router.
Modifier le fichier Module.php du module Admin:
public function onBootstrap(MvcEvent $e)
{
    $eventManager = $e->getApplication()->getEventManager();
    [...]
    $eventManager->attach('route', array($this, 'onPreRoute'), 100);   // Ca se passe ici
    $moduleRouteListener->attach($eventManager);
}

// Et là
public function onPreRoute($e)
{
    $app      = $e->getTarget();
    $serviceManager       = $app->getServiceManager();
    $serviceManager->get('router')->setTranslator($serviceManager->get('translator'));
}

Enfin, il faut choisir les segments traduisibles. Pour cela, rien de plus simple: il suffit d'insérer le segment entre { } et ajouter la traduction dans le fichier de traduction (fr_FR.php). Je vais vous montrer pour une route
Modifions de nouveau le fichier module.config.php du module Admin:
'router' => array(
    'router_class' => 'Zend\Mvc\Router\Http\TranslatorAwareTreeRouteStack',
    'routes' => array(
        'zfcadmin' => array(
            'child_routes' => array(
                'category' => array(
                    'type' => 'segment',
                    'options' => array(
                        'route' => '/{category}[/page/:page]', // ICI
                        'constraints' => array(
                            'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                            'id' => '[0-9]+',
                            'page' => '[0-9]+'
                        ),
                        'defaults' => array(
                            'controller' => 'Admin\Controller\Category',
                            'action' => 'index',
                            'page' => 1
                        )
                    ),
                ),
            ),
        ),
    ),
),

Si vous avez bien suivi, vous devriez voir les liens traduits. Allez sur la page d'accueil de l'administration et cliquez sur le lien Catégories. Vous devriez arriver ici (regardez bien l'url):
Nous avons bien categorie et non category, comme avant.

Nous en avons fini pour les traductions. Finissez de traduire les urls de l'administration. Le code est disponible en fin d'article

Régionalisation

Nous avons vu comment traduire les différents texte du site (label, messages d'erreurs, url, etc). Mais il faut aussi gérer les différents formats (date, chiffres, etc).

ZF2 fournit un ensemble d'aide de vue pour formater, en fonction de la locale, nos dates et nos chiffres (selon les pays, la virgule est remplacer par un point, le separateur des milliers est un espace/un point, etc). Les exemples ci-dessous sont tirés de la documentation officielle de ZF2

L'aide de vue DateFormat

Cette aide de vue peut être utilisée pour simplifier le rendu des valeurs date/heure localisées.
// Date et Heure
echo $this->dateFormat(
    new DateTime(),
    IntlDateFormatter::MEDIUM, // Date
    IntlDateFormatter::MEDIUM, // Heure
    "en_US"
);
// Affiche: "Jul 2, 2012 6:44:03 PM"

// Date uniquement
echo $this->dateFormat(
    new DateTime(),
    IntlDateFormatter::LONG, // Date
    IntlDateFormatter::NONE, // Heure
    "en_US"
);
// Affiche: "July 2, 2012"

// Heure seulement
echo $this->dateFormat(
    new DateTime(),
    IntlDateFormatter::NONE,  // Date
    IntlDateFormatter::SHORT, // Heure
    "en_US"
);
// Affiche: "6:44 PM"

L'aide de vue CurrencyFormat

Cette aide de vue peut être utilisée pour simplifier le rendu des valeurs monnaitaires localisées.
echo $this->currencyFormat(1234.56, 'USD', null, 'en_US');
// Affiche: "$1,234.56"

echo $this->currencyFormat(1234.56, 'EUR', null, 'de_DE');
// Affiche: "1.234,56 �"

echo $this->currencyFormat(1234.56, null, true);
// Affiche: "$1,234.56"

echo $this->currencyFormat(1234.56, null, false);
// Affiche: "$1,235"

L'aide de vue NumberFormat

Cette aide de vue peut être utilisée pour simplifier le rendu des nombres et pourcentage spécifiques aux paramètres régionaux
echo $this->numberFormat(
    1234567.891234567890000,
    NumberFormatter::DECIMAL,
    NumberFormatter::TYPE_DEFAULT,
    "de_DE"
);
// Affiche: "1.234.567,891"

echo $this->numberFormat(
    0.80,
    NumberFormatter::PERCENT,
    NumberFormatter::TYPE_DEFAULT,
    "en_US"
);
// Affiche: "80%"

echo $this->numberFormat(
    0.00123456789,
    NumberFormatter::SCIENTIFIC,
    NumberFormatter::TYPE_DEFAULT,
    "fr_FR"
);
// Affiche: "1,23456789E-3"

L'aide de vue TranslatePlural

Cette aide de vue est utile pour traduire une chaine au singulier ou au pluriel en fonction d'un nombre. (Ex pour traduire 1 job / X jobs)
Nous avons ce cas dans Jobeet (dans la pagination des offres du module Front).
// Dans la vue pagination.phtml
<?php echo $this->translatePlural('job in this category - page', 'jobs in this category - page', $this->totalItemCount);?>

// Dans le fichier de traduction
    '' => array(
        'plural_forms' => 'nplurals=2; plural=n!=1;' // La "règle" pour définir ce qu'est un singulier / un pluriel.
                                                        // Attention, elle change en fonction de la langue. Ici c'est pour le français
    ),
    'job in this category - page' => array(
        'annonce dans cette catégorie - page',
        'annonces dans cette catégorie - page',
    ),
Pour la règle singulier/pluriel, un site intéressant indique comment sont gérés les pluriels, selon les langues: http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html.
Je vous conseille de le mettre en favori, c'est intéressant à conserver.

Je ne m'attarderais pas sur ce point, mais ZF2 fournit aussi des validateurs liés à l'internationalisation (Float et Int).

Une dernière remarque: Le tuto ne fonctionne actuellement pas avec la version 2.3.0. (pour le moment, je reste en 2.2.6)
Je n'ai pas encore eu le temps de chercher pourquoi, mais si vous savez ce qu'il faut corriger, ça m'intéresse.


Voilà, nous en avons fini pour cette journée. Je vous donne rendez-vous pour la prochaine journée prochainement. Comme d'habitude, les sources sont disponibles sur mon compte Github.

Si vous souhaitez utiliser le format Gettext, je vous invite à lire cet article: http://aromatix.fr/?p=611
13 commentaires:
  1. La traduction sur Zf2 est largement plus facile avec l'utilisation de Poedit (gratuit), avec la possibilité de séparé les traductions des différents modules

    RépondreSupprimer
    Réponses
    1. Bonsoir
      Merci pour ton commentaire.
      L'utilisation de POedit ou des tableaux php est quasiment similaire. Apres, ca reste une question de goût. Pour moi, la version PHP est plus lisible (uniquement besoin d'un éditeur de texte) et plus rapide (je n'ai pas réalisé de benchmark, mais c'est reste un simple tableau PHP).
      Pour le tuto, je prefere rester simple. Le lien en fin d'article permet d'approfondir le sujet, avec l'utilisation de Gettext :-)

      Supprimer
  2. J'utilise aussi Poedit et ça fonctionne très bien, mais comment faire pour modifier la langue en fonction du choix de l'utilisateur ou d'une adresse IP ou autre si l'utilisateur ce connecte pour la première fois ?
    Mon site doit être multi-langues mais aussi cibler des liens différents en fonction de la localisation géographique (ex : itunes "fr ou us").
    J'ai encore quelques problèmes qui m'oblige à dupliquer du code car je découvre la POO en même temps.

    Pour le moment, lors de la 1ere connexion la langue par défaut est "en" et le pays "US" et si l'utilisateur se connecte je récupère ses variables en base pour les appliquer. mais j'ai inclut dans mon URL de cette manière : www.monsite.com/fr_US/controler... . "fr" pointant sur fr_FR pour la traduction et "US" pour les lien US.

    Je continu mes recherche, merci pour tes tutos.

    RépondreSupprimer
    Réponses
    1. Bonjour,

      Plusieurs solutions pour modifier la langue en fonction du choix de l'utilisateur:
      - selectbox + session
      - langue du navigateur
      - avec l'IP, tu peux geolocaliser l'utilisateur et lui forcer la langue.

      Autre solution, gerer plusieurs noms de domaines et forcer la locale en fonction du nom de domaine

      Supprimer
  3. Réponses
    1. No, sorry Maybe in few month :-)
      I want to finish all my articles in french first :-)

      Supprimer
  4. Salut romain, bravo pour ton travail j'ai pu trouver pas mal d'infos dans tous tes tutos c'est vraiment agréable pour la communauté zf2 d'avoir des gens comme toi en leur sein :)
    Aurait tu cependant une idée sur comment traduire les flashMessages ? J'ai beau chercher, vu que l'on met les messages autre part que dans la vue je ne vois pas comment les récupérer sur POedit. A moins qu'il faille faire autrement ?

    Si ton expérience peut répondre à cette question ce serait cool car je sèche.

    Encore bravo pour ton boulot !

    RépondreSupprimer
    Réponses
    1. Bonjour Jonathan,
      D'abord, Merci pour le compliment, ça fait plaisir :-)

      Pour les flashMessages, je ne vois pas de problème particulier, à partir du moment où tu peux utiliser le Translator là où tu gères les messages. Regarde le lien en fin d'article (http://aromatix.fr/?p=611) qui traite de Gettext et POEdit . L'article sur Aromatix aide à bien configurer POEdit pour récuperer les chaines à traduire (dans des .phtml, des .php). J'espere que cela t'aidera

      Cordialement,
      Romain

      Supprimer
    2. Bonjour :) De rien, pour le compliment.
      En effet la question était bidon, j'ai trouvé comment faire peu de temps après. Je me penche à présent sur les tests unitaire et c'est une autre paire de manche par contre. Mock & stub et compagnie...

      Supprimer
    3. Oui, Ce n'est pas forcement évident à aborder...
      Bon courage :-)

      Supprimer
  5. Thanks for sharing this information. SEO is one of the digital marketing techniques which is used to increase website traffic and organic search results. If anyone wants to get SEO Training in Chennai visit FITA Academy located at Chennai. Rated as No.1 SEO Training institute in Chennai.

    RépondreSupprimer