React / Reduxと多言語(国際化)アプリ-アーキテクチャ


119

複数の言語とロケールで利用できるようにする必要があるアプリを作成しています。

私の質問は、純粋に技術的なものではなく、アーキテクチャと、人々がこの問題を解決するために実際に本番で使用しているパターンについてです。そのための「クックブック」はどこにも見つからなかったので、お気に入りのQ / A Webサイトに移動します:)

ここに私の要件があります(それらは本当に「標準」です):

  • ユーザーは言語を選択できます(簡単です)
  • 言語を変更すると、インターフェースは自動的に新しく選択された言語に翻訳されます
  • 現時点では、数値や日付などのフォーマットについてあまり心配していません。文字列を翻訳するだけの簡単な解決策が欲しいのですが

ここに私が考えることができる可能な解決策があります:

各コンポーネントは個別に翻訳を処理します

つまり、各コンポーネントには、翻訳された文字列と一緒に、たとえばen.json、fr.jsonなどの一連のファイルがあります。また、選択した言語に応じて値を読み取るのに役立つヘルパー関数。

  • プロ:Reactの哲学を尊重し、各コンポーネントは「スタンドアロン」です
  • 短所:すべての翻訳をファイルに一元化することはできません(たとえば、誰かに新しい言語を追加させるため)
  • 短所:すべての流血のコンポーネントとその子供に、現在の言語を小道具として渡す必要があります

各コンポーネントは、小道具を介して翻訳を受け取ります

したがって、彼らは現在の言語を認識していません。たまたま現在の言語と一致する小道具として文字列のリストを取得します

  • プロ:これらの文字列は「上から」来るため、どこかに集中させることができます
  • 短所:各コンポーネントが翻訳システムに関連付けられているため、コンポーネントを再利用するだけではなく、毎回正しい文字列を指定する必要があります。

小道具を少しバイパスし、おそらくコンテキストの事を使用して現在の言語を伝えます

  • プロ:それはほとんど透過的で、現在の言語や翻訳をプロップを介して常に渡す必要はありません
  • 短所:使用するのが面倒に見える

他にアイデアがあれば、言ってください!

どうやってやるの?


2
私は、プロップとして渡される翻訳文字列を持つキーのオブジェクトのアイデアを好みます。各文字列をプロップとして個別に渡す必要はありません。これをトップレベルで変更すると、再レンダリングがトリガーされます。コンテキストを使用するのは良い考えではないと思います。翻訳ファイルにアクセスできる各コンポーネントを使用すると、「ダム」が減り、実際にimoが移植可能になります(言語の変更時にアプリで再レンダリングするのが難しくなります)。
ドミニク

1
実際にfacebook.github.io/react/docs/context.htmlによると、現在の言語を共有するためにコンテキストを使用することは、正当なユースケースの1つです。私が今試みているアプローチは、これに加えて高次コンポーネントを使用して、その特定のコンポーネントの文字列を抽出するロジックを処理することです(おそらく何らかのキーに基づく)
Antoine Jaussoin

1
また、Instantもご覧ください。彼らはこの問題を、Optimizely alafrontly(別名、ロード中にDOMを変更する)で取り組むことにより、まったく異なる方法で処理します。
Marcel Panse、2016年

1
悪くない、全く!それは確かに完全に別の獣です(それはあなたのウェブサイトが成長した場合にあなたが支払う必要があるかもしれないサービスにあなたを結び付けます)が、私はアイデアが好きであり、あなたがすぐに実行する必要がある小さなウェブサイトには確かに価値があります!
Antoine Jaussoin

4
また、あなたがInstantの共同創設者であることを、彼らとは何の関係もないかのように「彼らは」と言うのではなく、説明したいと思うかもしれません:)
Antoine Jaussoin

回答:


110

かなりの数のソリューションを試した後、私はうまく機能し、React 0.14の慣用的なソリューション(つまり、ミックスインを使用せず、高次コンポーネント)になるはずだと思いました(編集:もちろんReact 15でも完全に問題ありません! )。

したがって、ここではソリューションを下から始めます(個々のコンポーネント)。

コンポーネント

コンポーネントが(慣例により)必要とする唯一のものは、strings小道具です。これは、コンポーネントに必要なさまざまな文字列を含むオブジェクトである必要がありますが、実際の形状は自由です。

これにはデフォルトの翻訳が含まれているため、翻訳を提供する必要なしにコンポーネントをどこかで使用できます(デフォルトの言語、この例では英語でそのまま使用できます)

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

高次コンポーネント

前のスニペットでは、最後の行でこれに気づいたかもしれません: translate('MyComponent')(MyComponent)

