Comment internationaliser votre application AngularJS
Publié: 2022-03-11L'internationalisation de votre application peut faire du développement logiciel une expérience douloureuse, surtout si vous ne commencez pas à le faire dès le début ou si vous adoptez une approche bon gré mal gré.
Les applications modernes, où le front-end et le back-end sont nettement séparés l'un de l'autre, peuvent être encore plus difficiles à gérer en matière d'internationalisation. Soudain, vous n'avez plus accès à la pléthore d'outils éprouvés qui aidaient autrefois à internationaliser vos applications Web traditionnelles générées par des pages côté serveur.
En conséquence, une application AngularJS nécessite une livraison à la demande des données d'internationalisation (i18n) et de localisation (l10n) à livrer au client pour s'afficher dans les paramètres régionaux appropriés. Contrairement aux applications traditionnelles rendues côté serveur, vous ne pouvez plus compter sur le serveur pour fournir des pages déjà localisées. Vous pouvez en savoir plus sur la création d'une application PHP multilingue ici
Dans cet article, vous apprendrez comment vous pouvez internationaliser votre application AngularJS et découvrirez les outils que vous pouvez utiliser pour faciliter le processus. Rendre votre application AngularJS multilingue peut poser des défis intéressants, mais certaines approches peuvent faciliter la résolution de la plupart de ces défis.
Une application AngularJS simple compatible i18n
Pour permettre au client de modifier la langue et les paramètres régionaux à la volée en fonction des préférences de l'utilisateur, vous devrez prendre un certain nombre de décisions de conception clés :
- Comment concevez-vous votre application pour qu'elle soit indépendante de la langue et des paramètres régionaux dès le départ ?
- Comment structurez-vous les données i18n et l10n ?
- Comment livrez-vous efficacement ces données aux clients ?
- Comment faire abstraction d'un maximum de détails d'implémentation de bas niveau pour simplifier le flux de travail des développeurs ?
Répondre à ces questions le plus tôt possible peut aider à éviter les obstacles dans le processus de développement sur toute la ligne. Chacun de ces défis sera abordé dans cet article; certains via des bibliothèques AngularJS robustes, d'autres via certaines stratégies et approches.
Bibliothèques d'internationalisation pour AngularJS
Il existe un certain nombre de bibliothèques JavaScript spécialement conçues pour l'internationalisation des applications AngularJS.
angular-translate
est un module AngularJS qui fournit des filtres et des directives, ainsi que la possibilité de charger des données i18n de manière asynchrone. Il prend en charge la pluralisation via MessageFormat
et est conçu pour être hautement extensible et configurable.
Si vous utilisez angular-translate
dans votre projet, vous trouverez peut-être certains des packages suivants très utiles :
-
angular-sanitize
: peut être utilisé pour se prémunir contre les attaques XSS dans les traductions. -
angular-translate-interpolation-messageformat
: pluralisation avec prise en charge du formatage de texte sensible au genre. -
angular-translate-loader-partial
: utilisé pour livrer les chaînes traduites aux clients.
Pour une expérience vraiment dynamique, vous pouvez ajouter angular-dynamic-locale
au groupe. Cette bibliothèque vous permet de modifier dynamiquement les paramètres régionaux, et cela inclut la façon dont les dates, les nombres, les devises, etc. sont tous formatés.
Mise en route : installation des packages pertinents
En supposant que vous ayez déjà votre passe-partout AngularJS prêt, vous pouvez utiliser NPM pour installer les packages d'internationalisation :
npm i -S angular-translate angular-translate-interpolation-messageformat angular-translate-loader-partial angular-sanitize messageformat
Une fois les packages installés, n'oubliez pas d'ajouter les modules comme dépendances de votre application :
// /src/app/core/core.module.js app.module('app.core', ['pascalprecht.translate', ...]);
Notez que le nom du module est différent du nom du package.
Traduire votre première chaîne
Supposons que votre application ait une barre d'outils avec du texte et un champ avec du texte d'espace réservé :
<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="#">Hello</a> </div> <div class="collapse navbar-collapse"> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" ng-model="vm.query" placeholder="Search"> </div> ... </div> </div> </nav>
La vue ci-dessus contient deux morceaux de texte que vous pouvez internationaliser : "Bonjour" et "Rechercher". En termes de HTML, l'un apparaît comme le texte intérieur d'une balise d'ancrage, tandis que l'autre apparaît comme la valeur d'un attribut.
Pour les internationaliser, vous devrez remplacer les deux littéraux de chaîne par des jetons qu'AngularJS pourra ensuite remplacer par les chaînes traduites réelles, en fonction des préférences de l'utilisateur, lors du rendu de la page.
AngularJS peut le faire en utilisant vos jetons pour effectuer une recherche dans les tables de traduction que vous fournissez. Le module angular-translate
s'attend à ce que ces tables de traduction soient fournies sous forme d'objets JavaScript simples ou d'objets JSON (en cas de chargement à distance).
Voici un exemple de ce à quoi ces tables de traduction ressembleraient généralement :
// /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello", "SEARCH": "Search" } } // /src/app/toolbar/i18n/tr.json { "TOOLBAR": { "HELLO": "Merhaba", "SEARCH": "Ara" } }
Pour internationaliser la vue de la barre d'outils d'en haut, vous devez remplacer les littéraux de chaîne par des jetons qu'AngularJS peut utiliser pour rechercher dans la table de traduction :
<!-- /src/app/toolbar/toolbar.html --> <a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a> <!-- or --> <a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>
Remarquez comment, pour le texte intérieur, vous pouvez soit utiliser la directive translate
soit le filtre translate
. (Vous pouvez en savoir plus sur la directive translate
ici et sur les filtres de translate
ici.)
Avec ces modifications, lorsque la vue est rendue, angular-translate
insère automatiquement la traduction appropriée correspondant à TOOLBAR.HELLO
dans le DOM en fonction de la langue actuelle.
Pour tokeniser les littéraux de chaîne qui apparaissent en tant que valeurs d'attribut, vous pouvez utiliser l'approche suivante :
<!-- /src/app/toolbar/toolbar.html --> <input type="text" class="form-control" ng-model="vm.query" translate translate-attr-placeholder="TOOLBAR.SEARCH">
Et si vos chaînes tokenisées contenaient des variables ?
Pour gérer des cas tels que "Hello, {{name}}.", vous pouvez effectuer un remplacement de variable en utilisant la même syntaxe d'interpolation que celle déjà prise en charge par AngularJS :
Tableau de traduction :
// /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello, {{name}}." } }
Vous pouvez ensuite définir la variable de plusieurs façons. Voici quelques-uns:
<!-- /src/app/toolbar/toolbar.html --> <a ... translate="TOOLBAR.HELLO" translate-values='{ name: vm.user.name }'></a> <!-- or --> <a ... translate="TOOLBAR.HELLO" translate-value-name='{{vm.user.name}}'></a> <!-- or --> <a ...>{{'TOOLBAR.HELLO | translate:'{ name: vm.user.name }'}}</a>
Faire face à la pluralité et au genre
La pluralisation est un sujet assez difficile en ce qui concerne i18n et l10n. Différentes langues et cultures ont des règles différentes sur la façon dont une langue gère la pluralisation dans diverses situations.
En raison de ces défis, les développeurs de logiciels ne résolvent parfois tout simplement pas le problème (ou du moins ne le résolvent pas de manière adéquate), ce qui entraîne des logiciels qui produisent des phrases idiotes comme celles-ci :
He saw 1 person(s) on floor 1. She saw 1 person(s) on floor 3. Number of people seen on floor 2: 2.
Heureusement, il existe une norme pour gérer cela, et une implémentation JavaScript de la norme est disponible en tant que MessageFormat.
Avec MessageFormat, vous pouvez remplacer les phrases mal structurées ci-dessus par les suivantes :
He saw 1 person on the 2nd floor. She saw 1 person on the 3rd floor. They saw 2 people on the 5th floor.
MessageFormat
accepte des expressions telles que :
var message = [ '{GENDER, select, male{He} female{She} other{They}}', 'saw', '{COUNT, plural, =0{no one} one{1 person} other{# people}}', 'on the', '{FLOOR, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}', 'floor.' ].join(' ');
Vous pouvez créer un formateur avec le tableau ci-dessus et l'utiliser pour générer des chaînes :
var messageFormatter = new MessageFormat('en').compile(message); messageFormatter({ GENDER: 'male', COUNT: 1, FLOOR: 2 }) // 'He saw 1 person on the 2nd floor.' messageFormatter({ GENDER: 'female', COUNT: 1, FLOOR: 3 }) // 'She saw 1 person on the 3rd floor.' messageFormatter({ COUNT: 2, FLOOR: 5 }) // 'They saw 2 people on the 5th floor.'
Comment pouvez-vous utiliser MessageFormat
avec angular-translate
pour profiter de toutes ses fonctionnalités dans vos applications ?
Dans la configuration de votre application, vous indiquez simplement angular-translate
que l'interpolation du format de message est disponible comme suit :
/src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); });
Voici à quoi pourrait alors ressembler une entrée dans la table de traduction :
// /src/app/main/social/i18n/en.json { "SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this." }
Et dans la vue :
<!-- /src/app/main/social/social.html --> <div translate="SHARED" translate-values="{ GENDER: 'male' }" translate-interpolation="messageformat"></div> <div> {{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }} </div>
Ici, vous devez indiquer explicitement que l'interpolateur de format de message doit être utilisé à la place de l'interpolateur par défaut dans AngularJS. En effet, les deux interpolateurs diffèrent légèrement dans leur syntaxe. Vous pouvez en savoir plus à ce sujet ici.
Fournir des tables de traduction à votre application
Maintenant que vous savez comment AngularJS peut rechercher des traductions pour vos jetons à partir de tables de traduction, comment votre application connaît-elle les tables de traduction en premier lieu ? Comment indiquez-vous à votre application quel paramètre régional/langue doit être utilisé ?
C'est ici que vous en apprendrez plus sur $translateProvider
.
Vous pouvez fournir les tables de traduction pour chaque paramètre régional que vous souhaitez prendre en charge directement dans le fichier core.config.js
de votre application comme suit :
// /src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); $translateProvider.translations('en', { TOOLBAR: { HELLO: 'Hello, {{name}}.' } }); $translateProvider.translations('tr', { TOOLBAR: { HELLO: 'Merhaba, {{name}}.' } }); $translateProvider.preferredLanguage('en'); });
Ici, vous fournissez des tables de traduction en tant qu'objets JavaScript pour l'anglais (en) et le turc (tr), tout en déclarant que la langue actuelle est l'anglais (en). Si l'utilisateur souhaite changer de langue, vous pouvez le faire avec le service $translate :

