Internationalisation et localisation avec Sencha Ext JS

Le constat

La gestion du multilinguisme (où internationalisation) dans les applications est un problème fonctionnel récurrent, qui avec Ext JS est souvent traité lors de la phase de développement. Le cas de Ext JS 6 ne déroge pas à la règle et introduit par ailleurs de nouvelles problématiques.

Avant de proposer des solutions concrètes, revenons sur la problématique en elle-même.

Il s’agit aujourd’hui de permettre aux utilisateurs de choisir leur langue préférée lors de l’utilisation de leurs applications. Pour utiliser un terme plus technique, on parle souvent de « i18n » ou « internationalisation »: c’est à dire la capacité qu’à une application à être traduisible. Attention, cela ne se cantonne pas simplement à des compétences en traduction de libellés mais englobe des considérations et contraintes techniques, des besoins fonctionnel, des contraintes de temps, etc…

Bien que Sencha fournisse une solution « i18n » fonctionnelle, cette dernière reste perfectible, notamment parce qu’elle introduit certaines contraintes et s’avère mal adaptée aux prérogatives des différents acteurs du monde de l’entreprise. Par ailleurs, la documentation est sommaire et montre un système monolithique, laissant peu de place aux adaptations.

La voie officielle Ext

Vous pouvez retrouver plus en détails la solution officielle, avec ses avantages et ses inconvénients, dans un poste de Saki sur son propre blog : http://extjs.eu/localization-of-ext-applications/. Ce qu’il est important de retenir à ce stade, c’est que cette solution :

  • demande l’écriture de fichiers de langues (singleton ou override) directement dans l’application pour qu’ils puissent ensuite être incorporés au « build » de production,
  • va générer, grâce à l’outil Sencha Cmd, autant de « build » de production de la même application que de langues définies dans la configuration,
  • nécessite de recharger entièrement l’application à chaque changement de langue,
  • ne permet pas facilement de charger des ressources supplémentaires avant le démarrage de l’application,
  • n’autorise pas plusieurs langues au même moment, sur des parties différentes de l’application

Externalisation des sources de langues

La problématique de l’internationalisation a de multiples facettes et les entreprises ne traitent pas forcément chacune d’elles. Avant de choisir une solution, il est important de faire le point, dès la conception, sur les véritables besoins de l’entreprise vis-à-vis de cette problématique. Vous pourrez retrouver un topo complet sur la question dans le slideshow de Vincent Munier, Technical Manager chez Jnesis, présenté lors des Sencha Days 2015 de Paris et Zurich. Cependant, les entreprises d’une certaine taille arrivent généralement à la même conclusion : la gestion des langues est une problématique indépendante du framework de développement, qui met en jeu des profils et des workflow spécifiques de l’entreprise. Les traductions devraient se situer dans des fichiers externes à l’application ou récupérées au travers d’un ou plusieurs web-services. Les principaux intérêts souvent mis en avant, sont que :
  • les modifications des sources de langues ne nécessitent pas de reconstruire l’application, elle sont prises en compte directement après le rechargement,
  • les personnes qui sont en charges de la traduction de l’application ne sont pas forcément les mêmes que celles qui sont en charge d’écrire le code.
Dans ces conditions, les compétences des uns ne sont donc pas forcément connues des autres et inversement. Si bien qu’il parait compliqué de demander aux traducteurs de venir directement modifier des fichiers JavaScript, même basiques. Dans l’idéal, les traductions sont mêmes assurées par un outil tier intégré ou non à l’applicatif. Malheureusement, la solution officielle Ext JS ne permet pas nativement cette ouverture. Les solutions que nous proposons vont nécessiter une adaptation de la manière dont le code Ext JS sera écrit. Il s’agit donc de bonnes pratiques à introduire dès le démarrage du projet, sous peine de devoir procéder à une longue phase de « refactoring ».

La solution élégante

Commençons par suivre les recommandations du guide de développement de Sencha ainsi que ce conseil Saki dans son article de blog : changer les données de traduction avant que notre vue principale soit construite.

Il s’agit là d’un procédé assez proche de ce que l’on pouvait faire avant la version 5 de Ext JS, lorsque l’on ajoutait des dépendances à des ressources spécifiques dans le fichier « index.html » de l’application.

Pour commencer, nous allons stocker les traductions sous forme de variables contenues dans une classe « singleton », comme ceci :

Cela va permettre d’y accéder depuis n’importe quel endroit de l’application. Comment ? Aussi simplement qu’en tapant Jnesis.Labels.button ou Jnesis.Labels.title.

