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は除く)に適用されます。その他のすべての数は第三の形式を使用します。

アラビア語は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(基数)で、オブジェクトのカウントを処理します。オプションオブジェクトを渡すことで、序数(ordinal)用のルールも作成できます。

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のみを返します。これは英語が使用する形式がこの2つだけだからです。

より複雑なルールを持つアラビア語では、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 miles」のような分数量を表示する場合は、小数値を直接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'

他の言語では全く異なる序数システムがあります。フランス語では「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インスタンスの作成には、ロケールの解析と複数形ルールデータの読み込みが含まれます。これは各ロケールに対して一度だけ行い、関数呼び出しやレンダリングサイクルごとに行わないようにします。

// 良い例:一度作成して再利用
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つ以上の形式を持つ言語にスケールせず、ロジックを英語のルールに結合します。

// このパターンは避ける
function formatItems(count) {
  if (count === 1) {
    return `${count} item`;
  }
  return `${count} items`;
}

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

// このパターンを推奨
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マップだけが変更されます。

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

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

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

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

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

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