如何格式化相对时间(如 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 选项用于控制输出的详细程度。可用的三种样式为 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 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);
格式化后的字符串与其他字符串值一样,可以插入到文本内容、属性或任何向用户展示信息的场景中。