获取某语言环境中可用的所有复数形式

了解需要为哪些复数类别提供翻译

介绍

在构建多语言应用程序时,您需要为不同的数量提供不同的文本形式。在英语中,您会写 "1 item" 和 "2 items"。这看起来很简单,直到您开始支持其他语言。

俄语根据数量使用三种不同的形式。阿拉伯语使用六种。一些语言对所有数量使用相同的形式。在您为这些形式提供翻译之前,您需要了解每种语言中存在哪些形式。

JavaScript 提供了一种方法来发现某个语言环境使用的复数类别。在 PluralRules 实例上的 resolvedOptions() 方法会返回一个 pluralCategories 属性,该属性列出了该语言环境需要的所有复数形式。这可以准确告诉您需要提供哪些翻译,而无需猜测或维护特定语言的规则表。

什么是复数类别

复数类别是跨语言使用的不同复数形式的标准化名称。Unicode CLDR(通用语言数据存储库)定义了六个类别:zero、one、two、few、many 和 other。

并非每种语言都使用所有六个类别。英语仅使用两个:one 和 other。类别 one 适用于数量 1,而 other 适用于其他所有数量。

阿拉伯语使用所有六个类别。类别 zero 适用于 0,one 适用于 1,two 适用于 2,few 适用于像 3-10 这样的数量,many 适用于像 11-99 这样的数量,other 适用于像 100 及以上的数量。

俄语使用三种类别:one 适用于以 1 结尾的数量(11 除外),few 适用于以 2-4 结尾的数量(12-14 除外),many 适用于其他所有情况。

日语和中文仅使用 other 类别,因为这些语言不区分单数和复数形式。

这些类别代表了每种语言的语言规则。当您提供翻译时,您需要为该语言使用的每个类别创建一个字符串。

使用 resolvedOptions 获取复数类别

PluralRules 实例上的 resolvedOptions() 方法返回一个包含规则信息的对象,包括该语言环境使用的复数类别。

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

console.log(options.pluralCategories);
// 输出: ["one", "other"]

pluralCategories 属性是一个字符串数组。每个字符串是六个标准类别名称之一。数组仅包含该语言环境实际使用的类别。

对于英语,数组包含 one 和 other,因为英语区分单数和复数形式。

对于规则更复杂的语言,数组包含更多类别:

const arRules = new Intl.PluralRules('ar-EG');
const options = arRules.resolvedOptions();

console.log(options.pluralCategories);
// 输出: ["zero", "one", "two", "few", "many", "other"]

阿拉伯语使用所有六个类别,因此数组包含所有六个值。

查看不同语言环境的复数类别

不同语言有不同的复数规则,这意味着它们使用不同的类别集合。比较几种语言以查看差异:

const locales = ['en-US', 'ar-EG', 'ru-RU', 'pl-PL', 'ja-JP', 'zh-CN'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// 输出:
// en-US: [one, other]
// ar-EG: [zero, one, two, few, many, other]
// ru-RU: [one, few, many, other]
// pl-PL: [one, few, many, other]
// ja-JP: [other]
// zh-CN: [other]

英语有两个类别。阿拉伯语有六个。俄语和波兰语各有四个。日语和中文只有一个类别,因为它们完全不区分复数形式。

这种差异表明,不能假设每种语言都像英语一样工作。您需要检查每个语言环境使用的类别,并为每个类别提供适当的翻译。

理解每个语言环境中类别的含义

相同的类别名称在不同语言中可能意味着不同的内容。在英语中,"one" 类别仅适用于数字 1。而在俄语中,"one" 类别适用于以 1 结尾但不包括 11 的数字,因此包括 1、21、31、101 等。

测试不同语言环境中哪些数字对应哪些类别:

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

const numbers = [0, 1, 2, 3, 5, 11, 21, 22, 100];

console.log('英语:');
numbers.forEach(n => {
  console.log(`  ${n}: ${enRules.select(n)}`);
});

console.log('俄语:');
numbers.forEach(n => {
  console.log(`  ${n}: ${ruRules.select(n)}`);
});

// 输出:
// 英语:
//   0: other
//   1: one
//   2: other
//   3: other
//   5: other
//   11: other
//   21: other
//   22: other
//   100: other
// 俄语:
//   0: many
//   1: one
//   2: few
//   3: few
//   5: many
//   11: many
//   21: one
//   22: few
//   100: many

在英语中,只有数字 1 使用 "one" 类别。而在俄语中,1 和 21 都使用 "one",因为它们以 1 结尾。数字 2、3 和 22 使用 "few",因为它们以 2-4 结尾。数字 0、5、11 和 100 使用 "many"。

这表明在不了解语言规则的情况下,无法预测某个数字对应的类别。pluralCategories 数组告诉您有哪些类别,而 select() 方法告诉您每个数字对应的类别。

获取序数的类别

像 1st、2nd、3rd 这样的序数有不同于基数的复数规则。创建一个带有 type: 'ordinal'PluralRules 实例来获取序数的类别:

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

console.log('基数:', enCardinalRules.resolvedOptions().pluralCategories);
// 输出: 基数: ["one", "other"]

console.log('序数:', enOrdinalRules.resolvedOptions().pluralCategories);
// 输出: 序数: ["one", "two", "few", "other"]

英语基数使用两个类别。英语序数使用四个类别,因为序数需要区分 1st、2nd、3rd 和其他。

序数类别映射到序数后缀:

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

const numbers = [1, 2, 3, 4, 11, 21, 22, 23];

numbers.forEach(n => {
  const category = enOrdinalRules.select(n);
  console.log(`${n}: ${category}`);
});

// 输出:
// 1: one
// 2: two
// 3: few
// 4: other
// 11: other
// 21: one
// 22: two
// 23: few

类别 "one" 对应后缀 st(1st、21st),"two" 对应 nd(2nd、22nd),"few" 对应 rd(3rd、23rd),"other" 对应 th(4th、11th)。

不同语言有不同的序数类别:

const locales = ['en-US', 'es-ES', 'fr-FR'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// 输出:
// en-US: [one, two, few, other]
// es-ES: [other]
// fr-FR: [one, other]

西班牙语只使用一个序数类别,因为西班牙语序数遵循更简单的模式。法语使用两个类别来区分第一和其他位置。

使用复数类别构建翻译映射

当您知道某个语言环境使用的类别时,您可以构建一个具有准确条目数量的翻译映射:

function buildTranslationMap(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const map = new Map();

  categories.forEach(category => {
    if (translations[category]) {
      map.set(category, translations[category]);
    } else {
      console.warn(`缺少语言环境 "${locale}" 的类别 "${category}" 的翻译`);
    }
  });

  return map;
}

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

const arTranslations = {
  zero: 'لا توجد عناصر',
  one: 'عنصر واحد',
  two: 'عنصران',
  few: 'عناصر',
  many: 'عنصرًا',
  other: 'عنصر'
};

const enMap = buildTranslationMap('en-US', enTranslations);
const arMap = buildTranslationMap('ar-EG', arTranslations);

console.log(enMap);
// 输出: Map(2) { 'one' => 'item', 'other' => 'items' }

console.log(arMap);
// 输出: Map(6) { 'zero' => 'لا توجد عناصر', 'one' => 'عنصر واحد', ... }

此函数会检查您是否为所有必需类别提供了翻译,并在缺少任何翻译时发出警告。这可以防止在使用某个类别但没有翻译时出现运行时错误。

验证翻译的完整性

使用复数类别来验证您的翻译是否包含所有必要的形式,然后再部署到生产环境:

function validateTranslations(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const requiredCategories = rules.resolvedOptions().pluralCategories;
  const providedCategories = Object.keys(translations);

  const missing = requiredCategories.filter(cat => !providedCategories.includes(cat));
  const extra = providedCategories.filter(cat => !requiredCategories.includes(cat));

  if (missing.length > 0) {
    console.error(`语言环境 ${locale} 缺少类别: ${missing.join(', ')}`);
    return false;
  }

  if (extra.length > 0) {
    console.warn(`语言环境 ${locale} 包含未使用的类别: ${extra.join(', ')}`);
  }

  return true;
}

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

const incompleteArTranslations = {
  one: 'عنصر واحد',
  other: 'عنصر'
};

validateTranslations('en-US', enTranslations);
// 输出: true

validateTranslations('ar-EG', incompleteArTranslations);
// 输出: 语言环境 ar-EG 缺少类别: zero, two, few, many
// 输出: false

此验证可以在开发过程中捕获缺失的翻译,而不是在用户遇到未翻译的文本时才发现问题。

构建动态翻译界面

在为翻译人员构建工具时,可以查询复数类别以准确显示需要翻译的形式:

function generateTranslationForm(locale, key) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const form = document.createElement('div');
  form.className = 'translation-form';

  const heading = document.createElement('h3');
  heading.textContent = `为 ${locale} 翻译 "${key}"`;
  form.appendChild(heading);

  categories.forEach(category => {
    const label = document.createElement('label');
    label.textContent = `${category}:`;

    const input = document.createElement('input');
    input.type = 'text';
    input.name = `${key}.${category}`;
    input.placeholder = `输入 ${category} 形式`;

    const wrapper = document.createElement('div');
    wrapper.appendChild(label);
    wrapper.appendChild(input);
    form.appendChild(wrapper);
  });

  return form;
}

const enForm = generateTranslationForm('en-US', 'items');
const arForm = generateTranslationForm('ar-EG', 'items');

document.body.appendChild(enForm);
document.body.appendChild(arForm);

这段代码生成了一个表单,其中每个语言环境都有正确数量的输入字段。英语有两个字段(one 和 other),而阿拉伯语有六个字段(zero、one、two、few、many 和 other)。

比较不同语言环境的类别

在为多个语言环境管理翻译时,可以比较它们使用的类别以了解翻译的复杂性:

function compareLocalePluralCategories(locales) {
  const comparison = {};

  locales.forEach(locale => {
    const rules = new Intl.PluralRules(locale);
    const categories = rules.resolvedOptions().pluralCategories;
    comparison[locale] = categories;
  });

  return comparison;
}

const locales = ['en-US', 'es-ES', 'ar-EG', 'ru-RU', 'ja-JP'];
const comparison = compareLocalePluralCategories(locales);

console.log(comparison);
// 输出:
// {
//   'en-US': ['one', 'other'],
//   'es-ES': ['one', 'other'],
//   'ar-EG': ['zero', 'one', 'two', 'few', 'many', 'other'],
//   'ru-RU': ['one', 'few', 'many', 'other'],
//   'ja-JP': ['other']
// }

这表明英语和西班牙语具有相同的复数类别,因此可以轻松地在它们之间重用翻译结构。而阿拉伯语需要更多的翻译工作,因为它使用了六种类别。

检查某个语言环境是否使用特定类别

在代码中使用特定复数类别之前,请检查该语言环境是否实际使用该类别:

function localeUsesCategory(locale, category) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  return categories.includes(category);
}

