1〜3個のアイテムのような範囲に対する複数形の選択方法

数値の範囲を表示する際に正しい複数形を選択するためのJavaScriptの使用方法

はじめに

範囲は、値が2つの端点の間にあることを伝えます。ユーザーインターフェースでは、「10〜15件の一致が見つかりました」と表示する検索結果、「1〜3個のアイテムが利用可能」と表示する在庫システム、「2〜5個のオプションを選択」と表示するフィルターなどのコンテキストで範囲が表示されます。これらの範囲は、2つの数値と、範囲と文法的に一致する必要がある説明テキストを組み合わせたものです。

単一のカウントを表示する場合、単数形と複数形のどちらかを選択します。「1個のアイテム」と「2個のアイテム」のようにです。言語には、カウントに基づいてどちらの形式を適用するかを決定する規則があります。これらの規則は言語によって異なります。英語では、1の場合は単数形を使用し、その他すべてのカウントには複数形を使用します。ポーランド語では、1、2〜4、5以上で異なる形式を使用します。アラビア語には、カウントに基づいて6つの異なる形式があります。

範囲は異なる課題をもたらします。複数形は、単一の数値だけでなく、開始値と終了値の両方に依存します。英語では、「1〜2個のアイテム」は、範囲が1から始まっていても複数形を使用します。範囲にどの複数形を適用するかを決定する規則は、言語によって異なります。Intl.PluralRulesselectRange()メソッドは、これらの言語固有の規則を自動的に処理します。

範囲に異なる複数形規則が必要な理由

範囲の単一の数値に対してselect()メソッドを使用しても、すべての言語で正しく機能するわけではありません。範囲の終了値を使用することを考えるかもしれませんが、これは多くの言語で誤った結果を生成します。

範囲0〜1の英語を考えてみましょう。終了値に対してselect()を使用すると「one」が返され、「0〜1個のアイテム」と表示する必要があることを示唆します。これは文法的に正しくありません。正しい形式は、複数形を使用した「0〜1個のアイテム」です。

const rules = new Intl.PluralRules("en-US");

console.log(rules.select(1));
// Output: "one"

// But "0-1 item" is incorrect
// Correct: "0-1 items"

言語によっては、範囲に対する明示的なルールがあり、単一のカウントに対するルールとは異なります。スロベニア語では、範囲102-201は「few」形式を使用しますが、その範囲内の個々の数値は異なる形式を使用します。

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

console.log(slRules.select(102));
// Output: "few"

console.log(slRules.select(201));
// Output: "few"

console.log(slRules.selectRange(102, 201));
// Output: "few"

言語によっては、開始値を使用して形式を決定するもの、終了値を使用するもの、両方の値を組み合わせて使用するものがあります。selectRange()メソッドは、これらの言語固有のルールをカプセル化するため、手動で実装する必要はありません。

範囲用のPluralRulesインスタンスを作成する

Intl.PluralRulesインスタンスは、単一のカウントと同じ方法で作成します。このインスタンスは、単一の数値用のselect()と範囲用のselectRange()の両方を提供します。

const rules = new Intl.PluralRules("en-US");

インスタンスを作成する際にオプションを指定できます。これらのオプションは、単一のカウントと範囲の両方に適用されます。

const rules = new Intl.PluralRules("en-US", {
  type: "cardinal"
});

typeオプションのデフォルトは「cardinal」で、オブジェクトのカウントを処理します。位置を示す数値には「ordinal」も使用できますが、ユーザーインターフェースでは序数の範囲はあまり一般的ではありません。

複数の呼び出しで同じインスタンスを再利用してください。複数形化のたびに新しいインスタンスを作成するのは無駄です。インスタンスを変数に保存するか、ロケールごとにキャッシュしてください。

selectRangeを使用して範囲の複数形カテゴリを決定する

selectRange()メソッドは、範囲の開始と終了を表す2つの数値を受け取ります。適用される複数形カテゴリを示す文字列を返します:「zero」、「one」、「two」、「few」、「many」、または「other」。

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(0, 1));
// Output: "other"

console.log(rules.selectRange(1, 2));
// Output: "other"

console.log(rules.selectRange(5, 10));
// Output: "other"

英語では、範囲はほぼ常に「other」カテゴリを使用し、これは複数形に対応します。これは、英語話者が複数形の名詞を使用して範囲を自然に表現する方法と一致します。

複数形がより多い言語では、その言語固有のルールに基づいて異なるカテゴリを返します。

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

console.log(arRules.selectRange(0, 0));
// Output: "zero"

console.log(arRules.selectRange(1, 1));
// Output: "one"

console.log(arRules.selectRange(2, 2));
// Output: "two"

console.log(arRules.selectRange(3, 10));
// Output: "few"

戻り値は常に6つの標準的な複数形カテゴリ名のいずれかです。コードでこれらのカテゴリを適切なローカライズされたテキストにマッピングします。

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

各複数形カテゴリのテキスト形式をデータ構造に格納します。selectRange()が返すカテゴリを使用して、適切なテキストを検索します。

const rules = new Intl.PluralRules("en-US");
const forms = new Map([
  ["one", "item"],
  ["other", "items"]
]);

function formatRange(start, end) {
  const category = rules.selectRange(start, end);
  const form = forms.get(category);
  return `${start}-${end} ${form}`;
}

console.log(formatRange(1, 3));
// Output: "1-3 items"

console.log(formatRange(0, 1));
// Output: "0-1 items"

console.log(formatRange(5, 10));
// Output: "5-10 items"

このパターンは、複数形化ロジックをローカライズされたテキストから分離します。Intl.PluralRulesインスタンスが言語ルールを処理します。Mapが翻訳を保持します。関数がそれらを組み合わせます。

複数形カテゴリがより多い言語の場合、その言語が使用する各カテゴリのエントリを追加します。

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

function formatRange(start, end) {
  const category = arRules.selectRange(start, end);
  const form = arForms.get(category);
  return `${start}-${end} ${form}`;
}

console.log(formatRange(0, 0));
// Output: "0-0 عناصر"

console.log(formatRange(1, 1));
// Output: "1-1 عنصر"

常に言語が使用するすべてのカテゴリのテキストを提供してください。Unicode CLDRの複数形ルールを確認するか、APIを使用してさまざまな範囲でテストし、必要なカテゴリを特定します。

異なるロケールが範囲の複数形化を処理する方法

各言語には、範囲の複数形を決定するための独自のルールがあります。これらのルールは、ネイティブスピーカーがその言語で範囲を自然に表現する方法を反映しています。

const enRules = new Intl.PluralRules("en-US");
console.log(enRules.selectRange(1, 3));
// Output: "other"

const slRules = new Intl.PluralRules("sl");
console.log(slRules.selectRange(102, 201));
// Output: "few"

const ptRules = new Intl.PluralRules("pt");
console.log(ptRules.selectRange(102, 102));
// Output: "other"

const ruRules = new Intl.PluralRules("ru");
console.log(ruRules.selectRange(1, 2));
// Output: "few"

英語は範囲に対して一貫して「other」を使用し、範囲を常に複数形にします。スロベニア語は範囲内の特定の数値に基づいて、より複雑なルールを適用します。ポルトガル語はほとんどの範囲に対して「other」を使用します。ロシア語は特定の範囲に対して「few」を使用します。

これらの違いは、国際的なアプリケーションにおいて複数形ロジックをハードコーディングすることが失敗する理由を示しています。APIは各言語が範囲を処理する方法の知識をカプセル化します。

完全なフォーマットのためにIntl.NumberFormatと組み合わせる

実際のアプリケーションでは、数値とテキストの両方をフォーマットする必要があります。Intl.NumberFormatを使用してロケールの規則に従って範囲の端点をフォーマットし、次にselectRange()を使用して正しい複数形を選択します。

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 formatRange(start, end) {
  const startFormatted = numberFormat.format(start);
  const endFormatted = numberFormat.format(end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `${startFormatted}-${endFormatted} ${form}`;
}

console.log(formatRange(1, 3));
// Output: "1-3 items"

console.log(formatRange(1000, 5000));
// Output: "1,000-5,000 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 formatRange(start, end) {
  const startFormatted = numberFormat.format(start);
  const endFormatted = numberFormat.format(end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `${startFormatted}-${endFormatted} ${form}`;
}

console.log(formatRange(1000, 5000));
// Output: "1.000-5.000 Artikel"

ドイツ語では、千の位の区切り文字としてカンマではなくピリオドを使用します。数値フォーマッターはこれを自動的に処理します。複数形ルールは「Artikel」のどの形式を使用するかを決定します。

単一値のselectとselectRangeの比較

select()メソッドは単一のカウントを処理し、selectRange()は範囲を処理します。単一の数量を表示する場合はselect()を使用し、2つの値の間の範囲を表示する場合はselectRange()を使用してください。

const rules = new Intl.PluralRules("en-US");

// Single count
console.log(rules.select(1));
// Output: "one"

console.log(rules.select(2));
// Output: "other"

// Range
console.log(rules.selectRange(1, 2));
// Output: "other"

console.log(rules.selectRange(0, 1));
// Output: "other"

単一のカウントの場合、ルールはその1つの数値のみに依存します。範囲の場合、ルールは両方の端点を考慮します。英語では、1から始まる範囲でも、単一のカウント1が単数形を使用するにもかかわらず、複数形を使用します。

一部の言語では、単一カウントルールと範囲ルールの間により顕著な違いが見られます。

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

// Single counts in Slovenian
console.log(slRules.select(1));
// Output: "one"

console.log(slRules.select(2));
// Output: "two"

console.log(slRules.select(5));
// Output: "few"

// Range in Slovenian
console.log(slRules.selectRange(102, 201));
// Output: "few"

スロベニア語では、複雑なルールに基づいて、異なる単一カウントに対して「one」、「two」、「few」を使用します。範囲の場合、両方の数値を一緒に考慮する異なるロジックを適用します。

開始値と終了値が等しい範囲の処理

開始値と終了値が同じ場合、幅のない範囲を表示していることになります。一部のアプリケーションでは、範囲が期待されるコンテキストで正確な値を表すためにこれを使用します。

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(5, 5));
// Output: "other"

console.log(rules.selectRange(1, 1));
// Output: "one"

両方の値が1に等しい場合、英語は「one」を返し、単数形を使用する必要があることを示唆します。両方の値が他の任意の数値である場合、英語は「other」を返し、複数形を使用することを示唆します。

この動作は、範囲を「1-1アイテム」または単に「1アイテム」として表示する場合に意味があります。1以外の値の場合は、「5-5アイテム」または「5アイテム」と表示されます。

実際には、開始値と終了値が等しい場合を検出し、範囲ではなく単一の値を表示することが望ましい場合があります。

const rules = new Intl.PluralRules("en-US");
const forms = new Map([
  ["one", "item"],
  ["other", "items"]
]);

function formatRange(start, end) {
  if (start === end) {
    const category = rules.select(start);
    const form = forms.get(category);
    return `${start} ${form}`;
  }

  const category = rules.selectRange(start, end);
  const form = forms.get(category);
  return `${start}-${end} ${form}`;
}

console.log(formatRange(1, 1));
// Output: "1 item"

console.log(formatRange(5, 5));
// Output: "5 items"

console.log(formatRange(1, 3));
// Output: "1-3 items"

このアプローチでは、等しい値にはselect()を使用し、実際の範囲にはselectRange()を使用します。「1-1」や「5-5」の表示を避けることで、より自然な出力になります。

selectRangeでエッジケースを処理する

selectRange()メソッドは入力を検証します。いずれかのパラメータがundefinednull、または有効な数値に変換できない場合、メソッドはエラーをスローします。

const rules = new Intl.PluralRules("en-US");

try {
  console.log(rules.selectRange(1, undefined));
} catch (error) {
  console.log(error.name);
  // Output: "TypeError"
}

try {
  console.log(rules.selectRange(NaN, 5));
} catch (error) {
  console.log(error.name);
  // Output: "RangeError"
}

selectRange()に渡す前に入力を検証してください。これは、ユーザー入力や外部ソースからのデータを扱う場合に特に重要です。

function formatRange(start, end) {
  if (typeof start !== "number" || typeof end !== "number") {
    throw new Error("Start and end must be numbers");
  }

  if (isNaN(start) || isNaN(end)) {
    throw new Error("Start and end must be valid numbers");
  }

  const category = rules.selectRange(start, end);
  const form = forms.get(category);
  return `${start}-${end} ${form}`;
}

このメソッドは、数値、BigInt値、または数値として解析できる文字列を受け入れます。

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(1, 5));
// Output: "other"

