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/localizee 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.xlfe 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 $localizee 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.xlfcon 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/>&nbsp;! 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/>&nbsp;! 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|[email protected]@id:message.

Per esempio:

title = $localize`:greeting message with the number of users currently logged [email protected]@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.xlfo 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