如何为不同语言选择正确的复数形式?
使用 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”。当数量为 1 时和其他数字时,单词会发生变化。
其他语言的规则不同。波兰语根据复杂规则有三种形式。俄语有四种形式。阿拉伯语有六种形式。有些语言对所有数量只用一种形式。
以下是“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 也是如此。
每种语言中,哪些数字对应哪些类别有具体规则。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" 可以确定像“第 1”、“第 2”、“第 3”这样的序数的复数形式。
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 个项目、2 个项目”的表达方式。序数规则决定了“第 1 名、第 2 名、第 3 名、第 4 名”。返回的类别不同,是因为语法模式不同。
如何格式化小数数量
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"]
此技术会在一定范围内测试整数和半数值,以便捕获消息对象在特定语言环境下需要包含的类别。