Come internazionalizzare la tua app AngularJS

Pubblicato: 2022-03-11

L'internazionalizzazione della tua app può rendere lo sviluppo del software un'esperienza dolorosa, soprattutto se non inizi a farlo dall'inizio o se adotti un approccio volente o nolente.

Le app moderne, in cui il front-end e il back-end sono nettamente separati l'uno dall'altro, possono essere ancora più difficili da gestire quando si tratta di internazionalizzazione. Improvvisamente non hai più accesso alla pletora di strumenti collaudati che un tempo aiutavano a internazionalizzare le tue tradizionali app Web generate dalla pagina lato server.

Di conseguenza, un'app AngularJS richiede la consegna su richiesta dei dati di internazionalizzazione (i18n) e localizzazione (l10n) da consegnare al client per il rendering nella locale appropriata. A differenza delle tradizionali app con rendering lato server, non puoi più fare affidamento sul server per fornire pagine già localizzate. Puoi imparare a costruire un'applicazione PHP multilingue qui

In questo articolo imparerai come internazionalizzare la tua app AngularJS e imparerai gli strumenti che puoi utilizzare per facilitare il processo. Rendere la tua app AngularJS multilingue può porre alcune sfide interessanti, ma alcuni approcci possono rendere più facile aggirare la maggior parte di queste sfide.

Una semplice app AngularJS compatibile con i18n

Per consentire al cliente di modificare al volo la lingua e le impostazioni locali in base alle preferenze dell'utente, sarà necessario prendere una serie di decisioni di progettazione chiave:

  • In che modo progetti la tua app in modo che sia indipendente dalla lingua e dalle impostazioni locali sin dall'inizio?
  • Come strutturare i dati i18n e l10n?
  • In che modo fornisci questi dati in modo efficiente ai clienti?
  • Come astrarre la maggior parte dei dettagli di implementazione di basso livello per semplificare il flusso di lavoro degli sviluppatori?

Rispondere a queste domande il prima possibile può aiutare a evitare ostacoli nel processo di sviluppo su tutta la linea. Ognuna di queste sfide sarà affrontata in questo articolo; alcuni attraverso solide librerie AngularJS, altri attraverso determinate strategie e approcci.

Librerie di internazionalizzazione per AngularJS

Esistono numerose librerie JavaScript create appositamente per l'internazionalizzazione delle app AngularJS.

angular-translate è un modulo AngularJS che fornisce filtri e direttive, oltre alla possibilità di caricare i dati i18n in modo asincrono. Supporta la pluralizzazione tramite MessageFormat ed è progettato per essere altamente estensibile e configurabile.

Se stai usando angular-translate nel tuo progetto, potresti trovare alcuni dei seguenti pacchetti molto utili:

  • angular-sanitize : può essere utilizzato per proteggersi dagli attacchi XSS nelle traduzioni.
  • angular-translate-interpolation-messageformat : pluralizzazione con supporto per la formattazione del testo sensibile al genere.
  • angular-translate-loader-partial : utilizzato per fornire stringhe tradotte ai client.

Per un'esperienza davvero dinamica, puoi aggiungere angular-dynamic-locale al gruppo. Questa libreria ti consente di modificare le impostazioni locali in modo dinamico, e ciò include il modo in cui le date, i numeri, le valute, ecc. sono tutti formattati.

Per iniziare: installazione di pacchetti pertinenti

Supponendo che tu abbia già pronto il tuo standard AngularJS, puoi utilizzare NPM per installare i pacchetti di internazionalizzazione:

 npm i -S angular-translate angular-translate-interpolation-messageformat angular-translate-loader-partial angular-sanitize messageformat

Una volta installati i pacchetti, non dimenticare di aggiungere i moduli come dipendenze della tua app:

 // /src/app/core/core.module.js app.module('app.core', ['pascalprecht.translate', ...]);

Si noti che il nome del modulo è diverso dal nome del pacchetto.

Tradurre la tua prima stringa

Supponiamo che la tua app abbia una barra degli strumenti con del testo e un campo con del testo segnaposto:

 <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 vista sopra ha due frammenti di testo che puoi internazionalizzare: "Ciao" e "Cerca". In termini di HTML, uno appare come testo interno di un tag di ancoraggio, mentre l'altro appare come valore di un attributo.

