Reactアプリでのi18n対応(国際化対応)についての勉強メモ

年末〜年始にかけて、React with i18n対応(国際化対応)について調べていました。
下書きでずっと塩漬けになっているのを思い出し、`TODO`などを残したまま一旦公開します。
あとで、少しずつ内容を充実させていきたい。

前提として、自身はi18n対応の経験がほとんどなく初心者レベルであるゆえ、書いてある内容も基本的なものがほとんどだと思います。
(昔、railsアプリ書いていたときにちょっとやっていた程度です...)


また、i18n対応を行うにあたって`react-intl`を使用していますが、他の方の書かれた記事を見ていると、自分とは別のやり方をしていたり、いろいろとやり方がありそうな感じがしました。
実際に自身でもサンプルを作成してみて、言語ファイルの管理の仕方一つをとっても色々な手法が出てきそうな気がしたので、こんなやり方もあるんだなーという感じで見ていただいたほうが良いかもしれません。


## react-intlを使う

Reactでフロントエンドアプリを作成する場合、いろいろとライブラリはあるようですが、Yahooの`react-intl`が一番メジャーそうなので、これを使っていきます。　
https://github.com/yahoo/react-intl

ちなみに作ってみたサンプルアプリはこんな感じ。ボタンを押すと順繰りに言語が変更されていきます。

