1. ホーム
  2. architecture

[解決済み] React / Reduxと多言語(国際化)アプリ - アーキテクチャ

2022-08-02 20:48:23

質問

複数の言語とロケールに対応する必要があるアプリを構築しています。

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

以下は私の要件です(それらは本当に標準的なものです)。

  • ユーザーが言語を選択できる (些細なことです)
  • 言語を変更すると、インターフェイスは新しく選択された言語に自動的に翻訳されるはずです。
  • 私は今のところ、数字や日付などのフォーマットについてあまり心配していないので、単に文字列を翻訳するシンプルなソリューションが欲しいのです。

以下は、私が考えついた可能な解決策です。

各コンポーネントが単独で翻訳を扱う

これは、各コンポーネントが、例えば翻訳された文字列を含む en.json, fr.json などのファイル一式を一緒に持っていることを意味します。そして、選択された言語に応じてそれらから値を読み取るのを助けるためのヘルパー関数。

  • Pro: React の哲学をより尊重し、各コンポーネントは "スタンドアロンです。
  • 短所: すべての翻訳をファイルに一元化できない (たとえば、他の人が新しい言語を追加するため)
  • 短所: すべての血まみれコンポーネントとその子で、propとして現在の言語を渡す必要があります。

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

つまり、これらのコンポーネントは現在の言語を意識しておらず、現在の言語と一致する文字列のリストをpropsとして受け取るだけです。

  • 長所: これらの文字列はトップから来るので、どこかで集中管理することができます。
  • 短所: 各コンポーネントが翻訳システムに結び付けられるため、1 つだけ再利用することができず、毎回正しい文字列を指定する必要がある。

プロップを少し回避して、おそらくは コンテキスト のようなものを使って現在の言語

  • 長所: ほとんど透明で、現在の言語および/または翻訳を常にプロップ経由で渡す必要はありません。
  • 短所: 使うのが面倒に見える

他に何かアイデアがあれば、ぜひ言ってください。

どうやるんですか?

どのように解決するのですか?

いろいろ試した結果、React 0.14 のイディオム的な解決策になりそうなものを見つけました (つまり mixin ではなく Higher Order Components を使っています)。 編集 もちろん、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 の mixin を置き換えるものです)。

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

以下は、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/03/31】です。] 最近、React & Reduxで構築された、多言語対応のRetrospective Board(アジャイルレトロスペクティブ用)を手がけました。 コメントで実例を求める声がかなり多かったので、ここに紹介します。

コードはこちらでご覧いただけます。 https://github.com/antoinejaussoin/retro-board/tree/master