Cum să vă internaționalizați aplicația AngularJS
Publicat: 2022-03-11Internaționalizarea aplicației dvs. poate face ca dezvoltarea de software să fie o experiență dureroasă, mai ales dacă nu începeți să o faceți de la bun început sau dacă adoptați o abordare vrând-nevrând.
Aplicațiile moderne, în care front-end-ul și back-end-ul sunt distinct separat unul de celălalt, pot fi și mai dificil de tratat atunci când vine vorba de internaționalizare. Dintr-o dată nu mai aveți acces la multitudinea de instrumente testate în timp care au ajutat cândva la internaționalizarea aplicațiilor web tradiționale generate de pagina de server.
În consecință, o aplicație AngularJS necesită livrarea la cerere a datelor de internaționalizare (i18n) și localizare (l10n) pentru a fi livrate clientului pentru a se afișa în locația adecvată. Spre deosebire de aplicațiile tradiționale randate pe server, nu vă mai puteți baza pe server pentru a livra pagini care sunt deja localizate. Puteți afla despre construirea unei aplicații PHP multilingve aici
În acest articol, veți afla cum vă puteți internaționaliza aplicația AngularJS și veți afla despre instrumentele pe care le puteți utiliza pentru a ușura procesul. A face aplicația AngularJS multilingvă poate pune unele provocări interesante, dar anumite abordări pot face mai ușor să rezolvi majoritatea acestor provocări.
O aplicație simplă AngularJS capabilă de i18n
Pentru a permite clientului să schimbe din mers limba și localitatea în funcție de preferințele utilizatorului, va trebui să luați o serie de decizii cheie de proiectare:
- Cum vă proiectați aplicația astfel încât să fie independentă de limbă și localitate de la început?
- Cum structurați datele i18n și l10n?
- Cum furnizați aceste date în mod eficient clienților?
- Cum abstrageți cât mai mult din detaliile de implementare de nivel scăzut pentru a simplifica fluxul de lucru al dezvoltatorului?
Răspunsul la aceste întrebări cât mai devreme posibil poate ajuta la evitarea obstacolelor în procesul de dezvoltare pe linie. Fiecare dintre aceste provocări va fi abordată în acest articol; unele prin biblioteci robuste AngularJS, altele prin anumite strategii și abordări.
Biblioteci de internaționalizare pentru AngularJS
Există o serie de biblioteci JavaScript care sunt create special pentru internaționalizarea aplicațiilor AngularJS.
angular-translate
este un modul AngularJS care oferă filtre și directive, împreună cu capacitatea de a încărca datele i18n în mod asincron. Acceptă pluralizarea prin MessageFormat
și este conceput pentru a fi foarte extensibil și configurabil.
Dacă utilizați angular-translate
în proiectul dvs., puteți găsi unele dintre următoarele pachete super utile:
-
angular-sanitize
: poate fi folosit pentru a vă proteja împotriva atacurilor XSS în traduceri. -
angular-translate-interpolation-messageformat
: pluralizare cu suport pentru formatarea textului sensibilă la gen. -
angular-translate-loader-partial
: folosit pentru a livra șiruri de caractere traduse clienților.
Pentru o experiență cu adevărat dinamică, puteți adăuga angular-dynamic-locale
la grup. Această bibliotecă vă permite să schimbați localitatea în mod dinamic - și aceasta include modul în care datele, numerele, monedele etc. sunt toate formatate.
Noțiuni introductive: Instalarea pachetelor relevante
Presupunând că aveți deja gata AngularJS boilerplate, puteți utiliza NPM pentru a instala pachetele de internaționalizare:
npm i -S angular-translate angular-translate-interpolation-messageformat angular-translate-loader-partial angular-sanitize messageformat
Odată ce pachetele sunt instalate, nu uitați să adăugați modulele ca dependențe ale aplicației dvs.:
// /src/app/core/core.module.js app.module('app.core', ['pascalprecht.translate', ...]);
Rețineți că numele modulului este diferit de numele pachetului.
Traducerea primului șir
Să presupunem că aplicația dvs. are o bară de instrumente cu ceva text și un câmp cu ceva substituent:
<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>
Vederea de mai sus are două fragmente de text pe care le puteți internaționaliza: „Bună ziua” și „Căutare”. În ceea ce privește HTML, unul apare ca textul interior al unei etichete de ancorare, în timp ce celălalt apare ca o valoare a unui atribut.
Pentru a le internaționaliza, va trebui să înlocuiți ambii literali de șir cu token-uri pe care AngularJS le poate înlocui apoi cu șirurile de caractere traduse reale, în funcție de preferința utilizatorului, în timp ce redarea paginii.
AngularJS poate face acest lucru folosind token-urile pentru a efectua o căutare în tabelele de traducere pe care le furnizați. Modulul angular-translate
se așteaptă ca aceste tabele de traducere să fie furnizate ca obiecte JavaScript simple sau ca obiecte JSON (dacă se încarcă de la distanță).
Iată un exemplu despre cum ar arăta în general aceste tabele de traducere:
// /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello", "SEARCH": "Search" } } // /src/app/toolbar/i18n/tr.json { "TOOLBAR": { "HELLO": "Merhaba", "SEARCH": "Ara" } }
Pentru a internaționaliza vizualizarea barei de instrumente de sus, trebuie să înlocuiți literalele șir cu simboluri pe care AngularJS le poate folosi pentru a căuta în tabelul de traducere:
<!-- /src/app/toolbar/toolbar.html --> <a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a> <!-- or --> <a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>
Observați cum, pentru textul interior, puteți utiliza fie directiva de translate
, fie filtrul de translate
. (Puteți afla mai multe despre directiva de translate
aici și despre filtrele de translate
aici.)
Cu aceste modificări, atunci când vizualizarea este redată, angular-translate
va insera automat traducerea corespunzătoare corespunzătoare TOOLBAR.HELLO
în DOM pe baza limbii curente.
Pentru a tokeniza literalele șir care apar ca valori de atribut, puteți utiliza următoarea abordare:
<!-- /src/app/toolbar/toolbar.html --> <input type="text" class="form-control" ng-model="vm.query" translate translate-attr-placeholder="TOOLBAR.SEARCH">
Acum, ce se întâmplă dacă șirurile tale tokenizate au conținut variabile?
Pentru a gestiona cazuri precum „Bună ziua, {{nume}}.”, puteți efectua înlocuirea variabilelor folosind aceeași sintaxă a interpolatorului pe care o acceptă deja AngularJS:
Tabel de traducere:
// /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello, {{name}}." } }
Puteți defini apoi variabila în mai multe moduri. Iată câteva:
<!-- /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>
Confruntarea cu pluralizarea și genul
Pluralizarea este un subiect destul de greu când vine vorba de i18n și l10n. Diferite limbi și culturi au reguli diferite pentru modul în care o limbă gestionează pluralizarea în diferite situații.
Din cauza acestor provocări, dezvoltatorii de software uneori pur și simplu nu vor aborda problema (sau cel puțin nu o vor aborda în mod adecvat), rezultând un software care produce propoziții stupide ca acestea:
He saw 1 person(s) on floor 1. She saw 1 person(s) on floor 3. Number of people seen on floor 2: 2.
Din fericire, există un standard pentru cum să gestionați acest lucru și o implementare JavaScript a standardului este disponibilă ca MessageFormat.
Cu MessageFormat, puteți înlocui propozițiile prost structurate de mai sus cu următoarele:
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
acceptă expresii precum următoarele:
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(' ');
Puteți construi un formatator cu matricea de mai sus și îl puteți utiliza pentru a genera șiruri de caractere:
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.'
Cum puteți folosi MessageFormat
cu angular-translate
pentru a profita de funcționalitatea sa completă în aplicațiile dvs.?
În configurația aplicației, spuneți pur și simplu angular-translate
că interpolarea formatului de mesaj este disponibilă după cum urmează:
/src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); });
Iată cum ar putea arăta apoi o intrare din tabelul de traducere:
// /src/app/main/social/i18n/en.json { "SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this." }
Și în vedere:
<!-- /src/app/main/social/social.html --> <div translate="SHARED" translate-values="{ GENDER: 'male' }" translate-interpolation="messageformat"></div> <div> {{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }} </div>
Aici trebuie să indicați în mod explicit că interpolatorul de format de mesaj ar trebui să fie utilizat în locul interpolatorului implicit în AngularJS. Acest lucru se datorează faptului că cei doi interpolatori diferă ușor în sintaxa lor. Puteți citi mai multe despre asta aici.
Furnizarea de tabele de traducere pentru aplicația dvs
Acum că știți cum AngularJS poate căuta traduceri pentru jetoanele dvs. din tabelele de traducere, de unde știe aplicația dvs. despre tabelele de traducere în primul rând? Cum spuneți aplicației dvs. ce locală/limbă ar trebui utilizată?
Aici veți afla despre $translateProvider
.
Puteți furniza tabelele de traducere pentru fiecare localitate pe care doriți să o acceptați direct în fișierul core.config.js
al aplicației, după cum urmează:
// /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'); });
Aici furnizați tabele de traducere ca obiecte JavaScript pentru engleză (en) și turcă (tr), în timp ce declarați limba curentă ca fiind engleza (en). Dacă utilizatorul dorește să schimbe limba, puteți face acest lucru cu serviciul $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... }; });
Mai rămâne întrebarea ce limbă ar trebui folosită implicit. Codificarea tare a limbii inițiale a aplicației noastre poate să nu fie întotdeauna acceptabilă. În astfel de cazuri, o alternativă este să încercați să determinați automat limba folosind $translateProvider:
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.determinePreferredLanguage(); });
determinePreferredLanguage
caută valori în window.navigator
și selectează un implicit inteligent până când utilizatorul oferă un semnal clar.
Tabelele de traducere cu încărcare leneră
Secțiunea anterioară a arătat cum puteți furniza tabele de traducere direct în codul sursă ca obiecte JavaScript. Acest lucru poate fi acceptabil pentru aplicații mici, dar abordarea nu este scalabilă, motiv pentru care tabelele de traducere sunt adesea descărcate ca fișiere JSON de pe un server la distanță.
Menținerea tabelelor de traducere în acest fel reduce dimensiunea inițială a sarcinii utile livrate clientului, dar introduce o complexitate suplimentară. Acum vă confruntați cu provocarea de proiectare de a furniza date i18n către client. Dacă acest lucru nu este tratat cu atenție, performanța aplicației dvs. poate avea de suferit inutil.
De ce este atât de complex? Aplicațiile AngularJS sunt organizate în module. Într-o aplicație complexă, pot exista multe module, fiecare cu propriile sale date i18n distincte. Prin urmare, ar trebui evitată o abordare naivă, cum ar fi încărcarea și furnizarea de date i18n dintr-o dată.
Ceea ce aveți nevoie este o modalitate de a vă organiza datele i18n pe modul. Acest lucru vă va permite să încărcați exact ceea ce aveți nevoie atunci când aveți nevoie de el și să stocați în cache ceea ce a fost încărcat anterior pentru a evita reîncărcarea acelorași date (cel puțin până când memoria cache este invalidă).
Aici intervine partialLoader
.
Să presupunem că tabelele de traducere ale aplicației dvs. sunt structurate astfel:
/src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json
Puteți configura $translateProvider
să utilizeze partialLoader
cu un model URL care se potrivește cu această structură:
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: '/src/app/{part}/i18n/{lang}.json' }); });
După cum ar fi de așteptat, „lang” este înlocuit cu codul de limbă în timpul rulării (de exemplu, „en” sau „tr”). Dar „parte”? Cum știe $translateProvider ce „parte” să încarce?
Puteți furniza aceste informații în interiorul controlorilor cu $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'); });
Modelul este acum complet, iar datele i18n pentru o anumită vizualizare sunt încărcate atunci când controlerul său este executat pentru prima dată, ceea ce este exact ceea ce doriți.
Memorarea în cache: reducerea timpilor de încărcare
Ce zici de caching?
Puteți activa memoria cache standard în configurația aplicației cu $translateProvider
:
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoaderCache(true); // default is false });
Dacă trebuie să spargeți memoria cache pentru o anumită limbă, puteți utiliza $translate
:
$translate.refresh(languageKey); // omit languageKey to refresh all
Cu aceste piese la locul lor, aplicația dvs. este pe deplin internaționalizată și acceptă mai multe limbi.
Localizarea numerelor, valutelor și datelor
În această secțiune, veți afla cum puteți utiliza angular-dynamic-locale
pentru a sprijini formatarea elementelor UI, cum ar fi numere, monede, date și altele asemenea, într-o aplicație AngularJS.
Va trebui să instalați încă două pachete pentru aceasta:
npm i -S angular-dynamic-locale angular-i18n
Odată ce pachetele sunt instalate, puteți adăuga modulul la dependențele aplicației dvs.:
// /src/app/core/core.module.js app.module('app.core', ['tmh.dynamicLocale', ...]);
Reguli locale
Regulile locale sunt fișiere JavaScript simple care oferă specificații despre modul în care datele, numerele, monedele și altele asemenea ar trebui formatate de componente care depind de serviciul $locale.
Lista localurilor acceptate în prezent este disponibilă aici.
Iată un fragment din angular-locale_en-us.js
care ilustrează formatarea lunii și datei:
... "MONTH": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "SHORTDAY": [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], ...
Spre deosebire de datele i18n, regulile locale sunt globale pentru aplicație, necesitând ca regulile pentru un anumit local să fie încărcate dintr-o dată.
În mod implicit, angular-dynamic-locale
se așteaptă ca fișierele de reguli locale să fie localizate în angular/i18n/angular-locale_{{locale}}.js
. Dacă sunt situate în altă parte, tmhDynamicLocaleProvider
trebuie utilizat pentru a înlocui valoarea implicită:
// /src/app/core/core.config.js app.config(function (tmhDynamicLocaleProvider) { tmhDynamicLocaleProvider.localeLocationPattern( '/node_modules/angular-i18n/angular-locale_{{locale}}.js'); });
Memorarea în cache este gestionată automat de serviciul tmhDynamicLocaleCache
.
Invalidarea memoriei cache este mai puțin o problemă aici, deoarece regulile locale sunt mai puțin probabil să se schimbe decât traducerile șirurilor.
Pentru a comuta între locații, angular-dynamic-locale
oferă serviciul 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... }; });
Generarea tabelelor de traducere cu traducere automată
Regulile locale sunt livrate cu pachetul angular-i18n
, așa că tot ce trebuie să faceți este să faceți conținutul pachetului disponibil pentru aplicația dvs., după cum este necesar. Dar cum generați fișierele JSON pentru tabelele de traducere? Nu există exact un pachet pe care l-ați putea descărca și conecta la aplicația noastră.
O opțiune este să utilizați API-uri de traducere programatică, mai ales dacă șirurile din aplicația dvs. sunt literale simple, fără variabile sau expresii pluralizate.
Cu Gulp și câteva pachete suplimentare, solicitarea de traduceri programatice pentru aplicația dvs. este ușoară:
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 ...
Scriptul citește mai întâi toate tabelele de traducere în limba engleză, solicită în mod asincron traduceri pentru resursele lor de șir, apoi înlocuiește șirurile de caractere în limba engleză cu șirurile traduse pentru a produce un tabel de traducere într-o nouă limbă.
În cele din urmă, noul tabel de traducere este scris ca un frate al tabelului de traducere în engleză, rezultând:
/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 ...
Implementarea getTranslation
este, de asemenea, simplă:
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 }); }
Aici, folosim Microsoft Translate, dar s-ar putea folosi cu ușurință un alt furnizor, cum ar fi Google Translate sau Yandex Translate.
Deși traducerile programatice sunt convenabile, există mai multe dezavantaje, inclusiv:
- Traducerile robot sunt bune pentru șiruri scurte, dar chiar și atunci, ar putea exista capcane cu cuvinte care au semnificații diferite în contexte diferite (de exemplu, „piscine” poate însemna înot sau grupare).
- Este posibil ca API-urile să nu poată gestiona șiruri cu variabile sau șiruri care se bazează pe formatul mesajului.
În aceste cazuri și altele, pot fi necesare traduceri umane; totuși, acesta este un subiect pentru o altă postare pe blog.
Internaționalizarea front-end-urilor pare descurajantă
În acest articol, ați învățat cum să utilizați aceste pachete pentru a internaționaliza și a localiza aplicațiile AngularJS.
angular-translate
, angular-dynamic-locale
și gulp
sunt instrumente puternice pentru internaționalizarea unei aplicații AngularJS care încapsulează detalii dureroase de implementare la nivel scăzut.
Pentru o aplicație demonstrativă care ilustrează ideile discutate în această postare, consultați acest depozit GitHub.