Traduire son application AngularJS avec angular-gettext

Traduire son application permet de toucher un public beaucoup plus large, mais AngularJS n’intègre nativement aucun outil pour le faire.

Traduire son application, une étape souvent obligatoire.

Traduire son application, une étape souvent obligatoire.

Pour les applications desktop dans un environnement GNU, on utilise traditionnellement gettext. En java, il existe un plugin maven qui reprends exactement le même principe en utilisant une implémentation java de gettext : gettext-commons.

Aujourd’hui, je vous propose un petit tutoriel pour faire la même chose avec AngularJS grâce au module angular-gettext. Il s’agit d’une implémentation 100% JavaScript, spécialement conçue pour AngularJS qui s’intègre parfaitement à votre build Grunt.

Retour aux sources : gettext !

gettext est un utilitaire GNU capable d’extraire les chaines de caractères à traduire de vos sources. Il génère ainsi un fichier .pot, un format standard qui sert de modèle et contient l’ensemble des chaînes de caractères à traduire. Pour la traduction française par exemple, on pourra créer un fichier fr.po et définir, pour chaîne extraite, la chaîne traduite. Les fichiers .po sont les fichiers de traduction, uniques pour chaque langue, et le fichier .pot est un fichier modèle commun à toutes les langues.

gettext, un outil GNU à l'ancienne.

gettext, un outil GNU à l’ancienne.

L’avantage d’utiliser gettext par rapport à d’autres outils est qu’il existe tout un écosystème d’outils pour lire le modèle .pot et créer/modifier les fichiers de traduction .po. On a notamment poedit, un excellent éditeur opensource et multi-plateforme. Plus intéressant, il existe des plateformes collaboratives de traduction, comme Transifex qui est payant, mais aussi Pootle disponible gratuitement en opensource. En général, ces plateformes peuvent charger les fichiers gettext (.pot, .po) qui sont considérés comme le standard pour la traduction, donc il vaut mieux partir directement sur ce format si on souhaite à terme externaliser sa traduction.

Si vous ne voyez toujours pas l’intérêt à utiliser gettext, vous pouvez vous arrêter dés maintenant et utiliser plutôt angular-translate qui est plus simple à mettre en place. Sinon, vous pouvez suivre ce tutoriel.

Installer angular-gettext

Si vous développez avec AngularJS, vous utilisez déjà certainement yeomangruntbower et npm. Si ce n’est pas le cas, je vous conseille sérieusement de vous y mettre, cela vous facilitera largement la tâche pour ajouter n’importe quel module ou dépendance javascript à AngularJS …

bower install angular-gettext --save
  • Ajouter automatiquement la référence au script angular-gettext.js (avec grunt)
grunt wiredep

Le fichier index.html doit alors référencer le script angular-gettext.js. Si ce n’est pas le cas, ou que vous n’utilisez pas bower et grunt, vous pouvez toujours ajouter à la main

  • Ajouter à la main la référence au script angular-gettext.js dans index.html (sans grunt, facultatif, voir ci-dessus)
<script src="bower_components/angular-gettext/dist/angular-gettext.min.js"></script>
  • Charger le module gettext dans app.js
angular
  .module('myApp', [
    '...',
    '...',
    '...',
    'gettext'
  ]);

Annoter avec angular-gettext

Pour rendre votre template internationalisable, vous devez maintenant l’annoter en utilisant les directives angular-gettext. Cela permettra par la suite de générer le fichier modèle .pot et de remplacer à l’exécution les chaînes originales par les chaînes traduites dans les fichiers .po. Il y a différentes annotations pour …

  • Externaliser une chaîne, via directive pour les tags, mais aussi via filtre pour les attributs.
<h1 translate>Hello!</h1>
<input type="text" placeholder="{{'Username'|translate}}" />
  • Pluraliser une chaîne.
<div translate translate-n="count" translate-plural="{{count}} boats">One boat</div>
  • Commenter une chaîne. Ce commentaire sera intégré au fichier .pot, et affiché au moment de la traduction par l’éditeur.
<h1 translate-comment="Verb" translate>File</h1>
  • On ne change rien à ses habitudes, le code suivant fonctionne comme prévu, et une chaîne paramétrée sera externalisée
<div translate>Hello {{name}}!</div>

Extraire les chaines à traduire

Une fois le template annoté, on peut extraire les chaines à traduire pour créer le fichier .pot. Cette extraction se fait avec Grunt. C’est très bien documenté sur le site de angular-gettext, mais je vais tout de même détailler car certaines subtilités peuvent vous échapper.

  • Installer grunt-angular-gettext, la tâche angular-gettext pour grunt. (avec npm)
npm install grunt-angular-gettext --save-dev

Si vous utilisez yeoman avec angular-generator, vous devez avoir au début du fichier Gruntfile.js une ligne qui permet de charger automatiquement les tâches grunt : require('load-grunt-tasks')(grunt);. Vous pouvez dans ce cas passer l’étape suivante.

  • Ajouter manuellement la tâche dans Gruntfile.js. (facultatif, voir ci-dessus)