// /src/app/toolbar/toolbar.controller.js app.controller('ToolbarCtrl', function ($scope, $translate) { $scope.changeLanguage = function (languageKey) { $translate.use(languageKey); // Persist selection in cookie/local-storage/database/etc... }; });
Reste la question de savoir quelle langue doit être utilisée par défaut. Le codage en dur de la langue initiale de notre application n'est pas toujours acceptable. Dans de tels cas, une alternative consiste à tenter de déterminer automatiquement la langue à l'aide de $translateProvider :
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.determinePreferredLanguage(); });
determinePreferredLanguage
recherche des valeurs dans window.navigator
et sélectionne une valeur par défaut intelligente jusqu'à ce qu'un signal clair soit fourni par l'utilisateur.
Tables de traduction à chargement différé
La section précédente a montré comment vous pouvez fournir des tables de traduction directement dans le code source en tant qu'objets JavaScript. Cela peut être acceptable pour les petites applications, mais l'approche n'est pas évolutive, c'est pourquoi les tables de traduction sont souvent téléchargées sous forme de fichiers JSON à partir d'un serveur distant.
La maintenance des tables de traduction de cette manière réduit la taille de la charge utile initiale livrée au client, mais introduit une complexité supplémentaire. Vous êtes maintenant confronté au défi de conception consistant à fournir des données i18n au client. Si cela n'est pas géré avec soin, les performances de votre application peuvent en souffrir inutilement.
Pourquoi est-ce si complexe ? Les applications AngularJS sont organisées en modules. Dans une application complexe, il peut y avoir de nombreux modules, chacun avec ses propres données i18n distinctes. Une approche naïve, telle que charger et fournir des données i18n en une seule fois, doit donc être évitée.
Ce dont vous avez besoin, c'est d'un moyen d'organiser vos données i18n par module. Cela vous permettra de charger uniquement ce dont vous avez besoin quand vous en avez besoin et de mettre en cache ce qui a été précédemment chargé pour éviter de recharger les mêmes données (au moins jusqu'à ce que le cache soit invalide).
C'est là que partialLoader
entre en jeu.
Supposons que les tables de traduction de votre application soient structurées comme ceci :
/src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json
Vous pouvez configurer $translateProvider
pour utiliser partialLoader
avec un modèle d'URL qui correspond à cette structure :
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: '/src/app/{part}/i18n/{lang}.json' }); });
Comme on pouvait s'y attendre, « lang » est remplacé par le code de la langue au moment de l'exécution (par exemple « en » ou « tr »). Qu'en est-il de la "partie" ? Comment $translateProvider sait-il quelle "partie" charger ?
Vous pouvez fournir ces informations à l'intérieur des contrôleurs avec $translatePartialLoader
:
// /src/app/main/main.controller.js app.controller('MainCtrl', function ($translatePartialLoader) { $translatePartialLoader.addPart('main'); }); // /src/app/toolbar/toolbar.config.js app.controller('ToolbarCtrl', function ($translatePartialLoader) { $translatePartialLoader.addPart('toolbar'); });
Le modèle est maintenant terminé et les données i18n d'une vue donnée sont chargées lors de la première exécution de son contrôleur, ce qui correspond exactement à ce que vous souhaitez.
Mise en cache : réduction des temps de chargement
Qu'en est-il de la mise en cache ?
Vous pouvez activer le cache standard dans la configuration de l'application avec $translateProvider
:
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoaderCache(true); // default is false });
Si vous avez besoin de casser le cache pour une langue donnée, vous pouvez utiliser $translate
:
$translate.refresh(languageKey); // omit languageKey to refresh all
Avec ces éléments en place, votre application est entièrement internationalisée et prend en charge plusieurs langues.
Localisation des nombres, des devises et des dates
Dans cette section, vous apprendrez comment vous pouvez utiliser angular-dynamic-locale
pour prendre en charge le formatage des éléments de l'interface utilisateur tels que les nombres, les devises, les dates, etc., dans une application AngularJS.
Vous devrez installer deux packages supplémentaires pour cela :
npm i -S angular-dynamic-locale angular-i18n
Une fois les packages installés, vous pouvez ajouter le module aux dépendances de votre application :
// /src/app/core/core.module.js app.module('app.core', ['tmh.dynamicLocale', ...]);
Règles locales
Les règles locales sont de simples fichiers JavaScript qui fournissent des spécifications sur la façon dont les dates, les nombres, les devises, etc. doivent être formatés par les composants qui dépendent du service $locale.
La liste des paramètres régionaux actuellement pris en charge est disponible ici.
Voici un extrait de angular-locale_en-us.js
illustrant le formatage du mois et de la date :
... "MONTH": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "SHORTDAY": [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], ...
Contrairement aux données i18n, les règles de paramètres régionaux sont globales pour l'application, ce qui nécessite que les règles d'un paramètre régional donné soient chargées toutes en même temps.
Par défaut, angular-dynamic-locale
s'attend à ce que les fichiers de règles locales soient situés dans angular/i18n/angular-locale_{{locale}}.js
. S'ils sont situés ailleurs, tmhDynamicLocaleProvider
doit être utilisé pour remplacer la valeur par défaut :
// /src/app/core/core.config.js app.config(function (tmhDynamicLocaleProvider) { tmhDynamicLocaleProvider.localeLocationPattern( '/node_modules/angular-i18n/angular-locale_{{locale}}.js'); });
La mise en cache est automatiquement gérée par le service tmhDynamicLocaleCache
.
L'invalidation du cache est moins préoccupante ici, car les règles locales sont moins susceptibles de changer que les traductions de chaînes.
Pour basculer entre les paramètres régionaux, angular-dynamic-locale
fournit le service tmhDynamicLocale
:
// /src/app/toolbar/toolbar.controller.js app.controller('ToolbarCtrl', function ($scope, tmhDynamicLocale) { $scope.changeLocale = function (localeKey) { tmhDynamicLocale.set(localeKey); // Persist selection in cookie/local-storage/database/etc... }; });
Génération de tables de traduction avec traduction automatique
Les règles locales sont fournies avec le package angular-i18n
, il vous suffit donc de rendre le contenu du package disponible pour votre application selon vos besoins. Mais comment générer les fichiers JSON pour vos tables de traduction ? Il n'y a pas exactement de package que vous pouvez télécharger et brancher sur notre application.
Une option consiste à utiliser des API de traduction par programmation, en particulier si les chaînes de votre application sont de simples littéraux sans variables ni expressions au pluriel.
Avec Gulp et quelques packages supplémentaires, demander des traductions programmatiques pour votre application est un jeu d'enfant :
import gulp from 'gulp'; import map from 'map-stream'; import rename from 'gulp-rename'; import traverse from 'traverse'; import transform from 'vinyl-transform'; import jsonFormat from 'gulp-json-format'; function translateTable(to) { return transform(() => { return map((data, done) => { const table = JSON.parse(data); const strings = []; traverse(table).forEach(function (value) { if (typeof value !== 'object') { strings.push(value); } }); Promise.all(strings.map((s) => getTranslation(s, to))) .then((translations) => { let index = 0; const translated = traverse(table).forEach(function (value) { if (typeof value !== 'object') { this.update(translations[index++]); } }); done(null, JSON.stringify(translated)); }) .catch(done); }); }); } function translate(to) { return gulp.src('src/app/**/i18n/en.json') .pipe(translateTable(to)) .pipe(jsonFormat(2)) .pipe(rename({ basename: to })) .pipe(gulp.dest('src/app')); } gulp.task('translate:tr', () => translate('tr')); This task assumes the following folder structure: /src/app/main/i18n/en.json /src/app/toolbar/i18n/en.json /src/app/navigation/i18n/en.json ...
Le script lit d'abord toutes les tables de traduction en anglais, demande de manière asynchrone des traductions pour leurs ressources de chaîne, puis remplace les chaînes en anglais par les chaînes traduites pour produire une table de traduction dans une nouvelle langue.
Enfin, la nouvelle table de traduction est écrite comme une sœur de la table de traduction anglaise, ce qui donne :
/src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json /src/app/navigation/i18n/en.json /src/app/navigation/i18n/tr.json ...
La mise en œuvre de getTranslation
est également simple :
import bluebird from 'bluebird'; import MicrosoftTranslator from 'mstranslator'; bluebird.promisifyAll(MicrosoftTranslator.prototype); const Translator = new MicrosoftTranslator({ client_id: process.env.MICROSOFT_TRANSLATOR_CLIENT_ID, client_secret: process.env.MICROSOFT_TRANSLATOR_CLIENT_SECRET }, true); function getTranslation(string, to) { const text = string; const from = 'en'; return Translator.translateAsync({ text, from, to }); }
Ici, nous utilisons Microsoft Translate, mais on pourrait facilement utiliser un autre fournisseur tel que Google Translate ou Yandex Translate.
Bien que les traductions programmatiques soient pratiques, elles présentent plusieurs inconvénients, notamment :
- Les traductions de robots sont bonnes pour les chaînes courtes, mais même dans ce cas, il pourrait y avoir des pièges avec des mots qui ont des significations différentes dans différents contextes (par exemple, "piscine" peut signifier nager ou se regrouper).
- Les API peuvent ne pas être en mesure de gérer les chaînes avec des variables ou des chaînes qui reposent sur le format de message.
Dans ces cas et d'autres, des traductions humaines peuvent être nécessaires ; cependant, c'est un sujet pour un autre article de blog.
L'internationalisation des front-ends semble intimidante
Dans cet article, vous avez appris à utiliser ces packages pour internationaliser et localiser les applications AngularJS.
angular-translate
, angular-dynamic-locale
et gulp
sont des outils puissants pour internationaliser une application AngularJS qui encapsule des détails d'implémentation de bas niveau douloureux.
Pour une application de démonstration illustrant les idées discutées dans cet article, consultez ce référentiel GitHub.