如何格式化相对时间,例如 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 天前"
console.log(rtf.format(2, 'hour'));
// "2 小时后"
format() 方法接受两个参数。第一个是表示时间量的数字,第二个是指定时间单位的字符串。
负数表示过去的时间,而正数表示未来的时间。这种约定使得一旦理解了符号的含义,API 的使用变得直观。
格式化过去和未来的时间
值的符号决定了时间是在过去还是未来。负值生成过去时态,而正值生成未来时态。
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-5, 'minute'));
// "5 分钟前"
console.log(rtf.format(5, 'minute'));
// "5 分钟后"
console.log(rtf.format(-2, 'week'));
// "2 周前"
console.log(rtf.format(2, 'week'));
// "2 周后"
这种模式在所有时间单位和所有语言中都能一致工作。API 会根据值是正数还是负数自动选择正确的语法结构。
可用的时间单位
该 API 支持八种时间单位,涵盖了大多数相对时间格式化需求。您可以使用单数或复数形式,两者的效果完全相同。
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-30, 'second'));
// "30 秒前"
console.log(rtf.format(-15, 'minute'));
// "15 分钟前"
console.log(rtf.format(-6, 'hour'));
// "6 小时前"
console.log(rtf.format(-3, 'day'));
// "3 天前"
console.log(rtf.format(-2, 'week'));
// "2 周前"
console.log(rtf.format(-4, 'month'));
// "4 个月前"
console.log(rtf.format(-1, 'quarter'));
// "1 个季度前"
console.log(rtf.format(-2, 'year'));
// "2 年前"
该 API 接受单数形式(如 day)和复数形式(如 days)。两者的输出完全相同。季度单位对于处理财务周期的商业应用非常有用。
使用自然语言与数字 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"
此选项使界面更具对话感。用户会看到 "yesterday" 而不是 "1 day ago",这更自然。auto 选项适用于所有时间单位和所有语言,每种语言都提供其惯用的替代表达。
选择格式化样式
style 选项控制输出的详细程度。可用的三种样式是 long、short 和 narrow。
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 分钟前"
const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(threeDaysAgo));
// "3 天前"
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(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'));
// 输出因用户的区域设置而异
// 对于 en-US: "yesterday"
// 对于 es-ES: "ayer"
// 对于 fr-FR: "hier"
// 对于 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 分钟前
new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 小时前
new Date(Date.now() - 24 * 60 * 60 * 1000) // 1 天前
];
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);
格式化后的字符串与其他字符串值一样工作。您可以将它们插入到文本内容、属性或任何其他向用户显示信息的上下文中。