Per internazionalizzarle, dovrai sostituire entrambe le stringhe letterali con dei token che AngularJS potrà poi sostituire con le effettive stringhe tradotte, in base alle preferenze dell'utente, durante il rendering della pagina.

AngularJS può farlo usando i tuoi token per eseguire una ricerca nelle tabelle di traduzione che fornisci. Il modulo angular-translate prevede che queste tabelle di traduzione vengano fornite come oggetti JavaScript semplici o come oggetti JSON (se vengono caricati in remoto).

Ecco un esempio di come sarebbero generalmente queste tabelle di traduzione:

 // /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello", "SEARCH": "Search" } } // /src/app/toolbar/i18n/tr.json { "TOOLBAR": { "HELLO": "Merhaba", "SEARCH": "Ara" } }

Per internazionalizzare la vista della barra degli strumenti dall'alto, è necessario sostituire i valori letterali stringa con i token che AngularJS può utilizzare per cercare nella tabella di traduzione:

 <!-- /src/app/toolbar/toolbar.html --> <a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a> <!-- or --> <a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>

Nota come, per il testo interno, puoi usare la direttiva translate o il filtro translate . (Puoi saperne di più sulla direttiva translate qui e sui filtri translate qui.)

Con queste modifiche, al momento del rendering della vista, angular-translate inserirà automaticamente la traduzione appropriata corrispondente a TOOLBAR.HELLO nel DOM in base alla lingua corrente.

Per tokenizzare stringhe letterali che appaiono come valori di attributo, puoi utilizzare il seguente approccio:

 <!-- /src/app/toolbar/toolbar.html --> <input type="text" class="form-control" ng-model="vm.query" translate translate-attr-placeholder="TOOLBAR.SEARCH">

Ora, cosa succede se le stringhe tokenizzate contenessero variabili?

Per gestire casi come "Hello, {{name}}.", puoi eseguire la sostituzione delle variabili utilizzando la stessa sintassi dell'interpolatore che AngularJS supporta già:

Tabella di traduzione:

 // /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello, {{name}}." } }

È quindi possibile definire la variabile in diversi modi. Eccone alcuni:

 <!-- /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>

Affrontare la pluralizzazione e il genere

La pluralizzazione è un argomento piuttosto difficile quando si tratta di i18n e l10n. Lingue e culture diverse hanno regole diverse su come una lingua gestisce la pluralizzazione in varie situazioni.

A causa di queste sfide, gli sviluppatori di software a volte semplicemente non affrontano il problema (o almeno non lo affrontano adeguatamente), risultando in un software che produce frasi stupide come queste:

 He saw 1 person(s) on floor 1. She saw 1 person(s) on floor 3. Number of people seen on floor 2: 2.

Fortunatamente, esiste uno standard su come gestirlo e un'implementazione JavaScript dello standard è disponibile come MessageFormat.

Con MessageFormat, puoi sostituire le frasi mal strutturate di cui sopra con le seguenti:

 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 accetta espressioni come le seguenti:

 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(' ');

Puoi creare un formattatore con l'array sopra e usarlo per generare stringhe:

 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.'

Come puoi utilizzare MessageFormat con angular angular-translate per sfruttare tutte le sue funzionalità all'interno delle tue app?

Nella configurazione dell'app, dici semplicemente ad angular-translate che l'interpolazione del formato del messaggio è disponibile come segue:

 /src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); });

Ecco come potrebbe apparire una voce nella tabella di traduzione:

 // /src/app/main/social/i18n/en.json { "SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this." }

E nella vista:

 <!-- /src/app/main/social/social.html --> <div translate="SHARED" translate-values="{ GENDER: 'male' }" translate-interpolation="messageformat"></div> <div> {{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }} </div>

Qui è necessario indicare esplicitamente che l'interpolatore del formato del messaggio deve essere utilizzato al posto dell'interpolatore predefinito in AngularJS. Questo perché i due interpolatori differiscono leggermente nella loro sintassi. Puoi leggere di più su questo qui.

