Intl.PluralRules API
如何在 JavaScript 中正确处理复数形式
简介
复数化是根据数量显示不同文本的过程。在英文中,通常会针对单个项目显示“1 item”,多个项目则显示“2 items”。大多数开发者会用一个简单的条件判断,除了数量为 1 以外都加上“s”。
这种方法在非英语语言中就不适用了。例如,波兰语针对 1、2-4 和 5 及以上分别使用不同的形式。阿拉伯语有零、一、二、少(3-10)、多(11-99)和其他(100+)六种形式。威尔士语也有六种不同的复数形式。即使在英语中,像“person”到“people”这样的不规则复数也需要特殊处理。
Intl.PluralRules API 通过为任意数字和任意语言提供复数类别,解决了这个问题。你只需提供一个数量,API 就会根据目标语言的规则告诉你应该使用哪种形式。这样,你可以编写具备国际化能力的代码,无需手动编码每种语言的复数规则,也能正确处理多语言场景。
各语言如何处理复数形式
不同语言在表达数量时有很大差异。英语有两种形式:数量为 1 时用单数,其他情况用复数。这看似简单,但遇到其他复数系统的语言时就会变得复杂。
俄语和波兰语有三种形式。单数用于 1,特殊形式用于以 2、3、4 结尾的数字(但不包括 12、13、14),其余所有数量使用第三种形式。
阿拉伯语有六种形式:零、一、二、少(3-10)、多(11-99)和其他(100+)。威尔士语同样有六种形式,但数值区间不同。
某些语言,如中文和日语,完全不区分单数和复数。无论数量多少,形式都相同。
Intl.PluralRules API 通过基于 Unicode CLDR 复数规则的标准化类别名称,抽象了这些差异。六个类别分别是:zero、one、two、few、many 和 other。并非所有语言都使用这六个类别。英语只用 one 和 other。阿拉伯语则全部用到六类。
为某个语言环境创建 PluralRules 实例
Intl.PluralRules 构造函数接受一个语言环境标识符,并返回一个对象,可用于判断某个数字属于哪个复数类别。
const enRules = new Intl.PluralRules('en-US');
每个语言环境只需创建一个实例并复用。每次复数化都新建实例会造成资源浪费。建议将实例存储在变量中或使用缓存机制。
默认类型为 cardinal(基数),用于计数对象。你也可以通过传递 options 对象来创建序数(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'
返回值始终是六个类别名称之一:zero、one、two、few、many 或 other。英语只会返回 one 和 other,因为只用到这两种形式。
对于规则更复杂的阿拉伯语,会用到全部六个类别:
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)则例外。
当你指定 type: 'ordinal' 时,Intl.PluralRules API 会自动处理这些规则。
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'
您的文本应考虑到这一点。对于英语,建议显示“No items”而不是“0 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 实例时需要解析 locale 并加载复数规则数据。每个 locale 只需创建一次实例,不要在每次函数调用或渲染周期中重复创建。
// 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}`;
}
如果支持多个 locale,请为每个 locale 创建实例,并存储在 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');
这种模式可以将初始化成本分摊到多次调用中。
浏览器支持与兼容性
自 2019 年起,所有现代浏览器均已支持 Intl.PluralRules,包括 Chrome 63+、Firefox 58+、Safari 13+ 和 Edge 79+。Internet Explorer 不支持该特性。
对于面向现代浏览器的应用,可以直接使用 Intl.PluralRules,无需 polyfill。如果需要支持旧版浏览器,可通过 npm 上的 intl-pluralrules 等包获取 polyfill。
selectRange() 方法较新,支持范围略有限。该方法在 Chrome 106+、Firefox 116+、Safari 15.4+ 和 Edge 106+ 可用。如需支持旧版浏览器,请检查 selectRange() 的兼容性。
避免在逻辑中硬编码复数形式
不要通过判断 count 并在代码中分支来选择复数形式。这种方式无法适应拥有多于两种复数形式的语言,并且会将您的逻辑与英语规则耦合。
// 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}`;
}
这种模式在任何语言中都可以相同地工作。只需更换规则实例和 forms Map。
针对多种语言环境和边界情况进行测试
复数规则存在一些在仅用英语测试时容易忽略的边界情况。请至少用一种使用两种以上复数形式的语言(如波兰语或阿拉伯语)测试你的复数逻辑。
测试会触发不同类别的计数:
- 零
- 一
- 二
- 少量(阿拉伯语中为 3-10)
- 许多(阿拉伯语中为 11-99)
- 大数(100 及以上)
- 小数值(0.5、1.5、2.3)
- 如果你的 UI 会显示,负数
如果你使用序数规则,请测试会触发不同后缀的数字:1、2、3、4、11、21、22、23。这样可以确保你正确处理特殊情况。
及早用多种语言环境进行测试,可以避免后续添加新语言时出现意外情况。同时也能验证你的数据结构是否包含所有必要类别,以及你的逻辑是否能正确处理它们。