samedi 8 décembre 2012

Traduction des libellés dans les vues AngularJS

Voici une solution simple pour traduire les libellés des vues AngularJS dans plusieurs langues. Elle utilise deux directives presque identiques, et un fichier de traductions décliné dans chacune des langues disponibles.

Il ne s'agit pas de l'internationalisation complète d'une application, juste de la traduction des libellés des vues. L'internationalisation est un sujet bien plus vaste, et suppose aussi d'adapter aux langues et habitudes locales le format des dates, des nombres, des montants financiers, des adresses...

AngularJS ne fournit pas d'outil standard pour traduire les libellés qui sont en dur dans les vues. On peut bien sûr générer des vues différentes côté serveur, suivant la langue de l'utilisateur. Mais quel que soit le langage utilisé sur le serveur, ça suppose d'utiliser un système de templating ou quelque chose d'équivalent, au milieu des directives et expressions d'AngularJS. Un des nombreux atouts d'AngularJS est d'avoir des vues en HTML plutôt propres et lisibles, et ça serait vraiment dommage de les polluer en ajoutant du templating serveur à la place de tous les libellés.

Heureusement, on peut très facilement créer une directive AngularJS pour remplacer le contenu texte de certains éléments de la vue par des libellés provenant d'un fichier de traductions.

Voici le code de la directive :

directives.directive('i18n', ['I18N', function (I18N) {
    return {
        priority: 0,
        restrict: 'A',
        scope: false,
        compile: function compile(tElement, tAttrs, transclude) {
            if (tAttrs.i18n) {
                tElement.text(I18N.translate(tAttrs.i18n));
            }
            return {
                pre: function preLink(scope, iElement, iAttrs, controller) {},
                post: function postLink(scope, iElement, iAttrs, controller) {}
            };
        }
    };
}]);

La traduction des libellés se fait dans l'étape de compilation de la directive, plutôt que dans l'étape de link. Ainsi à l'intérieur d'un élément répété de nombreuses fois via un ng-repeat, la traduction n'est faite qu'une seule fois avant que l'élément soit répété, et non par sur chaque élément répété comme se serait le cas si elle se faisait dans l'étape de link.

Pour utiliser la directive, il suffit alors d'ajouter un attribut i18n aux éléments à traduire de la vue, dont la valeur est le code du libellé dans le fichier de traductions :

<span i18n="init.footer"></span>

On peut aussi avoir un libellé en dur dans la vue, de toute façon il sera remplacé par le libellé traduit :

<span i18n="init.footer">libellé du footer</span>

Le fichier des traductions est un module contenant le service I18n. Il suffit de créer une version de ce module pour chacune des traductions disponibles, et de charger uniquement le bon module dans la page index.html, en fonction de la langue de l'utilisateur connecté.

angular.module('App.I18N', [], function($provide) {
    $provide.factory('I18N', I18NFactory);
});

function I18NFactory() {
    var labels = {
        'init.start': "Initialisation de l'application.",
        'init.footer': "Texte de bas de page en français.",
        'global.backToList': "Retour à la liste",
        'global.loading': "Chargement des données..."
    };
    return {
        translate: function (id, text) {
            var text = labels[id];
            return text ? text : '*Traduction introuvable*';
        }
    };
}

Cette première directive permet de traduire le contenu texte de n'importe quel élément HTML, ce qui convient pour le texte dans un <span>, un <div>, un paragraphe <p>, ou le libellé d'un bouton <button>.

Mais pour traduire les bulles d'aides correspondant à l'attribut HTML title, qu'on ajoute notamment sur les images, on peut créer une seconde directive :

directives.directive('i18nTitle', ['I18N', function (I18N) {
    return {
        priority: 0,
        restrict: 'A',
        scope: false,
        compile: function compile(tElement, tAttrs, transclude) {
            if (tAttrs.i18nTitle) {
                tAttrs.$set('title', I18N.translate(tAttrs.i18nTitle));
            }
            return {
                pre: function preLink(scope, iElement, iAttrs, controller) {},
                post: function postLink(scope, iElement, iAttrs, controller) {}
            };
        }
    };
}]);

