これはi18n対応のために i18next
と react-i18next
を使ってみた備忘録となる。
目次
- 目次
- Reactアプリでのi18n対応(国際化/多言語対応)にi18nextを使ってみる
- react-intlとi18next
- サンプルアプリの雛形生成
- react-i18nextのセットアップ
- namespacesについて
- 言語を切り替える機能を実装する
- 翻訳した言語の管理方法
- Transを用いて、HTMLタグが使われている箇所を多言語化する
- addResourceBundleとremoveResourceBundleを用いて、必要て最低限のresourceのみを利用する方法
- react-i18nextでObject(...) is not a functionというエラーに悩まされた
Reactアプリでのi18n対応(国際化/多言語対応)にi18nextを使ってみる
仕事で多言語対応をすることになり、i18next
を使うことにした。
Introduction - i18next documentation
Introduction - react-i18next documentation
react-intlとi18next
ちなみに react i18n 対応
で検索すると、なんと驚いたことに過去にQiitaに書いた自分の記事が一番目にヒットした(2019年9月時点)。
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-app
でyarn 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のセットアップ
ここからreact
でi18next
を使っていくための設定を行っていく。
これらの記述はだいたい下記のドキュメントを読んでいけば、わかるようになっている。
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
i18nextのnamespaceについてだが、小規模なプロジェクトであればすべてのテキストを一つのファイルに纏めて利用する形で事足りると思うが、大規模なサービスを運用したい場合はそうも言ってられなくなる。
そこでnamespaceを区切って、必要なテキストだけを読み込んでいこうということになる。
当然全てのテキストファイルが一度に読まれなくなるので読み込み時間の短縮などにもつながる。
ただ、namespaceを分けることによって、テキストの管理コストは上がるため、そこも考えてプロジェクト内で慎重に検討していったほうが良さそうというのが個人的な考え。
ドメインごとのテキストと共通で利用するテキストなど、どういう考え方で分けていくか?などを考えていく必要がある。
ちなみに i18next
の公式サイトでは、下記のような分け方で分けているという書かれている。参考にしてみるのも良いかも。
common.json -> あらゆる場所で再利用されるもの、例えばボタンのラベル「save」や「cancel」など。
validation.json -> すべての検証テキスト
glossary.json -> テキストの中で一貫して使われて欲しい単語
https://www.i18next.com/principles/namespaces#semantic-reasons
translation というnamespaceの挙動について
translation
はdefaultで利用されるnamespaceとなる。
namespaceが必要がない場合は translation
というnamespaceがルートに位置する形となる。
例えば、このtranslation
というnamespaceがない場合、i18next自体が機能しなくなる。
例として、下記のような構成の場合、動かない。 (日本語の言語ファイルは割愛)
{ "Learn React": "Learn React", "Save to Reload text": "Edit <1>{{filename}}</1> and save to reload." }
import en from './en/translation'; import ja from './ja/translation'; export default { en, ja };
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import resources from './locales/resources'; i18n.use(initReactI18next).init({ resources, fallbackLng: 'en', debug: true, interpolation: { escapeValue: false } }); export default i18n;
この状態では動かない。
下記のように translation
をルートに持ってくることで動くことが確認できる。
{ "translation": { "Learn React": "Learn React", "Save to Reload text": "Edit <1>{{filename}}</1> and save to reload." } }
実際に動かすときは下記のように呼び出すことになる。
// 呼び出すとき t('Learn React')
この挙動からも translation
がルートには必要であることが分かる。
次はこの translation
を別の namespace1
というnamespaceに変えてみる。
すると呼び出し方が変わる。
{ "namespace1": { "Learn React": "Learn React", "Save to Reload text": "Edit <1>{{filename}}</1> and save to reload." } }
呼び出すときはこのnamespace1を込みで呼び出す必要がある。
(勿論この例はあくまでresourceファイルをそのままi18next側に読み込ませているからなわけで、読み込ませ方によって異なる呼び出し方になると思う)
// 呼び出すとき頭にnamespaceを指定する t('namespace1:Learn React')
さらには上の状態にルートに translation
を持ってくる
{ "translation": { "namespace1": { "Learn React": "Learn React", "Save to Reload text": "Edit <1>{{filename}}</1> and save to reload." } } }
この場合呼び出し方が下記のように変わってくる。
t('namespace1.Learn React')
以上のような挙動だが、たぶんここらへんで実装しているような気がするので、個人的なメモとして残しておく。
https://github.com/i18next/i18next/blob/master/src/ResourceStore.js#L36-L50
またこのような挙動については、ドキュメントの下記のページにも記載があるので、そちらを熟読したほうが本質的な理解に繋がりそう。
https://www.i18next.com/translation-function/essentials#accessing-keys-in-different-namespaces
Namespaces - i18next documentation
translationを言語のテキストファイルに含めたくない場合
このあとのサンプルにも登場させる書き方でいけば、言語テキストを格納するjsonファイルに translation
を含める必要はなくなる。
(こちらについては 翻訳した言語の管理方法
という項目に書いている)
かんたんなサンプルを下記に記載する。
{ "namespace1": { "Learn React": "Learn React", "Save to Reload text": "Edit <1>{{filename}}</1> and save to reload." } }
下記のように言語ファイルは一つに予めまとめておくようにして、その後まとめたこれらのresourceをi18nextの初期化処理時に食わせる流れとする。
import enTranslation from './en/translation'; import jaTranslation from './ja/translation'; export default { en: { translation: enTranslation }, ja: { translation: jaTranslation } };
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import resources from './locales/resources'; i18n.use(initReactI18next).init({ resources, fallbackLng: 'en', debug: true, interpolation: { escapeValue: false } }); export default i18n;
という形でセットすることで、下記のように呼び出すことが可能となる。
t('namespace1.Learn React')
言語を切り替える機能を実装する
i18n.changeLanguage
という関数が用意されているので、こちらで切り替えを実装できる。
(雑なサンプルで恐縮である)
import React from 'react'; import logo from './logo.svg'; import './App.css'; import './i18n'; 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> <Trans i18nKey="Save to Reload text"> Edit <code>{{ filename }}</code> and save to reload. </Trans> </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > {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> ); } export default withTranslation()(App);
翻訳した言語の管理方法
ここでは locales
というディレクトリを作り、その配下に各種言語ごとに翻訳したリソースを格納、
これらはresources.js
というものでまとめて export
することで、アプリ側に読み込んでいくような設定とする。
このやり方の場合、言語の変更を行う場合はソースコード自体を変更するしかないので、実際の運用ではもう少し考える必要がありそうだ。
本来であれば、翻訳した言語は別のSaaS
的なもので管理していき、ソースコードを変更せずとも言語ごとのテキストを変えられるようにしたほうが良いと思われる。
例えばi18next
では locize
という多言語管理用のSaaS
が親和性が高いようだ。が、結構エンジニアフレンドリーなサービスのようにも感じるので、多言語リソース管理をエンジニアではない人間が担当する場合は、管理を少しばかり考える必要がある。
localization & translation management platform | locize
例えば、先日ブログにも書いたが、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タグが使われている箇所を多言語化する
下記のようにTrans
componentを用いて多言語化できる。
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
addResourceBundleとremoveResourceBundleを用いて、必要て最低限のresourceのみを利用する方法
例えば一度に複数の言語ファイルを読み込みたくないという要望はあると思う。
そういうとき、必要な言語ファイルだけを読み込むにはどうすればよいか?
そこで登場するのが addResourceBundle
という関数になる。
また、逆にresourceを削除するための removeResourceBundle
という関数もある
https://www.i18next.com/how-to/add-or-load-translations#add-after-init
そこでこれらのコードを利用して、下記のようなコードを作成してみた
まずi18n.jsである。
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; i18n.use(initReactI18next).init({ resources: {}, fallbackLng: 'en', debug: true, interpolation: { escapeValue: false } }); export default i18n;
ここでは空のresourcesを渡しているが、最初からなんらかの言語情報をresourcesとして渡しても問題ない
ひとまずここでは空のresourcesを渡して、後で動的にリソースを格納していくことにする。
さらに言語resourceの追加・削除、並びに利用する言語の変更を行うための関数を i18-utils.js
という名前で作成した。
import resources from './locales/resources'; const resourceKeys = Object.keys(resources); export const changeLang = (i18n, lang) => { // 更新対象のresourceを追加する i18n.addResourceBundle(lang, 'translation', resources[lang].translation); // 言語変更処理 i18n.changeLanguage(lang); // 不要になったresourceは削除する const removeLnaguages = resourceKeys.filter((languag) => languag !== lang); for (const removeLang of removeLnaguages) { i18n.removeResourceBundle(removeLang, 'translation') } return; }
そしてこれらのコードを使った形で下記のようなコードを書く。
ここでは useTranslation
を用いたコードで書いていく。
import React, { useCallback, useEffect } from 'react'; import logo from './logo.svg'; import './App.css'; import './i18n'; import { useTranslation, Trans } from 'react-i18next'; import { changeLang } from './i18n-utils'; const App = () => { const [ t, i18n ] = useTranslation(); const filename = 'src/App.js'; const onClickChangeLanguage = useCallback((lang) => { changeLang(i18n, lang) }, [i18n]); useEffect(() => { changeLang(i18n, 'en') }, []) console.log(i18n.logger.options.resources) return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> <Trans i18nKey="Save to Reload text"> Edit <code>{{ filename }}</code> and save to reload. </Trans> </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > {t('Learn')} </a> <div> <p>Change Language</p> <button onClick={() => onClickChangeLanguage('en')}>en</button> <button onClick={() => onClickChangeLanguage('ja')}>ja</button> </div> {i18n.languages.map(lang => ( <p key={lang}>{lang}</p> ))} </header> </div> ); } export default App;
これを実行すると、正常に言語切替が行われる。
また言語切替が行われるタイミングでi18n内に格納されている言語のうち、選択されたもの以外のresourceが削除される。
これは console.log(i18n.logger.options.resources)
というデバッグ用の関数を差し込むことで確認が可能。
i18n.languagesの中身について
上のコードでデバッグ目的な下記のような記述を書いた
{i18n.languages.map(lang => ( <p key={lang}>{lang}</p> ))}
例えば英語を選択した場合、 en
が入るが、日本語を選択した場合、ここには en
, ja
という2つが入る。
なぜ en
はこの配列の中に居座り続けているのだろうかと考えたが、ここは
i18n.use(initReactI18next).init({ resources: {}, fallbackLng: 'en', debug: true, interpolation: { escapeValue: false } });
ここで fallbackLng
として指定していたからだった。
ここで指定すると、i18n.languages
の中には居座り続ける仕様となっている、というのは実装する上で頭の中に入れておいたほうが良いかもしれないと思った次第。
react-i18nextでObject(...) is not a functionというエラーに悩まされた
react-i18next
を触る上で、少しハマったことがあったので、最後に書き記しておく。
ここまで書いていたようなことを、とある環境で試していたところ、なぜだか Object(...) is not a function
というエラーが出る。
最初、自身のReactの使い方が間違っているのかと疑っていたが、なんてことはなく、react
, react-dom
のバージョンが古いだけだった。。
私はこの原因を突き止めるのに3時間ほどかけてしまった。猛省した。