Intl.PluralRules API

JavaScriptで複数形を正しく処理する方法

はじめに

複数形化とは、数量に基づいて異なるテキストを表示するプロセスです。英語では、単一のアイテムに対して「1 item」、複数のアイテムに対して「2 items」と表示することがあります。ほとんどの開発者は、1以外の数量に対して「s」を追加する単純な条件分岐でこれを処理しています。

このアプローチは英語以外の言語では機能しません。ポーランド語では、1、2〜4、5以上で異なる形式を使用します。アラビア語には、ゼロ、1、2、少数、多数の形式があります。ウェールズ語には6つの異なる形式があります。英語にとどまっても、「person」から「people」のような不規則な複数形には特別な処理が必要です。

Intl.PluralRules APIは、任意の言語における任意の数値の複数形カテゴリを提供することで、この問題を解決します。数量を指定すると、APIはターゲット言語のルールに基づいて使用すべき形式を通知します。これにより、言語固有のルールを手動でエンコードすることなく、言語間で正しく機能する国際化対応コードを記述できます。

言語における複数形の処理方法

言語によって数量の表現方法は大きく異なります。英語には2つの形式があります。1に対する単数形と、それ以外すべてに対する複数形です。これは単純に見えますが、異なるシステムを持つ言語に遭遇すると複雑になります。

ロシア語とポーランド語は3つの形式を使用します。単数形は1つのアイテムに適用されます。特別な形式は2、3、または4で終わる数量に適用されます(ただし12、13、14は除く)。その他すべての数量は3番目の形式を使用します。

アラビア語は6つの形式を使用します。ゼロ、1、2、少数(3〜10)、多数(11〜99)、その他(100以上)です。ウェールズ語も異なる数値境界を持つ6つの形式があります。

中国語や日本語などの一部の言語では、単数形と複数形の区別がまったくありません。どの数でも同じ形式が使用されます。

Intl.PluralRules APIは、Unicode CLDR複数形ルールに基づく標準化されたカテゴリ名を使用して、これらの違いを抽象化します。6つのカテゴリは、zero、one、two、few、many、otherです。すべての言語が6つのカテゴリすべてを使用するわけではありません。英語はoneとotherのみを使用します。アラビア語は6つすべてを使用します。

ロケール用のPluralRulesインスタンスを作成する

Intl.PluralRulesコンストラクタはロケール識別子を受け取り、指定された数値にどの複数形カテゴリが適用されるかを判断できるオブジェクトを返します。

const enRules = new Intl.PluralRules('en-US');

ロケールごとに1つのインスタンスを作成し、それを再利用してください。複数形化のたびに新しいインスタンスを構築するのは無駄です。インスタンスを変数に保存するか、キャッシュメカニズムを使用してください。

デフォルトのタイプはcardinalで、オブジェクトのカウントを処理します。オプションオブジェクトを渡すことで、序数用のルールを作成することもできます。

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

基数ルールは「1個のりんご、2個のりんご」のようなカウントに適用されます。序数ルールは「1位、2位」のような位置に適用されます。

select()を使用して数値の複数形カテゴリを取得する

select()メソッドは数値を受け取り、対象言語でそれがどの複数形カテゴリに属するかを返します。

const enRules = new Intl.PluralRules('en-US');

enRules.select(0);  // 'other'
enRules.select(1);  // 'one'
enRules.select(2);  // 'other'
enRules.select(5);  // 'other'

戻り値は常に6つのカテゴリ名のいずれかです:zero、one、two、few、many、otherです。英語はoneとotherのみを返します。これらが英語で使用される唯一の形式だからです。

より複雑なルールを持つアラビア語の場合、6つのカテゴリすべてが使用されます:

const arRules = new Intl.PluralRules('ar-EG');

arRules.select(0);   // 'zero'
arRules.select(1);   // 'one'
arRules.select(2);   // 'two'
arRules.select(6);   // 'few'
arRules.select(18);  // 'many'
arRules.select(100); // 'other'

カテゴリをローカライズされた文字列にマッピングする

APIは適用されるカテゴリのみを通知します。各カテゴリの実際のテキストは自分で提供します。テキスト形式をMapまたはオブジェクトに格納し、カテゴリ名をキーとして使用します。

const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatItems(count) {
  const category = enRules.select(count);
  const form = enForms.get(category);
  return `${count} ${form}`;
}

formatItems(1);  // '1 item'
formatItems(5);  // '5 items'

このパターンはロジックとデータを分離します。PluralRulesインスタンスがルールを処理し、Mapが翻訳を保持します。関数がそれらを組み合わせます。

より多くのカテゴリを持つ言語の場合、Mapにさらにエントリを追加します。

const arRules = new Intl.PluralRules('ar-EG');
const arForms = new Map([
  ['zero', 'عناصر'],
  ['one', 'عنصر واحد'],
  ['two', 'عنصران'],
  ['few', 'عناصر'],
  ['many', 'عنصرًا'],
  ['other', 'عنصر'],
]);

function formatItems(count) {
  const category = arRules.select(count);
  const form = arForms.get(category);
  return `${count} ${form}`;
}

言語が使用するすべてのカテゴリのエントリを必ず提供してください。カテゴリが欠落すると、未定義の参照が発生します。言語がどのカテゴリを使用するか不明な場合は、Unicode CLDRの複数形ルールを確認するか、さまざまな数値でAPIをテストしてください。

小数および分数のカウントを処理する

select()メソッドは小数で機能します。英語では、0から2の間の値であっても、小数は複数形として扱われます。

const enRules = new Intl.PluralRules('en-US');

enRules.select(1);    // 'one'
enRules.select(1.0);  // 'one'
enRules.select(1.5);  // 'other'
enRules.select(0.5);  // 'other'

他の言語では小数に対して異なるルールがあります。すべての小数を複数形として扱う言語もあれば、小数部分に基づいてより細かいルールを使用する言語もあります。

UIが「1.5 GB」や「2.7マイル」のような分数量を表示する場合は、分数をそのままselect()に渡してください。UIが表示値を丸める場合を除き、事前に丸めないでください。

1st、2nd、3rdのような序数をフォーマットする

序数は位置や順位を示します。英語では接尾辞を追加して序数を形成します。1st、2nd、3rd、4thです。パターンは単に「thを追加する」だけではありません。1、2、3には特別な形式があり、1、2、3で終わる数字は特別なルールに従います(21st、22nd、23rd)が、11、12、13で終わる場合は例外です(11th、12th、13th)。

Intl.PluralRules APIは、type: 'ordinal'を指定すると、これらのルールを処理します。

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

enOrdinalRules.select(1);   // 'one'
enOrdinalRules.select(2);   // 'two'
enOrdinalRules.select(3);   // 'few'
enOrdinalRules.select(4);   // 'other'
enOrdinalRules.select(11);  // 'other'
enOrdinalRules.select(21);  // 'one'
enOrdinalRules.select(22);  // 'two'
enOrdinalRules.select(23);  // 'few'

カテゴリを序数接尾辞にマッピングします。

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const enOrdinalSuffixes = new Map([
  ['one', 'st'],
  ['two', 'nd'],
  ['few', 'rd'],
  ['other', 'th'],
]);

function formatOrdinal(n) {
  const category = enOrdinalRules.select(n);
  const suffix = enOrdinalSuffixes.get(category);
  return `${n}${suffix}`;
}

formatOrdinal(1);   // '1st'
formatOrdinal(2);   // '2nd'
formatOrdinal(3);   // '3rd'
formatOrdinal(4);   // '4th'
formatOrdinal(11);  // '11th'
formatOrdinal(21);  // '21st'

他の言語には、まったく異なる序数システムがあります。フランス語では、1番目に「1er」を使用し、それ以外のすべてに「2e」を使用します。スペイン語には性別固有の序数があります。APIはカテゴリを提供し、ローカライズされた形式を提供します。

selectRange()で範囲を処理する

selectRange()メソッドは、「1-5アイテム」や「10-20件の結果」のような数値の範囲に対する複数形カテゴリを決定します。一部の言語では、個別のカウントとは異なる範囲の複数形ルールがあります。

const enRules = new Intl.PluralRules('en-US');

enRules.selectRange(1, 5);   // 'other'
enRules.selectRange(0, 1);   // 'other'

英語では、範囲が1から始まる場合でも、範囲はほぼ常に複数形です。他の言語には、より複雑な範囲ルールがあります。

const slRules = new Intl.PluralRules('sl');

slRules.selectRange(102, 201);  // 'few'

const ptRules = new Intl.PluralRules('pt');

ptRules.selectRange(102, 102);  // 'other'

UIで範囲を明示的に表示する場合は、selectRange()を使用します。単一のカウントの場合は、select()を使用します。

Intl.NumberFormatと組み合わせてローカライズされた数値表示を行う

複数形は、フォーマットされた数値と一緒に表示されることがよくあります。Intl.NumberFormatを使用してロケールの規則に従って数値をフォーマットし、次にIntl.PluralRulesを使用して正しいテキストを選択します。

const locale = 'en-US';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatCount(count) {
  const formattedNumber = numberFormat.format(count);
  const category = pluralRules.select(count);
  const form = forms.get(category);
  return `${formattedNumber} ${form}`;
}

formatCount(1);      // '1 item'
formatCount(1000);   // '1,000 items'
formatCount(1.5);    // '1.5 items'

ドイツ語の場合、千の区切り記号としてピリオドを使用し、小数点区切り記号としてコンマを使用します。

const locale = 'de-DE';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ['one', 'Artikel'],
  ['other', 'Artikel'],
]);

function formatCount(count) {
  const formattedNumber = numberFormat.format(count);
  const category = pluralRules.select(count);
  const form = forms.get(category);
  return `${formattedNumber} ${form}`;
}

formatCount(1);      // '1 Artikel'
formatCount(1000);   // '1.000 Artikel'
formatCount(1.5);    // '1,5 Artikel'

このパターンにより、数値のフォーマットとテキスト形式の両方が、ロケールに対するユーザーの期待に一致することが保証されます。

