Un aspecto clave de la experiencia de usuario es la internacionalización (i18n). Ahora bien, desde el momento en que decides construir tu aplicación mediante componentes de ReactJS, las opciones disponibles no son abundantes. Uno de los ejemplos más potentes de i18n con ReactJS es la librería de Yahoo, react-intl. Sin embargo, la solución de Yahoo de utilizar la i18n como un componente no es óptima por dos motivos: por un lado, porque pervierte el espíritu del desarrollo con componentes al realizar una operación puramente lógica (traducción) sin un verdadero efecto visual, ya que será otro componente el encargado de presentar el resultado en pantalla; por otro, porque al integrar la i18n en los componentes mismos, no se puede reutilizar la librería en proyectos legacy, por ejemplo.

Por estos motivos creé Rosetta, un intermediario entre los componentes de la aplicación y librerías de terceros, que son quienes realizan el trabajo duro de traducción. Así, Rosetta se convierte en una façade que expone una serie de métodos de conveniencia para realizar la traducción y trabajar con ReactJS. Esta solución permite reutilizar librerías de i18N geniales, al tiempo que mantiene la filosofía del desarrollo orientado a componentes.

Cómo funciona Rosetta

Para mostrar cómo funciona Rosetta voy a mostrar un ejemplo muy simple, donde un mensaje de bienvenida podrá cambiarse a varios idiomas.

Empiezo creando un componente para presentar el mensaje a lo grande con un H1.

// Header.js
import React, {Component} from 'react';

const Header = (props, context) => <h1>{props.literal}</h1>  
Header.contextTypes = {  
  i18n: React.PropTypes.object,
};

export default Header;  

Por otro lado, construí una matriz de botones que me permite cambiar de idiomas.

// Button.js
import React, {Component} from 'react';

const Button = (props, context) => (  
  <button onClick={props.handleClick.bind(props.handleClick, context.i18n, context.languages[props.isoCode])}>{props.label}</button>
);
Button.contextTypes = {  
  i18n: React.PropTypes.object,
  languages: React.PropTypes.object
};

export default Button;  

Con estos dos componentes en mente hice el Javascript que es el punto de entrada.

// app.js
import React, {Component} from 'react';  
import ReactDom from 'react-dom';

import Header from './Header';  
import Button from './Button';

Primero he importando lo básico, tanto los componentes como ReactJS. Luego cargo e instancio Rosetta, junto con el diccionario de literales:

// app.js

import Rosetta, {CHANGE_TRANSLATION_EVENT, rosetta} from '@schibstedspain/rosetta';  
import Polyglot from '@schibstedspain/rosetta/lib/adapters/polyglot';


const languages = {  
  'es-ES': {
    'hello world': 'Hola mundo',
    'spanish': 'Español',
    'catalan': 'Català',
    'english': 'English',
    'review the console': 'Abre la consola' 
  },
  'ca-ES': {
    'hello world': 'Hola món',
    'spanish': 'Español',
    'catalan': 'Català',
    'english': 'English',
    'review the console': 'Obre la consola' 
  },
  'en-GB': {
    'hello world': 'Hello world',
    'spanish': 'Español',
    'catalan': 'Català',
    'english': 'English',
    'review the console': 'Open the console'
  }
};

const i18n = new Rosetta();  
i18n.adapter = new Polyglot();  
i18n.setTranslationsSilent(languages['en-GB']);  

En resumen, he creado una instancia de Rosetta, junto con una instancia de un adaptador de Polyglot. Si no configuras el adaptador, por defecto utilizaría uno que solo devuelve la key sin traducir y loguea por consola que has tratado de traducir cierta key.

Por otra parte, configuré "silenciosamente" el primer diccionario que va a utilizar Rosetta. Cuando digo "silenciosamente" quiero decir que, como Rosetta es una subclase de EventEmitter, cada vez que se configura un idioma de la forma

i18n.translations = languages['es-ES'];  

lanzaremos el evento CHANGE_TRANSLATION_EVENT. Esto permite poder hacer cosas así:

i18n.on(CHANGE_TRANSLATION_EVENT, (dic) => console.log('The dictionary has been changed to', dic));  

De esta manera, el evento no se emitirá.

Solo queda, pues, crear el componente principal:

@rosetta(i18n, languages)
class App extends Component {  
  constructor(...args){
    super(...args);
    this.i18n = this.getChildContext().i18n;
  }

  changeLanguage(i18n, language){
    i18n.translations = language;
  }

  render(){
    return (
      <div>
        <Header literal={this.i18n.t('hello world')}/>
        <Button handleClick={this.changeLanguage.bind(this)} isoCode='es-ES' label={this.i18n.t('spanish')}/>
        <Button handleClick={this.changeLanguage.bind(this)} isoCode='ca-ES' label={this.i18n.t('catalan')}/>
        <Button handleClick={this.changeLanguage.bind(this)} isoCode='en-GB' label={this.i18n.t('english')}/>
        <br/>
        <small>*{this.i18n.t('review the console')}</small>
      </div>
    )

  }
}

Rosetta y React

La parte más importante de este código -- que es un componente bastante simple y que puedes encontrar en Github -- es que, para usar Rosetta, hay que decorar el componente root de la aplicación con el decorador @rosetta al cual le he pasado la instancia de Rosetta i18n y los idiomas, languages. Este decorador hará dos cosas súper importantes:

  • Agregar i18n y languages al contexto, para que cualquier hijo de nuestro componente puede acceder a ellas solo con definirlas en el contextTypes

  • Hacer un forceRender de toda la aplicación, cada vez que se asigna un nuevo idioma a Rosetta, para actualizarla con el nuevo idioma

Si hubiera usado react-router no hubiera podido acceder al componente root, porque sería una instancia de Router. Por eso Rosetta cuenta con un método de conveniencia para hacer lo mismo que hace el decorador pasando el constructor del componente:

i18n.addToContext(Router, languages);  

En cuanto al isomorfismo, os puedo decir por experiencia propia que, al ser instanciable, tanto Rosetta como el adaptador son perfectos para este escenario.

Bonus

Si estás desarrollando tus componentes siguiendo la arquitectura de contenedor-contenido, la experiencia ha demostrado que es mejor que la traducción se haga en el contenedor y se la pase al contenido mediante propiedades.

Si te ha gustado este post, difunde la palabra. Tampoco dudes en dejar comentarios u observaciones. ¡Gracias! :)

Suscríbete a mi lista de correo

* Campos obligatorios