React / Redux et applications multilingues (internationalisation) - Architecture

119

Je construis une application qui devra être disponible dans plusieurs langues et paramètres régionaux.

Ma question n'est pas purement technique, mais plutôt sur l'architecture et les modèles que les gens utilisent réellement en production pour résoudre ce problème. Je n'ai trouvé nulle part de "livre de recettes" pour ça, alors je me tourne vers mon site de questions / réponses préféré :)

Voici mes exigences (elles sont vraiment "standard"):

  • L'utilisateur peut choisir la langue (trivial)
  • Lors du changement de langue, l'interface doit se traduire automatiquement dans la nouvelle langue sélectionnée
  • Je ne suis pas trop préoccupé par le formatage des nombres, des dates, etc. pour le moment, je veux une solution simple pour simplement traduire des chaînes

Voici les solutions possibles auxquelles 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é de lui avec les chaînes traduites. Et une fonction d'aide pour aider à lire les valeurs de celles qui dépendent de la langue sélectionnée.

  • Pro: plus respectueux de la philosophie React, chaque composant est "autonome"
  • Inconvénients: vous ne pouvez pas centraliser toutes les traductions dans un fichier (pour que quelqu'un d'autre ajoute une nouvelle langue par exemple)
  • Inconvénients: vous devez toujours passer la langue actuelle comme accessoire, dans chaque composant sanglant et leurs enfants

Chaque composant reçoit les traductions via les accessoires

Donc, ils ne sont pas conscients de la langue actuelle, ils prennent juste une liste de chaînes comme accessoires qui correspondent à la langue actuelle

  • Pro: puisque ces chaînes viennent "du haut", elles peuvent être centralisées quelque part
  • Inconvénients: chaque composant est maintenant lié au système de traduction, vous ne pouvez pas simplement en réutiliser un, vous devez spécifier les bonnes chaînes à chaque fois

Vous contournez un peu les accessoires et utilisez éventuellement le truc de contexte pour transmettre la langue actuelle

  • Pro: c'est principalement transparent, pas besoin de passer la langue actuelle et / ou les traductions via des accessoires tout le temps
  • Inconvénients: il semble compliqué à utiliser

Si vous avez une autre idée, dites-le!

Comment faites-vous?

Antoine Jaussoin
la source
2
Je préfère l'idée d'un objet de clés avec des chaînes de traduction qui est transmis comme accessoire, vous n'avez pas à passer chaque chaîne comme accessoire individuellement. Changer cela à un niveau supérieur devrait déclencher un nouveau rendu. Je ne pense pas que l'utilisation du contexte soit une bonne idée pour cela, et chaque composant ayant accès au fichier de traduction les rend moins "stupides" et portables en fait imo (et plus difficile à obtenir le rendu de l'application lors du changement de langue).
Dominic
1
En fait, selon facebook.github.io/react/docs/context.html , l'utilisation du contexte pour partager la langue actuelle est l'un des cas d'utilisation légitimes. L'approche que j'essaie maintenant est d'utiliser ceci plus un composant d'ordre supérieur pour gérer la logique d'extraction des chaînes pour ce composant particulier (probablement basé sur une clé)
Antoine Jaussoin
1
Peut-être que vous pouvez également jeter un œil à Instant . Ils traitent ce problème d'une manière complètement différente en le traitant dans le frontend ala Optimizely (c'est-à-dire en modifiant le DOM lors du chargement).
Marcel Panse
1
Pas mal du tout! C'est en effet une bête complètement différente (qui vous lie à un service que vous devrez peut-être payer si votre site Web se développe), mais j'aime l'idée et cela en vaut probablement la peine pour un petit site Web dont vous avez besoin pour démarrer rapidement!
Antoine Jaussoin
4
Aussi, vous voudrez peut-être mentionner que vous êtes un co-fondateur d'Instant, au lieu de dire «Ils» comme si vous n'aviez rien à voir avec eux :)
Antoine Jaussoin

Réponses:

110

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 stringsaccessoire. 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 stringspar 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

