react i18n data architecture internationalization reactjs translation redux

architecture - i18n - react translate



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

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 acerca de la arquitectura y los patrones que las personas realmente usan en la producción para resolver este problema. No pude encontrar ningún "libro de cocina" para eso, así que me dirijo a mi sitio web favorito de preguntas y respuestas :)

Aquí están mis requisitos (son realmente "estándar"):

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

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

Cada componente se ocupa de la traducción 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 aquellos que dependen del idioma seleccionado.

  • Pro: más respetuoso de la filosofía React, cada componente es "independiente"
  • Contras: no puede centralizar todas las traducciones en un archivo (para que alguien más agregue un nuevo idioma, por ejemplo)
  • Contras: todavía necesita pasar el idioma actual como 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

  • Pro: 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 usa el context para pasar el idioma actual

  • Pro: es principalmente transparente, no tiene que pasar el idioma actual y / o las traducciones a través de accesorios todo el tiempo
  • Contras: parece engorroso de usar

Si tienes alguna otra idea, ¡por favor dilo!

¿Cómo lo haces?


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 soporte. 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 en la línea 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 la publicación sentry.io y esta publicación 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 expresada, mientras que i18next solo lo tiene como una opción.

UCI

Esto tiene más soporte 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 es desarrollado por la misma persona que escribió Jed. Él hace afirmaciones bastante duras por usar 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, podría sugerirle usar MessageFormat en su lugar, ya que tiene un mejor soporte para plurales / género y tiene datos locales integrados.

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 conjetura al leer la guide ):

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'' });


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

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

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

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

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

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

6- Puede tener sus cadenas de back-end 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 API normal y en el back-end, solo regreso i18n.getCatalog(req) esto devolverá todas las cadenas de usuario solo para el idioma actual

Mi sugerencia para el estado inicial de 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- string-template 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- human-format este módulo le permitirá convertir un número a / de 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 las bibliotecas npm de fechas y horas más famosas, puede traducir moment pero ya tiene una traducción incorporada solo necesita pasar el idioma de 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), personalmente recomendé I18next


Después de probar algunas 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 accesorio de strings . 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 (en este ejemplo, funcionaría de forma predeterminada con el idioma predeterminado, inglés)

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 utilizará para buscar las traducciones en el archivo de traducción (utilicé el nombre del componente aquí, pero podría ser cualquier cosa). El segundo (tenga en cuenta que la función es curry, para permitir decoradores ES7) es el Componente 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: solo leerá el lenguaje actual del contexto (y ese contexto no sangra por 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 desea.

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 está usando Redux como la implementación de tipo 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 resuelve todo el problema que estaba tratando de evitar en mi pregunta: la lógica de traducción no sangra por todo el código fuente, está bastante aislada y permite reutilizar los componentes sin ella.

Por ejemplo, MyComponent no necesita estar envuelto por translate () y podría estar separado, lo que permite su reutilización por cualquier otra persona que desee proporcionar las strings por su propio medio.

[Editar: 31/03/2016]: Recientemente trabajé en un tablero retrospectivo (para Agile Retrospectives), construido con React & Redux, y es multilingüe. Como 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


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

  • Utiliza el contexto Reaccionar 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 prácticas funciones de traducción como la pluralización y la interpolación.

Es por eso que construimos redux-polyglot sobre Redux y Polyglot de AirBNB.
(Soy uno de los autores)

Proporciona :

  • un reductor para almacenar el idioma y los mensajes correspondientes en su tienda Redux. Puede suministrar ambos por:
    • un middleware que puede configurar para capturar acciones específicas, deducir el idioma actual y obtener / recuperar mensajes asociados.
    • despacho directo de setLanguage(lang, messages)
  • Un getP(state) que recupera un objeto P que expone 4 métodos:
    • t(key) : función políglota T original
    • tc(key) : traducción en mayúscula
    • tu(key) : traducción en mayúsculas
    • tm(morphism)(key) : traducción personalizada
  • un getLocale(state) para obtener el idioma actual
  • un componente de translate orden superior para mejorar sus componentes React mediante la inyección del objeto p en accesorios

Ejemplo de uso simple:

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


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

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

El servidor web servirá el idioma correcto automáticamente, dependiendo del encabezado Accept-Language , o manualmente configurando una cookie .

Principalmente, 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 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 y 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 una variable de entorno de idioma a su paquete.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!

También 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);


Si aún no lo ha hecho, echar un vistazo a https://react.i18next.com/ podría ser un buen consejo. Se basa en i18next: aprender una vez, traducir a todas partes.

Su código se verá más o menos 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
  • dat
  • ...

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

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