こんにちは。西日本テクノロジー&イノベーション室の藤田です。日々、己の無知さに腿を強く殴りつつ(技術的に)強くなりたいと言っている新卒3年目です。
この記事では、サービス開発案件の開発ノウハウとして、ReactのContextを使ってメッセージをローカライズする方法を説明します。

はじめに

React+Railsで台湾向けのサービスを開発しています。
サービス提供国は今のところ台湾のみなので、サービスで扱うメッセージも台湾語のみです。利用者の使用言語に合わせてアプリケーションの言語を切り替える必要はありません。
しかし、我々開発者は日本人です。開発時からメッセージがすべて台湾語なのは厳しいものがあります。日本語と台湾語の対応一覧表はあるものの、一覧を参照しないとメッセージが読めませんし、そんな状態では開発に余計に時間がかかってしまいます。
ということで、一般的なローカライズとは動機が違いますが、開発時と本番とで言語を切り替えられるようにReactのContextを使ってメッセージをローカライズする仕組みを作りました。

Contextとは

Contextの公式ドキュメント(日本語)

コンポーネント間でデータを共有できる仕組みです。

親から子へ明示的に受け渡しをしていかなければならないpropsとは違って、深い階層のコンポーネントに、中間コンポーネントを飛ばしてデータを受け渡したいときに使われるのがContextです。
公式ドキュメントでは、Contextの使用例を次の通り示しています。

コンテクストを使うことが他の手法よりシンプルである一般的な例としては、現在のロケール、テーマ、またはデータキャッシュの管理が挙げられます。

今回は、アプリケーション内で共通して使用するメッセージをContextを使って受け渡します。

実装

説明に不要なディレクトリ・ファイル類を省くと次の構成になります。

src
├── components
   └── pages
       └── SamplePage.js
└── i18n
    ├── context.js
    ├── messages.ja.js
    ├── messages.js
    └── messages.en.js

i18nでmessagesを定義し、Contextを生成します。i18n配下の個別ファイルについてみると、以下のようになります。なお、以降の例は、分かりやすさのため台湾語ではなく英語のメッセージを用います。

messages.ja.js
日本語のメッセージを定義します。

messages.en.js
英語のメッセージを定義します。

messages.js
環境に応じてexportするメッセージファイルを切り替えます。

context.js
messages.jsでexportされたmessagesからContextを生成します。
context.jsで生成したContextを受け渡されているコンポーネントの一つがSamplePage.jsです。

次に、実際のコードを見ていきます。

メッセージを定義する

メッセージは関数で定義します。関数で定義する理由は後述します。 日本語のメッセージファイルと同ディレクトリに、英語のメッセージファイルも用意します。

import React from "react";

export default {
    name: () => "名前",
    phone: () => "電話番号"
    errorOccurred: () => "エラーが発生しました。",
    message1: () => "ここはサンプルページです。",
    message2: ({ name, phone }) => (<>
      <p>
        あなたの名前は  {name}<br />
        あなたの電話番号は {phone}
    </p>
    </>)
};
import React from "react";

export default {
    name: () => "name",
    phone: () => "phone number"
    errorOccurred: () => "An error occurred.",
    message1: () => "This is SamplePage.",
    message2: ({ name, phone }) => (<>
      <p>
        Your name is {name}.<br />
        Your phone number is {phone}.
    </p>
    </>)
};

環境に応じて使用するメッセージファイルを切り替える

本番だったら英語のメッセージファイルを、開発時であれば日本語のメッセージファイルをexportします。

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./messages.en.js');
} else {
  module.exports = require('./messages.ja.js');
}

Contextを生成する

messagesからContextを生成します。

import React from "react";
import messages from "./messages";

export default React.createContext({ messages });

コンポーネント側でContext内のメッセージを使う

contextTypecreateContext()で作成したContextオブジェクトを指定すると、this.contextを使って、Contextの最新の値を参照することができます。

import React from "react";

import I18nContext from "../../i18n";

export default class SamplePage extends React.Component {

  static contextType = I18nContext;

  constructor(props) {
    super(props);
    this.state = {
      name: "田中",
      phone: 1234567890
    };
  }

  render() {
    const { name, phone } = this.state;
    const { sample } = this.context.messages;
    return (
      <div>
        <p>{sample.message1()}</p>
        {sample.message2({ name, phone })} 
      </div>
    );
  }
}

メッセージを関数で定義する理由

エラーが発生するので呼び出す時にミスに気づける

メッセージを受け取るコンポーネント側で、存在しないメッセージを指定したとします。

  render() {
    const { name, phone } = this.state;
    const { sample } = this.context.messages;
    return (
      <div>
        {/* 存在しないメッセージを指定する */}
        <p>{sample.message3()}</p>   
      </div>
    );
  }
}

存在しないメッセージを関数呼び出した場合、次のエラーが発生します。

Uncaught TypeError: sample.message3 is not a function

sample.message3 は 定義されていないので undefined です。undefined を関数呼び出ししようとしているので TypeError が出ています。

メッセージが関数ではない場合、変数として参照しようとすると何も表示されないだけで特にエラーは起きません。定義されていない変数を参照してもundefinedがかえってくるだけだからです。

また、render()内でメッセージを関数呼び出しすべきところを変数として参照すると、Warningが出ます。

  render() {
    const { name, phone } = this.state;
    const { sample } = this.context.messages;
    return (
      <div>
        {/* message1を変数として参照する */}
        <p>{sample.message1}</p>   
      </div>
    );
  }
}

Warning: Functions are not valid as a React child.
このように、呼び出し方の誤りにすぐに気づくことができます。

プレースホルダを用意して変数をバインドできる

これはわかりやすいですね。 {}プレースホルダに変数をバインドして、動的にメッセージを生成できます。
引数を受け取ってメッセージに組み込みたい場合、関数で定義すると非常に便利です。

おわりに

Reactで開発しているサービスのメッセージローカライズ方法を書きました。 ポイントはメッセージ定義を関数にすることです。関数でメッセージを定義すると、以下の利点があります。

  • エラーにすぐに気づくことができる
  • プレースホルダを用意して変数をバインドできる

また、今回特に言及しませんでしたが、以下の二点も考慮に入れていました。

  • 翻訳を外注するときにメッセージ一覧があった方が管理しやすい
  • サービス提供する国を増やすかもしれないので拡張性があった方がいい

今回のローカライズ方法であれば、メッセージ一覧を作っているので管理しやすいですし、新たに言語を増やすとなっても、その言語のファイルを追加するだけで済みます。


本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。