React / Redux y aplicaciones multilingües (internacionalización) - Arquitectura

119

Estoy creando una aplicación que deberá estar disponible en varios idiomas y configuraciones regionales.

Mi pregunta no es puramente técnica, sino más bien sobre la arquitectura y los patrones que la gente está utilizando en la producción para resolver este problema. No pude encontrar ningún "libro de cocina" para eso, así que voy a mi sitio web favorito de preguntas y respuestas :)

Estos son mis requisitos (son realmente "estándar"):

  • El usuario puede elegir el idioma (trivial)
  • Al cambiar el idioma, la interfaz debería traducirse automáticamente al nuevo idioma seleccionado
  • No estoy demasiado preocupado por formatear números, fechas, etc. en este momento, quiero una solución simple para simplemente traducir cadenas

Aquí están las posibles soluciones en las que podría pensar:

Cada componente se ocupa de la traducción de forma aislada

Esto significa que cada componente tiene, por ejemplo, un conjunto de archivos en.json, fr.json, etc. junto con las cadenas traducidas. Y una función auxiliar para ayudar a leer los valores de los que dependen del idioma seleccionado.

  • Ventaja: más respetuoso con la filosofía React, cada componente es "independiente"
  • Contras: no puede centralizar todas las traducciones en un archivo (para que otra persona agregue un nuevo idioma, por ejemplo)
  • Contras: todavía necesita pasar el idioma actual como un accesorio, en cada componente sangriento y sus hijos

Cada componente recibe las traducciones a través de los accesorios.

Por lo tanto, no conocen el idioma actual, solo toman una lista de cadenas como accesorios que coinciden con el idioma actual

  • Ventaja: dado que esas cadenas vienen "desde arriba", se pueden centralizar en algún lugar
  • Contras: cada componente ahora está vinculado al sistema de traducción, no puede simplemente reutilizar uno, debe especificar las cadenas correctas cada vez

Omite un poco los accesorios y posiblemente use el contexto para transmitir el idioma actual

  • Ventaja: es mayormente transparente, no es necesario pasar el idioma actual y / o las traducciones a través de accesorios todo el tiempo
  • Contras: parece incómodo de usar

Si tienes alguna otra idea, ¡dilo!

¿Cómo lo haces?

Antoine Jaussoin
fuente
2
Prefiero la idea de un objeto de claves con cadenas de traducción que se transmite como un accesorio, no tienes que pasar cada cadena como un accesorio individualmente. Cambiar esto en un nivel superior debería desencadenar una nueva renderización. No creo que usar el contexto sea una buena idea para esto, y cada componente que tiene acceso al archivo de traducción los hace menos "tontos" y portátiles en realidad, en mi opinión (y más difícil hacer que la aplicación se vuelva a renderizar al cambiar de idioma).
Domingo
1
En realidad, de acuerdo con facebook.github.io/react/docs/context.html , usar el contexto para compartir el idioma actual es uno de los casos de uso legítimos. El enfoque que estoy tratando ahora es usar esto más un Componente de orden superior para lidiar con la lógica de extraer las cadenas para ese componente en particular (probablemente basado en alguna clave)
Antoine Jaussoin
1
Quizás también puedas echarle un vistazo a Instant . Ellos lidian con este problema de una manera completamente diferente al abordarlo en el frontend ala Optimizely (también conocido como alterar el DOM durante la carga).
Marcel Panse
1
¡No está mal! De hecho, es una bestia completamente diferente (que lo vincula a un servicio que podría necesitar pagar si su sitio web crece), pero me gusta la idea y, de hecho, ¡probablemente valga la pena para un sitio web pequeño que necesita para comenzar a funcionar rápidamente!
Antoine Jaussoin
4
Además, es posible que desee mencionar que es cofundador de Instant, en lugar de decir "Ellos" como si no tuviera nada que ver con ellos :)
Antoine Jaussoin

Respuestas:

110