translate この場合、コンポーネントをラップし、いくつかの追加機能を提供する高次コンポーネントです(この構造は、Reactの以前のバージョンのミックスインを置き換えます)。

最初の引数は、翻訳ファイル内の翻訳を検索するために使用されるキーです(ここではコンポーネントの名前を使用しましたが、何でもかまいません)。2つ目(ES7デコレーターを許可するために関数がカレー化されていることに注意)は、ラップするコンポーネント自体です。

以下は、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;
    };
}

それは魔法ではありません:コンテキストから現在の言語を読み取るだけで(そのコンテキストはコードベース全体に流れ出ることはなく、このラッパーで使用されるだけです)、ロードされたファイルから関連する文字列オブジェクトを取得します。このロジックの一部は、この例では非常にナイーブであり、実際に望む方法で実行できます。

重要な部分は、提供されたキーを指定して、コンテキストから現在の言語を取得し、それを文字列に変換することです。

階層の最上位

ルートコンポーネントでは、現在の状態から現在の言語を設定するだけです。次の例では、Fluxのような実装としてReduxを使用していますが、他のフレームワーク/パターン/ライブラリを使用して簡単に変換できます。

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

そして最後に、翻訳ファイル:

翻訳ファイル

// 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'
    }
};

皆さんはどう思いますか?

私が私の質問で回避しようとしていたすべての問題が解決されると思います:翻訳ロジックはソースコード全体に流れ出ることはなく、完全に分離されており、それなしでコンポーネントを再利用できます。

たとえば、MyComponentはtranslate()でラップする必要はなく、個別にすることもできるため、strings独自の方法でを提供したい他の人が再利用できます。

[編集:2016年3月31日]:最近、React&Reduxで構築された多言語対応のRetrospective Board(Agile Retrospectives用)に取り組みました。かなり多くの人々がコメントで実際の例を求めたので、それはここにあります:

ここでコードを見つけることができます:https : //github.com/antoinejaussoin/retro-board/tree/master


これはクールなソリューションです。数か月経ってもまだこの問題に取り組んでいるのでしょうか。私はこのオンラインのパターンに関するアドバイスの方法についてあまりアドバイスを見つけていません
デイモン

2
私は実際に、(私のニーズに対して)完全に機能することがわかりました。デフォルトでは、コンポーネントは翻訳なしで動作し、コンポーネントはそれを認識せずに翻訳をその上に
追加し

1
@ l.cetinsoy dangerouslySetInnerHTMLプロップを使用できますが、その影響に注意してください(手動で入力をサニタイズします)。facebook.github.io/react/tips/dangerously-set-inner-html.htmlを
Teodor Sandu

6
react-intlを試していない理由はありますか?
SureshCS 2016

1
本当にこのソリューションが好きです。一貫性と時間の節約に非常に役立つと私が付け加えた1つのことは、共通の文字列を持つ多くのコンポーネントがある場合、変数を利用してオブジェクトに分散できることです。たとえばconst formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Huw Davies

18

私の経験から、最善のアプローチは、i18n redux状態を作成して使用することです。これには多くの理由があります。

1-これにより、データベース、ローカルファイル、またはEJSやjadeなどのテンプレートエンジンから初期値を渡すことができます。

2-ユーザーが言語を変更すると、UIを更新することなく、アプリケーションの言語全体を変更できます。

3-ユーザーが言語を変更すると、API、ローカルファイル、または定数から新しい言語を取得することもできます

4-タイムゾーン、通貨、方向(RTL / LTR)、使用可能な言語のリストなどの文字列を使用して、他の重要なものを保存することもできます

5-変更言語を通常のreduxアクションとして定義できます

6-バックエンドとフロントエンドの文字列を1か所に置くことができます。たとえば、ローカリゼーションにi18n-nodeを使用し、ユーザーがUI言語を変更したときに、通常のAPI呼び出しを行うだけで、バックエンドでは返すだけです。i18n.getCatalog(req)これは、現在の言語のみのすべてのユーザー文字列を返します

i18nの初期状態に対する私の提案は次のとおりです。

{
  "language":"ar",
  "availableLanguages":[
    {"code":"en","name": "English"},
    {"code":"ar","name":"عربي"}
  ],
  "catalog":[
     "Hello":"مرحباً",
     "Thank You":"شكراً",
     "You have {count} new messages":"لديك {count} رسائل جديدة"
   ],
  "timezone":"",
  "currency":"",
  "direction":"rtl",
}

i18nの追加の有用なモジュール:

1- string-templateこれにより、たとえば次のように、カタログ文字列の間に値を挿入できます。

import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة

2- 人間形式のこのモジュールを使用すると、数値を人間が読み取れる文字列との間で変換できます。次に例を示します。

import humanFormat from "human-format";
//...
humanFormat(1337); // => '1.34 k'
// you can pass your own translated scale, e.g: humanFormat(1337,MyScale)

3- momentjsは最も有名な日付と時刻のnpmライブラリです。瞬間を翻訳できますが、現在の状態の言語を渡すだけで、組み込みの翻訳がすでにあります。次に例を示します。

import moment from "moment";

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

アップデート(14/06/2019)

現在、多くのフレームワークが(reuxなしで)反応コンテキストAPIを使用して同じコンセプトを実装しています。私は個人的にI18nextを推奨しました


このアプローチは3つ以上の言語でも機能しますか?カタログのセットアップを考える
tempranova

反対票。これは質問の答えにはなりません。OPは、i18nライブラリの提案や比較ではなく、アーキテクチャのアイデアを求めました。
TrungDQ 2017年

9
i18nカタログをredux状態として提案しましたが、reduxを理解していないようです
Fareed Alnamrouti 2017年

5

アントワーヌの解決策はうまく機能しますが、いくつかの注意点があります:

  • Reactコンテキストを直接使用します。これは、すでにReduxを使用している場合は避けがちです。
  • ファイルからフレーズを直接インポートします。これは、実行時にクライアント側で必要な言語をフェッチする場合に問題になる可能性があります
  • 軽量なi18nライブラリを使用していませんが、複数化や補間などの便利な翻訳機能にアクセスできません。

そのため、ReduxとAirBNBのPolyglotの両方の上にredux-polyglotを構築しました。 (私は著者の一人です)

それは提供します:

  • 言語と対応するメッセージをReduxストアに格納するためのレデューサー。あなたはどちらかで両方を供給することができます:
    • 特定のアクションをキャッチし、現在の言語を差し引き、関連するメッセージを取得/取得するように構成できるミドルウェア。
    • の直接発送 setLanguage(lang, messages)
  • 4つのメソッドを公開getP(state)するPオブジェクトを取得するセレクター:
    • t(key):オリジナルのpolyglot T関数
    • tc(key):大文字翻訳
    • tu(key):大文字の翻訳
    • tm(morphism)(key):カスタムモーフィング翻訳
  • getLocale(state)現在の言語を取得するためのセレクタ
  • translate注入することによって、あなたのリアクトコンポーネントを強化する高次成分のp小道具でオブジェクトを

簡単な使用例:

新しい言語を派遣:

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

ご不明な点がございましたら、お気軽にお問い合わせください。


1
翻訳するより良いオリジナルのフレーズ。そして_()、たとえば、これらの文字列をすべて取得するために、関数のすべてのコンポーネントを解析するツールを作成します。そのため、言語ファイルでより簡単に翻訳でき、クレイジーな変数を台無しにしないでください。場合によっては、ランディングページのレイアウトの特定の部分を別の方法で表示する必要があります。したがって、デフォルトを選択する方法と他の可能な選択を選択するいくつかのスマート機能も利用できるはずです。
ローマンM.コス、2016

こんにちは@Jalil、ミドルウェアの完全な例はどこにありますか?
ArkadyB 2017

こんにちは@ArkadyBです。オープンソースではないいくつかのプロジェクトのプロダクションで使用しています。モジュールREADMEの詳細情報を見つけることができます: npmjs.com/package/redux-polyglot それを使用して質問/難しさがありますか?
Jalil 2017

これとpolyglot.jsに関する私の大きな問題は、POファイルの上に構築するのではなく、ホイールを完全に再発明することです。この代替ライブラリは、有望なnpmjs.com/package/redux-i18nに見えます。私はそれが大して異なっているとは思わない-それはPOファイルとの間で変換するための追加のレイヤーを提供するだけです。
icc97

2

これに関する私の研究から、JavaScriptでi18nに使用されている2つの主要なアプローチ、ICUgettextがあるようです。

私はgettextを使用したことがないので、偏っています。

私を驚かせているのは、サポートがいかに貧弱かです。私は、CakePHPまたはWordPressのいずれかのPHPの世界から来ています。これらの状況の両方で、すべての文字列が単にで囲まれ__('')、さらに下に行くと、POファイルを使用して非常に簡単に翻訳を取得することが基本的な標準です。

gettext

文字列をフォーマットするためのsprintfを使い慣れているので、POファイルは何千もの異なる機関によって簡単に翻訳されます。

2つの一般的なオプションがあります。

  1. i18next、このarkency.comブログ投稿で説明されている使用法
  2. Jedsentry.ioの投稿とこのReact + Reduxの投稿で説明されている使用法で、

