如何格式化序数,如 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"

这种模式将规则对象和后缀映射封装在一起,使格式化器在整个应用中都能方便复用。