Después de probar bastantes soluciones, creo que encontré una que funciona bien y debería ser una solución idiomática para React 0.14 (es decir, no usa mixins, sino componentes de orden superior) ( editar : ¡también perfectamente bien con React 15, por supuesto! ).

Entonces aquí la solución, comenzando por la parte inferior (los componentes individuales):

El componente

Lo único que necesitaría su componente (por convención) es un stringsaccesorio. Debe ser un objeto que contenga las diversas cadenas que necesita su componente, pero realmente la forma depende de usted.

Contiene las traducciones predeterminadas, por lo que puede usar el componente en otro lugar sin la necesidad de proporcionar ninguna traducción (funcionaría de inmediato con el idioma predeterminado, inglés en este ejemplo)

import { default as React, PropTypes } from 'react';
import translate from './translate';

class MyComponent extends React.Component {
    render() {

        return (
             <div>
                { this.props.strings.someTranslatedText }
             </div>
        );
    }
}

MyComponent.propTypes = {
    strings: PropTypes.object
};

MyComponent.defaultProps = {
     strings: {
         someTranslatedText: 'Hello World'
    }
};

export default translate('MyComponent')(MyComponent);

El componente de orden superior

En el fragmento anterior, es posible que haya notado esto en la última línea: translate('MyComponent')(MyComponent)

translate en este caso, es un componente de orden superior que envuelve su componente y proporciona alguna funcionalidad adicional (esta construcción reemplaza los mixins de versiones anteriores de React).

El primer argumento es una clave que se usará para buscar las traducciones en el archivo de traducción (usé el nombre del componente aquí, pero podría ser cualquier cosa). El segundo (observe que la función está modificada, para permitir que los decoradores de ES7) sea el componente en sí para envolver.

Aquí está el código para el componente de traducción:

import { default as React } from 'react';
import en from '../i18n/en';
import fr from '../i18n/fr';

const languages = {
    en,
    fr
};

export default function translate(key) {
    return Component => {
        class TranslationComponent extends React.Component {
            render() {
                console.log('current language: ', this.context.currentLanguage);
                var strings = languages[this.context.currentLanguage][key];
                return <Component {...this.props} {...this.state} strings={strings} />;
            }
        }

        TranslationComponent.contextTypes = {
            currentLanguage: React.PropTypes.string
        };

        return TranslationComponent;
    };
}

No es mágico: simplemente leerá el idioma actual del contexto (y ese contexto no se desangra en toda la base del código, solo se usa aquí en este contenedor), y luego obtendrá el objeto de cadenas relevante de los archivos cargados. Esta pieza de lógica es bastante ingenua en este ejemplo, podría hacerse de la manera que realmente desee.

La pieza importante es que toma el lenguaje actual del contexto y lo convierte en cadenas, dada la clave proporcionada.

En lo más alto de la jerarquía

En el componente raíz, solo necesita establecer el idioma actual desde su estado actual. El siguiente ejemplo usa Redux como implementación similar a Flux, pero se puede convertir fácilmente usando cualquier otro marco / patrón / biblioteca.

import { default as React, PropTypes } from 'react';
import Menu from '../components/Menu';
import { connect } from 'react-redux';
import { changeLanguage } from '../state/lang';

class App extends React.Component {
    render() {
        return (
            <div>
                <Menu onLanguageChange={this.props.changeLanguage}/>
                <div className="">
                    {this.props.children}
                </div>

            </div>

        );
    }

    getChildContext() {
        return {
            currentLanguage: this.props.currentLanguage
        };
    }
}

App.propTypes = {
    children: PropTypes.object.isRequired,
};

App.childContextTypes = {
    currentLanguage: PropTypes.string.isRequired
};

function select(state){
    return {user: state.auth.user, currentLanguage: state.lang.current};
}

function mapDispatchToProps(dispatch){
    return {
        changeLanguage: (lang) => dispatch(changeLanguage(lang))
    };
}

export default connect(select, mapDispatchToProps)(App);

Y para terminar, los archivos de traducción:

