如何为区间(如 1-3 个项目)选择复数形式
使用 JavaScript 在显示数字区间时选择正确的复数形式
引言
区间用于表示一个值介于两个端点之间。用户界面中常见的区间场景包括:搜索结果显示“找到 10-15 个匹配项”、库存系统显示“1-3 个可用项目”,或筛选器显示“选择 2-5 个选项”。这些区间将两个数字与描述性文本结合,文本需要在语法上与区间保持一致。
当你显示单个计数时,需要在单数和复数形式之间选择:“1 个项目”与“2 个项目”。不同语言有各自的规则来决定使用哪种形式。英语在 1 时用单数,其他数字用复数。波兰语在 1、2-4 和 5 及以上时分别使用不同的形式。阿拉伯语则根据数量有六种不同的形式。
区间带来了不同的挑战。复数形式不仅取决于起始值,还取决于结束值,而不是单一数字。在英语中,“1-2 items”即使起始值为 1,也要用复数。不同语言对于区间使用哪种复数形式有不同的规则。selectRange() 方法在 Intl.PluralRules 上可以自动处理这些特定语言的规则。
为什么区间需要不同的复数规则
对区间中的单个数字使用 select() 方法,并不能在所有语言中得到正确的结果。你可能会考虑用区间的结束值,但在许多语言中这会导致错误的结果。
以英语的 0-1 区间为例。对结束值使用 select() 方法会返回“one”,这意味着你应该显示“0-1 item”。但这在语法上是错误的,正确的表达应为“0-1 items”,即使用复数形式。
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" 处理序数,尽管序数区间在用户界面中较为少见。
请在多次调用中复用同一个实例。每次复数化都新建实例会造成资源浪费。建议将实例存储在变量中或按 locale 缓存。
使用 selectRange 判断区间的复数类别
selectRange() 方法接收两个数字,分别表示区间的起始和结束。它会返回一个字符串,指示适用的复数类别:"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"
返回值始终是六个标准复数类别名称之一。您的代码需要将这些类别映射到相应的本地化文本。
将区间类别映射到本地化字符串
将每个复数类别的文本形式存储在一个数据结构中。使用 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" 形式。
单值时 selectRange 与 select 的对比
select() 方法用于处理单个数量,而 selectRange() 用于处理区间。当只显示单一数量时,使用 select();当显示两个值之间的区间时,使用 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 则用单数形式。
有些语言在单数规则和区间规则之间的差异更加明显。
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 item" 或仅为 "1 item",这种行为是合理的。对于不等于 1 的值,你会显示 "5-5 items" 或 "5 items"。
在实际应用中,你可能希望检测 start 是否等于 end,并在这种情况下只显示单个值而不是区间。
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() 方法会校验其输入参数。如果任一参数为 undefined、null,或无法转换为有效数字,该方法会抛出错误。
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"
数字格式化器可确保小数显示的一致性。复数规则会根据小数值确定正确的形式。
浏览器支持与兼容性
与其他 Intl API 方法相比,selectRange() 方法相对较新。它于 2023 年作为 Intl.NumberFormat v3 规范的一部分推出。
浏览器支持包括 Chrome 106 及以上版本、Firefox 116 及以上版本、Safari 15.4 及以上版本,以及 Edge 106 及以上版本。该方法在 Internet Explorer 或更早的浏览器版本中不可用。
对于面向现代浏览器的应用,可以直接使用 selectRange(),无需 polyfill。如果需要支持旧版浏览器,请在使用前检查该方法是否存在。
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 等包获取 polyfill。
何时使用 selectRange 与 select
当 UI 明确显示起始值和结束值的区间时,应使用 selectRange()。例如,搜索结果显示“找到 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"
此模式使用 formatRange()(来自 Intl.NumberFormat)来格式化数字,并使用 selectRange()(来自 Intl.PluralRules)来选择文本。两种方法都可用于区间,确保所有语言都能正确处理。