Integración de i18n en tu aplicación React: Automatizando las traducciones
31 de diciembre de 2022Al internacionalizar nuestra aplicación, tenemos que asegurarnos que nuestro contenido (texto, imágenes, monedas, etc) se adapta a las distintas culturas, regiones o idiomas de los usuarios que visitan nuestro sitio web.
Algunos ejemplos de esto son:
- Si un usuario de un pais de habla hispana visita nuestro sitio web, es importante que el contenido se muestre en español, y no en inglés (y viseversa, para los usuarios angloparlantes).
- Al mostrar monedas en nuestra aplicación (por ejemplo, en un carrito de compras), tenemos que tomar en cuenta el formato de la moneda de la región del usuario. Por ejemplo en algunas regiones, el simbolo de la moneda va antes del número (e.g. $100), y en otras regiones va después (e.g. 100 US$).
- Al mostrar fechas, tenemos que tomar en cuenta el formato de fecha de la región del usuario. Por ejemplo, en algunos países se usa el formato de fecha DD/MM/YYYY, y en otros el formato MM/DD/YYYY.
Todas estos elementos pueden volverse muy tediosos de mantener, y es por eso que existen herramientas que nos ayudan a automatizar este proceso. Este post va a estar enfocado en como traducir el contenido de nuestra aplicacion de la forma mas automatizada, y ademas nos va a dar las bases que necesitamos para poder manejar los distintos formatos de moneda, fechas, etc.
¿Cómo traducir el contenido de un sitio web?
Este es uno de los procesos que conlleva la internacionalización (i18n) de una aplicación. Es un proceso que se puede hacer de forma manual, pero que es muy tedioso y propenso a errores. Pero como toda tarea tediosa, existen herramientas que nos ayudan a automatizar este proceso.
Por ello vamos a ver como traducir el contenido de nuestra aplicacion de forma automática con React, utilizando la librería react-intl.
Setup de nuestra aplicación
Para este ejemplo, vamos a crear una aplicación desde cero utilizando Vite y React. Si ya tienes una aplicación creada, puedes saltarte este paso, pero asegurate de tener configurado ESLint
, ya que vamos a utilizarlo para automatizar parte del proceso.
Para crear nuestra aplicación, vamos a utilizar el template de React de Vite con TypeScript:
npm create vite@latest i18n-app --template react-ts
Una vez creado el proyecto, vamos a instalar las dependencias y vamos a correr el servidor de desarrollo:
cd i18n-appnpm installnpm run dev
Vamos a ver que nuestra aplicación se abre en el navegador, y que podemos ver los componentes iniciales de nuestro proyecto.
Configurando react-intl
Ahora que tenemos nuestra aplicación, vamos a instalar la librería react-intl
:
npm install react-intl
Una vez instalada, vamos a añadir el componente IntlProvider
al root de nuestra aplicación, para que todos los componentes puedan acceder a la información de internacionalización.
Nota: Si estas usando NextJS, Remix, Gatsby, etc, el archivo root de tu aplicación sera diferente, pero el proceso es el mismo.
El componente IntlProvider
recibe un prop llamado locale
, este prop indica el idioma que queremos utilizar en nuestra aplicación.
Cambiar el idioma de nuestra aplicación
Ahora que tenemos nuestro componente IntlProvider
configurado, vamos a definir una estrategia para mantener y cambiar el idioma de nuestra aplicación.
Existen varias formas de hacer esto, cada una con sus pros y contras.
- Almacenar el idioma en el
localStorage
osessionStorage
: Esta es una buena opción si tu aplicación es solamente client side, y no necesitas mantener el idioma entre sesiones. - Almacenar el idioma en una
cookie
: Esta opcion es buena si tu aplicación es server side, y necesitas mantener el idioma entre sesiones ya que te permite leer el idioma desde el servidor. - Almacenar el idioma en el
URL
: Esta opción es las mas flexible, ya que te permite mantener el idioma entre sesiones, y te permite compartir el idioma con otras personas (por ejemplo, si compartes un link con un idioma en particular, la persona que lo visite va a ver el sitio web en ese idioma). Es muy util para sitios web que tienen contenido en distintos idiomas, y que quieren que el usuario pueda compartir el contenido en el idioma que elija. Ademas se puede combinar con la opcion anterior, para mantener el idioma entre sesiones.
Nuestro IntlProvider
tiene un prop llamado defaultLocale
, este prop indica el idioma que queremos utilizar por defecto en nuestra aplicación, en caso de que no se haya definido un idioma.
Para este ejemplo, usaremos la opción del localStorage para mantener el idioma entre sesiones.
Vamos a modificar nuestro componente IntlProvider
para que lea el idioma del localStorage:
La function funcion getUserLocale
permite obtener el idioma que el usuario ha seleccionado. Si el usuario no ha seleccionado un idioma, se va a utilizar el idioma por defecto.
Mas adelante implementaremos un componente que nos permita cambiar el idioma de nuestra aplicación.
Como automatizar el proceso de traducción
Para agilizar el proceso de traducción, vamos a utilizar formatjs
(conjunto de librerías que incluye react-intl), esta nos provee de una herramienta que nos permite automatizar este proceso.
En su sitio web podemos ver un ejemplo de cómo funciona este flujo:
El flujo de trabajo propuesto por formatjs es el siguiente:
- Definir los mensajes que queremos traducir en nuestro sitio web.
- Extraer los mensajes a un archivo de traducción.
- Traducir los mensajes (manualmente o con una herramienta de traducción).
- Compilar los mensajes traducidos.
Definir los mensajes que queremos traducir
Para esto, vamos a utilizar el componente FormattedMessage
de react-intl
, este componente nos permite definir un mensaje en un idioma, y luego utilizarlo en cualquier otro idioma.
Vamos a definir un mensaje en alguno de nuestros componentes:
El componente FormattedMessage
recibe 3 props:
id
: Este prop es un identificador unico para el mensaje, este identificador es utilizado para buscar el mensaje en el archivo de traducciones.defaultMessage
: Este prop indica el mensaje por defecto (luego vamos a traducir este mensaje a los otros idiomas).description
: Este prop es una descripcion del mensaje, esta descripcion es utilizada para ayudar a los traductores a entender el contexto del mensaje.
Generar automaticamente los ids de los mensajes
Nombrar cosas en programación, es una de las tareas más difíciles, personalmente prefiero evitar nombrar cosas manualmente siempre que sea posible, y creo que las personas en formatjs
piensan lo mismo, ya que nos proveen de una herramienta para generar automaticamente los ids de los mensajes.
Para generar automaticamente los ids de los mensajes, vamos a utilizar el plugin es ESLint eslint-plugin-formatjs
, este plugin nos provee de una regla llamada formatjs/enforce-default-message
, esta nos permite definir un mensaje sin especificar el id, y el plugin va a generar automaticamente el id del mensaje.
Asi que asegurate de tener configurado ESLint en tu proyecto, si estas siguiendo este tutorial, en la siguiente sección te explico como configurar ESLint desde cero, sino, puedes saltarte esta sección.
Configurar ESLint desde cero (opcional)
npm install -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-react prettier
Ahora vamos a crear un archivo llamado .eslintrc
en la raiz de nuestro proyecto, y vamos a configurar ESLint para que use los plugins que instalamos anteriormente.
Ahora vamos a crear un .eslintignore
en la raiz de nuestro proyecto, y vamos a indicar que archivos queremos que ignore ESLint.
node_modules/dist/.prettierrc.js.eslintrc.jsenv.d.ts
Creamos un .prettierrc
y añadimos una configuracion basica para formatear nuestro codigo.
Por ultimo vamos a crear en nuestro archivo package.json
un script para correr ESLint.
eslint-plugin-formatjs
Instalando el plugin Teniendo configurado ESLint, vamos a instalar el plugin eslint-plugin-formatjs
:
npm install -D eslint-plugin-formatjs
Ahora vamos a configurar el plugin eslint-plugin-formatjs
en nuestro archivo .eslintrc
:
Esta configuracion va a forzar a que todos los mensajes que definamos, tengan un id, y que el id sea generado automaticamente usando el patron sha512:contenthash:base64:6
(este patron es el que usa react-intl
por defecto, no tienes que preocuparte por entenderlo).
Si vemos el <FormattedMessage/>
que definimos anteriormente, vamos a ver que nuestro IDE nos esta marcando un error, esto es porque el id del mensaje no sigue el patron que definimos en la configuracion de ESLint.
Si inspeccionamos el mensaje de error, vamos a ver que nos esta indicando que el id del mensaje debe ser generado automaticamente usando el patron sha512:contenthash:base64:6
.
"id" does not match with hash pattern [sha512:contenthash:base64:6]. Expected: G2U8Pi
Para solucionar este error, podemos correr el comando que tengamos configurado en nuestro archivo package.json
para correr ESLint, en este caso es npm run lint:fix
, y ESLint va a generar automaticamente el id del mensaje.
!Genial!, ya no tenemos que preocuparnos por crear ids para nuestros mensajes manualmente! Ahora podemos avanzar con la ultima parte de nuestra automatizacion.
Creando un script para extraer los mensajes
Ya solo nos hacen falta dos cosas para terminar de automatizar el proceso de extraccion de mensajes, la primera crear un script para extraer los mensajes y la segunda es crear un script para compilar los mensajes.
Lo primero que vamos a hacer es instalar la libreria @formatjs/cli
que nos va a permitir crear los scripts que necesitamos.
npm install -D @formatjs/cli
formatjs extract
Extrayendo los mensajes con Necesitamos crear un script que se encargue de leer todos los archivos .tsx, .jsx, .js
(archivos que contienen codigo de React) y extraer todos los mensajes que definamos en ellos en un archivo .json
que luego vamos a usar como base para realizar las traducciones a otros idiomas.
Mediante este script estamos ejecutando el comando formatjs extract
que es el comando que nos provee la libreria @formatjs/cli
para extraer los mensajes, y le estamos pasando como varios argumentos:
src/**/*.{js,ts,tsx,.jsx}
: Indicamos que queremos extraer los mensajes de todos los archivos que esten dentro de la carpetasrc
y que tengan la extension.tsx, .jsx, .js
. Asegurate de cambiarsrc
por la carpeta donde tengas tus archivos de React.--out-file src/translations/messages/es.json
:--out-file
sirve para indicarle aformatjs extract
en que archivo queremos que se guarden los mensajes extraídos. Cambiasrc/translations/messages/es.json
por la ruta donde quieras que se guarden los mensajes extraídos.--id-interpolation-pattern [sha512:contenthash:base64:6]
: Indicamos que queremos que los ids de los mensajes sean generados automaticamente usando el patronsha512:contenthash:base64:6
. Esto es lo mismo que definimos en la configuracion de ESLint. Mediante ESLint ya estamos forzando a que todos los mensajes tengan un id, pero en caso de que no estemos usando ESLint, esto nos permitiria generar los ids de los mensajes automaticamente. (Personalmente recomiendo usar ESLint para poder ver los ID de los mensajes en el codigo)--ignore src/vite-env.d.ts
: Indicamos que queremos ignorar el archivosrc/vite-env.d.ts
ya que este archivo genera un error al intentar extraer los mensajes.
Existen mas opciones que podemos pasarle a formatjs extract
, puedes ver todas las opciones disponibles en la documentacion de la libreria.
Si ejecutamos nuestro script extract-messages
vamos a ver que se crea el archivo src/translations/messages/es.json
con todos los mensajes que definimos en nuestros archivos de React.
Ahora que tenemos los mensajes extraídos en un archivo .json
, podemos usar este archivo como base para realizar las traducciones a otros idiomas.
Existen varias herramientas que nos permiten subir un archivo .json
y que nos generan los archivos .json
traducidos. Estas herramientas pueden requerir que el archivo .json
tenga una estructura especifica, para ello puedes pasarle el argumento --format
a formatjs extract
para que el archivo .json
tenga la estructura que requiera la herramienta que vayas a usar.
En la documentacion de la libreria puedes ver las opciones disponibles para el argumento --format
. En este tutorial no vamos a usar ninguna herramienta para traducir los mensajes, asi que no vamos a pasarle el argumento --format
.
Por simplificar el tutorial, vamos a crear otro archivo .json
que va a tener la misma estructura que el archivo es.json
pero con los mensajes traducidos a ingles.
Compilando los mensajes extraídos
Ahora que tenemos los mensajes extraídos en un archivo .json
y tenemos los mensajes traducidos a otros idiomas en otros archivos .json
, es recomendable que los mensajes sean compilados a otro formato (AST
) para que nuestra aplicación no tenga que hacerlo en runtime.
Para compilar los mensajes vamos a usar el comando formatjs compile
que nos provee la libreria @formatjs/cli
.
Nuestro nuevo script compile-messages
ejecuta el comando formatjs compile-folder
, el cual nos permite indicar que queremos compilar todos los archivos .json
que esten dentro de la carpeta src/translations/messages
y que los archivos compilados se guarden en la carpeta src/translations/compiled-messages
.
Tambien estamos pasando el argumento --ast
para indicar que queremos que los mensajes sean compilados a formato AST
.
Adicionalmente, podemos pasarle el argumento --format
, si estamos usando un formato especifico (el que hayamos indicado al momento de realizar la extraccion de los mensajes) para formatjs extract
entienda la estructura de los archivos .json
.
Cuando ejecutemos nuestro script vamos a ver que se ha creado la carpeta src/translations/compiled-messages
y que dentro de esta carpeta se han creado los archivos .json
compilados.
!Genial! Ahora que tenemos nuestros mensajes compilados ya podemos usarlos en nuestra aplicación.
Usando los mensajes en nuestra aplicación
Ahora que tenemos los mensajes traducidos y compilados, podemos usarlos en nuestra aplicación. Para ello vamos a modificar nuesto <IntlProvider>
para que use los mensajes compilados.
Lo primero que hacemos importar nuestros mensajes compilados y creamos un objeto translations
que tenga como llaves los idiomas disponibles y como valor los mensajes compilados en ese idioma.
A nuestro <IntlProvider>
le pasamos los mensajes compilados del idioma seleccionado, el idioma seleccionado y el idioma por defecto. Y listo, ya podemos usar nuestros mensajes traducidos en nuestra aplicación.
Vamos a modificar nuestro componente <App>
para que podamos cambiar el idioma de nuestra aplicación.
Y listo, !ya podemos cambiar el idioma de nuestra aplicación! 🎉
Conclusiones
Hemos visto como podemos manejar la traducción de nuestros mensajes en aplicaciones React usando formatjs
, esto es uno de los primeros pasos que podemos dar para tener una aplicación internacionalizada.
Pero la internacionalización no es solo traducir los mensajes de nuestra aplicación, también es tener en cuenta los cambios de los formatos en textos como fechas, monedas, unidades de medida, etc. Para esto podemos usar formatjs
y sus distintos formatters
. Te recomiendo que explores los distintos formatters que ofrece formatjs
para que puedas usarlos en tu aplicación.
Espero que sigas explorando formatjs
y que puedas usarlo en tus aplicaciones.