Aplicativos React / Redux e Multilingual (Internacionalização) - Arquitetura

119

Estou construindo um aplicativo que precisará estar disponível em vários idiomas e localidades.

Minha pergunta não é puramente técnica, mas sim sobre a arquitetura e os padrões que as pessoas estão realmente usando na produção para resolver esse problema. Não consegui encontrar nenhum "livro de receitas" para isso, então estou acessando meu site de perguntas e respostas favorito :)

Aqui estão meus requisitos (eles são realmente "padrão"):

  • O usuário pode escolher o idioma (trivial)
  • Ao alterar o idioma, a interface deve ser traduzida automaticamente para o novo idioma selecionado
  • Não estou muito preocupado com a formatação de números, datas, etc. no momento, quero uma solução simples para apenas traduzir strings

Aqui estão as possíveis soluções que eu poderia imaginar:

Cada componente lida com a tradução isoladamente

Isso significa que cada componente tem, por exemplo, um conjunto de arquivos en.json, fr.json etc. junto com as strings traduzidas. E uma função auxiliar para ajudar a ler os valores daqueles que dependem do idioma selecionado.

  • Pro: mais respeitoso com a filosofia React, cada componente é "autônomo"
  • Contras: você não pode centralizar todas as traduções em um arquivo (para alguém adicionar um novo idioma, por exemplo)
  • Contras: você ainda precisa passar a linguagem atual como adereço, em cada componente sangrento e seus filhos

Cada componente recebe as traduções por meio dos adereços

Portanto, eles não estão cientes do idioma atual, eles apenas pegam uma lista de strings como acessórios que por acaso correspondem ao idioma atual

  • Pro: como essas strings estão vindo "de cima", elas podem ser centralizadas em algum lugar
  • Contras: cada componente agora está vinculado ao sistema de tradução, você não pode apenas reutilizar um, você precisa especificar as strings corretas todas as vezes

Você contorna os adereços um pouco e possivelmente usa o contexto para passar o idioma atual

  • Pro: é quase sempre transparente, não precisa passar o idioma atual e / ou traduções por adereços o tempo todo
  • Contras: parece complicado de usar

Se você tiver alguma outra idéia, diga!

Como você faz isso?

Antoine Jaussoin
fonte
2
Eu prefiro a ideia de um objeto de chaves com strings de tradução que é passado como um suporte, você não tem que passar cada string como um suporte individualmente. Alterar isso em um nível superior deve acionar uma nova renderização. Não acho que usar contexto seja uma boa ideia para isso, e cada componente com acesso ao arquivo de tradução os torna menos "burros" e portáteis, na verdade, imo (e mais difícil de fazer o aplicativo renderizar novamente na mudança de idioma).
Dominic
1
Na verdade, de acordo com facebook.github.io/react/docs/context.html , usar o contexto para compartilhar o idioma atual é um dos casos de uso legítimos. A abordagem que estou tentando agora é usar isso mais um componente de ordem superior para lidar com a lógica de extração de strings para aquele componente específico (provavelmente com base em alguma chave)
Antoine Jaussoin
1
Talvez você também possa dar uma olhada no Instant . Eles lidam com esse problema de uma maneira completamente diferente, abordando-o no frontend ala Optimizely (também conhecido como alterando o DOM durante o carregamento).
Marcel Panse
1
Nada mal! Na verdade, é uma besta completamente diferente (que conecta você a um serviço que você pode precisar pagar se o seu site crescer), mas eu gosto da ideia e provavelmente vale a pena para um pequeno site que você precisa para começar a funcionar rapidamente!
Antoine Jaussoin
4
Além disso, você pode querer mencionar que é um cofundador do Instant, em vez de dizer "Eles" como se não tivesse nada a ver com eles :)
Antoine Jaussoin

Respostas:

110

Depois de tentar algumas soluções, acho que encontrei uma que funciona bem e deve ser uma solução idiomática para React 0.14 (ou seja, não usa mixins, mas componentes de ordem superior) ( editar : também perfeitamente adequado com React 15, é claro! )

Então aqui está a solução, começando pela parte inferior (os componentes individuais):

O componente

A única coisa que seu componente precisa (por convenção), são stringsadereços. Deve ser um objeto contendo as várias strings de que seu componente precisa, mas na verdade o formato dele é com você.

Ele contém as traduções padrão, então você pode usar o componente em outro lugar sem a necessidade de fornecer qualquer tradução (funcionaria fora da caixa com o idioma padrão, inglês neste exemplo)

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

O componente de ordem superior

No snippet anterior, você deve ter notado isso na última linha: translate('MyComponent')(MyComponent)

translate neste caso, é um componente de ordem superior que envolve seu componente e fornece algumas funcionalidades extras (esta construção substitui os mixins das versões anteriores do React).

O primeiro argumento é uma chave que será usada para pesquisar as traduções no arquivo de tradução (usei o nome do componente aqui, mas pode ser qualquer coisa). O segundo (observe que a função é curryed, para permitir decoradores ES7) é o próprio componente para embrulhar.

Aqui está o código para o componente de tradução:

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

Não é mágico: ele apenas lerá a linguagem atual do contexto (e esse contexto não se espalhará por toda a base de código, apenas usado aqui neste wrapper) e, em seguida, obterá o objeto de strings relevante dos arquivos carregados. Este pedaço de lógica é bastante ingênuo neste exemplo, poderia ser feito da maneira que você realmente quiser.

A parte importante é que ele pega o idioma atual do contexto e o converte em strings, dada a chave fornecida.

No topo da hierarquia

No componente raiz, você só precisa definir o idioma atual do seu estado atual. O exemplo a seguir está usando Redux como a implementação do tipo Flux, mas pode ser facilmente convertido usando qualquer outro framework / pattern / library.

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

E para finalizar, os arquivos de tradução:

Arquivos de tradução

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

O que é que vocês acham?

Acho que resolve todo o problema que estava tentando evitar na minha pergunta: a lógica da tradução não esgota o código-fonte, é bastante isolada e permite reutilizar os componentes sem ela.

Por exemplo, MyComponent não precisa ser encapsulado por translate () e pode ser separado, permitindo sua reutilização por qualquer pessoa que deseje fornecer o stringspor seu próprio meio.

[Edit: 31/03/2016]: Eu trabalhei recentemente em uma Retrospective Board (para Agile Retrospectives), construída com React & Redux, e é multilíngue. Já que muitas pessoas pediram um exemplo da vida real nos comentários, aqui está:

Você pode encontrar o código aqui: https://github.com/antoinejaussoin/retro-board/tree/master

Antoine Jaussoin
fonte
Esta é uma solução legal .. está se perguntando se você ainda concorda com isso depois de alguns meses? Não encontrei muitos conselhos sobre padrões para isso online
Damon
2
Estou, na verdade, achei que funcionava perfeitamente (para minhas necessidades). Faz com que o componente funcione sem tradução por padrão, e a tradução simplesmente vem em cima dele sem que o componente saiba disso
Antoine Jaussoin
1
@ l.cetinsoy você pode usar o dangerouslySetInnerHTMLsuporte, apenas esteja ciente das implicações ( limpe manualmente a entrada). Veja facebook.github.io/react/tips/dangerously-set-inner-html.html
Teodor Sandu
6
Existe uma razão para você não ter tentado o react-intl?
SureshCS de
1
Realmente gosto dessa solução. Uma coisa que eu acrescentaria que achamos muito útil para consistência e economia de tempo é que, se você tiver muitos componentes com strings comuns, poderá aproveitar as variáveis ​​e espalhar sobre objetos, por exemploconst formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Huw Davies
18

Pela minha experiência, a melhor abordagem é criar um estado redux i18n e usá-lo, por vários motivos:

1- Isso permitirá que você passe o valor inicial do banco de dados, arquivo local ou mesmo de um mecanismo de template como EJS ou jade

2- Quando o usuário altera o idioma, você pode alterar o idioma de todo o aplicativo sem nem mesmo atualizar a IU.

3- Quando o usuário altera o idioma, isso também permitirá que você recupere o novo idioma da API, arquivo local ou mesmo de constantes

4- Você também pode salvar outras coisas importantes com as strings, como fuso horário, moeda, direção (RTL / LTR) e lista de idiomas disponíveis

5- Você pode definir a mudança de idioma como uma ação normal de redux

6- Você pode ter suas strings de back-end e front end em um só lugar, por exemplo, no meu caso eu uso i18n-node para localização e quando o usuário altera o idioma da IU, eu apenas faço uma chamada API normal e no back-end, eu apenas retorno i18n.getCatalog(req)isso retornará todas as strings do usuário apenas para o idioma atual

Minha sugestão para o estado inicial i18n é:

{
  "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 extras úteis para i18n:

1- string-template isso permitirá que você injete valores entre suas strings de catálogo, por exemplo:

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

2- formato humano este módulo permitirá que você converta um número de / para uma string legível por humanos, por exemplo:

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

3- momentjs a biblioteca npm de datas e horas mais famosa, você pode traduzir moment, mas ela já tem uma tradução embutida, apenas você precisa passar o idioma do estado atual por exemplo:

import moment from "moment";

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

Atualização (14/06/2019)

Atualmente, existem muitos frameworks que implementam o mesmo conceito usando a API de contexto de reação (sem redux), eu pessoalmente recomendei o I18next

Fareed Alnamrouti
fonte
Essa abordagem funcionaria bem para mais de dois idiomas? Considerando a configuração do catálogo
tempranova 05 de
Votado para baixo. Isso não responde à pergunta. OP pediu uma ideia de arquitetura, não uma sugestão ou comparação com qualquer biblioteca i18n.
TrungDQ
9
Eu sugeri o catálogo i18n como estado redux, parece que você não entende redux
Fareed Alnamrouti
5

A solução de Antoine funciona bem, mas tem algumas ressalvas:

  • Ele usa o contexto React diretamente, o que eu tendo a evitar quando já uso o Redux
  • Ele importa frases diretamente de um arquivo, o que pode ser problemático se você quiser buscar a linguagem necessária em tempo de execução, do lado do cliente
  • Ele não usa nenhuma biblioteca i18n, que é leve, mas não dá acesso a funcionalidades de tradução úteis como pluralização e interpolação

É por isso que construímos redux-poliglota sobre Redux e Poliglota do AirBNB .
(Eu sou um dos autores)

Ele fornece :

  • um redutor para armazenar linguagem e mensagens correspondentes em sua loja Redux. Você pode fornecer ambos:
    • um middleware que você pode configurar para capturar uma ação específica, deduzir o idioma atual e obter / buscar mensagens associadas.
    • despacho direto de setLanguage(lang, messages)
  • um getP(state)seletor que recupera um Pobjeto que expõe 4 métodos:
    • t(key): função T poliglota original
    • tc(key): tradução em maiúsculas
    • tu(key): tradução em maiúsculas
    • tm(morphism)(key): tradução modificada personalizada
  • um getLocale(state)seletor para obter o idioma atual
  • um translatecomponente de ordem superior para aprimorar seus componentes React, injetando o pobjeto em adereços

Exemplo de uso simples:

enviar novo idioma:

import setLanguage from 'redux-polyglot/setLanguage';

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

no 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, me diga se você tem alguma dúvida / sugestão!

Jalil
fonte
1
Frases originais muito melhores para serem traduzidas. E para fazer uma ferramenta que analisa todos os componentes para _()funções, por exemplo, para obter todas essas strings. Assim você pode traduzir em um arquivo de linguagem mais fácil e não mexer com variáveis ​​malucas. Em alguns casos, as páginas de destino precisam que uma parte específica do layout seja exibida de forma diferente. Portanto, algumas funções inteligentes de como escolher o padrão versus outras opções possíveis também devem estar disponíveis.
Roman M. Koss
Olá @Jalil, existe algum exemplo completo com middleware?
ArkadyB
Olá, @ArkadyB, Nós o usamos na produção em vários projetos que não são de código aberto. Você pode encontrar mais informações no módulo README: npmjs.com/package/redux-polyglot. Você tem alguma dúvida / dificuldade em usá-lo?
Jalil de
Meu maior problema com este e o polyglot.js é que ele está reinventando completamente a roda, em vez de construir sobre arquivos PO. Esta biblioteca alternativa parece promissora npmjs.com/package/redux-i18n . Não acho que isso seja muito diferente - é apenas fornecer uma camada extra para converter de e para arquivos PO.
icc97 de
2

De minha pesquisa sobre isso, parece haver duas abordagens principais sendo usadas para i18n em JavaScript, ICU e gettext .

Eu só usei gettext, então sou tendencioso.

O que me surpreende é como o suporte é ruim. Eu venho do mundo do PHP, CakePHP ou WordPress. Em ambas as situações, é um padrão básico que todas as strings são simplesmente rodeadas por__('') , então mais adiante na linha você obtém traduções usando arquivos PO com muita facilidade.

gettext

Você obtém a familiaridade do sprintf para formatar strings e os arquivos PO serão traduzidos facilmente por milhares de agências diferentes.

Existem duas opções populares:

  1. em seguida , com o uso descrito por este postagem do blog arkency.com
  2. Jed , com uso descrito pela postagem sentry.io e esta postagem React + Redux ,

Ambos têm suporte ao estilo gettext, formatação do estilo sprintf de strings e importação / exportação para arquivos PO.

i18next tem uma extensão React desenvolvida por eles mesmos. Jed não. Sentry.io parece usar uma integração personalizada de Jed com React. O post React + Redux sugere o uso

Ferramentas: jed + po2json + jsxgettext

No entanto, Jed parece uma implementação mais focada no gettext - isto é, sua intenção expressa, onde, como i18next, apenas a tem como uma opção.

unidade de Terapia Intensiva

Isso tem mais suporte para os casos extremos em torno das traduções, por exemplo, para lidar com gênero. Acho que você verá os benefícios disso se tiver idiomas mais complexos para os quais traduzir.

Uma opção popular para isso é messageformat.js . Discutido brevemente neste tutorial do blog sentry.io . messageformat.js foi desenvolvido pela mesma pessoa que escreveu Jed. Ele faz afirmações bastante fortes sobre o uso da UTI :

Jed está completo em minha opinião. Fico feliz em corrigir bugs, mas geralmente não estou interessado em adicionar mais à biblioteca.

Eu também mantenho messageformat.js. Se você não precisa especificamente de uma implementação gettext, posso sugerir o uso de MessageFormat, pois ele tem melhor suporte para plurais / gênero e possui dados de localidade integrados.

Comparação aproximada

gettext com 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 (meu melhor palpite lendo o guia ):

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
fonte
Votado para baixo. Isso não responde à pergunta. OP pediu uma ideia de arquitetura, não uma sugestão ou comparação com qualquer biblioteca i18n.
TrungDQ
@TrungDQ Isto é o que o OP perguntou: "Minha pergunta não é puramente técnica, mas sim sobre a arquitetura e os padrões que as pessoas estão realmente usando na produção para resolver este problema." . Esses são dois padrões que estão sendo usados ​​na produção.
icc97
Em minha opinião, esta resposta não fornece as informações que eu (e outras pessoas) procuro. As informações fornecidas são úteis, mas talvez para outra pergunta. Eu só quero contribuir com meu voto negativo para fazer a resposta certa aparecer no topo (espero).
TrungDQ
@TrungDQ Se não for o que você está procurando, basta votar a favor daquele que você usou e ignorar os outros, em vez de votar contra respostas perfeitamente válidas que não correspondem à parte específica da pergunta em que você está interessado.
icc97
1

Se ainda não terminou, dê uma olhada em https://react.i18next.com/ pode ser um bom conselho. É baseado no i18next: aprender uma vez - traduzir em todos os lugares.

Seu código será semelhante a:

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

Vem com amostras para:

  • webpack
  • cra
  • expo.js
  • next.js
  • integração de livro de histórias
  • farra
  • dat
  • ...

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

Além disso, você também deve considerar o fluxo de trabalho durante o desenvolvimento e posteriormente para seus tradutores -> https://www.youtube.com/watch?v=9NOzJhgmyQE

Jamuhl
fonte
Isso não responde à pergunta. OP pediu uma ideia de arquitetura, não uma sugestão ou comparação com qualquer biblioteca i18n.
TrungDQ
@TrungDQ assim como com seu comentário sobre minha resposta que você votou negativamente - o OP pediu as soluções atuais usadas na produção. No entanto, eu havia sugerido i18 a seguir em minha resposta de fevereiro
icc97
0

Eu gostaria de propor uma solução simples usando criar-reagir-app .

O aplicativo será criado para cada idioma separadamente, portanto, toda a lógica de tradução será removida do aplicativo.

O servidor da web servirá o idioma correto automaticamente, dependendo do cabeçalho Accept-Language , ou manualmente, definindo um cookie .

Principalmente, não mudamos o idioma mais de uma vez, se é que o fazemos)

Os dados de tradução são colocados dentro do mesmo arquivo do componente que o utiliza, junto com os estilos, html e código.

E aqui temos um componente totalmente independente que é responsável por seu próprio estado, visão, tradução:

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

Adicione a variável de ambiente de linguagem ao seu 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",

É isso!

Além disso, minha resposta original incluía uma abordagem mais monolítica com um único arquivo json para cada tradução:

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
fonte
Não funcionaria apenas em tempo de compilação? Sem a capacidade do usuário de alterar o idioma em tempo real? Esse seria um caso de uso diferente.
Antoine Jaussoin
O aplicativo será compilado para cada idioma necessário. O servidor da web servirá a versão correta automaticamente, dependendo do cabeçalho "Accept-Language" ou por um cookie definido pelo usuário em tempo real. Ao fazer isso, toda a lógica de tradução pode ser removida do aplicativo.
Igor Sukharev