必要に応じてゼロのケースを明示的に処理する

ゼロの複数形化の方法は言語によって異なります。英語では通常、複数形を使用します。「0 items」、「0 results」。一部の言語では、ゼロに単数形を使用します。他の言語には、明確なゼロカテゴリがあります。

Intl.PluralRules APIは、言語ルールに基づいてゼロに適切なカテゴリを返します。英語では、ゼロは「other」を返し、複数形にマッピングされます。

const enRules = new Intl.PluralRules('en-US');

enRules.select(0);  // 'other'

アラビア語では、ゼロには独自のカテゴリがあります。

const arRules = new Intl.PluralRules('ar-EG');

arRules.select(0);  // 'zero'

テキストはこれを考慮する必要があります。英語の場合、UXを向上させるために「0 items」ではなく「No items」を表示することをお勧めします。複数形ルールを呼び出す前にこれを処理してください。

function formatItems(count) {
  if (count === 0) {
    return 'No items';
  }
  const category = enRules.select(count);
  const form = enForms.get(category);
  return `${count} ${form}`;
}

アラビア語の場合、翻訳で特定のゼロ形式を提供してください。

const arForms = new Map([
  ['zero', 'لا توجد عناصر'],
  ['one', 'عنصر واحد'],
  ['two', 'عنصران'],
  ['few', 'عناصر'],
  ['many', 'عنصرًا'],
  ['other', 'عنصر'],
]);

これにより、各言語の言語規則を尊重しながら、より良いユーザーエクスペリエンスのためにゼロケースをカスタマイズできます。

パフォーマンスのためにPluralRulesインスタンスを再利用する

PluralRulesインスタンスの作成には、ロケールの解析と複数形ルールデータの読み込みが含まれます。これは、関数呼び出しやレンダリングサイクルごとではなく、ロケールごとに1回実行してください。

// Good: create once, reuse
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatItems(count) {
  const category = enRules.select(count);
  const form = enForms.get(category);
  return `${count} ${form}`;
}

複数のロケールをサポートする場合は、各ロケールのインスタンスを作成し、Mapまたはキャッシュに保存してください。

const rulesCache = new Map();

function getPluralRules(locale) {
  if (!rulesCache.has(locale)) {
    rulesCache.set(locale, new Intl.PluralRules(locale));
  }
  return rulesCache.get(locale);
}

const rules = getPluralRules('en-US');

このパターンにより、多数の呼び出しにわたって初期化コストが償却されます。

ブラウザサポートと互換性

Intl.PluralRulesは、2019年以降のすべてのモダンブラウザでサポートされています。これには、Chrome 63以降、Firefox 58以降、Safari 13以降、Edge 79以降が含まれます。Internet Explorerではサポートされていません。

モダンブラウザをターゲットとするアプリケーションの場合、ポリフィルなしでIntl.PluralRulesを使用できます。古いブラウザをサポートする必要がある場合は、npmのintl-pluralrulesなどのパッケージを通じてポリフィルが利用可能です。

selectRange()メソッドは新しく、サポートがやや限定的です。Chrome 106以降、Firefox 116以降、Safari 15.4以降、Edge 106以降で利用可能です。selectRange()を使用し、古いブラウザバージョンをサポートする必要がある場合は、互換性を確認してください。

ロジックで複数形を直接記述しない

カウントをチェックしてコード内で分岐し、複数形を選択しないでください。このアプローチは、2つ以上の形式を持つ言語にスケールせず、ロジックが英語のルールに結合されます。

// Avoid this pattern
function formatItems(count) {
  if (count === 1) {
    return `${count} item`;
  }
  return `${count} items`;
}

Intl.PluralRulesとデータ構造を使用して形式を保存します。これにより、コードが言語に依存しなくなり、新しい翻訳を提供するだけで新しい言語を簡単に追加できます。

// Prefer this pattern
const rules = new Intl.PluralRules('en-US');
const forms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatItems(count) {
  const category = rules.select(count);
  const form = forms.get(category);
  return `${count} ${form}`;
}

このパターンは、どの言語でも同じように機能します。変更されるのは、rulesインスタンスとformsマップのみです。

複数のロケールとエッジケースでテストする

複数形のルールには、英語のみでテストしていると見逃しやすいエッジケースがあります。ポーランド語やアラビア語など、3つ以上の形式を使用する言語で複数形のロジックをテストしてください。

異なるカテゴリをトリガーする数値をテストします。

  • ゼロ
  • 1
  • 2
  • 少数(アラビア語では3〜10)
  • 多数(アラビア語では11〜99)
  • 大きな数値(100以上)
  • 小数値(0.5、1.5、2.3)
  • UIで表示する場合は負の数

序数ルールを使用する場合は、異なる接尾辞をトリガーする数値をテストします。1、2、3、4、11、21、22、23。これにより、特殊なケースを正しく処理できることを確認します。

早期に複数のロケールでテストすることで、後で新しい言語を追加する際の予期しない問題を防ぎます。また、データ構造に必要なすべてのカテゴリが含まれていること、およびロジックがそれらを正しく処理することを検証します。