Reactでi18nのjson書き出しサボり設定

# 使いどころ

[`create-react-app`](https://create-react-app.dev/docs/adding-typescript/)を使って作成したプロジェクトを、
[`i18n-next`](https://react.i18next.com/)で多言語化したとき、翻訳データの`*.json`を手書きするのが面倒。
`*.tsx`や`*.js`で翻訳する部分を自動で見つけて`*.json`に追記してほしい！

今回の例として紹介するのは、フロントエンドだけで完結する、非常に簡単なSPAです。
`i18n-next`は、[locize](https://locize.com/)を導入できたり、バックエンド開発にも使えるので、知っておくとお得です。

# ハマりポイント

先にハマりポイント解説します。

少し前は[`babel-plugin-i18next-extract`](https://www.npmjs.com/package/babel-plugin-i18next-extract)が流行ったようです。しかし、`webpack`の設定変更が必須となります。`create-react-app`を使って制作しているアプリケーションの場合、`webpack`の設定を変更することは、`npm eject`というコマンドを使うと実現できますが、不可逆であり得策ではありません。そのため、`babel-plugin-i18next-extract`を使うのはやめたほうが良さそうです。そこで、利用するのは、[公式ドキュメント](https://www.i18next.com/overview/plugins-and-utils#extraction-tools)にも記載のある、[`i18next-parser`](http://i18next.github.io/i18next-scanner/)です。

#extraction-tools)にも記載のある、[`i18next-parser`](http://i18next.github.io/i18next-scanner/)です。

まずは、[公式ドキュメント](https://www.i18next.com/overview/plugins-and-utils#extraction-tools)にも記載のある、[`i18next-parser`](http://i18next.github.io/i18next-scanner/)です。

```posh:terminal
npm install -g i18next-parser
```

# 設定

[公式ドキュメント](https://github.com/i18next/i18next-parser)を参考に、i18next-parserの設定を書きます。

```tsx:./i18next-parser.config.js
module.exports = {
    locales: ['en', 'ja', 'cn'],
    output: 'src/locales/$LOCALE/$NAMESPACE.json',
}
```

- locale: 欲しい分だけ言語を追記
- output: 位置がsrcの中でないと、tsxからimport出来ないので、デフォルト設定を上書き

npm の scripts を設定します。

```json-doc:./package.json
"scripts": {
    "i18next-extract": "i18next 'src/**/*.{tsx,ts}'"
}
```

ひとまず設定が終わったので、翻訳ファイルのjsonを書き出します。

```posh:terminal
npm run i18next-extract
```

すると、下記のような構成で出力されるはずです。

```shell:tree
./
┃
┣━ src
┃    ┣━ locales
┃    ┃    ┣━ cn
┃    ┃    ┃    ┗━ translation.json
┃    ┃    ┣━ en
┃    ┃    ┃    ┗━ translation.json
┃    ┃    ┗━ jp
┃    ┃         ┗━ translation.json
```

# コード変更

ここから、ソースコードに翻訳用の設定を足していきます。
まずは、トップレベルのReactファイルにi18nの初期化設定を書き込みます。
この時、先ほど出力された`translation.json`ファイルを設定します。

```diff_tsx:./src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.sass';
import reportWebVitals from './reportWebVitals';
import App from "./components/App";
import { createBrowserHistory } from "history"
import { Router } from 'react-router';
import HeaderApp from './components/header';
// i18n
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';

import jaCommon from './locales/ja/translation.json'
import enCommon from './locales/en/translation.json'
import cnCommon from './locales/cn/translation.json'

i18next.use(initReactI18next).init({
  debug: true,
  resources : {
    ja:{common:jaCommon},
    en:{common:enCommon},
    cn:{common:cnCommon},
  },
  lng: 'ja',
  fallbackLng: 'ja',
  keySeparator: false,
  interpolation: { escapeValue: false },
});

const history = createBrowserHistory({ basename: '/' });
ReactDOM.render(
  <React.StrictMode>
    <Router history={history}>
      <HeaderApp/>
      <App></App>
    </Router>
  </React.StrictMode>,
  document.getElementById('root')
);

reportWebVitals(console.log);
```

`<App />`の定義は下記です。

```tsx:./src/components/App.tsx
import React from 'react';
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react'

import ja_Trans from '../locales/ja/maincontent.json'
import en_Trans from '../locales/en/maincontent.json'
import cn_Trans from '../locales/cn/maincontent.json'

export function Translate() {
    const { t } = useTranslation('maincontent');
    useEffect(() => {
        i18next.addResources('ja', 'maincontent', ja_Trans);
        i18next.addResources('en', 'maincontent', en_Trans);
        i18next.addResources('cn', 'maincontent', cn_Trans);
    }, [])
    return (
        <React.StrictMode>
             <p>{t('message')}</p>
        </React.StrictMode>
    );
}
class App extends React.Component {
    render() {
        return <Translate/>;
    }
}

export default App;
```

これだけだと、まだ言語切り替えが出来ません。先ほどの`index.tsx`に書かれている`<HeaderApp />`の詳細を書いていきます。

```tsx:./src/components/header.tsx
import React from 'react';
import { FunctionComponent } from "react";
import { LanugageSelector } from "./LanguageSelector";
import { useTranslation } from 'react-i18next'
import { useState } from 'react';
import { useEffect } from 'react';

const HeaderApp: FunctionComponent = () => {
    const [t, i18n] = useTranslation();
    const languages = [{ code: "ja", name: "JAPANESE" }, { code: "en", name: "ENGLISH" }, { code: "cn", name: "CHINESE" }];
    const [currentLang, setCurrentLang] = useState("ja");
    // Change language of i18n
    useEffect(() => {
        i18n.changeLanguage(currentLang);
    }, [currentLang, i18n])
    // Handler
    const handleLanguageSelectionChange = (lang: string) => {
        setCurrentLang(lang);
    }
    return (
        <React.StrictMode>
            <header>
                <div>
                    <ul>
                        <LanugageSelector languages={languages} selectedLanguage={currentLang} onLanguageSelectionChange={handleLanguageSelectionChange} />
                    </ul>
                </div>
            </header>
        </React.StrictMode>
    );

}

export default HeaderApp;
```

`<LanugageSelector />`の定義は下記です。

```tsx:./src/components/LanguageSelector.tsx
import React, { FunctionComponent } from "react";

export type LanguageSelectorProps = {
    languages: { code: string, name: string }[],
    selectedLanguage: string,
    onLanguageSelectionChange: (lang: string) => void,
}

export const LanugageSelector: FunctionComponent<LanguageSelectorProps> = (props) => {
    const langs = props.languages.map((lang) => {
        return (lang.code === props.selectedLanguage) ? '' : <li><p onClick={() => props.onLanguageSelectionChange(lang.code)}>{lang.name}</p></li>
    });

    return (
        <div className="i18n">
            {langs}
        </div>
    );
}
```

この`<LanguageSelector/>`により、`<p>ENGLISH</p>`、`<p>JAPANESE</p>`、`<p>CHINESE</p>`が表示されます。これをクリックすると表示が切り替わる仕組みです。3項演算子で、「現在表示中の言語」は消しています。

# 再びのextract

ここまで書けたら、もう一度、先ほどのコマンドを実行してみます。

```posh:terminal
npm run i18next-extract
```

```shell:tree
./
┃
┣━ src
┃    ┣━ locales
┃    ┃    ┣━ cn
┃    ┃    ┃    ┣━ maincontent.json
┃    ┃    ┃    ┗━ translation.json
┃    ┃    ┣━ en
┃    ┃    ┃    ┣━ maincontent.json
┃    ┃    ┃    ┗━ translation.json
┃    ┃    ┗━ jp
┃    ┃    ┃    ┣━ maincontent.json
┃    ┃         ┗━ translation.json
```

ここまで来たら、おめでとうございます！
あとは、ひたすら翻訳を書いていくだけです。

# まとめ

`npm start`の中で`npm run i18next-extract`を動かすにはどうするのでしょう？

**Excelsior!**