Intl.PluralRules API

如何在 JavaScript 中正确处理复数形式

介绍

复数化是根据计数显示不同文本的过程。在英语中,您可能会显示“1 item”表示单个项目,而“2 items”表示多个项目。大多数开发人员通过一个简单的条件语句来处理这个问题,对于不是 1 的计数添加一个“s”。

这种方法在英语以外的语言中会失效。例如,波兰语对 1、2-4 和 5 或更多使用不同的形式。阿拉伯语对零、一个、两个、少量和大量有不同的形式。威尔士语有六种不同的形式。即使在英语中,不规则复数形式(如“person”到“people”)也需要特殊处理。

Intl.PluralRules API 通过为任何语言中的任何数字提供复数形式类别解决了这个问题。您提供一个计数,API 会根据目标语言的规则告诉您使用哪种形式。这使您能够编写国际化就绪的代码,在不同语言中正确工作,而无需手动编码特定语言的规则。

各种语言如何处理复数形式

语言在表达数量方面差异很大。英语有两种形式:单数用于一个,复数用于其他所有情况。这看起来很简单,直到您遇到使用不同系统的语言。

俄语和波兰语使用三种形式。单数适用于一个项目。特殊形式适用于以 2、3 或 4 结尾的计数(但不包括 12、13 或 14)。所有其他计数使用第三种形式。

阿拉伯语使用六种形式:零、一个、两个、少量(3-10)、大量(11-99)和其他(100+)。威尔士语也有六种形式,但数值范围不同。

一些语言(如中文和日语)根本不区分单数和复数。同一种形式适用于任何计数。

Intl.PluralRules API 使用基于 Unicode CLDR 复数规则的标准化类别名称抽象了这些差异。这六个类别是:零、一个、两个、少量、大量和其他。并非每种语言都使用所有六个类别。英语仅使用“一个”和“其他”。阿拉伯语使用所有六个类别。

为某个语言环境创建一个 PluralRules 实例

Intl.PluralRules 构造函数接受一个语言环境标识符,并返回一个对象,该对象可以确定给定数字适用的复数类别。

const enRules = new Intl.PluralRules('en-US');

为每个语言环境创建一个实例并重复使用。为每次复数化操作构造一个新实例是低效的。将实例存储在变量中或使用缓存机制。

默认类型是基数(cardinal),用于处理计数对象。您还可以通过传递一个选项对象来为序数(ordinal)创建规则。

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

基数规则适用于诸如“1 apple, 2 apples”这样的计数。序数规则适用于诸如“1st place, 2nd place”这样的位次。

使用 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'

其他语言对小数有不同的规则。有些语言将任何小数视为复数,而另一些语言则根据小数部分使用更复杂的规则。

如果您的用户界面显示分数数量,例如 "1.5 GB" 或 "2.7 miles",请将分数直接传递给 select() 方法。除非您的用户界面会对显示值进行四舍五入,否则不要先进行四舍五入。

格式化序数,如 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 items" 或 "10-20 results"。某些语言对范围的复数规则与单个计数的复数规则不同。

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 实例需要解析语言环境并加载复数规则数据。应在每个语言环境中只创建一次,而不是在每次函数调用或渲染周期中都创建。

// 推荐:创建一次,重复使用
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 而无需 polyfill。如果需要支持旧版浏览器,可以通过 npm 上的 intl-pluralrules 等包获取 polyfill。

selectRange() 方法较新,支持范围稍有限。它在 Chrome 106+、Firefox 116+、Safari 15.4+ 和 Edge 106+ 中可用。如果使用 selectRange() 并需要支持旧版浏览器,请检查兼容性。

避免在逻辑中硬编码复数形式

不要通过检查计数并在代码中分支来选择复数形式。这种方法无法扩展到具有两个以上形式的语言,并且将逻辑与英语规则耦合。

// 避免这种模式
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}`;
}

这种模式对任何语言都适用。只需更改规则实例和 forms Map。

使用多种语言环境和边界情况进行测试

复数规则有一些在仅用英语测试时容易忽略的边界情况。请至少使用一种具有两个以上形式的语言(如波兰语或阿拉伯语)测试您的复数化逻辑。

测试触发不同类别的计数:

  • 少量(阿拉伯语中为 3-10)
  • 多量(阿拉伯语中为 11-99)
  • 大数字(100+)
  • 小数值(0.5、1.5、2.3)
  • 如果您的 UI 显示负数,也要测试负数

如果使用序数规则,请测试触发不同后缀的数字:1、2、3、4、11、21、22、23。这可以确保您正确处理特殊情况。

在早期使用多种语言环境进行测试可以防止在添加新语言时出现意外。这还可以验证您的数据结构是否包含所有必要的类别,以及您的逻辑是否正确处理它们。