Internacionalización con React.js y FormatJS

Cuando hacemos una aplicación web que aspira a crecer y ser usada por personas de cualquier parte del planeta nos toca ofrecer nuestra aplicación en distintos idiomas para que cualquier lo pueda entender facilmente, hacer esto se lo conoce como internacionalización, abreviado i18n (esto significa que hay 18 letras entre la i y la n en la palabra inglesa internationalization).

Hay muchas formas de realizar esto dependiendo de las tecnologías usadas, cuando trabajamos con React.js la forma casi estandar de hacer esto es usar la librería FormatJS de Yahoo! que posee integración con varios sistema de vistas entre ellos React.

Integrando FormatJS a React

Lo primero que necesitamos es descargar la implementación para React de FormatJS, react-intl, para eso lo hacemos con npm usando el comando:

npm i -S react-intl

Una vez descargado lo importamos en nuestros componentes de la siguiente forma:

import React, {
createClass,
PropTypes,
} from 'react';
import {
IntlMixin,
} from 'react-intl';
const MyComponent = createClass({
displayName: 'My Component',
propTypes: {
locales: PropTypes.string.isRequired,
messages: PropTypes.objectOf(PropTypes.string).isRequired,
},
mixins: [
IntlMixin,
],
render() {
return (
<div>
{ this.getIntlMessage('helloWorld') }
</div>
);
},
});
export default MyComponent;

Veamos que se hacemos acá. Lo primero es cargar React, la función la crear un componente (createClass, muy mal nombre BTW) y el objeto PropTypes.

Luego cargamos de la librería react-intl de el mixin IntlMixin, esto nos incluye varias funciones para i18n en React.

A continuación creamos nuestro componente que va a recibír dos props, locales donde vamos a recibír un string con el idioma y messages donde recibimos un objeto con todos los strings de cada mensaje que vayamos a usar en nuestro component. A nuestro componente le pasamos también un listado de los mixins a usar, ahí pasamos el mixin IntlMixin, por último en el render de nuestro componente usamos la función getIntlMessage pasandole el key que usemos para identificar el mensaje a usar.

Por último renderizamos nuestro componente pasandole el lenguaje y los mensajes:

import React from 'react';
import ReactDOM from 'react-dom';
import MyComponent from './components/MyComponent';
const data = {
locales: 'es',
messages: {
helloWorld: 'Hola mundo',
},
};
const target = document.getElementById('renderTarget');
ReactDOM.render(<MyComponent {...data} />, target);

Al momento de renderizarse nuestro componente va a recibír el idioma y los mensajes que le pasamos y los va a usar para cargar los textos, sin embargo hasta ahora no estamos usando el idioma para nada, así que vamos a empezar a usarlo, el idioma nos sirve para cuando queremos usar las funciones para mostrar fechas según el idioma.

Mostrando fechas según el idioma

Para usar estas funciones necesitamos cargar un componte que nos provee react-intl llamado FormattedDate y luego pasarle la fecha en una propiedad llamada value y la forma en que queremos mostrarla en propiedades llamadas day, month y year.

<FormattedDate
value={new Date()}
day="numeric"
month="long"
year="numeric"
/>

Al momento de renderizar nuestro componente FormattedDate se va a encargar de mostrar la fecha según el idioma provisto en la propiedad locales, si el idioma es español se mostraría 26 de setiembre de 2015, si el idioma es inglés entonces se mostraría September 26, 2015.

Mostrando fechas relativas

Otra funcionalidad es la de mostrar fechas relativas, esto se hace con el componente FormattedRelative, este componente se cargar de la misma forma que FormattedDate aunque en este caso solo es necesario pasar la propiedad value la cual debe poseer una fecha diferente a la actual.

<FormattedRelative
value={Date.now() - (1000 * 60 * 60 * 24)}
/>
<FormattedRelative
value={Date.now() - (1000 * 60 * 60 * 2)}
/>
<FormattedRelative
value={Date.now() + (1000 * 60 * 51)}
/>

En este ejemplo estamos mostrando 3 fechas diferentes, la primera es exactamente un día antes de la actual, la segunda 2 horas y la tercera es dentro de 1 hora.

Si el locales que le pasamos a nuestro componente indica un idioma español estas fechas se mostrarías como ayer, hace 2 horas y en 1 hora. Si el idioma fuese inglés mostraría yestarday, 2 hours ago y in 1 hour.