Cette seconde directive s'utilise de la même façon que la première : on ajoute un attribut i18n-title avec comme valeur le code du libellé dans le même fichier de traductions, à utiliser comme title :

<img src="images/back.png" i18n-title="global.backToList"/>

Evidemment rien n'empêche de combiner les deux directives sur le même élément HTML, si l'on veut par exemple traduire à la fois le texte et la bulle d'aide d'un bouton.

9 commentaires:

  1. Vraiment très pratique ces petits morceaux de code
    Merci beaucoup! :)

    RépondreSupprimer
  2. J'ai cherché également une réponse sur le net et l'approche Angular me semble intéressante mais compliquée à mettre en oeuvre.
    Votre code est de qualité mais il va falloir traiter beaucoup de balises annexes pour couvrir tout HTML. Un ex, l'attribut placeholder ou value des input.
    Bref pour ma part, je vais partir sur une traduction au niveau du build, il faut alors convenir d'un token (ex @i18n:baseline) qui sera ensuite remplacé lors du build pour créer autant de fichiers html que de locales.
    Du coup chaque fichier html (y compris les templates) seront déclinées en version .fr.html et apache pourra alors servir le bon.

    RépondreSupprimer
    Réponses
    1. Ce commentaire a été supprimé par l'auteur.

      Supprimer
    2. Bonjour,
      Pouvez-vous votre exemple me semble correcte la traduction au niveau du build ou peut-être si vous connaissez comment l'on procède si on veut charger les langues depuis les fichiers Js sans utiliser le fichier Json.

      Supprimer
  3. Avec AngularJS, on ne met pas de value dans les champs input des templates HTML. S'il faut mettre des valeurs initiales, c'est dans le modèle de données qu'il faut les mettre, et dans ce cas la traduction se fera d'une façon différente. Il s'agit de traduction des données et non plus des libellés des templates.

    Par contre il faudrait en effet rajouter l'attribut placeholder, au même titre que l'attribut title.

    De toute façon la solution que je décris est une solution parmi d'autres, dont le principal avantage est d'être simple et de ne pas nécessiter d'autres traitements côté serveur. Mais c'est loin d'être la seule possible.

    Les templates peuvent effectivement être traduits côté serveur, soit lors du build, soit au moment de servir les fichier. Dans les deux cas, ça oblige à mélanger aux directives AngularJS un système de templating interprété côté serveur, c'est ce qui me gêne un peu, surtout si c'est fait au moment de servir les fichiers.

    Mais avec des attributs clairement identifiés comme cela et leur remplacement lors du build, c'est quand même la solution côté serveur qui perturbe le moins les templates AngularJS, du coup elle me plait bien.

    RépondreSupprimer
  4. Hey there! :)

    You might be interested in angular-translate http://pascalprecht.github.io/angular-translate

    RépondreSupprimer
  5. j'aime bien la simplicité de cette solution. Comme ce n'est pas facile ni très fiable de déterminer la culture locale depuis le navigateur, on peut le faire de façon plus fiable du côté serveur et adapter cette solution à un chargement dynamique. Pour la factory:

    function I18NFactory($http) {
    var promise = $http.post('/listlocalizedmessages');
    return {
    translate: function (id, element) {
    return promise;
    }
    };
    }

    et du côté directive :

    if (tAttrs.i18n) {
    I18N.translate().success(function(data, status) {
    var text = data[tAttrs.i18n];
    tElement.text(text ? text : '*Traduction introuvable*');
    });
    }

    L'avantage est qu'il n'y a qu'une factory pour toutes les langues

    RépondreSupprimer
  6. En utilisant https://github.com/lavinjj/angularjs-localizationservice.git vous pouvez alors choisir une locale dans n'importe quel attribut de la sorte :

    < input placeholder="{{'typehere'|i18n}}" >

    Mais si je suis arrivé sur ce post, c'est que je cherchais un moyen de gérer des traductions contenant des variables et des directives… Je crains que ça n'existe pas :(

    RépondreSupprimer
  7. Pour traduire les fichiers de langue c'est une bonne solution d'utiliser une plateforme de localisation comme https://poeditor.com

    RépondreSupprimer