Archivos de traducción

// en.js
export default {
    MyComponent: {
        someTranslatedText: 'Hello World'
    },
    SomeOtherComponent: {
        foo: 'bar'
    }
};

// fr.js
export default {
    MyComponent: {
        someTranslatedText: 'Salut le monde'
    },
    SomeOtherComponent: {
        foo: 'bar mais en français'
    }
};

¿Qué piensan ustedes?

Creo que esto resuelve todo el problema que estaba tratando de evitar en mi pregunta: la lógica de traducción no sangra en todo el código fuente, está bastante aislada y permite reutilizar los componentes sin ella.

Por ejemplo, MyComponent no necesita ser envuelto por translate () y podría estar separado, permitiendo su reutilización por cualquier otra persona que desee proporcionar el stringspor sus propios medios.

[Edición: 31/03/2016]: Recientemente trabajé en un tablero de retrospectiva (para retrospectivas ágiles), construido con React & Redux, y es multilingüe. Dado que muchas personas pidieron un ejemplo de la vida real en los comentarios, aquí está:

Puede encontrar el código aquí: https://github.com/antoinejaussoin/retro-board/tree/master

Antoine Jaussoin
fuente
Esta es una solución genial ... ¿se pregunta si todavía está de acuerdo con esto después de unos meses? No he encontrado muchos consejos en la forma de consejos sobre patrones para esto en línea
Damon
2
De hecho, encontré que funciona perfectamente (para mis necesidades). Hace que el componente funcione sin traducción de forma predeterminada, y la traducción simplemente viene encima sin que el componente lo sepa
Antoine Jaussoin
1
@ l.cetinsoy puede usar el dangerouslySetInnerHTMLaccesorio, solo tenga en cuenta las implicaciones (desinfecte manualmente la entrada). Ver facebook.github.io/react/tips/dangerously-set-inner-html.html
Teodor Sandu
6
¿Hay alguna razón por la que no haya probado react-intl?
SureshCS
1
Realmente me gusta esta solución. Una cosa que agregaría que nos pareció muy útil para la coherencia y el ahorro de tiempo es que si tiene muchos componentes con cadenas comunes, podría aprovechar las variables y la dispersión entre los objetos, por ejemploconst formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Huw Davies
18

Desde mi experiencia, el mejor enfoque es crear un estado i18n redux y usarlo, por muchas razones:

1- Esto te permitirá pasar el valor inicial desde la base de datos, archivo local o incluso desde un motor de plantilla como EJS o jade

2- Cuando el usuario cambia el idioma, puede cambiar todo el idioma de la aplicación sin siquiera actualizar la interfaz de usuario.

3- Cuando el usuario cambia el idioma, esto también le permitirá recuperar el nuevo idioma de API, archivo local o incluso de constantes

4- También puede guardar otras cosas importantes con las cadenas como zona horaria, moneda, dirección (RTL / LTR) y lista de idiomas disponibles

5- Puede definir el cambio de idioma como una acción normal de reducción

6- Puede tener sus cadenas de backend y front-end en un solo lugar, por ejemplo, en mi caso, uso i18n-node para la localización y cuando el usuario cambia el idioma de la interfaz de usuario, solo hago una llamada a la API normal y en el backend, simplemente regreso i18n.getCatalog(req)esto devolverá todas las cadenas de usuario solo para el idioma actual

Mi sugerencia para el estado inicial del i18n es:

{
  "language":"ar",
  "availableLanguages":[
    {"code":"en","name": "English"},
    {"code":"ar","name":"عربي"}
  ],
  "catalog":[
     "Hello":"مرحباً",
     "Thank You":"شكراً",
     "You have {count} new messages":"لديك {count} رسائل جديدة"
   ],
  "timezone":"",
  "currency":"",
  "direction":"rtl",
}

Módulos extra útiles para i18n:

1- plantilla de cadena esto le permitirá inyectar valores entre las cadenas de su catálogo, por ejemplo:

import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة

2- formato humano este módulo le permitirá convertir un número a / desde una cadena legible por humanos, por ejemplo:

import humanFormat from "human-format";
//...
humanFormat(1337); // => '1.34 k'
// you can pass your own translated scale, e.g: humanFormat(1337,MyScale)

3- momentjs la biblioteca npm de fechas y horas más famosas, puede traducir moment pero ya tiene una traducción incorporada, solo necesita pasar el idioma del estado actual, por ejemplo:

import moment from "moment";

const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م

Actualización (14/06/2019)

Actualmente, hay muchos marcos que implementan el mismo concepto usando la API de contexto de reacción (sin redux), yo personalmente recomendé I18next

Fareed Alnamrouti
fuente
¿Funcionaría este enfoque también para más de dos idiomas? Considerando la configuración del catálogo
tempranova
Votado en contra. Esto no responde a la pregunta. OP pidió una idea de arquitectura, no una sugerencia o comparación con cualquier biblioteca i18n.
TrungDQ
9
Sugerí el catálogo i18n como estado redux, parece que no entiendes redux
Fareed Alnamrouti
5

La solución de Antoine funciona bien, pero tiene algunas advertencias:

  • Utiliza el contexto React directamente, que tiendo a evitar cuando ya uso Redux
  • Importa directamente frases de un archivo, lo que puede ser problemático si desea obtener el idioma necesario en tiempo de ejecución, del lado del cliente
  • No utiliza ninguna biblioteca i18n, que es liviana, pero no le da acceso a funcionalidades de traducción útiles como pluralización e interpolación.

Es por eso que hemos construido redux-políglota en la parte superior de ambos Redux y de Airbnb Políglota .
(Soy uno de los autores)

Proporciona :

  • un reductor para almacenar el idioma y los mensajes correspondientes en su tienda Redux. Puede suministrar ambos mediante:
    • un middleware que puede configurar para capturar una acción específica, deducir el idioma actual y obtener / recuperar mensajes asociados.
    • envío directo de setLanguage(lang, messages)
  • un getP(state)selector que recupera un Pobjeto que expone 4 métodos:
    • t(key): función T políglota original
    • tc(key): traducción en mayúscula
    • tu(key): traducción en mayúsculas
    • tm(morphism)(key): traducción personalizada modificada
  • un getLocale(state)selector para obtener el idioma actual
  • un translatecomponente de orden superior para mejorar sus componentes de React inyectando el pobjeto en accesorios

Ejemplo de uso simple:

enviar nuevo idioma:

import setLanguage from 'redux-polyglot/setLanguage';

store.dispatch(setLanguage('en', {
    common: { hello_world: 'Hello world' } } }
}));

en componente:

import React, { PropTypes } from 'react';
import translate from 'redux-polyglot/translate';

const MyComponent = props => (
  <div className='someId'>
    {props.p.t('common.hello_world')}
  </div>
);
MyComponent.propTypes = {
  p: PropTypes.shape({t: PropTypes.func.isRequired}).isRequired,
}
export default translate(MyComponent);

¡Por favor dígame si tiene alguna pregunta / sugerencia!

Jalil
fuente
1
Mucho mejores frases originales para traducir. Y crear una herramienta que analice todos los componentes en busca de _()funciones, por ejemplo, para obtener todas esas cadenas. Por lo tanto, puede traducirlo en un archivo de idioma más fácilmente y no meterse con variables locas. En algunos casos, las páginas de destino necesitan que una parte específica del diseño se muestre de manera diferente. Por lo tanto, también debería estar disponible alguna función inteligente de cómo elegir la opción predeterminada frente a otras opciones posibles.
Roman M. Koss
Hola @Jalil, ¿hay algún ejemplo completo con middleware?
ArkadyB
Hola @ArkadyB, Lo usamos en producción en varios proyectos que no son de código abierto. Puede encontrar más información en el módulo README: npmjs.com/package/redux-polyglot ¿Tiene alguna pregunta / dificultad para usarlo?
Jalil
Mi principal problema con esto y polyglot.js es que está reinventando completamente la rueda en lugar de construir sobre archivos PO. Esta biblioteca alternativa parece prometedora npmjs.com/package/redux-i18n . No creo que sea muy diferente, solo proporciona una capa adicional para convertir hacia y desde archivos PO.
icc97
2

