Je construis une application qui devra être disponible dans plusieurs langues et régions.
Ma question n’est pas purement technique, mais concerne plutôt l’architecture et les modèles que les gens utilisent réellement dans la production pour résoudre ce problème. Je ne pouvais trouver nulle part un "livre de recettes" pour cela, alors je me tourne vers mon site Web Q/A préféré :)
Voici mes exigences (elles sont vraiment "standard"):
Voici les solutions possibles que je pourrais penser:
Chaque composant traite la traduction de manière isolée
Cela signifie que chaque composant a par exemple un ensemble de fichiers en.json, fr.json etc. à côté des chaînes traduites. Et une fonction d'assistance pour aider à lire les valeurs de celles qui dépendent de la langue sélectionnée.
Chaque composant reçoit les traductions via les accessoires
Donc, ils ne connaissent pas le langage actuel, ils prennent juste une liste de chaînes comme accessoires qui se trouvent correspondre au langage courant
Vous contournez un peu les accessoires et utilisez éventuellement le contexte thingy pour transmettre le langage courant
Si vous avez une autre idée, dites-le!
Comment faites-vous?
Après avoir essayé plusieurs solutions, je pense en avoir trouvé une qui fonctionne bien et qui devrait être une solution idiomatique pour React 0.14 (c’est-à-dire qu’elle n’utilise pas de mixeurs, mais des composants d’ordre supérieur) ( edit : aussi parfaitement bien avec React 15 bien sûr!).
Alors voici 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
props. Il doit s'agir d'un objet contenant les différentes chaînes dont votre composant a besoin, mais sa forme dépend de vous.
Il contient les traductions par défaut, vous pouvez donc utiliser le composant ailleurs sans qu'il soit nécessaire de fournir une traduction (il fonctionnerait immédiatement 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);
La composante d'ordre supérieur
Sur l'extrait précédent, vous avez peut-être remarqué ceci sur la dernière ligne: translate('MyComponent')(MyComponent)
translate
est dans ce cas 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 ce pourrait être n'importe quoi). Le second (notez que la fonction est curryed, pour permettre aux décorateurs de ES7) est le composant à envelopper.
Voici le code pour le composant de traduction:
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 courante dans le contexte (et ce contexte ne saigne pas partout dans la base de code, juste utilisé ici dans ce wrapper), puis récupérera l'objet de chaînes pertinent à partir de fichiers chargés. Cette logique est assez naïve dans cet exemple, pourrait être faite comme vous le souhaitez vraiment.
L’important est qu’il utilise le langage actuel du contexte et le convertisse en chaînes, en fonction de 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 en tant qu'implémentation de type Flux, mais il peut facilement être converti à l'aide de tout autre 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);
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 dans tout le code source, elle est assez isolée et permet de réutiliser les composants sans cela.
Par exemple, MyComponent n'a pas besoin d'être enveloppé par translate () et peut être séparé, ce qui permet sa réutilisation par quiconque souhaitant fournir le strings
par leur propre moyen.
[Edit: 31/03/2016]: J'ai récemment travaillé sur un tableau rétrospectif (pour Agile Retrospectives), construit avec React & Redux, et qui est multilingue. Comme beaucoup de gens ont demandé un exemple concret dans les commentaires, le voici:
Vous pouvez trouver le code ici: https://github.com/antoinejaussoin/retro-board/tree/master
D'après mon expérience, la meilleure approche consiste à créer un état redux i18n et à l'utiliser, pour de nombreuses raisons:
1- Cela vous permettra de passer la valeur initiale de la base de données, du fichier local ou même d'un moteur de template tel que EJS ou jade
2- Lorsque l'utilisateur change de langue, vous pouvez changer la langue de toute l'application sans même actualiser l'interface utilisateur.
3- Lorsque l'utilisateur change la langue, cela vous permettra également de récupérer la nouvelle langue à partir de l'API, du fichier local ou même des constantes.
4- Vous pouvez également enregistrer d'autres éléments importants avec les chaînes telles que le fuseau horaire, la devise, la direction (RTL/LTR) et la liste des langues disponibles.
5- Vous pouvez définir le changement de langue comme une action redux normale
6- Vous pouvez avoir vos chaînes backend et front end au même endroit, par exemple dans mon cas, j'utilise i18n-node pour la localisation et lorsque l'utilisateur change la langue de l'interface utilisateur, je fais juste un appel d'API normal et dans le backend, je retourne juste i18n.getCatalog(req)
cela retournera toutes les chaînes de l'utilisateur uniquement pour la langue en cours
Ma suggestion pour l'état initial de i18n est la suivante:
{
"language":"ar",
"availableLanguages":[
{"code":"en","name": "English"},
{"code":"ar","name":"عربي"}
],
"catalog":[
"Hello":"مرحباً",
"Thank You":"شكراً",
"You have {count} new messages":"لديك {count} رسائل جديدة"
],
"timezone":"",
"currency":"",
"direction":"rtl",
}
Modules supplémentaires utiles pour i18n:
1- string-template Cela vous permettra d'injecter des valeurs entre vos chaînes de catalogue, par exemple:
import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة
2- format humain ce module vous permettra de convertir un nombre en/à partir d'une chaîne lisible par l'homme, par exemple:
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 bibliothèque de dates et d'heures la plus célèbre de NPM, vous pouvez traduire moment mais elle a déjà une traduction intégrée, il vous suffit de transmettre la langue d'état actuelle, par exemple:
import moment from "moment";
const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م
Actuellement, de nombreux frameworks implémentent le même concept en utilisant l’API de contexte réactif (sans redux), j’ai personnellement recommandé I18next
La solution d'Antoine fonctionne bien, mais a quelques réserves:
C'est pourquoi nous avons construit redux-polyglot sur Redux et sur AirBNB Polyglot .
(Je suis l'un des auteurs)
setLanguage(lang, messages)
getP(state)
qui récupère un objet P
qui expose 4 méthodes: t(key)
: fonction T polyglotte d'originetc(key)
: traduction en majusculetu(key)
: traduction en majusculetm(morphism)(key)
: traduction morphée personnaliséegetLocale(state)
pour obtenir la langue actuelletranslate
d'ordre supérieur pour améliorer vos composants React en injectant l'objet p
dans les accessoiresimport setLanguage from 'redux-polyglot/setLanguage';
store.dispatch(setLanguage('en', {
common: { hello_world: 'Hello world' } } }
}));
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);
S'il vous plaît dites-moi si vous avez une question/suggestion!
D'après mes recherches, il semble y avoir deux approches principales utilisées pour i18n en JavaScript, ICU et gettext .
Je n'ai jamais utilisé que gettext, alors je suis partial.
Ce qui m'étonne, c'est à quel point le soutien est médiocre. Je viens du monde PHP, que ce soit CakePHP ou WordPress. Dans ces deux situations, il est de base que toutes les chaînes de caractères soient simplement entourées de __('')
, puis vous obtenez très facilement des traductions à l'aide de fichiers PO.
Vous obtenez la familiarité de sprintf pour le formatage des chaînes et les fichiers de commande seront facilement traduits par des milliers d'agences différentes.
Il y a deux options populaires:
Tous deux prennent en charge le style gettext, le formatage des chaînes dans le style sprintf et l'importation/exportation dans des fichiers PO.
i18next a un extension React développé par eux-mêmes. Jed non. Sentry.io semble utiliser une intégration personnalisée de Jed avec React. Le React + Redux post , suggère d'utiliser
Outils: jed + po2json + jsxgettext
Cependant, Jed semble être une implémentation plus ciblée sur gettext - c’est-à-dire son intention exprimée, pour laquelle i18next l’a en option.
Cela prend davantage en charge les cas Edge relatifs aux traductions, par exemple. pour faire face au genre. Je pense que vous en verrez les avantages si vous avez des langages plus complexes à traduire.
Une option populaire pour cela est messageformat.js . Discuté brièvement dans ce tutoriel du blog sentry.io . messageformat.js est en fait développé par la même personne que Jed. Il fait de très vives revendications pour utiliser des soins intensifs :
Jed est complet à mon avis. Je suis heureux de corriger les bugs, mais en général, je ne suis pas intéressé à ajouter plus à la bibliothèque.
Je maintiens également messageformat.js. Si vous n'avez pas spécifiquement besoin d'une implémentation de gettext, je vous suggèrerais plutôt d'utiliser MessageFormat, car il prend mieux en charge les pluriels/les genres et intègre des données de paramètres régionaux.
gettext avec 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 (ma meilleure estimation d'après la lecture du 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' });
Si ce n'est pas encore fait, jetez un œil à https://react.i18next.com/ pourrait être un bon conseil. Il est basé sur i18next: learn once - traduisez partout.
Votre code ressemblera à quelque chose comme:
<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>
Livré avec des échantillons pour:
https://github.com/i18next/react-i18next/tree/master/example
A côté de cela, vous devez également prendre en compte le flux de travail pendant le développement et ultérieurement pour vos traducteurs -> https://www.youtube.com/watch?v=9NOzJhgmyQE
Je voudrais proposer une solution simple en utilisant create-react-app .
L'application sera construite pour chaque langue séparément. Par conséquent, toute la logique de traduction sera déplacée hors de l'application.
Le serveur Web servira automatiquement la langue correcte, selon l'en-tête Accept-Language , ou manuellement en définissant un cookie . .
Généralement, nous ne changeons pas de langue plus d’une fois, voire jamais du tout)
Les données de traduction sont placées dans le même fichier de composant, qui les utilise, avec les styles, le HTML et le code.
Et nous avons ici un composant totalement indépendant responsable de son propre état, vue, traduction:
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',
}
);
Ajouter une variable d'environnement linguistique à votre 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",
c'est tout!
De plus, ma réponse initiale incluait une approche plus monolithique avec un seul fichier json pour chaque traduction:
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);