Fornire tabelle di traduzione alla tua app

Ora che sai come AngularJS può cercare traduzioni per i tuoi token dalle tabelle di traduzione, come fa la tua app a conoscere le tabelle di traduzione in primo luogo? Come dici alla tua app quale locale/lingua dovrebbe essere usata?

Qui è dove impari su $translateProvider .

Puoi fornire le tabelle di traduzione per ogni locale che desideri supportare direttamente nel file core.config.js della tua app come segue:

 // /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'); });

Qui stai fornendo tabelle di traduzione come oggetti JavaScript per l'inglese (en) e il turco (tr), dichiarando che la lingua corrente è l'inglese (en). Se l'utente desidera cambiare la lingua, può farlo con il servizio $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... }; });

C'è ancora la domanda su quale lingua dovrebbe essere usata per impostazione predefinita. L'hardcoding della lingua iniziale della nostra app potrebbe non essere sempre accettabile. In questi casi, un'alternativa è tentare di determinare automaticamente la lingua utilizzando $translateProvider:

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.determinePreferredLanguage(); });

determinePreferredLanguage ricerca i valori in window.navigator e seleziona un'impostazione predefinita intelligente finché l'utente non fornisce un segnale chiaro.

Tabelle di traduzione a caricamento lento

La sezione precedente ha mostrato come fornire tabelle di traduzione direttamente nel codice sorgente come oggetti JavaScript. Questo può essere accettabile per piccole applicazioni, ma l'approccio non è scalabile, motivo per cui le tabelle di traduzione vengono spesso scaricate come file JSON da un server remoto.

Mantenere le tabelle di traduzione in questo modo riduce la dimensione del carico utile iniziale consegnato al cliente ma introduce ulteriore complessità. Ora ti trovi di fronte alla sfida progettuale di fornire i dati i18n al cliente. Se questo non viene gestito con attenzione, le prestazioni dell'applicazione possono risentirne inutilmente.

Perché è così complesso? Le applicazioni AngularJS sono organizzate in moduli. In un'applicazione complessa possono essere presenti molti moduli, ciascuno con i propri dati i18n distinti. Un approccio ingenuo, come caricare e fornire dati i18n tutto in una volta, dovrebbe quindi essere evitato.

Quello di cui hai bisogno è un modo per organizzare i tuoi dati i18n per modulo. Ciò ti consentirà di caricare solo ciò di cui hai bisogno quando ne hai bisogno e di memorizzare nella cache ciò che è stato precedentemente caricato per evitare di ricaricare gli stessi dati (almeno fino a quando la cache non è valida).

È qui che entra in gioco partialLoader .

Supponiamo che le tabelle di traduzione della tua applicazione siano strutturate in questo modo:

 /src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json

Puoi configurare $translateProvider per utilizzare partialLoader con un pattern URL che corrisponda a questa struttura:

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: '/src/app/{part}/i18n/{lang}.json' }); });

Come ci si aspetterebbe, "lang" viene sostituito con il codice della lingua in fase di esecuzione (ad es. "en" o "tr"). E la "parte"? In che modo $translateProvider sa quale "parte" caricare?

Puoi fornire queste informazioni all'interno dei controller con $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'); });

Il pattern è ora completo e i dati i18n per una determinata vista vengono caricati quando il suo controller viene eseguito per la prima volta, che è esattamente quello che vuoi.

Memorizzazione nella cache: riduzione dei tempi di caricamento

E la memorizzazione nella cache?

Puoi abilitare la cache standard nella configurazione dell'app con $translateProvider :

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoaderCache(true); // default is false });

Se hai bisogno di svuotare la cache per una determinata lingua, puoi usare $translate :

 $translate.refresh(languageKey); // omit languageKey to refresh all

Con questi pezzi a posto, la tua applicazione è completamente internazionalizzata e supporta più lingue.

Localizzazione di numeri, valute e date

In questa sezione imparerai come utilizzare angular-dynamic-locale per supportare la formattazione di elementi dell'interfaccia utente come numeri, valute, date e simili, in un'applicazione AngularJS.

Dovrai installare altri due pacchetti per questo:

 npm i -S angular-dynamic-locale angular-i18n