どちらもgettextスタイルのサポート、文字列のsprintfスタイルのフォーマット、POファイルへのインポート/エクスポートを備えています。

i18nextには、独自に開発したReact拡張機能があります。ジェドはしません。Sentry.ioはJedとReactのカスタム統合を使用しているようです。+ Reduxのポストに反応、使用することを提案しています

ツール:jed + po2json + jsxgettext

ただし、Jedはよりgettextに重点を置いた実装のように見えます。つまり、意図が表現されているため、i18nextにはオプションとしてのみ実装されています。

ICU

これは、翻訳に関するエッジケース、例えばジェンダーを扱うためのより多くのサポートを持っています。翻訳する言語がもっと複​​雑であれば、この利点がわかると思います。

このための一般的なオプションはmessageformat.jsです。このsentry.ioブログチュートリアルで簡単に説明しました。messageformat.jsは、実際にはJedを書いた人と同じ人が開発しました。彼はICUを使用することについてかなり強い主張をしています:

私の意見では、Jedは完全な機能です。私はバグを修正してうれしいですが、一般的にライブラリにさらに追加することに興味はありません。

messageformat.jsも維持しています。gettext実装が特に必要ない場合は、複数形/性別のサポートが改善されており、ロケールデータが組み込まれているため、代わりにMessageFormatを使用することをお勧めします。

大まかな比較

sprintfを使用したgettext:

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(ガイドを読むことからの私の推測):

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

反対票。これは質問の答えにはなりません。OPは、i18nライブラリの提案や比較ではなく、アーキテクチャのアイデアを求めました。
TrungDQ 2017年

@TrungDQこれは、OPが尋ねたものです。「私の質問は、純粋に技術的なものではなく、アーキテクチャと、人々が実際にこの問題を解決するために実際に使用しているパターンについてです。」。これらは、本番環境で使用されている2つのパターンです。
icc97 2017年

私の意見では、この回答は私が探している(そして他の人が探している)情報を提供していません。あなたが提供した情報は役に立ちますが、別の質問のためかもしれません。私は自分の反対票を投稿して、正しい答えが上にポップアップ表示されるようにしたいと思っています(私はそう思います)。
TrungDQ 2017年

それはあなたが探しているもの、そしてちょうどあなたが使用をしたものをupvoteむしろpefectly質問しますよ興味中の特定の部分と一致しない有効な答えをdownvotingよりも他人を無視しない場合は@TrungDQ。
icc97

1

まだ行っていない場合は、https://react.i18next.com/を確認することをお勧めします。それはi18nextに基づいています:一度学ぶ-どこでも翻訳。

コードは次のようになります。

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

以下のサンプルが付属しています:

  • ウェブパック
  • cra
  • expo.js
  • next.js
  • ストーリーブックの統合
  • 騒々しい
  • データ
  • ...

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

さらに、開発中および翻訳者のためのワークフローも検討する必要があります-> https://www.youtube.com/watch?v=9NOzJhgmyQE


これは質問の答えにはなりません。OPは、i18nライブラリの提案や比較ではなく、アーキテクチャのアイデアを求めました。
TrungDQ 2017年

@TrungDQは、あなたが反対票を投じたという私の回答に対するコメントと同様に、OPは本番環境で使用されている現在のソリューションを求めました。しかし、私は私の答えで i18nextを提案しました 2月に戻ってから
icc97 '27

0

create-react-appを使用した簡単なソリューションを提案したいと思います

アプリケーションは言語ごとに個別に構築されるため、翻訳ロジック全体がアプリケーションから移動されます。

ウェブサーバーは、Accept-Languageヘッダーに応じて自動的に、またはCookieを設定して手動で適切な言語を提供します。

ほとんどの場合、言語を変更することはありません。

翻訳データは、スタイル、html、コードに沿って、それを使用する同じコンポーネントファイル内に配置されます。

そしてここには、独自の状態、ビュー、翻訳を担当する完全に独立したコンポーネントがあります。

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

言語環境変数を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",

それだ!

また、私の元の答えには、翻訳ごとに単一のjsonファイルを使用する、よりモノリシックなアプローチが含まれていました。

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

コンパイル時にしか機能しないのですか?ユーザーがオンザフライで言語を変更する機能がない場合は、その場合、別のユースケースになります。
Antoine Jaussoin、

アプリは必要なすべての言語用にコンパイルされます。Webサーバーは、「Accept-Language」ヘッダーに応じて、またはユーザーがその場で設定したCookieに応じて、正しいバージョンを自動的に提供します。これにより、翻訳ロジック全体をアプリの外に移動できます。
Igor Sukharev
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.