A partir de mi investigación sobre esto, parece haber dos enfoques principales que se utilizan para i18n en JavaScript, ICU y gettext .

Solo he usado gettext, así que soy parcial.

Lo que me sorprende es lo pobre que es el apoyo. Vengo del mundo PHP, ya sea CakePHP o WordPress. En ambas situaciones, es un estándar básico que todas las cadenas estén simplemente rodeadas por__('') , luego, más adelante, obtienes traducciones usando archivos PO muy fácilmente.

gettext

Obtiene la familiaridad de sprintf para formatear cadenas y los archivos PO serán traducidos fácilmente por miles de agencias diferentes.

Hay dos opciones populares:

  1. i18next , con el uso descrito en esta publicación de blog de arkency.com
  2. Jed , con el uso descrito por el publicación de sentry.io y esta publicación de React + Redux ,

Ambos tienen soporte de estilo gettext, formato de cadenas de estilo sprintf e importación / exportación a archivos PO.

i18next tiene una extensión React desarrollada por ellos mismos. Jed no lo hace. Sentry.io parece usar una integración personalizada de Jed con React. La publicación React + Redux , sugiere usar

Herramientas: jed + po2json + jsxgettext

Sin embargo, Jed parece una implementación más centrada en gettext, es decir, su intención expresa, mientras que i18next solo la tiene como una opción.

UCI

Esto tiene más apoyo para los casos extremos en torno a las traducciones, por ejemplo, para tratar el género. Creo que verá los beneficios de esto si tiene idiomas más complejos para traducir.

Una opción popular para esto es messageformat.js . Discutido brevemente en este tutorial del blog sentry.io . messageformat.js fue desarrollado por la misma persona que escribió Jed. Hace afirmaciones bastante sólidas sobre el uso de la UCI :

Jed es una característica completa en mi opinión. Estoy feliz de corregir errores, pero generalmente no estoy interesado en agregar más a la biblioteca.

También mantengo messageformat.js. Si no necesita específicamente una implementación de gettext, le sugiero que use MessageFormat en su lugar, ya que tiene mejor soporte para plurales / género y tiene datos de configuración regional incorporados.

Comparación aproximada

gettext con sprintf:

i18next.t('Hello world!');
i18next.t(
    'The first 4 letters of the english alphabet are: %s, %s, %s and %s', 
    { postProcess: 'sprintf', sprintf: ['a', 'b', 'c', 'd'] }
);

messageformat.js (mi mejor suposición al leer la guía ):

mf.compile('Hello world!')();
mf.compile(
    'The first 4 letters of the english alphabet are: {s1}, {s2}, {s3} and {s4}'
)({ s1: 'a', s2: 'b', s3: 'c', s4: 'd' });
icc97
fuente
Votado en contra. Esto no responde a la pregunta. OP pidió una idea de arquitectura, no una sugerencia o comparación con cualquier biblioteca i18n.
TrungDQ
@TrungDQ Esto es lo que preguntó el OP: "Mi pregunta no es puramente técnica, sino más bien sobre la arquitectura y los patrones que la gente está utilizando en la producción para resolver este problema". . Estos son dos patrones que se utilizan en la producción.
icc97
En mi opinión, esta respuesta no proporciona la información que estoy (y otros están) buscando. La información que proporcionó es útil, pero tal vez para otra pregunta. Solo quiero contribuir con mi voto negativo para que la respuesta correcta aparezca en la parte superior (espero).
TrungDQ
@TrungDQ Si no es lo que estás buscando, vota a favor de la que usaste e ignora las demás en lugar de rechazar las respuestas perfectamente válidas que no coinciden con la parte específica de la pregunta que te interesa
icc97
1

