1〜3アイテムのような範囲に対する複数形の選び方
数値の範囲を表示する際に正しい複数形を選択するためのJavaScriptの使用方法
はじめに
範囲は値が2つのエンドポイント間に収まることを示します。ユーザーインターフェースでは、「10〜15件の一致が見つかりました」という検索結果、「1〜3個のアイテムが利用可能」という在庫システム、または「2〜5個のオプションを選択」というフィルターなどのコンテキストで範囲が表示されます。これらの範囲は、2つの数値と、その範囲と文法的に一致する必要がある説明テキストを組み合わせています。
単一のカウントを表示する場合、「1 item」と「2 items」のように単数形と複数形を選択します。言語には、カウントに基づいてどの形式が適用されるかを決定するルールがあります。これらのルールは言語によって異なります。英語では1に対して単数形を使用し、他のすべてのカウントに対して複数形を使用します。ポーランド語では1、2-4、5以上に対して異なる形式を使用します。アラビア語にはカウントに基づいて6つの異なる形式があります。
範囲は異なる課題をもたらします。複数形は単一の数値ではなく、開始値と終了値の両方に依存します。英語では、「1-2 items」は範囲が1から始まっていても複数形を使用します。範囲にどの複数形が適用されるかを決定するルールは言語によって異なります。Intl.PluralRulesのselectRange()メソッドは、これらの言語固有のルールを自動的に処理します。
範囲に異なる複数形ルールが必要な理由
範囲から単一の数値に対してselect()メソッドを使用することは、すべての言語で正しく機能しません。範囲の終了値を使用することを考えるかもしれませんが、これは多くの言語で不正確な結果を生成します。
0-1の範囲を持つ英語を考えてみましょう。終了値に対してselect()を使用すると「one」が返され、「0-1 item」と表示すべきことを示唆します。これは文法的に不正確です。正しい形式は複数形の「0-1 items」です。
const rules = new Intl.PluralRules("en-US");
console.log(rules.select(1));
// 出力: "one"
// しかし「0-1 item」は不正確
// 正しい: "0-1 items"
異なる言語には、単一のカウントのルールと一致しない範囲に対する明示的なルールがあります。スロベニア語では、102-201の範囲は「few」形式を使用しますが、その範囲内の個々の数値は異なる形式を使用します。
const slRules = new Intl.PluralRules("sl");
console.log(slRules.select(102));
// 出力: "few"
console.log(slRules.select(201));
// 出力: "few"
console.log(slRules.selectRange(102, 201));
// 出力: "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));
// 出力: "other"
console.log(rules.selectRange(1, 2));
// 出力: "other"
console.log(rules.selectRange(5, 10));
// 出力: "other"
英語では、範囲はほとんど常に「other」カテゴリを使用します。これは複数形に対応しています。これは英語話者が自然に複数形の名詞で範囲を表現する方法と一致しています。
より多くの複数形を持つ言語では、その特定のルールに基づいて異なるカテゴリを返します。
const arRules = new Intl.PluralRules("ar-EG");
console.log(arRules.selectRange(0, 0));
// 出力: "zero"
console.log(arRules.selectRange(1, 1));
// 出力: "one"
console.log(arRules.selectRange(2, 2));
// 出力: "two"
console.log(arRules.selectRange(3, 10));
// 出力: "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));
// 出力: "1-3 items"
console.log(formatRange(0, 1));
// 出力: "0-1 items"
console.log(formatRange(5, 10));
// 出力: "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));
// 出力: "0-0 عناصر"
console.log(formatRange(1, 1));
// 出力: "1-1 عنصر"
言語が使用するすべてのカテゴリのテキストを必ず提供してください。Unicode CLDRの複数形ルールを確認するか、異なる範囲でAPIをテストして、必要なカテゴリを特定してください。
異なるロケールが範囲の複数形をどのように処理するか
各言語には、範囲の複数形を決定するための独自のルールがあります。これらのルールは、ネイティブスピーカーがその言語で自然に範囲を表現する方法を反映しています。
const enRules = new Intl.PluralRules("en-US");
console.log(enRules.selectRange(1, 3));
// 出力: "other"
const slRules = new Intl.PluralRules("sl");
console.log(slRules.selectRange(102, 201));
// 出力: "few"
const ptRules = new Intl.PluralRules("pt");
console.log(ptRules.selectRange(102, 102));
// 出力: "other"
const ruRules = new Intl.PluralRules("ru");
console.log(ruRules.selectRange(1, 2));
// 出力: "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));
// 出力: "1-3 items"
console.log(formatRange(1000, 5000));
// 出力: "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));
// 出力: "1.000-5.000 Artikel"
ドイツ語ではカンマの代わりにピリオドを桁区切り記号として使用します。数値フォーマッタはこれを自動的に処理します。複数形ルールは「Artikel」のどの形式を使用するかを決定します。
単一値に対するselectとselectRangeの比較
select()メソッドは単一のカウントを処理し、selectRange()は範囲を処理します。単一の数量を表示する場合はselect()を使用し、2つの値の間の範囲を表示する場合はselectRange()を使用します。
const rules = new Intl.PluralRules("en-US");
// 単一カウント
console.log(rules.select(1));
// 出力: "one"
console.log(rules.select(2));
// 出力: "other"
// 範囲
console.log(rules.selectRange(1, 2));
// 出力: "other"
console.log(rules.selectRange(0, 1));
// 出力: "other"
単一カウントの場合、ルールはその1つの数字にのみ依存します。範囲の場合、ルールは両方の端点を考慮します。英語では、1から始まる範囲でも、単一カウント1が単数形を使用するにもかかわらず、複数形を使用します。
一部の言語では、単一カウントルールと範囲ルールの間でより劇的な違いが見られます。
const slRules = new Intl.PluralRules("sl");
// スロベニア語の単一カウント
console.log(slRules.select(1));
// 出力: "one"
console.log(slRules.select(2));
// 出力: "two"
console.log(slRules.select(5));
// 出力: "few"
// スロベニア語の範囲
console.log(slRules.selectRange(102, 201));
// 出力: "few"
スロベニア語では、複雑なルールに基づいて異なる単一カウントに「one」、「two」、「few」を使用します。範囲の場合、両方の数字を一緒に考慮する異なるロジックを適用します。
開始値と終了値が等しい範囲の処理
開始値と終了値が同じ場合、幅のない範囲を表示することになります。一部のアプリケーションでは、範囲が想定される文脈で正確な値を表現するためにこれを使用します。
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(5, 5));
// 出力: "other"
console.log(rules.selectRange(1, 1));
// 出力: "one"
両方の値が1の場合、英語では「one」を返し、単数形を使用すべきことを示唆しています。両方の値が1以外の数値の場合、英語では「other」を返し、複数形を示唆しています。
この動作は「1-1 item」または単に「1 item」として範囲を表示する場合に理にかなっています。1以外の値の場合は「5-5 items」または「5 items」と表示します。
実際には、開始値と終了値が等しい場合を検出し、範囲ではなく単一の値を表示することが望ましいでしょう。
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));
// 出力: "1 item"
console.log(formatRange(5, 5));
// 出力: "5 items"
console.log(formatRange(1, 3));
// 出力: "1-3 items"
このアプローチでは、等しい値にはselect()を使用し、実際の範囲にはselectRange()を使用します。「1-1」や「5-5」の表示を避けるため、出力はより自然に読めるようになります。
selectRangeでのエッジケースの処理
selectRange()メソッドは入力値を検証します。いずれかのパラメータがundefined、null、または有効な数値に変換できない場合、メソッドはエラーをスローします。
const rules = new Intl.PluralRules("en-US");
try {
console.log(rules.selectRange(1, undefined));
} catch (error) {
console.log(error.name);
// 出力: "TypeError"
}
try {
console.log(rules.selectRange(NaN, 5));
} catch (error) {
console.log(error.name);
// 出力: "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));
// 出力: "other"
console.log(rules.selectRange(1n, 5n));
// 出力: "other"
console.log(rules.selectRange("1", "5"));
// 出力: "other"
文字列入力は数値として解析されます。これによりメソッドの呼び出し方に柔軟性が生まれますが、明確さのために可能な限り実際の数値型を渡すことが望ましいでしょう。
小数範囲の処理
selectRange()メソッドは小数にも対応しています。これは測定値や統計などの小数量の範囲を表示する際に便利です。
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(1.5, 2.5));
// 出力: "other"
console.log(rules.selectRange(0.5, 1.0));
// 出力: "other"
console.log(rules.selectRange(1.0, 1.5));
// 出力: "other"
英語ではこれらの小数範囲はすべて複数形として扱われます。他の言語では小数範囲に対して異なるルールが適用される場合があります。
小数範囲をフォーマットする際は、適切な小数精度で設定されたIntl.NumberFormatとselectRange()を組み合わせて使用します。
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));
// 出力: "1.5-2.5 kilometers"
console.log(formatRange(0.5, 1.0));
// 出力: "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") {
// 範囲の複数形化にselectRangeを使用
console.log(rules.selectRange(1, 3));
} else {
// selectRangeが利用できない場合は終了値でselectにフォールバック
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));
// 出力: "Found 10–15 results"
このパターンでは、Intl.NumberFormatのformatRange()を使用して数値をフォーマットし、Intl.PluralRulesのselectRange()を使用してテキストを選択します。どちらのメソッドも範囲に対して操作するため、すべての言語で正しく処理されることが保証されます。