如何为不同语言选择正确的复数形式?

使用 JavaScript 的 Intl.PluralRules,根据语言特定规则选择单个项目、两个项目、少量项目或大量项目

介绍

当您显示带有数量的文本时,不同的数量需要不同的消息。在英语中,您会写 "1 file",但会写 "2 files"。最简单的方法是将数字与单词连接起来,并在需要时添加 "s"。

function formatFileCount(count) {
  return count === 1 ? `${count} file` : `${count} files`;
}

这种方法有三个问题。首先,它会为零生成不正确的英语("0 files" 应该是 "no files")。其次,它在处理复杂的复数形式时会失败,例如 "1 child, 2 children" 或 "1 person, 2 people"。第三,也是最重要的,其他语言有完全不同的复数规则,这段代码无法处理。

JavaScript 提供了 Intl.PluralRules 来解决这个问题。此 API 根据 Unicode CLDR 标准确定在任何语言中针对任何数字应使用的复数形式,该标准被全球专业翻译系统广泛使用。

为什么不同语言需要不同的复数形式

英语使用两种复数形式。您会写 "1 book" 和 "2 books"。当数量正好为一时,单词会发生变化,而其他数字则不同。

其他语言的规则不同。波兰语根据复杂规则使用三种形式。俄语使用四种形式。阿拉伯语使用六种形式。有些语言对所有数量只使用一种形式。

以下是展示不同语言中 "apple" 一词如何根据数量变化的示例:

英语: 1 apple, 2 apples, 5 apples, 0 apples

波兰语: 1 jabłko, 2 jabłka, 5 jabłek, 0 jabłek

俄语: 1 яблоко, 2 яблока, 5 яблок, 0 яблок

阿拉伯语: 根据数量是零、一个、两个、几个、许多或其他情况使用六种不同的形式

Unicode CLDR 定义了每种语言中何时使用每种形式的确切规则。您无法记住这些规则或将其硬编码到您的应用程序中。您需要一个了解这些规则的 API。

什么是 CLDR 复数类别

Unicode CLDR 标准定义了六种复数类别,涵盖了所有语言:

  • zero:在某些语言中用于表示正好为零的项目
  • one:用于单数形式
  • two:用于具有双数形式的语言
  • few:在某些语言中用于表示少量
  • many:在某些语言中用于表示较大量或分数
  • other:默认形式,用于不适用其他类别的情况

每种语言都使用 other 类别。大多数语言总共只使用两到三个类别。这些类别并不直接对应于数量。例如,在波兰语中,数字 5 使用 many 类别,但 0、25 和 1.5 也使用 many 类别。

不同语言中,数字与类别的对应规则各不相同。JavaScript 通过 Intl.PluralRules API 处理这种复杂性。

如何确定使用哪个复数形式

Intl.PluralRules 对象可以确定某个数字在特定语言中属于哪个复数类别。您可以使用一个语言环境创建一个 PluralRules 对象,然后调用其 select() 方法并传入一个数字。

const rules = new Intl.PluralRules('en-US');
console.log(rules.select(0));  // "other"
console.log(rules.select(1));  // "one"
console.log(rules.select(2));  // "other"
console.log(rules.select(5));  // "other"

在英语中,select() 对于数字 1 返回 "one",对于其他数字返回 "other"

波兰语使用三个类别,规则更复杂:

const rules = new Intl.PluralRules('pl-PL');
console.log(rules.select(0));   // "many"
console.log(rules.select(1));   // "one"
console.log(rules.select(2));   // "few"
console.log(rules.select(5));   // "many"
console.log(rules.select(22));  // "few"
console.log(rules.select(25));  // "many"

阿拉伯语使用六个类别:

const rules = new Intl.PluralRules('ar-EG');
console.log(rules.select(0));   // "zero"
console.log(rules.select(1));   // "one"
console.log(rules.select(2));   // "two"
console.log(rules.select(3));   // "few"
console.log(rules.select(11));  // "many"
console.log(rules.select(100)); // "other"

select() 方法返回一个字符串,用于标识类别。您可以使用此字符串选择适合的消息进行显示。

如何将复数类别映射到消息

在确定复数类别后,您需要选择正确的消息来显示给用户。创建一个对象,将每个类别映射到其对应的消息,然后使用类别字符串查找消息。

const messages = {
  one: '{count} file',
  other: '{count} files'
};

function formatFileCount(count, locale) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category];
  return message.replace('{count}', count);
}

console.log(formatFileCount(1, 'en-US'));  // "1 file"
console.log(formatFileCount(5, 'en-US'));  // "5 files"

这种模式适用于任何语言。对于波兰语,您需要为该语言使用的所有三个类别提供消息:

const messages = {
  one: '{count} plik',
  few: '{count} pliki',
  many: '{count} plików'
};

function formatFileCount(count, locale) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category];
  return message.replace('{count}', count);
}

console.log(formatFileCount(1, 'pl-PL'));   // "1 plik"
console.log(formatFileCount(2, 'pl-PL'));   // "2 pliki"
console.log(formatFileCount(5, 'pl-PL'));   // "5 plików"
console.log(formatFileCount(22, 'pl-PL'));  // "22 pliki"

代码结构在不同语言中保持一致。唯一变化的是消息对象。这种分离允许翻译人员为其语言提供正确的消息,而无需修改代码。

如何处理缺失的复数类别

您的消息对象可能不包含所有六种可能的类别。大多数语言只使用两到三种。当 select() 返回的类别不在您的消息对象中时,请回退到 other 类别。

function formatFileCount(count, locale, messages) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category] || messages.other;
  return message.replace('{count}', count);
}

const englishMessages = {
  one: '{count} file',
  other: '{count} files'
};

