web-dev-qa-db-ja.com

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

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

私の質問は純粋に技術的なものではなく、アーキテクチャ、そしてこの問題を解決するために実際に本番環境で使用されているパターンに関するものです。そのための「料理の本」はどこにも見つからなかったので、お気に入りのQ/Aウェブサイトに目を向けています:)

私の要件は次のとおりです(これらは実際には「標準」です)。

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

考えられる解決策は次のとおりです。

各コンポーネントは単独で翻訳を処理します

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

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

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

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

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

小道具を少しバイパスし、おそらく context thingyを使用して現在の言語を伝えます

  • Pro:ほとんど透過的で、常に現在の言語や翻訳を小道具で渡す必要はありません
  • 短所:使用するのが面倒

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

どうやってやるの?

109

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

したがって、ここで解決策は、下から始まります(個々のコンポーネント):

コンポーネント

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

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

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デコレータを許可するために関数がカリー化されていることに注意)は、コンポーネント自体がラップすることです。

翻訳コンポーネントのコードは次のとおりです。

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

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

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

階層の最上部

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

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で構築された多言語対応のレトロスペクティブボード(アジャイルレトロスペクティブ用)に取り組みました。非常に多くの人がコメントで実際の例を求めたので、ここにあります:

コードはここにあります: https://github.com/antoinejaussoin/retro-board/tree/master

103

私の経験から、最善のアプローチは、i18n redux stateを作成し、それを使用することです。

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

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

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

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

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

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

国際化の初期状態に対する私の提案は次のとおりです:

{
  "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- hum​​an-format このモジュールを使用すると、人間が読み取り可能な文字列との間で数値を変換できます。たとえば、

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)

現在、反応コンテキストAPI(reduxなし)を使用して同じ概念を実装するフレームワークが多数あります。個人的に I18next をお勧めします

16

アントワーヌのソリューションは正常に動作しますが、いくつかの注意事項があります。

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

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

以下を提供します。

  • 言語と対応するメッセージをReduxストアに保存するレデューサー。どちらでも提供できます:
    • 特定のアクションをキャッチし、現在の言語を差し引き、関連するメッセージを取得/取得するように設定できるミドルウェア。
    • setLanguage(lang, messages)の直接ディスパッチ
  • 4つのメソッドを公開するPオブジェクトを取得するgetP(state)セレクター:
    • t(key):元のポリグロットT関数
    • tc(key):大文字の翻訳
    • tu(key):大文字変換
    • tm(morphism)(key):カスタムモーフィング翻訳
  • 現在の言語を取得するgetLocale(state)selector
  • translateオブジェクトをpropsに注入してReactコンポーネントを強化する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);

質問/提案があれば教えてください!

5
Jalil

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

私はgettextのみを使用したことがあるため、偏見があります。

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

gettext

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

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

  1. i18next 、これで説明されている使用法 arkency.comブログ投稿
  2. Jed 、使用法は sentry.io post およびこの React + Redux post で説明されています。

どちらもgettextスタイルのサポート、文字列のsprintfスタイルのフォーマット、POファイルへのインポート/エクスポートが可能です。

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

ツール: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' });
2
icc97

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
  • ストーリーブック統合
  • ラズル
  • dat
  • ...

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

それに加えて、開発中および後で翻訳者のためにワークフローも考慮する必要があります-> https://www.youtube.com/watch?v=9NOzJhgmyQE

1
jamuhl

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

アプリケーションはすべての言語に対して個別に構築されるため、翻訳ロジック全体がアプリケーションの外に移動されます。

Webサーバーは、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);
1
Igor Sukharev