Este componente muestra por defecto el que considere el mejor mensaje posible, es posible además forzar la forma en que se va a mostrar pasandole una propiedad llamada units.

<FormattedRelative
value={Date.now() - (1000 * 60 * 60 * 22)}
units="minute"
/>

Si lo usamos de esta forma mostraría el mensaje hace 1.320 minutos en español o 1,320 minutos ago si esta inglés, acá podemos ver además que dependiendo del idioma cambia el separador de miles entre coma o punto.

Pluralización de mensajes

Además de elegír mensajes o formatear fechas es posible pluralizar mensajes, esto quiere decír uqe muestra palabras distintas dependiendo de una cantidad, para eso usamos el componente FormattedMessage que recibiría un mensaje obtenido con getIntlMessage y cualquier variable que queramos pasarle, y en este caso lo importante esta en el mensaje que pasamos con getIntlMessage donde hay que usar un formato similar a este:

`El {takenDate, date, long}, {name} {numPhotos, plural,
=0 {no}
other {}
} sacó {numPhotos, plural,
=0 {ninguna foto.}
=1 {una foto.}
other {# fotos.}
}

De esta forma podemos pasarle este mensaje al componente FormattedMessage junto con los valores takenDate (el cual sería una fecha y se mostraría en formato largo (con el nombre completo), name donde indicaríamos un nombre y numPhotos el cual sería un número.

<FormattedMessage
message={this.getIntlMessage('photos')}
name="Sergio"
numPhotos={1000}
takenDate={new Date()}
/>
<FormattedMessage
message={this.getIntlMessage('photos')}
name="Sergio"
numPhotos={0}
takenDate={new Date()}
/>
<FormattedMessage
message={this.getIntlMessage('photos')}
name="Sergio"
numPhotos={1}
takenDate={new Date()}
/>

Estos tres casos se mostrarían los mensajes:

El 26 de setiembre de 2015, Sergio sacó 1.000 fotos.
El 26 de setiembre de 2015, Sergio no sacó ninguna foto.
El 26 de setiembre de 2015, Sergio sacó una foto.

De esta forma podemos facilmente modificar el mensaje dependiendo de algún número evitando hacer cosas como mostrar un (s) al final de una palabra que puede o no estar en plural.

Usando HTML en los mensajes

Cuando queremos usar un mensaje con HTML vemos que FormattedMessage no funciona, en este caso necesitamos usar el componente FormattedHTMLMessage, este componente funcionaría de forma similiar a FormattedMessage con la diferencia de que ahora podemos meter código HTML dentro del string del mensaje.

{numComments, plural,
=0 {sin comentarios}
=1 {<strong>1</strong> comentario}
other {<strong>#</strong> comentarios}
}

Este mensaje podemos luego pasarselo a FormattedHTMLMessage así:

<FormattedHTMLMessage
message={this.getIntlMessage('comments')}
numComments={0}
/>
<FormattedHTMLMessage
message={this.getIntlMessage('comments')}
numComments={1}
/>
<FormattedHTMLMessage
message={this.getIntlMessage('comments')}
numComments={42}
/>

Y al renderizarse estos tres componentes se mostrarían los mensajes:

sin comentarios
1 comentario
42 comentarios

Con el 1 y el 42 en negrita gracias a la etiqueta <strong> que pusimos en nuestro mensaje.

Formateando números

Por últimos tenemos al componente FormattedNumber que nos permite elegír como mostrar números con un separador de comas dependiendo del idioma, agregar un símbolo de porcentaje o mostrar una moneda (incluyendo el símbolo).

<FormattedNumber value={42000} />
<FormattedNumber value={0.9} format="percentage" />
<FormattedNumber value={29} format="USD" />

Estos tres números con idioma español se mostrarían:

42.000
90%
US$29

Y si el idioma es inglés se mostrarían:

42,000
90%
$29

Como se puede ver gracias a FormatJS es posible aplicar i18n a nuestros componentes de React facilmente dándonos funcionalidad para trabajar con fechas, fechas relativas, pluralización de mensajes con y sin HTML o formatear números y monedas.

Para quien quiera leer más sobre como armar mensajes para la pluralización puede hacerlo en la documentación de FormatJS.