![実際に作成してみたサンプルのgif demo](https://github.com/shinshin86/try-i18n-at-react/blob/master/gif/demo.gif?raw=true)

## 基本的な使い方

基本的には[react-intl - wiki](https://github.com/yahoo/react-intl/wiki)と[examples](https://github.com/yahoo/react-intl/tree/master/examples)を見ておけば、なんとなくわかりそうでした。

自身で作成した実装のサンプルをタグも付けた状態でGithubにあげています。
https://github.com/shinshin86/try-i18n-at-react/releases/tag/v0.0.1

まず、言語ファイルを作成します。
下記を`ja.js(日本語の言語ファイル)`としています。同じように`en.js(英語)`や`ko.js(韓国語)`なども作成しています。


```js
const ja = {
  'Top.Title': 'サンプルタイトル',
  'Top.Message': 'こんにちは! { name }!',
  'ChangeLocale.Btn': '言語を切り替える',
  'ChangeLocale.Msg': '言語を韓国語に切り替えます。よろしいですか?'
};

export default ja;
```


`{ name }`と書くことで、下記のように値をパラメータとして動的に渡すことが可能です。

```jsx
const name = 'John'
・
・
・
<FormattedMessage id="Top.Message" values={{ name }} />
{ // => こんにちは！ John! }
```

### 余談
**最初、ハマりそうになったこと**なのですが、言語ファイルはobjectのようにネストして管理ができません。

ただし、`json`や`yml`で記述できるようにすることは可能です。
後ほど下記のライブラリを用いて`yml`で管理していこうと思います。
https://github.com/akameco/yaml-flat-loader

本当は👇のように書いて、`<FormattedMessage id="Top.Message" values={{ name }} />`で参照できたら良かったんだけど...

```js
const ja = {
  Top : {
    Title: 'サンプルタイトル',
    Message: 'こんにちは! { name }!'
  },
  ChangeLocale: {
    Btn: '言語を切り替える',
    Msg: '言語を韓国語に切り替えます。よろしいですか?'
  }
};
```

設定した言語ファイルを実際に使用するためには、下記のような設定が必要となります。

```js
import { addLocaleData } from 'react-intl';
import jaLocaleData from 'react-intl/locale-data/ja';
import enLocaleData from 'react-intl/locale-data/en';
import koLocaleData from 'react-intl/locale-data/ko';

import en from './en';
import ja from './ja';
import ko from './ko';

addLocaleData(enLocaleData);
addLocaleData(jaLocaleData);
addLocaleData(koLocaleData);
```

また言語を切り替える仕組みとして下記のように`chooseLocale`関数を作成し、ページ初期化時や言語切替時に切り替えられるようにしました。
詳しくはソースコード内の実際の記述に譲りますが、ここでは`state`で選択した言語を保持して、反映させるようにしています。
(愚直な感じのコードを書いてしまっています。こればかりは自分の非力さを呪うばかり...)

ただし、実際には`Redux`や`Context API`を使ったほうが現実的だと思います。


```jsx
export const translationMessages = {
  en,
  ja,
  ko
};

export const chooseLocale = locale => {
  console.log('Your locale :', locale);
  switch (locale) {
    case 'en-GB':
      return translationMessages.en;
    case 'ja':
      return translationMessages.ja;
    case 'ko':
      return translationMessages.ko;
    default:
      return translationMessages.en;
  }
};
```

さて、上で諸々設定していますが、これらを実際にReact上で使用するために、下記のように`IntlProvider`で囲んでやります。

```jsx
  render() {
    const { locale } = this.state;
    return (
      <IntlProvider locale={locale} messages={chooseLocale(locale)}>
        <Main {...this.state} localeUpdate={this.localeUpdate} />
      </IntlProvider>
    );
  }
```

あとは下記のように`FormattedMessage`から呼び出せます。

```jsx
        <Title>
          <FormattedMessage id="Top.Title" />
        </Title>
        <p>
          <FormattedMessage id="Top.Message" values={{ name }} />
        </p>
```

### pure functionとして使いたい場合

ちなみに純粋な関数として使いたい場合は`injectIntl`を使うことで、props.intlから参照することが可能のようです。

```js
// 下記のように`components`をラップすることで`this.props.intl`から参照できるようになる
export default injectIntl(Main);
```

👇

```
const { locale, messages } = this.props.intl;
```

まだ自分は試していませんが、他にも下記のようなやり方などがあるようでした。
https://blog.mitsuruog.info/2016/10/using-react-intl-make-react-app-as-i18n.html


## 言語ファイルの管理をもっと良い感じにしたい

ここからは`akameco`さんの書かれた、この素晴らしい記事の受け売りにほぼなるかと思います。

コンポーネント時代のi18n
https://qiita.com/akameco/items/ccf32dedb3630f774358


実際に`yml`管理に変更したバージョンのソースコードは下記の通りとなります。

https://github.com/shinshin86/try-i18n-at-react/releases/tag/v0.0.2

`yaml-flat-loader`を使用することで`yml`での管理を行えるようにします。
また上に書いたネストできない問題も解消します。

https://github.com/akameco/yaml-flat-loader


実際に、言語ファイルは下記のように可読性が向上しています。

```yml
Top:
  Title: サンプルタイトル
  Message: こんにちは! { name }!
ChangeLocale:
  Btn: 言語を切り替える
  Msg: 言語を英語に切り替えます。よろしいですか?
```

実際にどう書き換えたかについてはコミットログに説明を譲ります。
(規模も小さいゆえ、変更量も非常に少ないです)

https://github.com/shinshin86/try-i18n-at-react/commit/49fe29b0d0fb39bd704876af4de2c59b19d392ec

(`prettier`の設定を修正したりしていますが、そこは関係ないので適当に読み流してください)

## スナップショットテスト
アプリの規模が膨大になってきた場合、テストは必須となるでしょう。
ここでチェックしたいことは下記のようなことになるでしょうか？

* すべての言語に値が入っているか？
* keyのtypoチェック
* ライセンスなど、言語が変わっても同一のテキストが本当に同一か？<br>
⇒TODO: こういう共通の値ってどうやって管理するのが良いのか自分の中で見えていないので、後日調べる

(typoチェックは`flowtype`を使っても実現できそうですが、今回はテストに寄せます。 → `flowtype`による保管の恩恵を受けられないようなパターンを想定しています)

```js
const en = require('./en.yml');
const ja = require('./ja.yml');
const ko = require('./ko.yml');

const getBlanks = obj => Object.keys(obj).filter(v => obj[v] === '');
const getAllKeys = obj => Object.keys(obj);

test('snapshot [en]', () => {
  expect(en).toMatchSnapshot();
});
test('snapshot [ja]', () => {
  expect(ja).toMatchSnapshot();
});
test('snapshot [ko]', () => {
  expect(ko).toMatchSnapshot();
});
test('check whitelist [en]', () => {
  const blanks = getBlanks(en);
  expect(blanks).toMatchSnapshot();
});
test('check whitelist [ja]', () => {
  const blanks = getBlanks(ja);
  expect(blanks).toMatchSnapshot();
});
test('check whitelist [ko]', () => {
  const blanks = getBlanks(ko);
  expect(blanks).toMatchSnapshot();
});

test('check key equals', () => {
  const langList = [en, ja, ko];
  const allLangKeyList = [];
  for (var lang of langList) {
    expect(getAllKeys(lang)).toMatchSnapshot();

    if (allLangKeyList > 1) {
      expect(allLangKeyList[allLangKeyList.length - 1]).toEquals(
        allLangKeyList[allLangKeyList.length]
      );
    }
  }
});
```



### 余談、というかTODO
どうしても下記のエラーが解消できずに、`const en = require('./en.yml')`使っています...
`babelrc`の設定がおかしいのかと思うのですが、後日解消します。。。)

```bash
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import en from './en.yml';
                                                                                                    ^^

    SyntaxError: Unexpected identifier

    at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:403:17)
```

## meta要素のi18n対応
metaタグの対応を行う場合、Reactを用いているケースでは`react-helmet`を使用するのが一般的かと思います。
(あくまで私個人の観測圏内におけるデファクトスタンダードですが)

非常にシンプルなパターンを下に書き出します。

`MetaTags`というメタタグ用のComponentを用意し、そちらに`this.props.intl`をぶち込みます。

```jsx
import MetaTags from './MetaTags';
・
・
・
    return (
      <Container>
        <MetaTags {...this.props.intl} />
        <Title>
          <FormattedMessage id="Top.Title" />
        </Title>
        <p>
```

あとは渡ってきた値から必要となるものを当てはめていくだけです。
`{messages['Meta.Title']}`という形で参照しているのが微妙な感じはありますが、ひとまずやりたいことの実現はできています。
(ここについては、もっとスマートなやり方があるかもしれません)


```jsx
import React from 'react';
import { Helmet } from 'react-helmet';

const MetaTags = ({ messages }) => (
  <Helmet>
    <title>{messages['Meta.Title']}</title>
    <meta name="description" content={messages['Meta.Description']} />
  </Helmet>
);

export default MetaTags;
```

## 終わりに
なんとか最後まで書ききりましたが、振り返ってみると課題やTODOが盛りだくさんです。
また、実際に`react-intl`を使って運用していく上で見えてくることなども多いと思うので、少しずつ修正しながら今後内容を充実させていけたらと考えています。


## i18next ことはじめ(追記)
最近`React`アプリでの多言語対応にて、`i18next`, `react-i18next`について調べていました。
こちらも`react-intl`同様、`i18n対応`では、よく使われているライブラリのようです。
触ってみた際の内容を下記にまとめたので、ポストのリンクを下記に貼らせてもらいます。

[Reactアプリでのi18n対応(国際化/多言語対応)にi18nextを使ってみる](https://shinshin86.hateblo.jp/entry/2019/09/28/084835)