Commit c7225e9c by Sebastián Tobar

initial commit, with source code

parent a43d7081
import React from 'react';
import translate from './translate-context.jsx';
function Example1(props) {
return (
<div className="container">
<h1>{props.strings.administrate}</h1>
<div className="users">
<h2>{props.strings.users}</h2>
<p>User1</p>
<p>User2</p>
</div>
<div className="posts">
<h2>{props.strings.posts}</h2>
<p>Post1</p>
<p>Post2</p>
</div>
</div>
)
}
Example1.propTypes = {
strings: PropTypes.object,
};
Example1.defaultProps = {
strings: {
administrate: 'Administrar',
users: 'Usuarios',
posts: 'Artículos',
},
};
export default translate('Example1')(Example1);
import es from './i18n/es';
import en from './i18n/en';
const LanguageHelper = {
availableLanguages: () => ({
es,
en,
}),
isLanguageDefined: () => (
localStorage.getItem('language') !== null
),
initializeLanguage: () => {
if (!LanguageHelper.isLanguageDefined()) { // no hay idioma definido aún, setearlo
const availableLangs = Object.keys(LanguageHelper.availableLanguages()); // ["en", "es"]
const navLangString = navigator.language.split('-')[0]; // navigator.language es de la forma "en-us", "es-la", etc
const navigatorLang = availableLangs.filter(lang => navLangString === lang)[0];
if (navigatorLang) { // usar idioma encontrado
localStorage.setItem('language', navigatorLang);
} else { // usar idioma inglés
localStorage.setItem('language', LanguageHelper.availableLanguages().en.code);
}
} else { // sí hay idioma definido con anterioridad; ver si ese idioma está disponible aún
const currentLanguage = localStorage.getItem('language');
if (Object.keys(LanguageHelper.availableLanguages()).indexOf(currentLanguage) === -1) {
console.error('language on localstorage not available:', currentLanguage);
localStorage.setItem('language', LanguageHelper.availableLanguages().en.code);
}
}
},
getCurrentLanguage: () => {
LanguageHelper.initializeLanguage();
return localStorage.getItem('language');
},
changeLanguage: (lang) => {
localStorage.setItem('language', lang);
},
};
export default LanguageHelper;
Introducción
-
Alrededor de agosto del 2016 nos pidieron que uno de nuestros sistemas tuviera soporte para múltiples idiomas. La idea era que los usuarios pudieran usar la aplicación en el idioma que ellos quisieran, que en un principio iba a ser, aparte del español original, inglés y portugués.
Puesto que la aplicación en cuestión se estructura en un backend y en un frontend, la internacionalización (o i18n para abreviar) se tuvo que hacer en ambos componentes. En este artículo voy a hablar de la i18n del frontend de la aplicación.
La traducción de cualquier tipo de frontend implica que, en vez de usarse strings estáticos para desplegar texto, se deben usar strings dinámicos, las cuales vienen de _alguna parte_ y dependen de un parámetro idioma, el cual se fija de _algún modo_. Hay muchas alternativas para fijar el idioma y proveer los strings dinámicos.
Para esta implementación me basé en [una respuesta específica de stackoverflow](http://stackoverflow.com/a/33422278), e implementé en base a ella un sistema de provisión de strings para componentes de React.
Parámetro idioma
-
Para configurar el idioma de forma global se usa el contexto de React, guardándolo también en localStorage para hacerlo persistente; sin embargo, como los cambios en el contexto no actualizan todos los componentes, hace falta recargar la aplicación completa, configurando el contexto al inicializar la aplicación.
La primera vez que se fija el idioma, este se obtiene del parámetro `navigator.language`, y se revisa si existe en los idiomas disponibles. Si no existe, se deja un lenguaje por defecto.
De ser posible, es muy recomendable usar una store como Redux para guardar el parámetro idioma, con lo cual al cambiar el idioma todos los componentes se traducen automáticamente, sin necesidad de recargar.
Diccionario de strings dinámicos
-
Tras fijar el parámetro idioma, se pueden generar los strings dinámicos. Para este caso, estos están centralizados en un archivo JSON: estos definen un objeto con la siguiente estructura:
`code: 'es',
Languages: {
es: 'Español',
en: 'Inglés'
},
FirstComponent: {
first: 'Primero',
other: 'Otro',
},
SecondComponent: {
second: 'Segundo',
stuff: 'Cosas',
}`
En la estructura va un parámetro `code`, que identifica al idioma; un parámetro `Languages`, que nombra el resto de los idiomas; y un objeto por cada componente que quiera traducirse, compuesto de claves y valores que después son accedidas directamente por el componente a traducir.
Una posibilidad era hacer un diccionario completo de strings que podría usar cualquiera de los componentes que requieran traducción; sin embargo se descartó esa opción por considerar esta más ordenada. Cada grupo de componentes tiene su propio subdiccionario, los cuales no interactúan entre sí, pudiendo dejar cada frase con su propio contexto.
Provisión de diccionarios a componentes
-
Luego de hacer un diccionario para cada idioma, es necesaria alguna manera de pasarle este diccionario a cada componente que lo requiera. Esto se logra en React con componentes de alto orden (o _higher order component_ en inglés), los cuales son componentes que devuelven otro componente, con alguna modificación.
En este caso, se usan componentes de alto orden para inyectar a un componente el diccionario de strings.
Este componente realiza los siguientes pasos:
* carga los diccionarios de todos los idiomas disponibles
* obtiene el idioma seleccionado mediante el contexto, y lo ocupa para cargar el diccionario adecuado
* recibe la clave del subdiccionario deseado, como `FirstComponent` o `SecondComponent`
* obtiene el subdiccionario y lo inyecta al componente con el nombre "strings"
Agregando las validaciones correspondientes a la obtención del diccionario de idioma y al subdiccionario de componente, resulta una implementación robusta que no deshabilita el programa completo, si es que se le pide un idioma que no existe o un componente mal escrito.
Usando el diccionario
-
Una vez que ya se le pasa el subdiccionario al componente, basta con que este acceda a el, mediante sus props. Esto implica reemplazar cada instancia de un string por su equivalente `this.props.strings.string`, lo cual es la parte más larga de toda la operación.
A modo de ilustración: implementar la solución de stackoverflow tomó alrededor de tres días. Reemplazar los 750 strings que tenía el programa tomó más de dos semanas.
Desde entonces, recomiendo encarecidamente que, si usted está planeando una aplicación y quisiera que tuviera más de un idioma, se avise lo antes posible: reemplazar todas esas líneas es carísimo...
Hacer que un componente dependa de un prop `strings` tiene el potencial de aumentar mucho el acoplamiento; se recomienda usar el parámetro `defaultProps` para configurar un componente de forma inicial. Así, un componente puede reutilizarse fácilmente en otros proyectos.
Conclusión
-
He intentado describir a grandes rasgos los pasos tomados para implementar una traducción de una aplicación en React.
La solución implementada es altamente especializada, sin embargo se puede reutilizar con poco trabajo; teniendo un lugar donde poner el idioma, y pudiendo usar componentes de alto orden, no debería haber mucha dificultad en replicar este método. Espero que quede más clara la explicación mirando el código fuente.
En una próxima entrada, explicaré cómo se implementó la traducción del backend de la misma aplicación, el cual es usado para obtener distintos tipos de datos, cada uno con su nombre y características especiales.
import es from './i18n/es';
import en from './i18n/en';
const LanguageHelper = {
availableLanguages: () => ({
es,
en,
}),
isLanguageDefined: () => (
localStorage.getItem('language') !== null
),
initializeLanguage: () => {
if (!LanguageHelper.isLanguageDefined()) {
const availableLangs = Object.keys(LanguageHelper.availableLanguages()); // ["en", "es"]
const navLangString = navigator.language.split('-')[0];
const navigatorLang = availableLangs.filter(lang => navLangString === lang)[0];
if (navigatorLang) {
// console.log('navigator lang', navigatorLang);
localStorage.setItem('language', navigatorLang);
} else {
localStorage.setItem('language', LanguageHelper.availableLanguages().en.code);
}
} else {
const currentLanguage = localStorage.getItem('language');
if (Object.keys(LanguageHelper.availableLanguages()).indexOf(currentLanguage) === -1) {
console.error('language on localstorage not available:', currentLanguage); // eslint-disable-line
localStorage.setItem('language', LanguageHelper.availableLanguages().en.code);
}
}
},
getCurrentLanguage: () => {
LanguageHelper.initializeLanguage();
return localStorage.getItem('language');
},
changeLanguage: (lang) => {
localStorage.setItem('language', lang);
},
};
export default LanguageHelper;
export default {
code: 'en',
Languages: {
es: 'Spanish',
en: 'English'
},
Example1: {
administrate: 'Administrate',
users: 'Users',
posts: 'Posts',
},
Example2: {
searchUsers: 'Search users',
searchPosts: 'Search posts',
},
}
export default {
code: 'es',
Languages: {
es: 'Español',
en: 'Inglés'
},
Example1: {
administrate: 'Administrar',
users: 'Usuarios',
posts: 'Artículos',
},
Example2: {
searchUsers: 'Buscar usuarios',
searchPosts: 'Buscar artículos',
},
}
import { default as React } from 'react'
import LanguageHelper from './LanguageHelper'
const languages = LanguageHelper.availableLanguages()
export default function translate(key) {
return Component => {
class TranslationComponent extends React.Component {
render() {
let strings = null
let language = null
// Revisar que venga un lenguaje en el contexto
if (typeof(this.context.language) !== 'undefined') {
language = languages[this.context.language]
}
// Revisar que el lenguaje en el contexto esté disponible
if (typeof(language) !== 'undefined' && language !== null) {
strings = language[key]
}
// Revisar que el subdiccionario esté disponible
if (typeof(strings) !== 'undefined' && strings !== null) {
return <Component {...this.props} {...this.state} strings={strings} />
}
// Si algo falla, devolver el componente sin modificar
return <Component {...this.props} {...this.state} />
}
}
// Este contexto se configura en el componente padre, usando getChildContext
TranslationComponent.contextTypes = {
language: React.PropTypes.string
}
return TranslationComponent
}
}
import React from 'react';
import { connect } from 'react-redux';
import LanguageHelper from './LanguageHelper';
const languages = LanguageHelper.availableLanguages();
export default function translate(key) {
return (Component) => {
class TranslationComponent extends React.Component {
render() {
let strings = null;
let language = null;
// Revisar que venga un lenguaje en el estado de redux
if (typeof (this.props.language) !== 'undefined') {
language = languages[this.props.language];
}
// Revisar que el lenguaje en redux esté disponible
if (typeof (language) !== 'undefined' && language !== null) {
strings = language[key];
}
// Revisar que el subdiccionario esté disponible
if (typeof (strings) !== 'undefined' && strings !== null) {
return <Component {...this.props} {...this.state} strings={strings} />;
}
// Si algo falla, devolver el componente sin modificar
return <Component {...this.props} {...this.state} />;
}
}
TranslationComponent.propTypes = {
language: React.PropTypes.string,
};
// Se asume que el estado de redux tiene un string llamado "language",
// inicializado en un componente padre.
function mapStateToProps(state) {
return {
language: state.language,
};
}
// Inyectar los props de redux
return connect(mapStateToProps)(TranslationComponent);
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment