hi18nとは
hi18n は現在Wantedlyで開発中の、TypeScript/JavaScript向け翻訳テキスト管理ライブラリ (i18nライブラリの一種) です。
基本の使い方は以下の記事で説明しています。
本稿では発展的な使い方として、条件に応じて異なるメッセージを出し分ける方法について説明します。
何が問題なのか
たとえばWebサイトのナビゲーションのためにメニューを用意することを考えます。メニューのレンダリングをループで以下のように書いたとします。
const menus = ["posts", "profile", "settings"];
menus.map((menu) => (
<li><a href={`/${menu}`}>{menu}</a></li>
);
ここでメニューのタイトルをより説明的にするために、翻訳関数 t を使って以下のように書いたとします。
const menus = ["posts", "profile", "settings"];
menus.map((menu) => (
<li><a href={`/${menu}`}>{t(`example/menu/${menu}/title`)}</a></li>
);
この場合、以下の3つの翻訳を定義すれば動作します。
- example/menu/posts/title
- example/menu/profile/title
- example/menu/settings/title
しかし、このようにテンプレートリテラル等を使って動的に翻訳IDを生成する方法はhi18nでは推奨されていません。それは利用箇所をツールで正しく検出できないからです。
この状態でhi18nのCLIツールで同期コマンドを実行したとします。
yarn hi18n sync 'src/**/*.ts' 'src/**/*.tsx'
すると、定義していたはずの翻訳IDはコメントアウトされてしまいます。
export default new Catalog({
// 実際には使っているのにコメントアウトされてしまう
// "example/menu/posts/title": msg("投稿"),
// "example/menu/profile/title": msg("プロフィール"),
// "example/menu/settings/title": msg("設定"),
});
hi18nではt関数の引数の文字列リテラルを調べることで翻訳が使用済みかどうかを判定しているため、このようなケースでは誤検出が発生してしまうのです。
(もしこのようなケースを正しく扱おうとすると、 menu
という変数がどのような値を取りうるのかを解析しなければなりません。この対応はコストのわりに利点が少ないため、hi18nでは一律でルールに沿っていないものを無視しています)
Step1: 文字列連結をやめる
hi18nに翻訳IDを正しく認識させるには、まず文字列の連結をやめる必要があります。連結済みの文字列をどこかに保管しておき、その文字列を参照して使います。これには大きく2つの戦略があります。
ひとつは、他のデータと一緒に保管する方法です。以下の例では、メニューの名前の配列を拡張し、メニューの情報を一括でオブジェクトとして保管するようにしています。
const menus = [
{
name: "posts",
titleId: "example/menu/posts/title",
},
{
name: "profile",
titleId: "example/menu/profile/title",
},
{
name: "settings",
titleId: "example/menu/settings/title",
},
];
menus.map(({ name, titleId }) => (
<li><a href={`/${name}`}>{t(titleId)}</a></li>
);
もう一つは、別途翻訳IDのマップを用意する方法です。
const menus = ["posts", "profile", "settings"];
const menuTitleIds = {
posts: "example/menu/posts/title",
profile: "example/menu/profile/title",
settings: "example/menu/settings/title",
};
menus.map((menu) => (
<li><a href={`/${menu}`}>{t(menuTitleIds[menu])}</a></li>
);
どちらの方法でもOKです。このように文字列連結をやめることができたら次のステップに進めます。
Step 2: 文字列をマークする
Step1で、翻訳IDが文字列リテラルとして明示的に現れる状態になりました。しかし、その文字列リテラルが翻訳IDであるということはまだhi18nのCLIには認識できません。
hi18nのCLIに翻訳IDを認識させるために、文字列リテラルをtranslationId関数で囲みます。第一引数には、useI18n等に渡しているのと同じbookのインスタンスを渡す必要があります。
const menus = [
{
name: "posts",
titleId: translationId(book, "example/menu/posts/title"),
},
{
name: "profile",
titleId: translationId(book, "example/menu/profile/title"),
},
{
name: "settings",
titleId: translationId(book, "example/menu/settings/title"),
},
];
menus.map(({ name, titleId }) => (
<li><a href={`/${name}`}>{t(titleId)}</a></li>
);
これでhi18nのCLIが正しく翻訳IDを検出できるようになりました。
Step 3: t.dynamicを使う
ここまででCLIは正しく動くようになりますが、以下の問題があります。
- t関数で呼び出すときの型の不一致。
- hi18nのESLint ruleを使っている場合、t関数の位置で不要な警告が発生してしまう。
そこで、翻訳IDがtranslationIdで登録済みであることをhi18nに伝えるために、tのかわりにt.dynamicを使うようにします。
const menus = [
{
name: "posts",
titleId: translationId(book, "example/menu/posts/title"),
},
{
name: "profile",
titleId: translationId(book, "example/menu/profile/title"),
},
{
name: "settings",
titleId: translationId(book, "example/menu/settings/title"),
},
];
menus.map(({ name, titleId }) => (
<li><a href={`/${name}`}>{t.dynamic(titleId)}</a></li>
);
もしt関数ではなく <Translate> コンポーネントを使っている場合は、同等機能を提供する <Translate.Dynamic> を使うことができます。
ESLint ruleを使う
t関数を正しく使っているかをsyncコマンドを動かしながら調べるのは大変です。hi18nにはESLint pluginが同梱されており、syncコマンドがうまく動かないパターンに対して警告を出してくれます。
ESLintを導入していない場合は、まずESLintのセットアップを行う必要があります。
ESLintが導入できたら、ESLint pluginをインストールします。
npm install -D @hi18n/eslint-plugin
# または:
yarn add -D @hi18n/eslint-plugin
設定ファイルのextendsでプラグインの推奨設定を参照します。
// .eslintrc.jsの場合
module.exports = {
extends: [/* ... */, "plugin:@hi18n/recommended"],
/* ... */
};
これでhi18nの使い方に問題があるときにはESLintが警告してくれるようになります。
次に読む