Integración de i18n en tu aplicación React: Automatizando las traducciones

31 de diciembre de 2022

Al 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-app
npm install
npm run dev

Vamos a ver que nuestra aplicación se abre en el navegador, y que podemos ver los componentes iniciales de nuestro proyecto. Vite React App

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.

main.tsx
Copy

import { IntlProvider } from "react-intl";
ReactDOM.render(
<React.StrictMode>
<IntlProvider locale="en" defaultLocale="en">
<App />
</IntlProvider>
</React.StrictMode>,
document.getElementById("root"),
);

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 o sessionStorage: 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:

src/main.tsx
Copy

//...
const getUserLocale = () => {
const locale = localStorage.getItem("locale");
if ((locale && locale === "es") || locale === "en") {
return locale;
}
return "es";
};
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<IntlProvider
locale={getUserLocale()}
defaultLocale="es"
>
<App />
</IntlProvider>
</React.StrictMode>,
);

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: How to handle i18n for React Apps

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:

App.tsx
Copy

import { FormattedMessage } from "react-intl";
const App = () => {
return (
<div>
<FormattedMessage
id="app.title"
defaultMessage="Como manejar i18n para aplicaciones React"
description="Titulo de la aplicacion"
/>
</div>
);
};

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.

.eslintrc
Copy

{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {}
}

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.js
env.d.ts

Creamos un .prettierrc y añadimos una configuracion basica para formatear nuestro codigo.

.prettierrc
Copy

{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 120,
"bracketSpacing": true
}

Por ultimo vamos a crear en nuestro archivo package.json un script para correr ESLint.

package.json
Copy

"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
}

Instalando el plugin eslint-plugin-formatjs

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:

.eslintrc
Copy

{
// Añade formatjs a la lista de plugins de ESLint
"plugins": ["formatjs"],
// Añade la regla formatjs/enforce-id a la lista de reglas de ESLint
"rules": {
"formatjs/enforce-id": [
"error",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]"
}
]
}
}

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.

Imagen de un mensaje de error en el IDE

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.

App.tsx
Copy

import { FormattedMessage } from "react-intl";
const App = () => {
return (
<div>
<FormattedMessage
id="G2U8Pi"
defaultMessage="Como manejar i18n para aplicaciones React"
description="Titulo de la aplicacion"
/>
</div>
);
};

!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

Extrayendo los mensajes con formatjs extract

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.

package.json
Copy

"scripts": {
"extract-messages": "formatjs extract \"src/**/*.{js,ts,tsx,.jsx}\" --out-file src/translations/messages/es.json --id-interpolation-pattern [sha512:contenthash:base64:6] --ignore src/vite-env.d.ts",
}

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 carpeta src y que tengan la extension .tsx, .jsx, .js. Asegurate de cambiar src por la carpeta donde tengas tus archivos de React.
  • --out-file src/translations/messages/es.json: --out-file sirve para indicarle a formatjs extract en que archivo queremos que se guarden los mensajes extraídos. Cambia src/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 patron sha512: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 archivo src/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.

src/translations/messages/es.json
Copy

{
"G2U8Pi": {
"defaultMessage": "Como manejar i18n para aplicaciones React",
"description": "Titulo de la aplicacion"
}
}

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.

src/translations/messages/en.json
Copy

{
"G2U8Pi": {
"defaultMessage": "How to handle i18n for React applications",
"description": "Application title"
}
}

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.

package.json
Copy

"scripts": {
"compile-messages": "formatjs compile-folder --ast src/translations/messages src/translations/compiled-messages"
}

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.

src/translations/compiled-messages/es.json
src/translations/compiled-messages/en.json
Copy

{
"G2U8Pi": [
{
"type": 0,
"value": "Como manejar i18n para aplicaciones React"
}
]
}

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

src/main.tsx
Copy

import { IntlProvider } from "react-intl";
import messagesInEnglish from "./translations/compiled-messages/en.json";
import messagesInSpanish from "./translations/compiled-messages/es.json";
const translations = { en: messagesInEnglish, es: messagesInSpanish };
const getUserLocale = () => {
const locale = localStorage.getItem("locale");
if ((locale && locale === "es") || locale === "en") {
return locale;
}
return "es";
};
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<IntlProvider
messages={translations[getUserLocale()]}
locale={getUserLocale()}
defaultLocale="es"
>
<App />
</IntlProvider>
</React.StrictMode>,
);

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.

src/App.tsx
Copy

import { FormattedMessage } from "react-intl";
const App = () => {
const changeLanguage = (locale: string) => {
localStorage.setItem("locale", locale);
// Vamos a recargar la pagina para que se actualice el idioma en nuestro provider
// En una aplicacion real, recomendaría buscar una solucion mas elegante
window.location.reload();
};
return (
<div>
<FormattedMessage
id="G2U8Pi"
defaultMessage="Como manejar i18n para aplicaciones React"
description="Titulo de la aplicacion"
/>
<div
style={{
display: "flex",
justifyContent: "center",
gap: "10px",
padding: "10px",
}}
>
<button onClick={() => changeLanguage("es")}>ES</button>
<button onClick={() => changeLanguage("en")}>EN</button>
</div>
</div>
);
};

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.