Si aún no ha terminado, eche un vistazo a https://react.i18next.com/ puede ser un buen consejo . Se basa en i18next: aprender una vez - traducir en todas partes.

Su código se verá así:

<div>{t('simpleContent')}</div>
<Trans i18nKey="userMessagesUnread" count={count}>
  Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>

Viene con muestras para:

  • paquete web
  • cra
  • expo.js
  • next.js
  • integración de libro de cuentos
  • borrachera
  • eso
  • ...

https://github.com/i18next/react-i18next/tree/master/example

Además de eso, también debe considerar el flujo de trabajo durante el desarrollo y luego para sus traductores -> https://www.youtube.com/watch?v=9NOzJhgmyQE

Jamuhl
fuente
Esto no responde a la pregunta. OP pidió una idea de arquitectura, no una sugerencia o comparación con cualquier biblioteca i18n.
TrungDQ
@TrungDQ al igual que con su comentario sobre mi respuesta que votó en contra, el OP solicitó las soluciones actuales utilizadas en producción. Sin embargo, sugerí i18next en mi respuesta de febrero
icc97
0

Me gustaría proponer una solución simple usando create-react-app .

La aplicación se creará para cada idioma por separado, por lo que toda la lógica de traducción se moverá fuera de la aplicación.

El servidor web proporcionará el idioma correcto automáticamente, según el encabezado Accept-Language , o manualmente mediante la configuración de una cookie. .

En general, no cambiamos el idioma más de una vez, si es que lo hacemos)

Los datos de traducción se colocan dentro del mismo archivo de componente, que lo usa, junto con estilos, html y código.

Y aquí tenemos un componente totalmente independiente que es responsable de su propio estado, vista, traducción:

import React from 'react';
import {withStyles} from 'material-ui/styles';
import {languageForm} from './common-language';
const {REACT_APP_LANGUAGE: LANGUAGE} = process.env;
export let language; // define and export language if you wish
class Component extends React.Component {
    render() {
        return (
            <div className={this.props.classes.someStyle}>
                <h2>{language.title}</h2>
                <p>{language.description}</p>
                <p>{language.amount}</p>
                <button>{languageForm.save}</button>
            </div>
        );
    }
}
const styles = theme => ({
    someStyle: {padding: 10},
});
export default withStyles(styles)(Component);
// sets laguage at build time
language = (
    LANGUAGE === 'ru' ? { // Russian
        title: 'Транзакции',
        description: 'Описание',
        amount: 'Сумма',
    } :
    LANGUAGE === 'ee' ? { // Estonian
        title: 'Tehingud',
        description: 'Kirjeldus',
        amount: 'Summa',
    } :
    { // default language // English
        title: 'Transactions',
        description: 'Description',
        amount: 'Sum',
    }
);

Agregue la variable de entorno de idioma a su package.json

"start": "REACT_APP_LANGUAGE=ru npm-run-all -p watch-css start-js",
"build": "REACT_APP_LANGUAGE=ru npm-run-all build-css build-js",

¡Eso es!

Además, mi respuesta original incluía un enfoque más monolítico con un solo archivo json para cada traducción:

lang / ru.json

{"hello": "Привет"}

lib / lang.js

export default require(`../lang/${process.env.REACT_APP_LANGUAGE}.json`);

src / App.jsx

import lang from '../lib/lang.js';
console.log(lang.hello);
Igor Sukharev
fuente
¿No funcionaría solo en tiempo de compilación? ¿Sin la posibilidad de que el usuario cambie el idioma sobre la marcha? Entonces sería un caso de uso diferente.
Antoine Jaussoin
La aplicación se compilará para todos los idiomas necesarios. El servidor web ofrecerá la versión correcta de forma automática, según el encabezado "Accept-Language", o mediante una cookie establecida por el usuario sobre la marcha. Al hacer esto, toda la lógica de traducción se podría mover fuera de la aplicación.
Igor Sukharev