L’intérêt de cette approche, en comparaison à l’utilisation « override », est de pouvoir centraliser l’ensemble des traductions dans un même fichier et de pouvoir mutualiser l’utilisation d’une même variable à différents endroits dans le code, comme indiqué plus haut.

L’idée maintenant est de venir surcharger ce fichier de langue par défaut (« Jnesis.Labels ») avec les traductions de la langue choisie.

Pour cela, deux cas sont envisagés :

  • Récupérer des informations au travers de web-services

Ou les données renvoyées par le serveur ressembleraient à ceci :

Il faudra alors parser les données JSON récupérées et surcharger notre singleton de traduction avec ces-dernières :

Ext.define('Jnesis.Application', {
    launch: function () {
        Ext.Ajax.request({
            url: 'get-localization',
            params: {locale:'fr'},
            callback: function (options, success, response) {
                var data = Ext.decode(response.responseText, true);
                Ext.override(Jnesis.Labels, data);
                Ext.create('Jnesis.view.main.Main');
            }
        });
    }
}); 
  • Charger directement des fichiers de « override » Ext JS via la fonction Ext.Loader.loadScript() :
Ext.define('Jnesis.locale.fr.Labels', {
    override: 'Jnesis.Labels',
    button: 'Mon bouton',
    title: 'Mon titre'
}); 
Ext.define('Jnesis.Application', {
    launch: function () {
        var lang = 'fr',
            url = 'resources/locale/'+lang+'/Labels.js';
        Ext.Loader.loadScript({
            url: url,
            onLoad: function (options) {
                Ext.create('Jnesis.view.main.Main');
            }
        });
    }
});     

Une fois le chargement de nos traductions mis en place, il faut maintenant affecter ses traductions aux différents labels, titres etc… de notre application. Pour cela, nous allons profiter des mécanismes d ‘héritage du framework et définir une nouvelle propriété (« localized » par exemple) sur le composant de base pour y gérer les attributs à traduire.

Cette propriété n’est qu’un simple objet JavaScript contenant des clés et des valeurs (rappelez-vous des Jnesis.Labels.button et Jnesis.Labels.title).

Et pour que la valorisation des variables de traductions ne se fasse pas au moment de la déclaration, mais uniquement lorsque l’objet « localized » est parcouru, nous allons passer ces clés sous forme de chaînes de caractères :

Ext.define('Jnesis.view.main.Main', {
    extend: 'Ext.panel.Panel',
    initComponent: function () {
        this.title = Jnesis.Labels.title;
        this.buttons = [{
            text: Jnesis.Labels.button,
            handler: 'onClickButton'
        }];
        this.callParent(arguments);
    }
}); 

Il reste alors qu’à interpréter cette nouvelle propriété « localized » en réalisant une surcharge de la méthode « initComponent » de la classe de base « Ext.Component » et à évaluer les clés passées pour que cela puisse s’appliquer peu importe le composant Ext JS utilisé :

Ext.define('overrides.localized.Component', {
    overrride: 'Ext.Component',
    initComponent: function () {
        var me = this,
            localized = me.localized,
            value;
        if (Ext.isObject(Localized)) {
            for (var prop in localized) {
                value = localized[prop];
                if (value) {
                    me[prop] = eval(value);
                }
            }
        }
        me.callParent(arguments);
    }
}); 

Grâce à cela, vous obtenez une solution qui fonctionne à n’importe qu’elle niveau de hiérarchie dans la déclaration de composant, aussi bien sur la configuration d’un conteneur que sur celle de ses items, et cela de tacon transparente.

Pour aller plus loin

La solution proposée fonctionne avec les versions 5 et 6 (classique) du framework Ext JS. Elle pourrait encore être améliorée sur certains points, vous trouverez ci-dessous les adaptations complémentaires possibles déjà réalisées par le passé par Jnesis :

  • Déporter les fonctions de chargement et leurs traitements associés dans une classe spécifique de type « Ext.mixin.Mashup » qui permet de s’assurer que l’ensemble de ces traitements sont réalisés avant le chargement en mémoire de la classe sur laquelle la « mixin » est posée.
  • Assurée une compatibilité avec la version moderne de Ext JS 6, la fonction « initComponent » n’existant pas, il faudrait regarder du coté de la propriété « config » et de la génération de méthode « apply » associée.
  • Charger et interpréter les variables de traduction de façon dynamique, sans avoir à recharger l’intégralité de l’application.

Le code source est disponible ici.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *