如何格式化相对时间(如 3 天前 或 2 小时后)?

使用 Intl.RelativeTimeFormat,可在任意语言中自动处理复数和本地化,显示如 3 天前 或 2 小时后的时间

简介

社交媒体动态、评论区和活动日志通常会显示诸如“5 分钟前”、“2 小时前”或“3 天后”这样的时间戳。这些相对时间戳可以帮助用户快速了解事件发生的时间,无需解析具体日期。

如果你将这些字符串硬编码为英文,就默认所有用户都懂英语并遵循英语语法规则。但不同语言表达相对时间的方式各不相同。例如,西班牙语用“hace 3 días”而不是“3 days ago”,日语则用完全不同的结构“3日前”。每种语言还有独特的复数规则,决定何时用单数或复数形式。

JavaScript 提供了 Intl.RelativeTimeFormat API,可自动处理相对时间的格式化。本文将介绍如何利用该内置 API,为任意语言正确格式化相对时间。

为什么相对时间格式化需要国际化

不同语言表达相对时间的方式各不相同。英语在表示过去时间时将时间单位放在“ago”前,表示将来时间时则放在“in”后。其他语言可能采用不同的词序、介词,甚至完全不同的语法结构。

const rtfEnglish = new Intl.RelativeTimeFormat('en');
console.log(rtfEnglish.format(-3, 'day'));
// "3 days ago"

const rtfSpanish = new Intl.RelativeTimeFormat('es');
console.log(rtfSpanish.format(-3, 'day'));
// "hace 3 días"

const rtfJapanese = new Intl.RelativeTimeFormat('ja');
console.log(rtfJapanese.format(-3, 'day'));
// "3 日前"

每种语言都会生成符合其习惯的自然表达。你无需了解这些规则或维护翻译文件,API 会自动处理所有格式细节。

各语言的复数规则也有很大差异。英语区分“1 day”和“2 days”,阿拉伯语根据数量有六种不同的复数形式,日语则无论数量多少都用同一种形式。Intl.RelativeTimeFormat API 会为每种语言自动应用正确的复数规则。

Intl.RelativeTimeFormat API

Intl.RelativeTimeFormat 构造函数用于创建一个格式化器,将数值和时间单位转换为本地化字符串。您需要将语言环境标识符作为第一个参数传入,然后调用 format() 方法,并传入数值和单位。

const rtf = new Intl.RelativeTimeFormat('en-US');

console.log(rtf.format(-1, 'day'));
// "1 day ago"

console.log(rtf.format(2, 'hour'));
// "in 2 hours"

format() 方法接收两个参数。第一个参数是表示时间数量的数字,第二个参数是指定时间单位的字符串。

负数表示过去的时间,正数表示将来的时间。只要理解了正负号的约定,这个 API 的用法就非常直观。

格式化过去和将来的时间

数值的正负决定了时间是在过去还是将来。负值会生成过去时,正值会生成将来时。

const rtf = new Intl.RelativeTimeFormat('en-US');

console.log(rtf.format(-5, 'minute'));
// "5 minutes ago"

console.log(rtf.format(5, 'minute'));
// "in 5 minutes"

console.log(rtf.format(-2, 'week'));
// "2 weeks ago"

console.log(rtf.format(2, 'week'));
// "in 2 weeks"

这种模式在所有时间单位和所有语言中都能保持一致。API 会根据数值的正负自动选择正确的语法结构。

可用的时间单位

该 API 支持八种时间单位,覆盖了大多数相对时间格式化需求。您可以使用单数或复数形式,两者效果相同。

const rtf = new Intl.RelativeTimeFormat('en-US');

console.log(rtf.format(-30, 'second'));
// "30 seconds ago"

console.log(rtf.format(-15, 'minute'));
// "15 minutes ago"

console.log(rtf.format(-6, 'hour'));
// "6 hours ago"

console.log(rtf.format(-3, 'day'));
// "3 days ago"

console.log(rtf.format(-2, 'week'));
// "2 weeks ago"

console.log(rtf.format(-4, 'month'));
// "4 months ago"

console.log(rtf.format(-1, 'quarter'));
// "1 quarter ago"

console.log(rtf.format(-2, 'year'));
// "2 years ago"

API 同时接受单数形式(如 day)和复数形式(如 days),两者输出完全一致。quarter 单位对于处理财务周期的业务应用非常有用。

使用带有数字 auto 的自然语言

numeric 选项用于控制格式化器是使用数字还是自然语言表达。默认值为 always,始终显示数字。

const rtfAlways = new Intl.RelativeTimeFormat('en-US', {
  numeric: 'always'
});

console.log(rtfAlways.format(-1, 'day'));
// "1 day ago"

console.log(rtfAlways.format(0, 'day'));
// "in 0 days"

console.log(rtfAlways.format(1, 'day'));
// "in 1 day"

numeric 设置为 auto,可以让某些值的表达更加自然。

const rtfAuto = new Intl.RelativeTimeFormat('en-US', {
  numeric: 'auto'
});

console.log(rtfAuto.format(-1, 'day'));
// "yesterday"

console.log(rtfAuto.format(0, 'day'));
// "today"

console.log(rtfAuto.format(1, 'day'));
// "tomorrow"

此选项让界面更具对话感。用户会看到“昨天”而不是“1 天前”,这样读起来更自然。auto 选项适用于所有时间单位和所有语言,每种语言都会提供其本地化的表达方式。

选择格式化样式

style 选项用于控制输出的详细程度。可用的三种样式为 longshortnarrow

const rtfLong = new Intl.RelativeTimeFormat('en-US', {
  style: 'long'
});
console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

const rtfShort = new Intl.RelativeTimeFormat('en-US', {
  style: 'short'
});
console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

const rtfNarrow = new Intl.RelativeTimeFormat('en-US', {
  style: 'narrow'
});
console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

long 样式为默认选项,适用于大多数界面。short 样式适合移动端或表格等空间有限的场景。narrow 样式则适用于极度受限的空间,输出最为紧凑。

计算时间差

Intl.RelativeTimeFormat API 只负责格式化值,不负责计算。你需要自行计算时间差,然后将结果传递给格式化器。

要计算时间差,需用当前日期减去目标日期,然后将结果从毫秒转换为所需的时间单位。

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

function formatDaysAgo(date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));

  return rtf.format(diffInDays, 'day');
}

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

console.log(formatDaysAgo(yesterday));
// "yesterday"

const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

console.log(formatDaysAgo(tomorrow));
// "tomorrow"

此函数用于计算目标日期与当前时间之间的天数差。计算方法是将毫秒数除以一天的毫秒数,并四舍五入为最接近的整数。

表达式 date - now 在目标日期为过去时会得到负值,未来时为正值。这与 format() 方法所要求的符号约定一致。

构建完整的实用函数

对于通用的相对时间格式化器,需要根据时间差的大小选择最合适的时间单位。

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

const units = {
  year: 24 * 60 * 60 * 1000 * 365,
  month: 24 * 60 * 60 * 1000 * 365 / 12,
  week: 24 * 60 * 60 * 1000 * 7,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
};

function formatRelativeTime(date) {
  const now = new Date();
  const diffInMs = date - now;
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatRelativeTime(fiveMinutesAgo));
// "5 minutes ago"

const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(threeDaysAgo));
// "3 days ago"

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(tomorrow));
// "tomorrow"

该函数会从最大到最小依次遍历时间单位,选择第一个绝对差值超过该单位毫秒值的单位。回退到秒可以确保函数始终返回结果。

单位定义采用了近似值。月份按一年 1/12 计算,而不是考虑不同月份的天数。对于相对时间显示,这种近似比精确值更实用。

针对用户语言环境进行格式化

无需硬编码特定语言环境,可以直接使用浏览器中用户的首选语言。

const userLocale = navigator.language;
const rtf = new Intl.RelativeTimeFormat(userLocale, { numeric: 'auto' });

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(rtf.format(-1, 'day'));
// Output varies by user's locale
// For en-US: "yesterday"
// For es-ES: "ayer"
// For fr-FR: "hier"
// For de-DE: "gestern"

这种方式会根据每位用户的语言偏好显示相对时间,无需手动选择语言环境。浏览器会提供语言偏好,API 会应用相应的格式化规范。

在不同语言中看到相同的时间

相同的相对时间值在不同语言环境下会产生不同的输出。每种语言都有自己的词序、语法和复数规则。

const threeDaysAgo = -3;

const rtfEnglish = new Intl.RelativeTimeFormat('en-US');
console.log(rtfEnglish.format(threeDaysAgo, 'day'));
// "3 days ago"

const rtfSpanish = new Intl.RelativeTimeFormat('es-ES');
console.log(rtfSpanish.format(threeDaysAgo, 'day'));
// "hace 3 días"

const rtfFrench = new Intl.RelativeTimeFormat('fr-FR');
console.log(rtfFrench.format(threeDaysAgo, 'day'));
// "il y a 3 jours"

const rtfGerman = new Intl.RelativeTimeFormat('de-DE');
console.log(rtfGerman.format(threeDaysAgo, 'day'));
// "vor 3 Tagen"

const rtfJapanese = new Intl.RelativeTimeFormat('ja-JP');
console.log(rtfJapanese.format(threeDaysAgo, 'day'));
// "3 日前"

const rtfArabic = new Intl.RelativeTimeFormat('ar-SA');
console.log(rtfArabic.format(threeDaysAgo, 'day'));
// "قبل 3 أيام"

每种语言都会生成母语者在对话中常用的自然表达。API 处理了不同语法结构、不同书写系统和不同文本方向的所有复杂性。

复用格式化器以提升性能

创建一个 Intl.RelativeTimeFormat 实例时需要加载语言环境数据并处理选项。如果要格式化多个时间戳,建议只创建一次格式化器并复用。

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

const timestamps = [
  new Date(Date.now() - 5 * 60 * 1000),      // 5 minutes ago
  new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
  new Date(Date.now() - 24 * 60 * 60 * 1000) // 1 day ago
];

timestamps.forEach(date => {
  const diffInMs = date - new Date();
  const diffInMinutes = Math.round(diffInMs / (60 * 1000));
  console.log(rtf.format(diffInMinutes, 'minute'));
});

这种方法比为每个时间戳创建新的格式化器更高效。当需要在活动动态或评论区格式化数百甚至数千个时间戳时,性能差异会非常明显。

在界面中使用相对时间

你可以在任何向用户展示时间戳的地方使用相对时间格式化。这包括社交媒体动态、评论区、活动日志、通知系统,以及任何需要让用户了解事件发生时间的界面。

const rtf = new Intl.RelativeTimeFormat(navigator.language, {
  numeric: 'auto'
});

function updateTimestamp(element, date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInMinutes = Math.round(diffInMs / (60 * 1000));

  element.textContent = rtf.format(diffInMinutes, 'minute');
}

const commentDate = new Date('2025-10-15T14:30:00');
const timestampElement = document.getElementById('comment-timestamp');

updateTimestamp(timestampElement, commentDate);

格式化后的字符串与其他字符串值一样,可以插入到文本内容、属性或任何向用户展示信息的场景中。