console.log(rules.selectRange(1n, 5n));
// Output: "other"

console.log(rules.selectRange("1", "5"));
// Output: "other"

文字列入力は数値として解析されます。これによりメソッドの呼び出し方法に柔軟性が生まれますが、明確性のために可能な限り実際の数値型を渡すことをお勧めします。

小数範囲を処理する

selectRange()メソッドは小数で動作します。これは、測定値や統計などの小数量の範囲を表示する場合に便利です。

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(1.5, 2.5));
// Output: "other"

console.log(rules.selectRange(0.5, 1.0));
// Output: "other"

console.log(rules.selectRange(1.0, 1.5));
// Output: "other"

英語では、これらの小数範囲はすべて複数形として扱われます。他の言語では、小数範囲に対して異なる規則がある場合があります。

小数範囲をフォーマットする場合は、selectRange()を適切な小数精度で構成されたIntl.NumberFormatと組み合わせてください。

const locale = "en-US";
const numberFormat = new Intl.NumberFormat(locale, {
  minimumFractionDigits: 1,
  maximumFractionDigits: 1
});
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ["one", "kilometer"],
  ["other", "kilometers"]
]);

function formatRange(start, end) {
  const startFormatted = numberFormat.format(start);
  const endFormatted = numberFormat.format(end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `${startFormatted}-${endFormatted} ${form}`;
}

console.log(formatRange(1.5, 2.5));
// Output: "1.5-2.5 kilometers"

console.log(formatRange(0.5, 1.0));
// Output: "0.5-1.0 kilometers"

数値フォーマッターは一貫した小数点表示を保証します。複数形ルールは小数値に基づいて正しい形式を決定します。

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

selectRange()メソッドは、Intl APIの他の部分と比較して比較的新しいものです。これは2023年にIntl.NumberFormat v3仕様の一部として利用可能になりました。

ブラウザサポートには、Chrome 106以降、Firefox 116以降、Safari 15.4以降、Edge 106以降が含まれます。このメソッドはInternet Explorerや古いブラウザバージョンでは利用できません。

最新のブラウザをターゲットとするアプリケーションの場合、ポリフィルなしでselectRange()を使用できます。古いブラウザをサポートする必要がある場合は、使用する前にメソッドの存在を確認してください。

const rules = new Intl.PluralRules("en-US");

if (typeof rules.selectRange === "function") {
  // Use selectRange for range pluralization
  console.log(rules.selectRange(1, 3));
} else {
  // Fall back to select with the end value
  console.log(rules.select(3));
}

このフォールバックアプローチは、selectRange()が利用できない場合に、終了値に対してselect()を使用します。これはすべての言語において言語学的に完璧ではありませんが、古いブラウザに対して妥当な近似値を提供します。

古い環境に対する包括的なサポートが必要な場合は、@formatjs/intl-pluralrulesなどのパッケージを通じてポリフィルが利用可能です。

selectRangeとselectの使い分け

selectRange()は、UIが開始値と終了値の両方をユーザーに明示的に表示する範囲を表示する場合に使用します。これには、「10〜15件の一致が見つかりました」と表示する検索結果、「1〜3個の在庫があります」と表示する在庫、「2〜5個のオプションを選択」と表示するフィルターなどのコンテキストが含まれます。

select()は、その数が概算値または要約値を表す場合でも、単一のカウントを表示する場合に使用します。たとえば、「約10件の結果」はselect(10)を使用します。これは範囲ではなく単一の数値を表示しているためです。

数値に Intl.NumberFormat.formatRange() を使用して範囲を表示する場合は、付随するテキストに selectRange() を使用してください。これにより、数値の書式設定とテキストの複数形化の一貫性が保たれます。

const locale = "en-US";
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ["one", "result"],
  ["other", "results"]
]);

function formatSearchResults(start, end) {
  const rangeFormatted = numberFormat.formatRange(start, end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `Found ${rangeFormatted} ${form}`;
}

console.log(formatSearchResults(10, 15));
// Output: "Found 10–15 results"

このパターンは、Intl.NumberFormatformatRange() を使用して数値を書式設定し、Intl.PluralRulesselectRange() を使用してテキストを選択します。両方のメソッドは範囲に対して動作し、すべての言語で正しく処理されることを保証します。