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 strings
adereç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 strings
por 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