Antoine Jaussoin
la source
C'est une solution sympa ... vous vous demandez si vous êtes toujours d'accord avec cela après quelques mois? Je n'ai pas trouvé beaucoup de conseils en matière de conseils sur les modèles pour cela en ligne
Damon
2
Je suis en fait, j'ai trouvé que cela fonctionnait parfaitement (pour mes besoins). Cela permet au composant de fonctionner sans traduction par défaut, et la traduction vient juste au-dessus sans que le composant en soit conscient
Antoine Jaussoin
1
@ l.cetinsoy vous pouvez utiliser le dangerouslySetInnerHTMLprop, soyez juste conscient des implications (nettoyez manuellement l'entrée). Voir facebook.github.io/react/tips/dangerously-set-inner-html.html
Teodor Sandu
6
Y a-t-il une raison pour laquelle vous n'avez pas essayé react-intl?
SureshCS
1
Vraiment cette solution. Une chose que j'ajouterais que nous avons trouvée très utile pour la cohérence et le gain de temps est que si vous avez beaucoup de composants avec des chaînes communes, vous pouvez tirer parti des variables et de la répartition sur des objets, par exempleconst formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Huw Davies
18

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 depuis la base de données, le fichier local ou même depuis un moteur de template tel que EJS ou jade

2- Lorsque l'utilisateur change la langue, vous pouvez changer toute la langue de 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 choses importantes 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 la langue de changement 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 API normal et dans le backend, je viens de retourner i18n.getCatalog(req)cela renverra toutes les chaînes utilisateur uniquement pour la langue actuelle

Ma suggestion pour l'état initial de l'i18n est:

{
  "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 utiles supplémentaires 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- au 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 les dates et heures les plus célèbres de la bibliothèque npm, vous pouvez traduire moment mais il a déjà une traduction intégrée juste dont vous avez besoin pour passer la langue de l'état actuel par exemple:

import moment from "moment";

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

Mise à jour (14/06/2019)

Actuellement, il existe de nombreux frameworks implémentant le même concept en utilisant l'API de contexte de réaction (sans redux), j'ai personnellement recommandé I18next

Tarif Alnamrouti
la source
Cette approche fonctionnerait-elle également pour plus de deux langues? Compte tenu de la configuration du catalogue
tempranova
Bas voté. Cela ne répond pas à la question. OP a demandé une idée d'architecture, pas une suggestion ou une comparaison d'une bibliothèque i18n.
TrungDQ
9
J'ai suggéré le catalogue i18n en tant qu'état de redux, il semble que vous ne comprenez pas le redux
Fareed Alnamrouti
5

La solution d'Antoine fonctionne bien, mais il y a quelques mises en garde:

  • Il utilise directement le contexte React, ce que j'ai tendance à éviter lorsque j'utilise déjà Redux
  • Il importe directement des phrases à partir d'un fichier, ce qui peut être problématique si vous souhaitez récupérer la langue nécessaire au moment de l'exécution, côté client
  • Il n'utilise aucune bibliothèque i18n, qui est légère, mais ne vous donne pas accès à des fonctionnalités de traduction pratiques comme la pluralisation et l'interpolation

C'est pourquoi nous avons construit redux-polyglot au-dessus de Redux et AirBNB's Polyglot .
(Je suis l'un des auteurs)

Il offre :

  • un réducteur pour stocker la langue et les messages correspondants dans votre boutique Redux. Vous pouvez fournir les deux soit:
    • un middleware que vous pouvez configurer pour intercepter une action spécifique, déduire la langue actuelle et obtenir / récupérer les messages associés.
    • envoi direct de setLanguage(lang, messages)
  • un getP(state)sélecteur qui récupère un Pobjet qui expose 4 méthodes:
    • t(key): fonction polyglotte T d'origine
    • tc(key): traduction en majuscule
    • tu(key): traduction en majuscule
    • tm(morphism)(key): traduction morphée personnalisée
  • un getLocale(state)sélecteur pour obtenir la langue courante
  • un translatecomposant d'ordre supérieur pour améliorer vos composants React en injectant l' pobjet dans des accessoires

Exemple d'utilisation simple:

envoyer une nouvelle langue:

import setLanguage from 'redux-polyglot/setLanguage';

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

dans le composant:

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

Veuillez me dire si vous avez des questions / suggestions!

Jalil
la source
1
De bien meilleures phrases originales à traduire. Et pour créer un outil qui analyse tous les composants à la recherche de _()fonctions, par exemple pour obtenir toutes ces chaînes. Ainsi, vous pouvez dans un fichier de langue le traduire plus facilement et ne pas jouer avec des variables folles. Dans certains cas, les pages de destination nécessitent une partie spécifique de la mise en page pour être affichées différemment. Ainsi, une fonction intelligente de sélection par défaut par rapport à d'autres choix possibles devrait également être disponible.
Roman M. Koss
Salut @Jalil, y a-t-il un exemple complet de middleware?
ArkadyB
Salut @ArkadyB, Nous l'utilisons en production sur plusieurs projets qui ne sont pas open-source. Vous pouvez trouver plus d'informations sur le module README: npmjs.com/package/redux-polyglot Avez-vous des questions / difficultés à l'utiliser?
Jalil
Mon problème majeur avec ceci et polyglot.js est qu'il réinvente complètement la roue plutôt que de construire sur des fichiers PO. Cette bibliothèque alternative semble prometteuse npmjs.com/package/redux-i18n . Je ne pense pas que ce soit très différent - il s'agit simplement de fournir une couche supplémentaire à convertir vers et à partir de fichiers PO.
icc97
2

D'après mes recherches à ce sujet, il semble y avoir deux approches principales utilisées pour i18n en JavaScript, ICU et gettext .

Je n'ai jamais utilisé que gettext, donc je suis partial.

Ce qui m'étonne, c'est la faiblesse du soutien. Je viens du monde PHP, que ce soit CakePHP ou WordPress. Dans ces deux situations, c'est une norme de base que toutes les chaînes sont simplement entourées __(''), puis plus loin dans la ligne, vous obtenez des traductions à l'aide de fichiers PO très facilement.

gettext

Vous obtenez la familiarité de sprintf pour le formatage des chaînes et les fichiers PO seront traduits facilement par des milliers d'agences différentes.

Il existe deux options populaires:

  1. i18next , avec l'utilisation décrite par ce billet de blog arkency.com
  2. Jed , avec l' utilisation décrite par le poste sentry.io et ce React + post Redux ,

Les deux prennent en charge le style gettext, le formatage des chaînes de style sprintf et l'importation / exportation vers des fichiers PO.

i18next a une extension React développée par eux-mêmes. Jed ne le fait pas. Sentry.io semble utiliser une intégration personnalisée de Jed avec React. Le post React + Redux suggère d'utiliser

Outils: jed + po2json + jsxgettext

Cependant, Jed semble être une implémentation plus centrée sur gettext - c'est son intention exprimée, alors que i18next l'a juste comme option.

ICU

Cela a plus de support pour les cas marginaux autour des traductions, par exemple pour traiter le genre. Je pense que vous en verrez les avantages si vous avez des langues plus complexes à traduire.

Une option populaire pour cela est messageformat.js . Discuté brièvement dans ce tutoriel de blog sentry.io . messageformat.js est en fait développé par la même personne qui a écrit Jed. Il fait des déclarations assez sévères pour utiliser l'ICU :

Jed est une fonctionnalité complète à mon avis. Je suis heureux de corriger les bogues, mais je ne suis généralement pas intéressé à en ajouter d'autres à la bibliothèque.

Je maintiens également messageformat.js. Si vous n'avez pas spécifiquement besoin d'une implémentation gettext, je pourrais suggérer d'utiliser MessageFormat à la place, car il prend mieux en charge les pluriels / genre et possède des données de localisation intégrées.

Comparaison approximative

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 en lisant le 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' });
icc97
la source
Bas voté. Cela ne répond pas à la question. OP a demandé une idée d'architecture, pas une suggestion ou une comparaison d'une bibliothèque i18n.
TrungDQ
@TrungDQ Voici ce que l'OP a demandé: "Ma question n'est pas purement technique, mais plutôt sur l'architecture et les modèles que les gens utilisent réellement en production pour résoudre ce problème." . Ce sont deux modèles qui sont utilisés dans la production.
icc97
À mon avis, cette réponse ne fournit pas les informations que je recherche (et que d'autres recherchent). Les informations que vous avez fournies sont utiles, mais peut-être pour une autre question. Je veux juste contribuer mon vote défavorable pour que la bonne réponse apparaisse en haut (j'espère).
TrungDQ
@TrungDQ Si ce n'est pas ce que vous recherchez, alors votez simplement pour celle que vous avez utilisée et ignorez les autres plutôt que de voter contre des réponses parfaitement valides qui ne correspondent pas à la partie spécifique de la question qui vous intéresse.
icc97
1

Si ce n'est pas encore fait, jetez un œil à https://react.i18next.com/ pourrait être un bon conseil. Il est basé sur i18next: apprenez une fois - 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:

  • Webpack
  • cra
  • expo.js
  • next.js
  • intégration de livre de contes
  • éblouir
  • dat
  • ...

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

En plus de cela, vous devriez également considérer le flux de travail pendant le développement et plus tard pour vos traducteurs -> https://www.youtube.com/watch?v=9NOzJhgmyQE

Jamuhl
la source
Cela ne répond pas à la question. OP a demandé une idée d'architecture, pas une suggestion ou une comparaison d'une bibliothèque i18n.
TrungDQ
@TrungDQ comme avec votre commentaire sur ma réponse que vous avez déclinée - l'OP a demandé des solutions actuelles utilisées en production. Cependant, j'avais suggéré i18next dans ma réponse de février
icc97
0

Je voudrais proposer une solution simple en utilisant create-react-app .

L'application sera construite pour chaque langue séparément, donc toute la logique de traduction sera déplacée hors de l'application.

Le serveur Web servira automatiquement la langue correcte, en fonction de l'en - tête Accept-Language , ou manuellement en définissant un cookie .

La plupart du temps, 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 composant, qui l'utilise, le long des styles, du html et du code.

Et ici, nous avons un composant totalement indépendant qui est 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',
    }
);

Ajoutez une variable d'environnement de langage à 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 ça!

Ma réponse originale incluait également 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);
Igor Sukharev
la source
Cela ne fonctionnerait-il pas uniquement au moment de la compilation? Sans la possibilité pour l'utilisateur de changer la langue à la volée? Ce serait alors un cas d'utilisation différent.
Antoine Jaussoin
L'application sera compilée pour chaque langue nécessaire. Le serveur Web servira automatiquement la version correcte, en fonction de l'en-tête "Accept-Language", ou par un cookie défini par l'utilisateur à la volée. En faisant cela, toute la logique de traduction pourrait être déplacée hors de l'application.
Igor Sukharev