Internacionalización con @angular/localize

Se ha hecho un gran progreso en el frente i18n¡ Un nuevo paquete llamado @angular/localize ha sido introducido en Angular 9.0.

Se utiliza bajo el capó para darnos las mismas características que teníamos anteriormente:traducciones en plantillas en tiempo de compilación.

Pero nos permite esperar más en el futuro,con características no documentadas ya disponibles,como traducciones en código,o traducciones en tiempo de ejecución en lugar de traducciones de compilación solamente 😎.

Comencemos por ver lo que podemos hacer con la ayuda de @angular/localizey el CLI en v9.

i18n en plantillas

El nuevo paquete @angular/localize ofrece una función llamada $localize.

El soporte de i18n existente en Angular ahora utiliza $localize, lo que significa que las plantillas como:

<h1 i18n>Hello</h1>

se compilarán a llamadas $localize.

Si ejecuta ng serve con una plantilla de este tipo, se encontrará con un error de ejecución:

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

El error se explica por sí mismo: como los atributos i18n se convierten ahora en llamadas $localizeen el código generado, tenemos que cargar la función $localize.Se podría hacer por defecto, pero como sólo se requiere si usted está utilizando la internacionalización, no tendría realmente sentido.

Es por eso que si su aplicación, o una de sus dependencias, utiliza i18n atributos en sus plantillas, entonces usted tendrá que añadir import '@angular/localize/init' a su polyfills!

La CLI ofrece un esquema para hacer esto para usted.Simplemente ejecuta:

ng add @angular/localize

y la CLI añade el paquete a tus dependencias y la importación necesaria a tus polyfills.

Después, cuando ejecutes tu aplicación con un simple ng serve,$localize simplemente muestra el mensaje original.

¿Ahora cómo traduces estos mensajes?

El proceso es muy similar al que teníamos anteriormente.Primero ejecutas ng xi18n para extraer los mensajes en un archivo messages.xlf.Luego traduces el archivo para tus locales, por ejemplo messages.fr.xlfy messages.es.xlf.

Entonces tienes que configurar la CLI, en 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" } } } // ...}

Ahora, las configuraciones es o fr permiten ejecutar:

ng serve --configuration=fr

¡Y la aplicación servida está ahora en francés!

También puedes construir la aplicación con una localización específica:

ng build --configuration=production,es

o con todas las localizaciones a la vez:

ng build --prod --localize

Esto es un gran progreso en comparación con las versiones anteriores de Angular.Antes teníamos que construir la misma aplicación para cada localización, ya que la traducción era parte de la compilación.Ahora, cuando Angular compila la aplicación, genera llamadas a $localize.Luego, cuando esto se hace, una herramienta toma la aplicación compilada y reemplaza todas las llamadas a $localize con las traducciones apropiadas.Esto es súper rápido.Entonces tienes un paquete que no contiene llamadas a $localize y todas las cadenas i18n han sido traducidas.

Hasta ahora, tenías que construir tu aplicación una vez por localización,y esto era una construcción completa. ¡Así que digamos que es una compilación de 30s, y querías 4 locales, entonces estabas en 2 minutos.

Con el nuevo enfoque, la compilación se hace una vez, y luego las diversas versiones i18n se generan en unos pocos segundos(incluso se genera en paralelo si es posible).Así que pasas de 2 minutos a ~40 segundos! 🌈

Entonces tienes varios paquetes, uno por localización, y puedes servir el apropiado a tus usuarios dependiendo de sus preferencias, como solías hacer.

Esta estrategia se llama inlining en tiempo de compilación, ya que inlinas las traducciones directamente, y entonces no hay nada que hacer en tiempo de ejecución.

Ahora hablemos de algo todavía no documentado, que puede cambiar en el futuro, pero que sigue siendo interesante conocer: ¡ahora también podemos traducir mensajes en nuestro código TypeScript!

La función $localize de la que he hablado puede usarse directamente. Es una función peculiar que puedes utilizar para etiquetar una cadena de plantilla para su localización.

¿Pero quizás deberíamos empezar explicando qué es una cadena de plantilla etiquetada?

Cadenas de plantilla y funciones de etiqueta

Cuando se utilizan cadenas de plantilla, se puede definir una función de etiqueta, y aplicarla a una cadena de plantilla.Aquí askQuestion añade un punto de interrogación al final de la cadena:

const askQuestion = strings => strings + '?';const template = askQuestion`Is anyone here`;

¿Entonces cuál es la diferencia con una función simple?La función de etiqueta, de hecho, recibe varios argumentos:

  • un array de las partes estáticas de la cadena
  • los valores resultantes de la evaluación de las expresiones

Por ejemplo, si tenemos una cadena de plantilla que contiene expresiones:

const person1 = 'Cedric';const person2 = 'Agnes';const template = `Hello ${person1}! Where is ${person2}?`;

entonces la función de etiqueta recibirá las distintas partes estáticas y dinámicas.Aquí tenemos una función etiqueta para poner en mayúsculas los nombres de los protagonistas:

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 en código TypeScript

$localize utiliza esta mecánica para permitirnos escribir:

@Component({ template: '{{ title }}'})export class HomeComponent { title = $localize`You have 10 users`;}

Nota que no tienes que importar la función.Siempre que añada import '@angular/localize/init' una vez en su aplicación,$localize se añadirá al objeto global.

Puede entonces traducir el mensaje de la misma manera que lo haría para una plantilla.Pero, ahora mismo (v9.0.0), la CLI no extrae estos mensajes con el comando xi18n como lo hace para las plantillas.

Si sirves la aplicación y no se encuentra ninguna traducción, $localize simplemente muestra la cadena original, y registra una advertencia en la consola:

No translation found for "6480943972743237078" ("You have 10 users").

Así que tienes que añadirlo manualmente a tu messages.fr.xlfcon el ID dadosi quieres probar:

<trans-unit> <source>You have 10 users</source> <target>Vous avez 10 utilisateurs</target></trans-unit>

¡La plantilla de mi HomeComponent muestra entonces Vous avez 10 utilisateurs!

¿Qué pasa si tienes alguna expresión dinámica en tu cadena de plantilla?

title = $localize`Hi ${this.name}! You have ${this.users.length} users.`;

Las expresiones se llamarán automáticamente PH y PH_1 (PH es para el marcador de posición).A continuación, puede utilizar estos marcadores de posición donde quiera en las traducciones:

<trans-unit> <source>Hi <x/>! You have <x/> users.</source> <target>Bonjour <x/>&nbsp;! Vous avez <x/> utilisateurs.</target></trans-unit>

Pero la mejor práctica es dar un nombre de marcador de posición significativo a la expresión usted mismo, y puede hacerlo utilizando la sintaxis ${expression}:placeholder:.

title = $localize`Hi ${this.name}:name:! You have ${this.users.length}:userCount: users.`;

Entonces puede utilizar este marcador de posición donde quiera en las traducciones:

<trans-unit> <source>Hi <x/>! You have <x/> users.</source> <target>Bonjour <x/>&nbsp;! Vous avez <x/> utilisateurs.</target></trans-unit>

Identificadores personalizados

Nótese que si tiene traducciones con identificadores personalizados, éstos son utilizados por $localize (como era el caso anteriormente):

<h1 i18n="@@home.greetings">Hello</h1>

Entonces tu traducción queda como:

<trans-unit> <source>Hello</source> <target>Bonjour</target></trans-unit>

que obviamente es más agradable de usar.

¿Qué hay de las traducciones en código?$localize también entiende una sintaxis que permite especificar un ID:

title = $localize`:@@home.users:You have 10 users`;

La sintaxis para el ID personalizado es la misma que en las plantillas,y el ID está rodeado de dos puntos para separarlo del contenido de la traducción.

Al igual que en la sintaxis de las plantillas, también se puede especificar una descripción y un significado, para ayudar a los traductores con un poco de contexto: :meaning|[email protected]@id:message.

Por ejemplo:

title = $localize`:greeting message with the number of users currently logged [email protected]@home.users:You have 10 users`;

Tenga en cuenta que se trata de una API de bajo nivel, sin documentar.El equipo de Angular o la comunidad probablemente ofrecerá funciones de mayor nivelcon una mejor experiencia para el desarrollador (bueno, eso espero!).Locl de Olivier Combe, el autor de ngx-translateprobablemente vale la pena mantener un ojo en 🧐.

Traducciones en tiempo de ejecución

Como decía, si se utilizan los comandos CLI anteriores (ng serve --configuration=fr o ng build --localize) la aplicación se compila y se traduce antes de llegar al navegador, por lo que no hay llamadas a $localize en tiempo de ejecución.

Pero $localize se ha diseñado para ofrecer otra posibilidad: traducciones en tiempo de ejecución.¿Qué significa? Bueno, podríamos enviar una sola aplicación, que contenga las llamadas de $localize, y antes de que la aplicación se inicie, podríamos cargar las traducciones que queramos.Se acabaron las N construcciones y los N paquetes para N locales \N -o/

Sin entrar demasiado en los detalles, esto ya es posible con la v9, utilizando la función loadTranslations que ofrece @angular/localize.Pero esto tiene que hacerse antes de que la aplicación se inicie.

Puedes cargar tus traducciones en polyfills.ts con:

import '@angular/localize/init';import { loadTranslations } from '@angular/localize';loadTranslations({ '1815172606781074132': 'Bonjour {$name}\xa0! Vous avez {$userCount} utilisateurs.'});

Como puedes ver no hay ninguna consideración de localización:sólo cargas tu traducción como un objeto,cuyas claves son las cadenas a traducir,y los valores, sus traducciones.

Ahora si ejecuta un simple ng serve, el título se muestra en francés! Y no hay más necesidad de ng xi18n, o messages.fr.xlf o la configuración específica para cada configuración regional en angular.json.En el largo plazo, cuando esto será debidamente apoyado y documentado, debemos ser capaces de cargar archivos JSON en tiempo de ejecución, como la mayoría de las bibliotecas i18n hacer.Incluso se podría lograr en v9, es sólo un poco de trabajo manual, pero es factible.

¿Qué pasa con el cambio de la configuración regional sobre la marcha entonces?podemos cargar otro conjunto de traducciones cuando se inicia la aplicación?Bueno, no. La forma actual en que se generan las llamadas $localize hace imposible cambiarlas después: hay que reiniciar la aplicación.Pero si no te importa refrescar el navegador, es posible.He probado una estrategia sencilla que funciona:

  • un usuario selecciona un nuevo idioma (por ejemplo el español).
  • almacenamos el idioma en el navegador (por ejemplo en el localStorage)
  • recargamos la página, que reinicia la aplicación
  • en polyfills.tsempezamos por leer el idioma almacenado
  • cargamos el conjunto adecuado de traducciones para el español con loadTranslations.

Por supuesto, esto será más suave en el futuro, ya sea en una futura versión de Angular,o a través de una librería del ecosistema.De todos modos, cada vez estamos más cerca de enviar sólo una versión de nuestra aplicación, y sólo cargar las traducciones en tiempo de ejecución \No/

Todos nuestros materiales (ebook, formación online y entrenamiento) están actualizados con estos cambios si quieres aprender más!

Leave a Reply