reactの言語切替の単純な実装（国際化・他言語化）

実装したいもの
---
ボタン押したら英語と日本語に切り替わるやつ
![](https://i.gyazo.com/5db07c6623ccd3f8f36e1d18dea694e4.gif)


Reactの国際化／多言語化について少し探すと[react-intl](https://github.com/yahoo/react-intl/tree/v2.4.0/#readme), [react-i18next](https://github.com/i18next/react-i18next/tree/v7.7.0/#readme)の２つが挙がり、結論としては

* [前者`react-intl`で画面のものを実装したところ、約63行になり、reduxが必要になった。](https://codesandbox.io/s/xp36r03qn4)

* [後者`react-i18next`で画面のものを実装したところ、約40行になった。](https://codesandbox.io/s/zkn071oxql)

以下は書きながら感じたことを雑多に記す。

`react-intl`
---
[言語切替のサンプルコード](https://github.com/yahoo/react-intl/tree/v2.4.0/examples/translations)を見てみると、urlの`?locale`を見て生成するコードを変更するSPAに適さない手法が使われており、参考にならなかった。
代わりに、[issues内の投稿で参考になるコード](https://github.com/yahoo/react-intl/issues/243#issuecomment-165924794)があったので、そちらを真似した。

```jsx:codesandbox.io/s/xp36r03qn4
import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { connect, Provider } from "react-redux";
import { addLocaleData, IntlProvider, FormattedMessage } from "react-intl";

///...

const initialState = {
  intl: locales.ja
};
const store = createStore((state, action) => {
  if (action.error) throw action.error;
  switch (action.type) {
    case "CHANGE_LOCALE":
      return Object.assign({}, state, { intl: locales[action.payload] });
    default:
      return state;
  }
}, initialState);

const ConnectedIntlProvider = connect(state => state.intl)(IntlProvider);

const App = () => (
  <Provider store={store}>
    <ConnectedIntlProvider>
      <div>
        <h1>
          <FormattedMessage id="greet" />
        </h1>
        {["en", "ja"].map(locale => (
          <button
            key={locale}
            onClick={() => {
              store.dispatch({ type: "CHANGE_LOCALE", payload: locale });
            }}
          >
            {locale}
          </button>
        ))}
      </div>
    </ConnectedIntlProvider>
  </Provider>
);

render(<App />, document.querySelector("#root"));
```

`<IntlProvider>`は言語切替機能を提供せず、単一の翻訳情報を`props`に求める。上記コードは現在の言語を`redux`に持たせ、`react-redux.connect`で反映する形にした。

ただ、`react-intl`の初期化の手順が複雑で、以下のようなコードで動作した。

```jsx:codesandbox.io/s/xp36r03qn4
///...
const locales = {
  en: {
    locale: "en",
    messages: {
      greet: "hello world"
    },
    // 数値整形関数。addLocaleData時に定義しないとconsoleで警告が出る
    pluralRuleFunction: a => a
  },
  ja: {
    locale: "ja",
    messages: {
      greet: "ハローワールド"
    },
    pluralRuleFunction: a => a
  }
};
addLocaleData(locales.en);
addLocaleData(locales.ja);
///...
```

`addLocaleData`時に`pluralRuleFunction`が必須らしい。ドキュメントで確認できず、console上のエラーの意味を理解するのに時間が掛かった。

![](https://i.gyazo.com/5db07c6623ccd3f8f36e1d18dea694e4.gif)

`react-i18next`
---

初期化が楽。i18nextインスタンス（以下`i18n`と呼ぶ）に、デフォルト言語と多言語情報を設定する。

```jsx:codesandbox.io/s/zkn071oxql
// ...
const locales = {
  en: {
    greet: "hello world"
  },
  ja: {
    greet: "ハローワールド"
  }
};

const i18n = i18next.init({ fallbackLng: "en", fallbackNS: "translation" });
i18n.addResourceBundle("en", "translation", locales.en);
i18n.addResourceBundle("ja", "translation", locales.ja);
// ...
```

[react-i18nextのTranslate HOC](https://react.i18next.com/components/translate-hoc)で梱包した`Main`は`props`に翻訳関数である`.t`を持つ。

```jsx:codesandbox.io/s/zkn071oxql
import React from "react";
import { render } from "react-dom";
import i18next from "i18next";
import { I18nextProvider, translate } from "react-i18next";

//...

const Main = translate()(props => (
  <div>
    <h1>{props.t("greet")}</h1>
    {["en", "ja"].map(locale => (
      <button
        key={locale}
        onClick={() => {
          i18n.changeLanguage(locale);
        }}
      >
        {locale}
      </button>
    ))}
  </div>
));

const App = () => (
  <I18nextProvider i18n={i18n}>
    <Main />
  </I18nextProvider>
);

render(<App />, document.querySelector("#root"));
```

`i18n.changeLanguage('ja')`で`I18nextProvider`内の`translate`した`Main`に即時反映されるため、下記のように言語がすぐに切り替わる。

![](https://i.gyazo.com/5db07c6623ccd3f8f36e1d18dea694e4.gif)

結論
---
単純なkey-value置換と即時反映なら`react-i18next`で良い。