如何为不同语言选择正确的复数形式?
使用 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"]
此技术会测试整数和半值的范围。它捕获了您的消息对象在给定语言环境下需要包含的类别。