年末〜年始にかけて、React with i18n対応(国際化対応)について調べていました。
下書きでずっと塩漬けになっているのを思い出し、TODO
などを残したまま一旦公開します。
あとで、少しずつ内容を充実させていきたい。
前提として、自身はi18n対応の経験がほとんどなく初心者レベルであるゆえ、書いてある内容も基本的なものがほとんどだと思います。
(昔、railsアプリ書いていたときにちょっとやっていた程度です...)
また、i18n対応を行うにあたってreact-intl
を使用していますが、他の方の書かれた記事を見ていると、自分とは別のやり方をしていたり、いろいろとやり方がありそうな感じがしました。
実際に自身でもサンプルを作成してみて、言語ファイルの管理の仕方一つをとっても色々な手法が出てきそうな気がしたので、こんなやり方もあるんだなーという感じで見ていただいたほうが良いかもしれません。
react-intlを使う
Reactでフロントエンドアプリを作成する場合、いろいろとライブラリはあるようですが、Yahooのreact-intl
が一番メジャーそうなので、これを使っていきます。
https://github.com/yahoo/react-intl
ちなみに作ってみたサンプルアプリはこんな感じ。ボタンを押すと順繰りに言語が変更されていきます。
基本的な使い方
基本的にはreact-intl - wikiとexamplesを見ておけば、なんとなくわかりそうでした。
自身で作成した実装のサンプルをタグも付けた状態でGithubにあげています。
https://github.com/shinshin86/try-i18n-at-react/releases/tag/v0.0.1
まず、言語ファイルを作成します。
下記をja.js(日本語の言語ファイル)
としています。同じようにen.js(英語)
やko.js(韓国語)
なども作成しています。
const ja = {
'Top.Title': 'サンプルタイトル',
'Top.Message': 'こんにちは! { name }!',
'ChangeLocale.Btn': '言語を切り替える',
'ChangeLocale.Msg': '言語を韓国語に切り替えます。よろしいですか?'
};
export default ja;
{ name }
と書くことで、下記のように値をパラメータとして動的に渡すことが可能です。
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 }} />
で参照できたら良かったんだけど...
const ja = {
Top : {
Title: 'サンプルタイトル',
Message: 'こんにちは! { name }!'
},
ChangeLocale: {
Btn: '言語を切り替える',
Msg: '言語を韓国語に切り替えます。よろしいですか?'
}
};
設定した言語ファイルを実際に使用するためには、下記のような設定が必要となります。
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
を使ったほうが現実的だと思います。
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
で囲んでやります。
render() {
const { locale } = this.state;
return (
<IntlProvider locale={locale} messages={chooseLocale(locale)}>
<Main {...this.state} localeUpdate={this.localeUpdate} />
</IntlProvider>
);
}
あとは下記のようにFormattedMessage
から呼び出せます。
<Title>
<FormattedMessage id="Top.Title" />
</Title>
<p>
<FormattedMessage id="Top.Message" values={{ name }} />
</p>
pure functionとして使いたい場合
ちなみに純粋な関数として使いたい場合はinjectIntl
を使うことで、props.intlから参照することが可能のようです。
// 下記のように`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
管理に変更したバージョンのソースコードは下記の通りとなります。
yaml-flat-loader
を使用することでyml
での管理を行えるようにします。
また上に書いたネストできない問題も解消します。
実際に、言語ファイルは下記のように可読性が向上しています。
Top:
Title: サンプルタイトル
Message: こんにちは! { name }!
ChangeLocale:
Btn: 言語を切り替える
Msg: 言語を英語に切り替えます。よろしいですか?
実際にどう書き換えたかについてはコミットログに説明を譲ります。
(規模も小さいゆえ、変更量も非常に少ないです)
(prettier
の設定を修正したりしていますが、そこは関係ないので適当に読み流してください)
スナップショットテスト
アプリの規模が膨大になってきた場合、テストは必須となるでしょう。
ここでチェックしたいことは下記のようなことになるでしょうか?
- すべての言語に値が入っているか?
- keyのtypoチェック
- ライセンスなど、言語が変わっても同一のテキストが本当に同一か?
⇒TODO: こういう共通の値ってどうやって管理するのが良いのか自分の中で見えていないので、後日調べる
(typoチェックはflowtype
を使っても実現できそうですが、今回はテストに寄せます。 → flowtype
による保管の恩恵を受けられないようなパターンを想定しています)
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
の設定がおかしいのかと思うのですが、後日解消します。。。)
({"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
をぶち込みます。
import MetaTags from './MetaTags';
・
・
・
return (
<Container>
<MetaTags {...this.props.intl} />
<Title>
<FormattedMessage id="Top.Title" />
</Title>
<p>
あとは渡ってきた値から必要となるものを当てはめていくだけです。
{messages['Meta.Title']}
という形で参照しているのが微妙な感じはありますが、ひとまずやりたいことの実現はできています。
(ここについては、もっとスマートなやり方があるかもしれません)
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対応
では、よく使われているライブラリのようです。
触ってみた際の内容を下記にまとめたので、ポストのリンクを下記に貼らせてもらいます。