Après avoir essayé plusieurs solutions, je pense en avoir trouvé une qui fonctionne bien et devrait être une solution idiomatique pour React 0.14 (c'est-à-dire qu'elle n'utilise pas de mixins, mais des composants d'ordre supérieur) ( modifier : aussi parfaitement bien avec React 15 bien sûr! ).
Voici donc la solution, en commençant par le bas (les composants individuels):
Le composant
La seule chose dont votre composant aurait besoin (par convention) est un strings
accessoire. Il doit s'agir d'un objet contenant les différentes chaînes dont votre composant a besoin, mais la forme en dépend en réalité.
Il contient les traductions par défaut, vous pouvez donc utiliser le composant ailleurs sans avoir à fournir de traduction (cela fonctionnerait hors de la boîte avec la langue par défaut, l'anglais dans cet exemple)
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);
Le composant d'ordre supérieur
Sur l'extrait de code précédent, vous avez peut-être remarqué ceci sur la dernière ligne:
translate('MyComponent')(MyComponent)
translate
dans ce cas, il s'agit d'un composant d'ordre supérieur qui enveloppe votre composant et fournit des fonctionnalités supplémentaires (cette construction remplace les mixins des versions précédentes de React).
Le premier argument est une clé qui sera utilisée pour rechercher les traductions dans le fichier de traduction (j'ai utilisé le nom du composant ici, mais cela pourrait être n'importe quoi). Le second (notez que la fonction est curry, pour permettre aux décorateurs ES7) est le composant lui-même à envelopper.
Voici le code du composant translate:
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;
};
}
Ce n'est pas magique: il lira simplement la langue actuelle à partir du contexte (et ce contexte ne saignera pas partout dans la base de code, juste utilisé ici dans ce wrapper), puis obtiendra l'objet de chaînes approprié à partir des fichiers chargés. Ce morceau de logique est assez naïf dans cet exemple, pourrait être fait comme vous le souhaitez vraiment.
L'élément important est qu'il prend la langue actuelle du contexte et la convertit en chaînes, étant donné la clé fournie.
Tout en haut de la hiérarchie
Sur le composant racine, il vous suffit de définir la langue actuelle à partir de votre état actuel. L'exemple suivant utilise Redux comme implémentation de type Flux, mais il peut facilement être converti en utilisant n'importe quel autre framework / modèle / bibliothèque.
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);
Et pour finir, les fichiers de traduction:
Fichiers de traduction
// 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'en pensez-vous?
Je pense que cela résout tout le problème que j'essayais d'éviter dans ma question: la logique de traduction ne saigne pas partout dans le code source, elle est assez isolée et permet de réutiliser les composants sans elle.
Par exemple, MyComponent n'a pas besoin d'être encapsulé par translate () et pourrait être séparé, permettant sa réutilisation par toute autre personne souhaitant fournir le strings
par ses propres moyens.
[Edit: 31/03/2016]: J'ai récemment travaillé sur un tableau rétrospectif (pour Agile Retrospectives), construit avec React & Redux, et est multilingue. Comme beaucoup de gens ont demandé un exemple réel dans les commentaires, le voici:
Vous pouvez trouver le code ici: https://github.com/antoinejaussoin/retro-board/tree/master