Tomoya Yamaji
next-localization で Pluralization を行う際は、辞書キーの配置に気をつけよう
2021/06/29
Table of Contents
時間がない人向け3行説明
- next-localization っていう Next.js 用のi18nがある
- Pluralizationという、「単数形・複数形」をいい感じに切り替えれる機能がある
- Pluralizationを行う際は、キーを辞書の最上位層に置かないと機能しない
next-localization
- これです
- ミニマルに対応できるのが魅力
- こんな感じで使います ↓
import { useI18n } from "next-localization";
const Profile: React.FC<Props> = () => {
const { t } = useI18n();
return <p>{t('user.word.follower')}</p>
}
Pluralization
- 「複数化」という意味
- これです
- 例えば、フォロワーが1人の時は、「follower」、そうでない時は 「followers」 のように切り替えたい時に使います
- 使い方はこんな感じ↓
import { useI18n } from "next-localization";
const Profile: React.FC<Props> = () => {
const i18n = useI18n();
const pt = i18n.withPlural({ type: 'cardinal' });
return <p>{pt('user.word.follower', { followers: 2 })}</p>
}
また、辞書ファイルは以下のように作っておく必要があります。
{
en: {
user: {
word: {
follower: {{ followers }}
}
},
followers: {
one: follower
other: followers
}
}
}
簡単に解説
user.word.follower
は、followers
という引数を渡す想定がされていますi18n.withPlural()
で得られる関数を使うと、{ followers: 2 } の部分が、{ followers: t('followers.other'} }
となって再度翻訳関数に渡されます- 結果として、
pt('user.word.follower', { followers: 2 })
は、t('lab.word.follower', { followers: t('followers.other'} })
となり、「followers」 が表示されます - 実装箇所
もう少し深く解説
- 内部実装には、Intl.PluralRulesが使われています
new Intl.PluralRules(locale).select(number);
で、one
とかother
とかが返ってきますtype
という引数があり、序数か枢機卿数かを選ぶことができますIntl.PluralRules
のデフォルトは枢機卿数(cardinal
) ですが、next-localizationでは序数(ordinal
)にされているので、再度設定を上書きしています- 該当部分
辞書キーをもっとわかりやすいところに置きたい → 無理です
- 先程の辞書ファイルにおいて、
followers
はuser.word
配下でなく、最上位のen
直下に置かれていました - なぜでしょうか? 実は、
user.word.followers.other
のようなキー配置では、next-localization の Pluralization を利用することはできません
withPlural の実装を覗いてみる
withPlural(pluralRulesOptions = { type: 'ordinal' }) {
const PR = new Intl.PluralRules(r.locale(), pluralRulesOptions);
return (key, params, ...args) => {
Object.keys(params).map((k) => {
if (typeof params[k] === 'number') {
let pkey = PR.select(params[k]);
params[k] = this.t(`${k}.${pkey}`);
}
});
return this.t(key, params, ...args);
};
}
コメントをつけて読みやすくしてみました。
withPlural(pluralRulesOptions = { type: 'ordinal' }) {
// PR: PR.select(1) とか PR.select(100) とかで、 `one` とか `other` を返してくれるやつ
const PR = new Intl.PluralRules(r.locale(), pluralRulesOptions);
// ここでのparams は `{ followers: 2 }`
return (key, params, ...args) => {
Object.keys(params).map((k) => {
// 引数の値が数値だったら、PRに噛ませて `one` とか `other` を得る
if (typeof params[k] === 'number') {
let pkey = PR.select(params[k]);
// params の書き換え。この時点で `{ followers: 'other' }` になっている。
params[k] = this.t(`${k}.${pkey}`);
}
});
// 通常の t 関数に噛ませなおす
// 今回のケースだと、ここで t('user.word.follower', { followers: t('followers.other'} }) になる
return this.t(key, params, ...args);
};
}
うまくいかない理由
if (typeof params[k] === 'number')
としてるせいで、1層目までしか型をみてないからparams[k] = this.t(${k}.${pkey});
となっているため、直下のものしかみようとしてないから
です。
{ followers: 2} でなくて、{ 'user.word.followers': 2 } のような記述なら可能か?
const RGX = /{{(.*?)}}/g;
export default function (str, mix) {
return str.replace(RGX, (x, key, y) => {
x = 0;
y = mix;
key = key.trim().split('.');
while (y && x < key.length) {
y = y[key[x++]];
}
return y != null ? y : '';
});
}
- 端的にいうと、
aaa.bbb.ccc
がマッチした場合、mix
は{aaa: {bbb: {ccc: 'text'}}}
という形でであることが期待されています - よって、
{ 'user.word.followers': 2 }
のような形ではうまくいきません - 反対に、
{user: {word: {followers: 2}}}
のような形では、前項の「if (typeof params[k] === 'number')
としてるせいで、1層目までしか型をみてないから」 に引っかかり、うまくいきません - next-localization のほうで、
Object.keys(params).map((k) =>
ではなく、再帰的に潜って最後のvalueがnumberかどうかでチェックされるようにするとうまくいきそうですね
まとめ
- OSSのコードを追うといろんな周辺知識がつくのでおすすめです