Fusic Tech Blog

Fusion of Society, IT and Culture

next-localization で Pluralization を行う際は、辞書キーの配置に気をつけよう
2021/06/29

next-localization で Pluralization を行う際は、辞書キーの配置に気をつけよう

時間がない人向け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)にされているので、再度設定を上書きしています
  • 該当部分

辞書キーをもっとわかりやすいところに置きたい → 無理です

  • 先程の辞書ファイルにおいて、followersuser.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);
    };
}

うまくいかない理由

  1. if (typeof params[k] === 'number') としてるせいで、1層目までしか型をみてないから
  2. params[k] = this.t(${k}.${pkey}); となっているため、直下のものしかみようとしてないから

です。

{ followers: 2} でなくて、{ 'user.word.followers': 2 } のような記述なら可能か?

  • 不可能です
  • next-localization が依存している rossetaが依存しているtempliteの実装を覗いてみます
  • templite は値の埋め込み等をサポートしてくれるやつです
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のコードを追うといろんな周辺知識がつくのでおすすめです

Tomoya Yamaji

Tomoya Yamaji

Company: Fusic CO., LTD. blog: http://at274.hatenablog.com/archive Program Language: Ruby, Python Interest: Competitive programming