console.log(localeUsesCategory('en-US', 'zero'));
// 输出: false

console.log(localeUsesCategory('ar-EG', 'zero'));
// 输出: true

console.log(localeUsesCategory('ja-JP', 'one'));
// 输出: false

这可以防止您假设每个语言环境都有 zero 类别或 one 类别。使用此检查可以安全地实现类别特定的逻辑。

理解 other 类别

每种语言都使用 other 类别。此类别作为默认情况,当没有其他类别适用时使用。

在英语中,other 包括除 1 以外的所有计数。在阿拉伯语中,other 包括 100 及以上的大数字。在日语中,other 包括所有计数,因为日语不区分复数形式。

始终为 other 类别提供翻译。此类别在每个语言环境中都保证存在,并将在没有更具体类别匹配时使用。

const locales = ['en-US', 'ar-EG', 'ru-RU', 'ja-JP'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  const hasOther = categories.includes('other');
  console.log(`${locale} 使用 "other": ${hasOther}`);
});

// 输出:
// en-US 使用 "other": true
// ar-EG 使用 "other": true
// ru-RU 使用 "other": true
// ja-JP 使用 "other": true

获取所有解析选项

resolvedOptions() 方法返回的不仅仅是复数类别。它还包括有关语言环境、类型和数字格式选项的信息:

const rules = new Intl.PluralRules('de-DE', {
  type: 'cardinal',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

const options = rules.resolvedOptions();

console.log(options);
// 输出:
// {
//   locale: 'de-DE',
//   type: 'cardinal',
//   pluralCategories: ['one', 'other'],
//   minimumIntegerDigits: 1,
//   minimumFractionDigits: 2,
//   maximumFractionDigits: 2,
//   minimumSignificantDigits: undefined,
//   maximumSignificantDigits: undefined
// }

pluralCategories 属性是解析选项对象中的一部分信息。其他属性告诉您 PluralRules 实例使用的确切配置,包括任何设置为默认值的选项。

缓存复数类别以提升性能

创建 PluralRules 实例并调用 resolvedOptions() 是有成本的。为每个语言环境缓存结果,而不是反复查询它们:

const categoriesCache = new Map();

function getPluralCategories(locale, type = 'cardinal') {
  const key = `${locale}:${type}`;

  if (categoriesCache.has(key)) {
    return categoriesCache.get(key);
  }

  const rules = new Intl.PluralRules(locale, { type });
  const categories = rules.resolvedOptions().pluralCategories;

  categoriesCache.set(key, categories);

  return categories;
}

const enCardinal = getPluralCategories('en-US', 'cardinal');
const enOrdinal = getPluralCategories('en-US', 'ordinal');
const arCardinal = getPluralCategories('ar-EG', 'cardinal');

console.log('en-US cardinal:', enCardinal);
console.log('en-US ordinal:', enOrdinal);
console.log('ar-EG cardinal:', arCardinal);

// 后续调用使用缓存结果
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// 未创建新的 PluralRules 实例

这种模式在格式化许多复数化字符串或支持多种语言环境的应用程序中尤为重要。

浏览器支持和兼容性

resolvedOptions()pluralCategories 属性于 2020 年被添加到 JavaScript 中。它在 Chrome 106+、Firefox 116+、Safari 15.4+ 和 Edge 106+ 中受支持。

支持 Intl.PluralRules 但不支持 pluralCategories 的旧版浏览器将返回 undefined。在使用之前检查其是否存在:

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const options = rules.resolvedOptions();

  if (options.pluralCategories) {
    return options.pluralCategories;
  }

  // 旧版浏览器的回退方案
  return ['one', 'other'];
}

此回退方案假设一个简单的两类别系统,这适用于英语和许多欧洲语言,但对于具有更复杂规则的语言可能不正确。为了更好的兼容性,请提供特定语言的回退方案或使用 polyfill。