console.log(formatFileCount(1, 'en-US', englishMessages));  // "1 file"
console.log(formatFileCount(5, 'en-US', englishMessages));  // "5 files"

这种模式确保即使消息对象不完整,您的代码也能正常工作。other 类别在每种语言中始终存在,因此是一个安全的回退选项。

如何使用序数的复数规则

Intl.PluralRules 构造函数接受一个 type 选项,用于更改类别的确定方式。默认类型是 "cardinal",用于计数项目。设置 type: "ordinal" 来确定序数的复数形式,例如 "1st"、"2nd"、"3rd"。

const cardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
console.log(cardinalRules.select(1));  // "one"
console.log(cardinalRules.select(2));  // "other"
console.log(cardinalRules.select(3));  // "other"

const ordinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
console.log(ordinalRules.select(1));   // "one"
console.log(ordinalRules.select(2));   // "two"
console.log(ordinalRules.select(3));   // "few"
console.log(ordinalRules.select(4));   // "other"

基数规则决定 "1 item, 2 items"。序数规则决定 "1st place, 2nd place, 3rd place, 4th place"。返回的类别不同是因为语法模式不同。

如何格式化分数数量

select() 方法可以处理小数。不同语言对分数映射到复数类别有特定规则。

const rules = new Intl.PluralRules('en-US');
console.log(rules.select(1));    // "one"
console.log(rules.select(1.0));  // "one"
console.log(rules.select(1.5));  // "other"
console.log(rules.select(0.5));  // "other"

在英语中,1.0 使用单数形式,但 1.5 使用复数形式。一些语言对分数有不同的规则,将它们视为单独的类别。

const messages = {
  one: '{count} file',
  other: '{count} files'
};

const rules = new Intl.PluralRules('en-US');
const count = 1.5;
const category = rules.select(count);
const message = messages[category];
console.log(message.replace('{count}', count));  // "1.5 files"

将小数直接传递给 select() 方法。该方法会自动应用正确的语言规则。

如何创建可重用的复数格式化器

为了避免在应用程序中重复相同的模式,可以创建一个封装复数选择逻辑的可重用函数。

class PluralFormatter {
  constructor(locale) {
    this.locale = locale;
    this.rules = new Intl.PluralRules(locale);
  }

  format(count, messages) {
    const category = this.rules.select(count);
    const message = messages[category] || messages.other;
    return message.replace('{count}', count);
  }
}

const formatter = new PluralFormatter('en-US');

const fileMessages = {
  one: '{count} file',
  other: '{count} files'
};

const itemMessages = {
  one: '{count} item',
  other: '{count} items'
};

console.log(formatter.format(1, fileMessages));  // "1 file"
console.log(formatter.format(5, fileMessages));  // "5 files"
console.log(formatter.format(1, itemMessages));  // "1 item"
console.log(formatter.format(3, itemMessages));  // "3 items"

这个类创建了一个 PluralRules 对象,并在多个格式化操作中重复使用。您可以扩展它以支持更高级的功能,例如在将计数插入消息之前使用 Intl.NumberFormat 格式化计数。

如何将复数规则与翻译系统集成

专业的翻译系统会存储带有复数类别占位符的消息。当您翻译文本时,您需要提供目标语言所需的所有复数形式。

const translations = {
  'en-US': {
    fileCount: {
      one: '{count} file',
      other: '{count} files'
    },
    downloadComplete: {
      one: 'Download of {count} file complete',
      other: 'Download of {count} files complete'
    }
  },
  'pl-PL': {
    fileCount: {
      one: '{count} plik',
      few: '{count} pliki',
      many: '{count} plików'
    },
    downloadComplete: {
      one: 'Pobieranie {count} pliku zakończone',
      few: 'Pobieranie {count} plików zakończone',
      many: 'Pobieranie {count} plików zakończone'
    }
  }
};

class Translator {
  constructor(locale, translations) {
    this.locale = locale;
    this.translations = translations[locale] || {};
    this.rules = new Intl.PluralRules(locale);
  }

  translate(key, count) {
    const messages = this.translations[key];
    if (!messages) return key;

    const category = this.rules.select(count);
    const message = messages[category] || messages.other;
    return message.replace('{count}', count);
  }
}

const translator = new Translator('en-US', translations);
console.log(translator.translate('fileCount', 1));         // "1 file"
console.log(translator.translate('fileCount', 5));         // "5 files"
console.log(translator.translate('downloadComplete', 1));  // "Download of 1 file complete"
console.log(translator.translate('downloadComplete', 5));  // "Download of 5 files complete"

const polishTranslator = new Translator('pl-PL', translations);
console.log(polishTranslator.translate('fileCount', 1));   // "1 plik"
console.log(polishTranslator.translate('fileCount', 2));   // "2 pliki"
console.log(polishTranslator.translate('fileCount', 5));   // "5 plików"

这种模式将翻译数据与代码逻辑分离。翻译人员为其语言使用的每个复数类别提供消息。您的代码会自动应用这些规则。

如何检查某个语言环境使用的复数类别

resolvedOptions() 方法会返回有关 PluralRules 对象的信息,但不会列出语言环境使用的类别。要找出某个语言环境使用的所有类别,可以测试一系列数字并收集返回的唯一类别。

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const categories = new Set();

  for (let i = 0; i <= 100; i++) {
    categories.add(rules.select(i));
    categories.add(rules.select(i + 0.5));
  }

  return Array.from(categories).sort();
}

console.log(getPluralCategories('en-US'));  // ["one", "other"]
console.log(getPluralCategories('pl-PL'));  // ["few", "many", "one"]
console.log(getPluralCategories('ar-EG'));  // ["few", "many", "one", "other", "two", "zero"]

此技术会测试整数和半值的范围。它捕获了您的消息对象在给定语言环境下需要包含的类别。