如何格式化序数,如 1st、2nd、3rd
使用 JavaScript 按照本地化规则显示带有适当后缀和格式的序数
简介
序数用于表示在序列中的位置或排名。在英文中,1st、2nd、3rd、4th 用于描述比赛名次或列表中的项目。这些后缀有助于区分序数和普通计数数字。
不同语言对序数的表达方式完全不同。英文添加 st、nd、rd、th 等后缀。法语用上标字母,如 1er 和 2e。德语在数字后加点,如 1. 和 2.。日语则在数字前加“第”字。如果你硬编码英文序数后缀,就默认所有用户都遵循同样的规则。
JavaScript 提供了 Intl.PluralRules API,并支持 ordinal 类型,可以自动处理这些差异。本教程将介绍什么是序数、为什么不同语言的格式不同,以及如何为任意本地化环境正确格式化序数。
什么是序数
序数用于表示在序列中的位置、排名或顺序。它们回答“第几个”而不是“多少个”。数字 1st、2nd、3rd 描述比赛名次。first、second、third 描述列表中的项目。
基数用于表示数量或数目。它们回答“多少个”。数字 1、2、3 描述对象的数量。one、two、three 描述数量。
同一个数字在不同语境下可以有不同用途。例如,“5 apples”中的 5 是基数,“5th place”中的 5 是序数。区分这两者很重要,因为许多语言对序数和基数的格式要求不同。
在英语中,10以内的序数词有独特的单词形式。First(第一)、second(第二)、third(第三)、fourth(第四)、fifth(第五)都是独立的词。10以上的序数词则通过添加后缀构成,如 eleventh(第十一)、twelfth(第十二)、twentieth(第二十)、twenty-first(第二十一)等,这些都需要加上特定的后缀。
当用数字而不是单词书写序数时,英语会在数字后添加后缀 st、nd、rd 或 th。这些后缀的使用有特定规则,取决于数字的最后一位。
为什么序数格式因语言而异
不同语言发展出了各自表达序数的体系。这些约定反映了每种语言独特的语法规则、书写系统和文化习惯。
在英语中,序数词有四种不同的后缀。以 1 结尾的数字用 st,以 2 结尾的用 nd,以 3 结尾的用 rd,其他数字用 th。但以 11、12、13 结尾的数字都用 th。因此有 1st、2nd、3rd、4th、11th、12th、13th、21st、22nd、23rd 等形式。
在法语中,序数词使用上标缩写。第一个项目用 1er(阳性)或 1re(阴性),其他序数用上标 e,如 2e、3e、4e。格式上不仅仅是字母后缀,还包括上标排版。
在德语中,序数词在数字后加句点。例如 1.、2.、3. 分别表示第一、第二、第三。这个句点提示读者在朗读时要加上相应的语法结尾。
在西班牙语中,序数词使用性别化的上标标记。阳性序数用 1.º、2.º、3.º,阴性序数用 1.ª、2.ª、3.ª。句点将数字和标记分开。
在日语中,序数词在数字前加前缀“第”。第一、第二、第三分别写作“第一”、“第二”、“第三”。这个前缀将基数词变为序数词。
当你通过将数字与硬编码的后缀拼接来构建序数字符串时,会让所有用户都必须按照英文的习惯来理解。这会让你的应用程序对那些期望不同格式的用户来说更难使用。
理解 Intl.PluralRules 的 ordinal 类型
Intl.PluralRules API 用于确定某个数字在指定语言环境下属于哪个复数类别。虽然这个 API 通常用于在单数和复数词形之间做选择,但它同样可以处理序数。
构造函数接受一个语言环境标识符和一个选项对象。将 type 选项设置为 "ordinal",即可处理序数而不是基数。
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
这样就创建了一个能够理解英文序数模式的规则对象。select() 方法会返回你传入的任意数字所属的类别名称。
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
console.log(rules.select(1));
// Output: "one"
console.log(rules.select(2));
// Output: "two"
console.log(rules.select(3));
// Output: "few"
console.log(rules.select(4));
// Output: "other"
返回的类别是语言学术语,而不是实际的后缀。类别 "one" 对应英文中的 st 后缀,类别 "two" 对应 nd 后缀,类别 "few" 对应 rd 后缀,类别 "other" 对应 th 后缀。
你需要将这些类别名称映射为适合你本地语言环境的后缀。API 会自动处理判断每个数字属于哪个类别的复杂规则。
构建序数格式化函数
要格式化序数,可以将 Intl.PluralRules 与类别到后缀的映射结合起来。创建一个格式化函数,接收一个数字并返回格式化后的字符串。
function formatOrdinal(number, locale) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
const category = rules.select(number);
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
const suffix = suffixes[category];
return `${number}${suffix}`;
}
console.log(formatOrdinal(1, 'en-US'));
// Output: "1st"
console.log(formatOrdinal(2, 'en-US'));
// Output: "2nd"
console.log(formatOrdinal(3, 'en-US'));
// Output: "3rd"
console.log(formatOrdinal(4, 'en-US'));
// Output: "4th"
该函数每次运行时都会新建一个 PluralRules 实例。为了获得更好的性能,建议只创建一次规则对象,并在多个数字间复用。
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
function formatOrdinal(number) {
const category = rules.select(number);
const suffix = suffixes[category];
return `${number}${suffix}`;
}
console.log(formatOrdinal(1));
// Output: "1st"
console.log(formatOrdinal(21));
// Output: "21st"
console.log(formatOrdinal(22));
// Output: "22nd"
console.log(formatOrdinal(23));
// Output: "23rd"
API 能够正确处理像 11、12 和 13 这样的数字,这些数字虽然结尾不同,但都使用 th 作为后缀。
console.log(formatOrdinal(11));
// Output: "11th"
console.log(formatOrdinal(12));
// Output: "12th"
console.log(formatOrdinal(13));
// Output: "13th"
复数规则编码了该语言环境下的所有特殊情况和例外。你无需编写条件逻辑来处理这些边缘情况。
针对不同语言环境格式化序数
复数类别及其含义在不同语言环境中会有所变化。有些语言的类别比英语少,有些则有不同的规则来决定数字属于哪个类别。
威尔士语采用了不同的分类系统。其规则定义了更多类别,每个类别对应威尔士语中的不同序数形式。
const enRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const cyRules = new Intl.PluralRules('cy', { type: 'ordinal' });
console.log(enRules.select(1));
// Output: "one"
console.log(cyRules.select(1));
// Output: "one"
console.log(enRules.select(2));
// Output: "two"
console.log(cyRules.select(2));
// Output: "two"
console.log(enRules.select(5));
// Output: "other"
console.log(cyRules.select(5));
// Output: "many"
为了支持多语言环境,你需要为每个语言环境配置不同的后缀映射。类别保持一致,但后缀会变化。
const ordinalSuffixes = {
'en-US': {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
},
'fr-FR': {
one: 'er',
other: 'e'
}
};
function formatOrdinal(number, locale) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
const category = rules.select(number);
const suffixes = ordinalSuffixes[locale];
const suffix = suffixes[category] || suffixes.other;
return `${number}${suffix}`;
}
console.log(formatOrdinal(1, 'en-US'));
// Output: "1st"
console.log(formatOrdinal(1, 'fr-FR'));
// Output: "1er"
console.log(formatOrdinal(2, 'en-US'));
// Output: "2nd"
console.log(formatOrdinal(2, 'fr-FR'));
// Output: "2e"
当你可以控制每个语言环境的后缀字符串时,这种方法效果很好。但这需要为你支持的每个语言环境维护后缀数据。
理解序数复数类别
Intl.PluralRules API 支持六种可能的类别名称。不同的语言环境会用到这些类别的不同子集。
这些类别包括 zero、one、two、few、many 和 other。并非所有语言都区分这六个类别。英语序数只用到 four:one、two、few 和 other。
类别名称与数值本身并不直接对应。类别 "one" 包含 1、21、31、41 以及所有以 1 结尾但不包括 11 的数字。类别 "two" 包含 2、22、32、42 以及所有以 2 结尾但不包括 12 的数字。
你可以通过调用 resolvedOptions() 方法,并查看 pluralCategories 属性,来检查某个语言环境使用了哪些类别。
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const options = rules.resolvedOptions();
console.log(options.pluralCategories);
// Output: ["one", "two", "few", "other"]
这表明英语序数词使用了四种类别。其他语言环境则采用不同的类别体系。
const rules = new Intl.PluralRules('fr-FR', { type: 'ordinal' });
const options = rules.resolvedOptions();
console.log(options.pluralCategories);
// Output: ["one", "other"]
法语序数词只区分 one 和 other。这种更简单的分类方式反映了法语更简单的后缀规则。
针对用户语言环境格式化序数
你可以使用浏览器中用户的首选语言,而不是硬编码特定的语言环境。navigator.language 属性会返回用户的首选语言。
const userLocale = navigator.language;
const rules = new Intl.PluralRules(userLocale, { type: 'ordinal' });
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
function formatOrdinal(number) {
const category = rules.select(number);
const suffix = suffixes[category] || suffixes.other;
return `${number}${suffix}`;
}
console.log(formatOrdinal(1));
// Output varies by user's locale
这种方法会自动适配用户的语言偏好。但你仍需为应用支持的每个语言环境提供合适的后缀映射。
对于没有特定后缀映射的语言环境,可以采用默认行为,或仅显示数字而不加后缀。
function formatOrdinal(number, locale = navigator.language) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
const category = rules.select(number);
const localeMapping = ordinalSuffixes[locale];
if (!localeMapping) {
return String(number);
}
const suffix = localeMapping[category] || localeMapping.other || '';
return `${number}${suffix}`;
}
当某个语言环境没有后缀映射时,此函数只返回数字本身。
序数的常见使用场景
序数在用户界面中有多种常见应用场景。了解这些用例有助于你判断何时应将数字格式化为序数。
排行榜和积分榜会显示用户的排名。例如,游戏应用会显示“第 1 名”、“第 2 名”、“第 3 名”,而不是“名次 1”、“名次 2”、“名次 3”。
function formatRanking(position) {
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const category = rules.select(position);
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
const suffix = suffixes[category];
return `${position}${suffix} place`;
}
console.log(formatRanking(1));
// Output: "1st place"
console.log(formatRanking(42));
// Output: "42nd place"
日期格式有时会用序数表示月份中的某一天。有些语言环境会写作“1 月 1 日”而不是“1 月 1”。
分步骤操作说明会用序数为每一步编号。例如,教程会显示“第 1 步:安装软件”、“第 2 步:配置设置”、“第 3 步:启动应用”。
在长序列的列表项中,如果需要强调位置而不仅仅是编号,采用序数格式会更合适。
复用规则对象以提升性能
创建新的 Intl.PluralRules 实例时,需要加载本地化数据并处理相关选项。当你需要用同一 locale 格式化多个序数时,建议只创建一次规则对象并重复使用。
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
function formatOrdinal(number) {
const category = rules.select(number);
const suffix = suffixes[category];
return `${number}${suffix}`;
}
const positions = [1, 2, 3, 4, 5];
positions.forEach(position => {
console.log(formatOrdinal(position));
});
// Output:
// "1st"
// "2nd"
// "3rd"
// "4th"
// "5th"
这种方式比为每个数字都新建规则对象更高效。当你需要格式化包含数百或数千个值的数组时,性能差异会非常明显。
你还可以创建一个格式化器工厂,返回针对特定 locale 配置的函数。
function createOrdinalFormatter(locale, suffixMapping) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
return function(number) {
const category = rules.select(number);
const suffix = suffixMapping[category] || suffixMapping.other || '';
return `${number}${suffix}`;
};
}
const formatEnglishOrdinal = createOrdinalFormatter('en-US', {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
});
console.log(formatEnglishOrdinal(1));
// Output: "1st"
console.log(formatEnglishOrdinal(2));
// Output: "2nd"
这种模式将规则对象和后缀映射封装在一起,使格式化器在整个应用中都能方便复用。