Una volta installati i pacchetti, puoi aggiungere il modulo alle dipendenze della tua app:

 // /src/app/core/core.module.js app.module('app.core', ['tmh.dynamicLocale', ...]);

Regole locali

Le regole di localizzazione sono semplici file JavaScript che forniscono specifiche su come date, numeri, valute e simili devono essere formattati dai componenti che dipendono dal servizio $locale.

L'elenco delle localizzazioni attualmente supportate è disponibile qui.

Ecco uno snippet di angular-locale_en-us.js illustra la formattazione del mese e della data:

 ... "MONTH": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "SHORTDAY": [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], ...

A differenza dei dati di i18n, le regole delle impostazioni locali sono globali per l'applicazione, richiedendo che le regole per una determinata locale vengano caricate tutte in una volta.

Per impostazione predefinita, angular-dynamic-locale prevede che i file delle regole locali si trovino in angular/i18n/angular-locale_{{locale}}.js . Se si trovano altrove, è necessario utilizzare tmhDynamicLocaleProvider per sovrascrivere l'impostazione predefinita:

 // /src/app/core/core.config.js app.config(function (tmhDynamicLocaleProvider) { tmhDynamicLocaleProvider.localeLocationPattern( '/node_modules/angular-i18n/angular-locale_{{locale}}.js'); });

La memorizzazione nella cache viene gestita automaticamente dal servizio tmhDynamicLocaleCache .

Invalidare la cache è meno preoccupante in questo caso, poiché è meno probabile che le regole locali cambino rispetto alle traduzioni di stringhe.

Per passare da una localizzazione all'altra, angular angular-dynamic-locale fornisce il servizio 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... }; });

Generazione di tabelle di traduzione con traduzione automatica

Le regole locali vengono fornite con il pacchetto angular-i18n , quindi tutto ciò che devi fare è rendere il contenuto del pacchetto disponibile per la tua applicazione secondo necessità. Ma come si generano i file JSON per le tabelle di traduzione? Non c'è esattamente un pacchetto che puoi scaricare e collegare alla nostra applicazione.

Un'opzione consiste nell'utilizzare API di traduzione programmatica, soprattutto se le stringhe nell'applicazione sono semplici letterali senza variabili o espressioni pluralizzate.

Con Gulp e un paio di pacchetti extra, richiedere traduzioni programmatiche per la tua applicazione è un gioco da ragazzi:

 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 ...

Lo script prima legge tutte le tabelle di traduzione in inglese, richiede in modo asincrono le traduzioni per le relative risorse di stringa, quindi sostituisce le stringhe in inglese con le stringhe tradotte per produrre una tabella di traduzione in una nuova lingua.

Infine, la nuova tabella di traduzione viene scritta come un fratello della tabella di traduzione inglese, ottenendo:

 /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 ...

Anche l'implementazione di getTranslation è semplice:

 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 }); }

Qui utilizziamo Microsoft Translate, ma si potrebbe facilmente utilizzare un altro provider come Google Translate o Yandex Translate.

Sebbene le traduzioni programmatiche siano convenienti, ci sono diversi inconvenienti, tra cui:

  • Le traduzioni dei robot sono buone per stringhe brevi, ma anche in questo caso potrebbero esserci delle insidie ​​con parole che hanno significati diversi in contesti diversi (ad es. "piscina" può significare nuotare o raggrupparsi).
  • Le API potrebbero non essere in grado di gestire stringhe con variabili o stringhe che si basano sul formato del messaggio.

In questi casi e in altri possono essere richieste traduzioni umane; tuttavia, questo è un argomento per un altro post sul blog.

L'internazionalizzazione dei front-end sembra solo scoraggiante

In questo articolo, hai imparato come utilizzare questi pacchetti per internazionalizzare e localizzare le applicazioni AngularJS.

angular-translate , angular-dynamic-locale e gulp sono strumenti potenti per internazionalizzare un'applicazione AngularJS che incapsula dettagli di implementazione di basso livello dolorosi.

Per un'app demo che illustri le idee discusse in questo post, dai un'occhiata a questo repository GitHub.