Internazionalizzazione con @angular/localize
Sono stati fatti grandi progressi sul fronte i18n! Un nuovo pacchetto chiamato @angular/localize
è stato introdotto in Angular 9.0.
E’ usato sotto il cofano per darci le stesse caratteristiche che avevamo prima: traduzioni nei template al momento della compilazione.
Ma ci lascia sperare di più in futuro, con caratteristiche non documentate già disponibili, come le traduzioni nel codice, o le traduzioni a runtime invece di quelle solo in compilazione 😎.
Iniziamo a vedere cosa possiamo fare con l’aiuto di @angular/localize
e la CLI nella v9.
i18n nei template
Il nuovo pacchetto @angular/localize
offre una funzione chiamata $localize
.
Il supporto i18n esistente in Angular ora usa $localize
, il che significa che template come:
<h1 i18n>Hello</h1>
saranno compilati in chiamate $localize
.
Se eseguite ng serve
con un tale template, incontrerete un errore di runtime:
Error: It looks like your application or one of its dependencies is using i18n.Angular 9 introduced a global `$localize()` function that needs to be loaded.Please run `ng add @angular/localize` from the Angular CLI.(For non-CLI projects, add `import '@angular/localize/init';` to your `polyfills.ts` file.For server-side rendering applications add the import to your `main.server.ts` file.)
L’errore è autoesplicativo: poiché gli attributi i18n
sono ora convertiti in chiamate $localize
nel codice generato, abbiamo bisogno di caricare la funzione $localize
.Potrebbe essere fatto di default, ma siccome è richiesto solo se stai usando l’internazionalizzazione, non avrebbe davvero senso.
Ecco perché se la tua applicazione, o una delle sue dipendenze, usa gli attributi i18n
nei suoi template, allora dovrai aggiungere import '@angular/localize/init'
ai tuoi polyfill!
La CLI offre uno schema per fare questo per te.Esegui semplicemente:
ng add @angular/localize
e la CLI aggiunge il pacchetto alle tue dipendenze e l’importazione necessaria ai tuoi polyfill.
Quindi quando esegui la tua applicazione con un semplice ng serve
,$localize
visualizza semplicemente il messaggio originale.
Ora come traduci questi messaggi?
Il processo è molto simile a quello precedente.Prima si esegue ng xi18n
per estrarre i messaggi in un file messages.xlf
, poi si traduce il file per i propri locali, per esempio messages.fr.xlf
e messages.es.xlf
.
Poi devi configurare il CLI, in angular.json
{ "projects": { "ponyracer": { "projectType": "application", // ... "i18n": { "locales": { "fr": "src/locale/messages.fr.xlf", "es": "src/locale/messages.es.xlf", } }, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", // ... "configurations": { "production": { // ... }, "fr": { "localize": }, "es": { "localize": } } }, "serve": { // ... "configurations": { "production": { // ... }, "fr": { "browserTarget": "ponyracer:build:fr" }, "es": { "browserTarget": "ponyracer:build:es" } } } // ...}
Ora, le configurazioni es
o fr
permettono di eseguire:
ng serve --configuration=fr
E l’applicazione servita è ora in francese!
Puoi anche costruire l’applicazione con un locale specifico:
ng build --configuration=production,es
o con tutti i locali in una volta sola:
ng build --prod --localize
Questo è un grande progresso rispetto alle versioni precedenti di Angular: prima dovevamo costruire la stessa applicazione per ogni locale, poiché la traduzione era parte della compilazione.Ora, quando Angular compila l’applicazione, genera le chiamate $localize
.Poi, quando questo è fatto, uno strumento prende l’applicazione compilata e sostituisce tutte le chiamate $localize
con le traduzioni appropriate.Questo è super veloce.Si ha quindi un bundle che non contiene chiamate a $localize
e tutte le stringhe i18n sono state tradotte.
Fino ad ora, si doveva costruire l’applicazione una volta per locale, e questa era una build completa. Quindi diciamo che è una compilazione di 30s, e volevi 4 locali, allora ti aspettavano 2 minuti.
Con il nuovo approccio, la compilazione viene fatta una volta sola, e poi le varie versioni i18n vengono generate in pochi secondi (viene anche generata in parallelo se possibile).Quindi si passa da 2 minuti a ~40 secondi! 🌈
Allora hai diversi bundle, uno per locale, e puoi servire quello appropriato ai tuoi utenti a seconda delle loro preferenze, come facevi prima.
Questa strategia è chiamata inlining compile-time, perché inlini le traduzioni direttamente, e poi non c’è più niente da fare a runtime.
Ora parliamo di qualcosa di ancora non documentato, che potrebbe cambiare in futuro, ma comunque interessante da sapere: ora possiamo anche tradurre messaggi nel nostro codice TypeScript!
La funzione $localize
di cui ho parlato può essere usata direttamente. È una funzione particolare che si può usare per etichettare una stringa template per la localizzazione.
Ma forse dovremmo iniziare a spiegare cos’è una stringa template etichettata?
Stringhe template e funzioni tag
Quando si usano le stringhe template, si può definire una funzione tag, e applicarla a una stringa template.Qui askQuestion
aggiunge un punto di interrogazione alla fine della stringa:
const askQuestion = strings => strings + '?';const template = askQuestion`Is anyone here`;
E qual è la differenza con una semplice funzione?La funzione tag infatti riceve diversi argomenti:
- un array delle parti statiche della stringa
- i valori risultanti dalla valutazione delle espressioni
Per esempio se abbiamo una stringa template contenente espressioni:
const person1 = 'Cedric';const person2 = 'Agnes';const template = `Hello ${person1}! Where is ${person2}?`;
allora la funzione tag riceve le varie parti statiche e dinamiche.Qui abbiamo una funzione tag per mettere in maiuscolo i nomi dei protagonisti:
const uppercaseNames = (strings, ...values) => { // `strings` is an array with the static parts // `values` is an array with the evaluated expressions const names = values.map(name => name.toUpperCase()); // `names` now has // let's merge the `strings` and `names` arrays return strings.map((string, i) => `${string}${names ? names : ''}`).join('');};const result = uppercaseNames`Hello ${person1}! Where is ${person2}?`;// returns 'Hello CEDRIC! Where is AGNES?'
i18n con $localize nel codice TypeScript
$localize
usa questa meccanica per farci scrivere:
@Component({ template: '{{ title }}'})export class HomeComponent { title = $localize`You have 10 users`;}
Nota che non è necessario importare la funzione.Se aggiungi import '@angular/localize/init'
una volta nella tua applicazione, $localize
sarà aggiunto all’oggetto globale.
Puoi poi tradurre il messaggio come faresti per un template.Ma, al momento (v9.0.0), la CLI non estrae questi messaggi con il comando xi18n
come fa per i template.
Se si serve l’applicazione e non viene trovata alcuna traduzione, $localize
mostra semplicemente la stringa originale e registra un avviso nella console:
No translation found for "6480943972743237078" ("You have 10 users").
Quindi devi aggiungerlo manualmente al tuo messages.fr.xlf
con l’ID dato se vuoi provare:
<trans-unit> <source>You have 10 users</source> <target>Vous avez 10 utilisateurs</target></trans-unit>
Il template del mio HomeComponent
visualizza quindi Vous avez 10 utilisateurs
!
Cosa succede se hai qualche espressione dinamica nella tua stringa template?
title = $localize`Hi ${this.name}! You have ${this.users.length} users.`;
Le espressioni saranno automaticamente chiamate PH
e PH_1
(PH
è per il segnaposto).Poi puoi usare questi segnaposto ovunque tu voglia nelle traduzioni:
<trans-unit> <source>Hi <x/>! You have <x/> users.</source> <target>Bonjour <x/> ! Vous avez <x/> utilisateurs.</target></trans-unit>
Ma la pratica migliore è quella di dare tu stesso un segnaposto significativo all’espressione, e puoi farlo usando la sintassi ${expression}:placeholder:
.
title = $localize`Hi ${this.name}:name:! You have ${this.users.length}:userCount: users.`;
Poi puoi usare questo segnaposto ovunque tu voglia nelle traduzioni:
<trans-unit> <source>Hi <x/>! You have <x/> users.</source> <target>Bonjour <x/> ! Vous avez <x/> utilisateurs.</target></trans-unit>
Identificativi personalizzati
Nota che se hai traduzioni con ID personalizzati, essi sono usati da $localize
(come era il caso prima):
<h1 i18n="@@home.greetings">Hello</h1>
Allora la tua traduzione appare come:
<trans-unit> <source>Hello</source> <target>Bonjour</target></trans-unit>
che è ovviamente più bella da usare.
Come per le traduzioni in codice? $localize
comprende anche una sintassi che permette di specificare un ID:
title = $localize`:@@home.users:You have 10 users`;
La sintassi per l’ID personalizzato è la stessa dei template, e l’ID è circondato da due punti per separarlo dal contenuto della traduzione.
Come per la sintassi dei template, puoi anche specificare una descrizione e un significato, per aiutare i traduttori con un po’ di contesto: :meaning|description@@id:message
.
Per esempio:
title = $localize`:greeting message with the number of users currently logged in@@home.users:You have 10 users`;
Tenete a mente che questa è un’API di basso livello, non documentata.Il team di Angular o la comunità probabilmente offriranno funzioni di livello superiore con una migliore esperienza di sviluppo (beh, lo spero!).Locl da Olivier Combe, l’autore di ngx-translate è probabilmente da tenere d’occhio 🧐.
Traduzioni runtime
Come dicevo, se usi i comandi CLI di cui sopra (ng serve --configuration=fr
o ng build --localize
) allora l’applicazione viene compilata e poi tradotta prima di arrivare al browser, quindi non ci sono chiamate $localize
a runtime.
Ma $localize
è stato progettato per offrire un’altra possibilità:traduzioni runtime.Cosa significa? Beh, saremmo in grado di spedire solo un’applicazione, contenente chiamate $localize
, e prima che l’applicazione parta, potremmo caricare le traduzioni che vogliamo.Niente più N build e N bundle per N locali \o/
Senza entrare troppo nei dettagli, questo è già possibile con v9, usando la funzione loadTranslations
offerta da @angular/localize
.Ma questo deve essere fatto prima che l’applicazione parta.
Puoi caricare le tue traduzioni in polyfills.ts
con:
import '@angular/localize/init';import { loadTranslations } from '@angular/localize';loadTranslations({ '1815172606781074132': 'Bonjour {$name}\xa0! Vous avez {$userCount} utilisateurs.'});
Come puoi vedere non c’è nessuna considerazione di locale: carichi semplicemente la tua traduzione come un oggetto, le cui chiavi sono le stringhe da tradurre e i valori, le loro traduzioni.
Ora se esegui un semplice ng serve
, il titolo viene visualizzato in francese! E non c’è più bisogno di ng xi18n
, o messages.fr.xlf
o di una configurazione specifica per ogni locale in angular.json
.A lungo termine, quando questo sarà adeguatamente supportato e documentato, dovremmo essere in grado di caricare file JSON a runtime, come fanno la maggior parte delle librerie i18n.Potresti anche realizzarlo nella v9, è solo un po’ di lavoro manuale, ma è fattibile.
Che mi dici del cambio di locale al volo allora? Possiamo caricare un altro set di traduzioni quando l’applicazione viene avviata? Beh, no. Il modo attuale in cui vengono generate le chiamate $localize
le rende impossibili da cambiare dopo: devi riavviare l’applicazione.Ma se non ti dispiace aggiornare il browser, è possibile.Ho testato una semplice strategia che funziona:
- un utente seleziona una nuova lingua (per esempio lo spagnolo).
- memorizziamo la lingua nel browser (per esempio nel localStorage)
- ricarichiamo la pagina, che riavvia l’applicazione
- in
polyfills.ts
, iniziamo a leggere la lingua memorizzata - carichiamo il set di traduzioni corretto per lo spagnolo con
loadTranslations
.
Ovviamente, questo sarà più fluido in futuro, o in una futura versione di Angular, o tramite una libreria dell’ecosistema.Comunque, ci stiamo avvicinando a spedire solo una versione della nostra applicazione, e caricare solo le traduzioni a runtime \o/
Tutti i nostri materiali (ebook, formazione online e training) sono aggiornati con questi cambiamenti se volete saperne di più!
Leave a Reply