at backyard

Color my life with the chaos of trouble.

Reactアプリでのi18n対応(国際化/多言語対応)にi18nextを使ってみる

Reactアプリでのi18n対応(国際化/多言語対応)にi18nextを使ってみる

仕事で多言語対応をすることになり、i18nextを使うことにした。

Introduction - i18next documentation

Introduction - react-i18next documentation

react-intlとi18next

ちなみに react i18n 対応 で検索すると、なんと驚いたことに過去にQiitaに書いた自分の記事が一番目にヒットした(2019年9月時点)。

qiita.com

2019年9月時点で 16イイね がついているし、私にしては大健闘な状況である。
QiitaのSEO効果の偉大さを改めて思い知った。

こちらの記事では react-intl を使用しており、一応の使い方は理解したつもりだが、なぜ今回 i18next を使うことにしたかというと、うちの会社の別のサービスで既に導入実績があるからだ。
私が勤めている会社は、基本的に私以外は皆 凄腕エンジニアばかりで(もちろん、これはお世辞ではない)、 多言語対応周りの実装のレビューをしたりしながら、ふむふむこうやって使うのか、なるほどなーといつも勉強させてもらっていた。
そうこうしているうちに自分がメインで担当しているサービスでも一部多言語対応をしていくことになったため、既に皆が触っている i18next のほうがメンテナンスもしやすいし、社内に知見もたまってきているというわけで、こちらを導入することにしたというわけである。

というわけで、これは i18next を使ってみた、的な備忘録となる。

実際の仕事で使うやり方とは多少異なってくると思うが、自分の理解を深めるために、一通り触ってみたメモをこちらに残しておく。

サンプルアプリの雛形生成

create-react-appを使ってサンプルアプリの雛形を作成していく

npx create-react-app react-i18next-sample
cd react-i18next-sample
yarn start

create-react-appでブラウザが勝手に立ち上がらないようにする

ちなみに私はcreate-react-appyarn startしたときに勝手にブラウザが立ち上がる挙動が許せない質なので、下記のようにpackage.jsonを書き換えている。

diff --git a/package.json b/package.json
index 52a2f12..0730227 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
     "react-scripts": "3.1.2"
   },
   "scripts": {
-    "start": "react-scripts start",
+    "start": "BROWSER=none react-scripts start",
     "build": "react-scripts build",
     "test": "react-scripts test",
     "eject": "react-scripts eject"

これらの設定は下記に記述がある。

Advanced Configuration · Create React App

react-i18nextのセットアップ

ここからreacti18nextを使っていくための設定を行っていく。 これらの記述はだいたい下記のドキュメントを読んでいけば、わかるようになっている。

Getting started - i18next documentation

Getting started - react-i18next documentation

実際の実装については下記が参考になる

react-i18next/example/react at master · i18next/react-i18next · GitHub

一つだけ注意点。
自分がここから書いていく記述では、 SSR は考慮しないものとしていることを先に書いておく

yarn add i18next react-i18next 

i18n.js というファイルを作成する

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

const resources = {
  en: {
    translation: {
      'Learn React': 'Learn React'
    }
  },
  ja: {
    translation: {
      'Learn React': 'Reactを学ぶ'
    }
  }
};

i18n.use(initReactI18next).init({
  resources,
  fallbackLng: 'en',
  debug: true,

  interpolation: {
    escapeValue: false
  }
});

export default i18n;

HOCで使う場合はwithTranslationを使う

HOCを使う場合は withTranslation を用い、下記のようにしてi18nextの機能を使う

src/App.jsを下記のように書き換える

import React from 'react';
import logo from './logo.svg';
import './App.css';
import './i18n';
import { withTranslation } from 'react-i18next';

function App({ t }) {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          {t('Learn React')}
        </a>
      </header>
    </div>
  );
}

export default withTranslation()(App);

i18n.js内のfallbackLng: 'en'fallbackLng: 'ja'に書き換えることで言語が変わるのが確認できる。

hookで使う場合はuseTranslationを使う

hookで使う場合は useTranslation を使う。
上のサンプルと見比べてみると、使い方の違いがより分かるかと思う。

import React from 'react';
import logo from './logo.svg';
import './App.css';
import './i18n';
import { useTranslation } from 'react-i18next';

function App() {
  const { t, i18n } = useTranslation();
  //  下記のような書き方でもOK
  //  const [ t, i18n ] = useTranslation();

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          {t('Learn React')}
        </a>
      </header>
    </div>
  );
}

export default App;

namespacesについてはここでは割愛

ちなみにここまでしれっと translation という namespacesを使ってきたが、ここについては下記のドキュメントが参考になるかと思う。

Namespaces - i18next documentation

自分自身ここらへんはまだちゃんと理解しきっているわけではないので、割愛する

言語を切り替える機能を実装する

i18n.changeLanguage という関数が用意されているので、こちらで切り替えを実装できる。 (雑なサンプルで恐縮である)

diff --git a/src/App.js b/src/App.js
index 89ee7ec..67b04a9 100644
--- a/src/App.js
+++ b/src/App.js
@@ -4,7 +4,11 @@ import './App.css';
 import './i18n';
 import { withTranslation } from 'react-i18next';

-function App({ t }) {
+function changeLanguage(i18n, lang) {
+  i18n.changeLanguage(lang);
+}
+
+function App({ t, i18n }) {
   return (
     <div className="App">
       <header className="App-header">
@@ -20,6 +24,11 @@ function App({ t }) {
         >
           {t('Learn React')}
         </a>
+        <div>
+          <p>Change Language</p>
+          <button onClick={() => changeLanguage(i18n, 'en')}>en</button>
+          <button onClick={() => changeLanguage(i18n, 'ja')}>ja</button>
+        </div>
       </header>
     </div>
   );

f:id:shinshin86:20190926084817g:plain

翻訳した言語の管理方法

ここでは localesというディレクトリを作り、その配下に各種言語ごとに翻訳したリソースを格納、 これらはresources.jsというものでまとめて export することで、アプリ側に読み込んでいくような設定とする。

このやり方の場合、言語の変更を行う場合はソースコード自体を変更するしかないので、実際の運用ではもう少し考える必要がありそうだ。

本来であれば、翻訳した言語は別のSaaS的なもので管理していき、ソースコードを変更せずとも言語ごとのテキストを変えられるようにしたほうが良いと思われる。
例えばi18nextでは locize という多言語管理用のSaaSが親和性が高いようだ。が、結構エンジニアフレンドリーなサービスのようにも感じるので、多言語リソース管理をエンジニアではない人間が担当する場合は、管理を少しばかり考える必要がある。

locize - continuous localization as a service

例えば、先日ブログにも書いたが、contentfulなどのHeadless CMSなどで管理していくのも一つだと思う。

Headless CMS の Contentful からデータを取得して、Next.jsのページで表示させるメモ - at backyard

少し話がそれてしまったが、多言語リソースの管理について書いていく。

先程も書いたが、localesというディレクトリを作成し、下記のように言語を管理していく

src/locales
├── en
 │   └── translation.json
├── ja
 │   └── translation.json
└── resources.js

ここのtranslation.json というファイルには、下記のようにそれぞれの言語でのテキストが入ることになる。

en/translation.json

{
  "Learn React": "Learn React"
}

ja/translation.json

{
  "Learn React": "Reactを学ぶ"
}

これらの言語ファイルをresources.jsでまとめてexportする形だ

import enTranslation from './en/translation';
import jaTranslation from './ja/translation';

export default {
  en: {
    translation: enTranslation
  },
  ja: {
    translation: jaTranslation
  }
};

そして、i18n.jsでは下記のようにresouces.jsを読み込む形に修正する

diff --git a/src/i18n.js b/src/i18n.js
index 5673005..bed5f11 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1,18 +1,6 @@
 import i18n from 'i18next';
 import { initReactI18next } from 'react-i18next';
-
-const resources = {
-  en: {
-    translation: {
-      'Learn React': 'Learn React'
-    }
-  },
-  ja: {
-    translation: {
-      'Learn React': 'Reactを学ぶ'
-    }
-  }
-};
+import resources from './locales/resources';

 i18n.use(initReactI18next).init({
   resources,

これでひとまずのところ、言語ファイルを分離して管理することができるようになった。
ここについては正直もう少し考えてみる必要がありそうだが、とりあえずの ことはじめ として一旦このような形にした。

Transを用いて、HTMLタグが使われている箇所を多言語化する

下記のようにTranscomponentを用いて多言語化できる。

diff --git a/src/App.js b/src/App.js
index 67b04a9..bbaa97d 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,19 +2,23 @@ import React from 'react';
 import logo from './logo.svg';
 import './App.css';
 import './i18n';
-import { withTranslation } from 'react-i18next';
+import { withTranslation, Trans } from 'react-i18next';

 function changeLanguage(i18n, lang) {
   i18n.changeLanguage(lang);
 }

 function App({ t, i18n }) {
+  const filename = 'src/App.js';
+
   return (
     <div className="App">
       <header className="App-header">
         <img src={logo} className="App-logo" alt="logo" />
         <p>
-          Edit <code>src/App.js</code> and save to reload.
+          <Trans i18nKey="Save to Reload text">
+            Edit <code>{{ filename }}</code> and save to reload.
+          </Trans>
         </p>
         <a
           className="App-link"

このように記述した場合、テキストファイルには下記のように記述する。

日本語テキスト

{
  "Learn React": "Reactを学ぶ",
  "Save to Reload text": "<1>{{filename}}</1>を編集して保存すると、リロードされます。"
}

英語テキスト

{
  "Learn React": "Reactを学ぶ",
  "Save to Reload text": "<1>{{filename}}</1>を編集して保存すると、リロードされます。"
}

<1> で囲むというところが若干分かりにくい気もするが、、、こういうものなのだろうか?
自分自身あまり使い込めていないので、とりあえず使っていってみる!

この Trans コンポーネントに関する詳細については、下記のページに詳しく書かれている。

Trans Component - react-i18next documentation

react-i18nextでObject(...) is not a functionというエラーに悩まされた

react-i18nextを触る上で、少しハマったことがあったので、書き記しておく。

ここまで書いていたようなことを、とある環境で試していたところ、なぜだか Object(...) is not a function というエラーが出る。
最初、自身のReactの使い方が間違っているのかと疑っていたが、なんてことはなく、react, react-domのバージョンが古いだけだった。。

github.com

私はこの原因を突き止めるのに3時間ほどかけてしまった。猛省した。