web-dev-qa-db-fra.com

React / Applications Redux et multilingues (internationalisation) - Architecture

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"):

  • 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 m'inquiète pas trop du formatage des chiffres, des dates, etc. Pour le moment, je souhaite une solution simple pour traduire simplement des chaînes.

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.

  • 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 tous les composants sanglants et leurs enfants

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

  • 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 chaînes correctes à chaque fois.

Vous contournez un peu les accessoires et utilisez éventuellement le contexte thingy pour transmettre le langage courant

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

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

Comment faites-vous?

112
Antoine Jaussoin

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

107
Antoine Jaussoin

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'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م

Mise à jour (14/06/2019)

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

17

La solution d'Antoine fonctionne bien, mais a quelques réserves:

  • Il utilise directement le contexte React, ce que j’ai tendance à éviter lorsque je travaille déjà avec Redux.
  • Il importe directement les phrases d'un fichier, ce qui peut poser problème si vous souhaitez extraire la langue requise 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 sur Redux et sur AirBNB Polyglot .
(Je suis l'un des auteurs)

Il offre :

  • un réducteur pour stocker la langue et les messages correspondants dans votre magasin Redux. Vous pouvez fournir à la fois:
    • 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 sélecteur getP(state) qui récupère un objet P qui expose 4 méthodes:
    • t(key): fonction T polyglotte d'origine
    • tc(key): traduction en majuscule
    • tu(key): traduction en majuscule
    • tm(morphism)(key): traduction morphée personnalisée
  • un sélecteur getLocale(state) pour obtenir la langue actuelle
  • un composant translate d'ordre supérieur pour améliorer vos composants React en injectant l'objet p dans les accessoires

Exemple d'utilisation simple:

envoyer une nouvelle langue:

import setLanguage from 'redux-polyglot/setLanguage';

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

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

S'il vous plaît dites-moi si vous avez une question/suggestion!

5
Jalil

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.

gettext

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:

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

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.

ICU

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.

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

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:

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

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

1
jamuhl

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);
0
Igor Sukharev