grunt.loadNpmTasks('grunt-angular-gettext');
  • Ajouter la configuration de nggettext_extract dans la fonction grunt.initConfig()
nggettext_extract: {
  pot: {
    files: {
      'po/template.pot': ['app/*.html', 'app/views/*.html', 'app/directives/*.html']
      }
    }
  },
  • Ajouter nggettext_extract à la tâche principale build
grunt.registerTask('build', [
 'clean:dist',
 'wiredep',
 'nggettext_extract', // <= Ici, c'est pas mal.
 'useminPrepare',
 'concurrent:dist',
 'autoprefixer',
 'concat',
 'ngmin',
 'copy:dist',
 'cdnify',
 'cssmin',
 'uglify',
 'filerev',
 'usemin',
 'htmlmin'
 ]);
  • Exécuter le build pour s’assurer que tout fonctionne
grunt build

Vous devez normalement trouver votre fichier ‘po/template.pot’, et vous êtes prêt à traduire votre application !

Ça ne marche pas ?

Si vous utilisez le générateur yeoman angular-generator, vous avez certainement la validation JSHint activée avec l’option camelCase. J’ai ouvert un ticket sur grunt-angular-gettext, car les tâches de grunt-angular-gettext ne sont pas nommées en camelCase. Celà pose un problème à l’exécution de grunt build, car le fichier Gruntfile.js est lui même validé par JSHint et le build échoue.

Vous pouvez appliquer le workaround suivant :

  • Désactiver l’option de validation camelCase pour le fichier Gruntfile.js
/*jshint camelcase: false */
  • Exécuter le build pour s’assurer que tout fonctionne.
grunt build

Créer les fichiers de traduction

Pour créer les fichiers de traduction, je vous conseille d’utiliser poedit dans un premier temps. Sous Ubuntu, c’est aussi simple que ça :

sudo apt-get install poedit

Il ne faut surtout pas traduire directement dans le fichier .pot, mais bien créer un fichier par langue .po en prenant le fichier  .pot comme modèle. Le fichier .pot est la version originale de votre application, et ne doit jamais être modifié manuellement, puisqu’il est généré et remplacé à chaque build.

Dans le menu de poedit, vous trouvez une entrée pour créer le fichier .po a partir du template .pot. Lorsque vous avez un .po d’une ancienne version de votre application, poedit peut aussi appliquer les modifications de la nouvelle version du template .pot au fichier .po, en ajoutant les nouvelles clés et en supprimant les clés qui n’existent plus.

D’une manière générale, évitez d’éditer ces fichiers à la main, et passez par les outils spécialisés.

Compiler les fichiers de traduction

angular-gettext n’est pas capable de lire directement les fichiers de traduction .po. Il est nécessaire de passer par une étape de compilation qui va gérer un fichier .js à partir des fichiers de traduction .po. Vous l’avez peut-être deviné, mais on va faire ça avec grunt.

  • Modifier Gruntfile.js pour ajouter la tâche nggettext_compile dans la fonction grunt.initConfig()
  nggettext_compile: {
   all: {
     options: {
       module: 'myApp'
     },
   files: {
     'src/js/translations.js': ['po/*.po']
     }
   },
 },
  • Enregistrer nggettext_compile dans la cible principale build, exactement comme pour nggettext_extract.
grunt.registerTask('build', [
 'clean:dist',
 'wiredep',
 'nggettext_extract',
 'nggettext_compile',// <= Ici, juste après nggettext_extract, c'est pas mal.
 'useminPrepare',
 'concurrent:dist',
 'autoprefixer',
 'concat',
 'ngmin',
 'copy:dist',
 'cdnify',
 'cssmin',
 'uglify',
 'filerev',
 'usemin',
 'htmlmin'
 ]);
  • Référencer le fichier compilé dans index.html
<script src="scripts/translations.js"></script>

Activer la bonne langue

Maintenant il faut charger la bonne langue dans AngularJS.

  • Définir la langue via un simple appel de méthode sur le service gettextCatalog.
angular.module('myApp').run(function (gettextCatalog) {
 gettextCatalog.setCurrentLanguage('Français'); // Corresponds au header 'Language' du fichier .po;
 gettextCatalog.debug = true;
});

La ligne gettextCatalog.debug = true permet d’afficher le texte [MISSING]: devant chaque libellé non traduit.

Votre application est polyglote !

Votre application est maintenant prête à parler toutes les langues ! Et surtout, vous utilisez gettext et les fichiers .po/.pot qui sont la base d’une approche collaborative pour la traduction.

Si vous souhaitez aller plus loin, je vous invite à essayer Pootle, la plateforme opensource de traduction collaborative. C’est un outil génial, et il peut s’interfacer avec les logiciels de gestion de versions comme Git ou SVN. Ça fera l’objet d’un autre tutoriel dans quelques temps.

En attendant, je retourne au boulot sur mon projet 30 45 jours. Vous l’aurez compris, ce projet sera internationalisé !