Índice de contenidos
1. Entorno
Este tutorial está escrito usando el siguiente entorno:
- Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
- Sistema Operativo: LUbuntu 18.04
- StencilJS 0.15.2
2. Introducción
Ya llevo tiempo diciendo que StencilJS me parece la mejor tecnología para la creación de librerías de Web Components 100% nativos y reutilizables en cualquier aplicación web con Angular, Vue, React o ninguno de ellos.
Además, en las últimas versiones han añadido mejoras en el desarrollo como un starter mucho más simple y comprensible que el antiguo clone de su proyecto starter y la generación automática de la documentación de los componentes en función del uso de los decoradores y las anotaciones en JSDoc, entre otras.
Lo que no tienen de serie es una solución estándar para la internacionalización de los componentes desarrollados con esta tecnología; pero como veremos en este tutorial no es complicado hacer la implementación necesaria para el soporte de internacionalización.
3. Vamos al lío
Partimos de que ya tenemos una librería de componentes implementada con StencilJS y queremos que alguno de ellos se pueda internacionalizar. Cada componente será responsable de sus propios ficheros de internaciolización que tendrán extensión .json y el siguiente formato: nombre-componente.i18n.locale.json, por ejemplo, tnt-hello.i18n.es.json, y este fichero contendrá las claves con sus valores de esta forma:
{ "hello": "Hola, mi nombre es " }
Y en Inglés:
{ "hello": "Hello, my name is " }
A fin de que estos ficheros se distribuyan dentro de una carpeta global «i18n» vamos a modificar el fichero stencil.config.js para copiar todos los ficheros de idiomas de todos los componentes en la carpeta «i18n», el fichero quedaría de esta forma:
import { Config } from '@stencil/core'; export const config: Config = { namespace: 'my-lib', outputTargets:[ { type: 'dist' }, { type: 'docs' }, { type: 'www', serviceWorker: null // disable service workers } ], copy: [{ src: "**/*.i18n.*.json", dest: "assets/i18n" }] };
Una vez preparados los ficheros de idiomas, ahora necesitamos una forma de cargarlos de forma dinámica en función del lenguaje seleccionado. Para ello vamos a crear el fichero ./src/utils/locale.ts donde vamos a establecer una serie de funciones.
La primera de ellas nos va a calcular el lenguaje seleccionado en función de la propiedad lang que esté más cerca de nuestro componente. Es decir, si la propiedad lang está definida a nivel global de la página, cogerá ese locale y si esta definido a nivel de componente cogerá el locale del componente.
function getComponentClosestLanguage(element: HTMLElement): string { let closestElement = element.closest('[lang]') as HTMLElement; return closestElement ? closestElement.lang : 'en'; }
Ahora vamos a añadir la función encargada de cargar el json adecuado en función del nombre del componente y del locale. Para ello no hacemos uso de ninguna librería, simplemente creamos una promesa con el resultado de la función fetch.
function fetchLocaleStringsForComponent(componentName: string, locale: string): Promise { return new Promise((resolve, reject): void => { fetch(`/assets/i18n/${componentName}.i18n.${locale}.json`) .then((result) => { if (result.ok) resolve(result.json()); else reject(); }, () => reject()); }); }
Por último, vamos a exportar una función que se va a encargar de devolver las cadenas del json seleccionado por componente y locale, y en caso de que no exista el locale seleccionado devolver el de Inglés por defecto y mostrar un mensaje de warning en el consola del navegador.
export async function getLocaleComponentStrings(element: HTMLElement): Promise { let componentName = element.tagName.toLowerCase(); let componentLanguage = getComponentClosestLanguage(element); let strings; try { strings = await fetchLocaleStringsForComponent(componentName, componentLanguage); } catch (e) { console.warn(`no locale for ${componentName} (${componentLanguage}) loading default locale en.`); strings = await fetchLocaleStringsForComponent(componentName, 'en'); } return strings; }
Ahora dentro de los componentes de los que queramos hacer uso de la internacionalización, tenemos que cargar las cadenas específicas dentro de la función componentWillLoad() definida en el ciclo de vida del componente. De esta forma nos aseguramos de que el componente no se carga hasta que no se resuelva la promesa de recuperación de los strings a mostrar.
También hacemos uso de @Element para pasar la referencia del componente a la función y definimos un atributo strings donde vamos a almacenar las cadenas a poder utilizar.
import { getLocaleComponentStrings } from '../../utils/locale'; ... @Element() element: HTMLElement; strings: any; async componentWillLoad(): Promise { this.strings = await getLocaleComponentStrings(this.element); }
Ahora para utilizar estas cadenas simplemente tenemos que hacer uso del atributo «strings» de esta forma:
render() { return (<p>{this.strings.hello} {this.name}</p>); }
Ahora cuando hagamos uso de nuestro componente, el idioma se ajustará al idioma definido en el HTML donde se incruste de forma automática, y si queremos que tenga un idioma distinto al marcado para la página, podemos especificárselo gracias al atributo lang que existe en todos los elementos HTML de esta forma:
<tnt-hello name="Ruben" lang="es"></tnt-hello>
En caso de no definir ningún atributo lang, ni en el componente ni en la página, se pondrá el idioma por defecto definido en la función «getLocaleComponentStrings», en nuestro caso, Inglés.
Ahora cuando queramos utilizar esta solución en una aplicación de Angular debemos mapear los ficheros de idiomas para que Angular los pueda tener en cuenta. Para ello editamos el fichero angular.json y añadimos la siguiente línea en la sección de «assets»:
... "assets": [ "assets", "favicon.ico", { "glob": "**/*", "input": "./node_modules/nombre-libreria", "output": "/nombre-libreria/" }, {"glob": "**/*", "input": "./node_modules/nombre-libreria/dist/collection/assets/i18n", "output": "/i18n/" } ], ...
Haciéndolo de esta forma nuestro componente adoptará por defecto los ficheros de idiomas definidos en la librería de StencilJS, pero si queremos cambiar el texto de un idioma en la aplicación de Angular, solo tenemos que crear la carpeta «i18n» dentro de la carpeta «assets» y copiar el fichero que queremos cambiar respetando el formato nombre-componente.i18n.locale.json
4. Conclusiones
Hemos visto cómo creando unas pocas funciones y sin hacer uso de pesadas librerías podemos añadirle fácilmente el soporte de i18n a nuestros componentes con StencilJS que pueden ser utilizados en cualquier sitio.
Cualquier duda o sugerencia en la zona de comentarios.
Saludos