Intl.RelativeTimeFormat API

使用完整的国际化支持在 JavaScript 中格式化相对时间字符串

介绍

在 Web 应用程序中,显示类似于“3 小时前”或“2 天后”的相对时间戳是一项常见需求。社交媒体动态、评论区、通知系统和活动日志都需要随着内容时间推移而更新的人性化时间显示。

从头构建此功能会面临许多挑战。您需要计算时间差、选择合适的时间单位、处理跨语言的复数规则,并维护每个支持语言的翻译。这种复杂性解释了为什么开发者传统上会选择像 Moment.js 这样的库,尽管它会为看似简单的格式化增加显著的包大小。

Intl.RelativeTimeFormat API 提供了一种原生解决方案。它可以格式化相对时间字符串,具有完整的国际化支持,自动处理复数规则和文化习惯。该 API 在所有主流浏览器中均可使用,覆盖率达 95%,无需外部依赖,同时能够以数十种语言生成自然的输出。

基本用法

Intl.RelativeTimeFormat 构造函数创建一个格式化器实例,将数值和时间单位转换为本地化的字符串。

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

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

console.log(rtf.format(2, 'hour'));
// "2 小时后"

console.log(rtf.format(-3, 'month'));
// "3 个月前"

format() 方法接受两个参数:

  • value:表示时间量的数字
  • unit:指定时间单位的字符串

负值表示过去的时间,正值表示未来的时间。该 API 会自动处理复数规则,例如根据值生成“1 天前”或“2 天前”。

支持的时间单位

该 API 支持八种时间单位,每种单位都接受单数和复数形式:

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

// 这些输出相同的结果
console.log(rtf.format(-5, 'second'));
// "5 秒前"

console.log(rtf.format(-5, 'seconds'));
// "5 秒前"

从最小到最大的可用单位:

  • secondseconds
  • minuteminutes
  • hourhours
  • daydays
  • weekweeks
  • monthmonths
  • quarterquarters
  • yearyears

季度单位在跟踪财务周期的商业应用中非常有用,而其他单位则涵盖了典型的相对时间格式化需求。

自然语言输出

numeric 选项控制格式化器是使用数值还是自然语言替代形式。

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

console.log(rtfNumeric.format(-1, 'day'));
// "1 天前"

console.log(rtfNumeric.format(0, 'day'));
// "0 天后"

console.log(rtfNumeric.format(1, 'day'));
// "1 天后"

numeric 设置为 auto 可以为常见值生成更符合习惯的表达方式:

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

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

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

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

这种自然语言输出可以创建更具对话性的界面。auto 选项适用于所有时间单位,但效果在天数上最为明显。其他语言有其特定的习惯表达方式,API 会自动处理。

格式化样式

style 选项可根据不同的界面上下文调整输出的详细程度:

const rtfLong = new Intl.RelativeTimeFormat('en', {
  style: 'long'
});

console.log(rtfLong.format(-2, 'hour'));
// "2 小时前"

const rtfShort = new Intl.RelativeTimeFormat('en', {
  style: 'short'
});

console.log(rtfShort.format(-2, 'hour'));
// "2 小时前"

const rtfNarrow = new Intl.RelativeTimeFormat('en', {
  style: 'narrow'
});

console.log(rtfNarrow.format(-2, 'hour'));
// "2 小时前"

使用 long 样式(默认)适用于标准界面,注重可读性。使用 short 样式适用于空间受限的布局,例如移动界面或数据表格。使用 narrow 样式适用于极度紧凑的显示场景,每个字符都至关重要。

计算时间差

Intl.RelativeTimeFormat API 负责格式化值,但不负责计算值。您需要自行计算时间差并选择合适的单位。这种关注点分离让您可以控制计算逻辑,同时将格式化的复杂性交给 API 处理。

基本时间差计算

对于特定的时间单位,计算两个日期之间的差值:

const rtf = new Intl.RelativeTimeFormat('en', { 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 nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);

console.log(formatDaysAgo(nextWeek));
// "in 7 days"

这种方法适用于您明确知道哪个单位适合您的使用场景的情况。例如,评论时间戳可能总是使用小时或天,而事件安排可能更关注天或周。

自动单位选择

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

const rtf = new Intl.RelativeTimeFormat('en', { 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"

此实现从最大的单位到最小的单位进行迭代,选择时间差超过该单位毫秒值的第一个单位。回退到秒确保函数始终返回结果。

单位阈值使用近似值。月份按一年的 1/12 计算,而不是考虑月份长度的变化。这种近似方法非常适合相对时间显示,在这种情况下,可读性比精确性更重要。

国际化支持

格式化器会遵循与语言环境相关的相对时间显示约定。不同语言有不同的复数规则、词序和惯用表达。

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

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

const rtfSpanish = new Intl.RelativeTimeFormat('es', {
  numeric: 'auto'
});

console.log(rtfSpanish.format(-1, 'day'));
// "ayer"

const rtfJapanese = new Intl.RelativeTimeFormat('ja', {
  numeric: 'auto'
});

console.log(rtfJapanese.format(-1, 'day'));
// "昨日"

复数规则在不同语言中差异很大。例如,英语区分单数和复数(1 day vs 2 days)。阿拉伯语根据数量有六种复数形式。日语无论数量如何都使用相同的形式。API 会自动处理这些复杂性。

const rtfArabic = new Intl.RelativeTimeFormat('ar');

console.log(rtfArabic.format(-1, 'day'));
// "قبل يوم واحد"

console.log(rtfArabic.format(-2, 'day'));
// "قبل يومين"

console.log(rtfArabic.format(-3, 'day'));
// "قبل 3 أيام"

console.log(rtfArabic.format(-11, 'day'));
// "قبل 11 يومًا"

格式化器还会处理从右到左语言的文本方向,并应用符合文化习惯的格式化约定。这种自动本地化功能消除了维护翻译文件或实现自定义复数逻辑的需求。

使用 formatToParts 进行高级格式化

formatToParts() 方法将格式化后的字符串作为对象数组返回,允许对各个组件进行自定义样式或操作。

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

const parts = rtf.formatToParts(-5, 'second');

console.log(parts);
// [
//   { type: 'integer', value: '5', unit: 'second' },
//   { type: 'literal', value: ' seconds ago' }
// ]

每个部分对象包含:

  • typeinteger 表示数值,literal 表示文本
  • value:该部分的字符串内容
  • unit:时间单位(仅在数值部分存在)

这种结构支持自定义渲染,例如可以将数字与文本样式区分开,或提取特定组件进行显示:

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

function formatWithStyledNumber(value, unit) {
  const parts = rtf.formatToParts(value, unit);

  return parts.map(part => {
    if (part.type === 'integer') {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  }).join('');
}

console.log(formatWithStyledNumber(-5, 'hour'));
// "<strong>5</strong> hours ago"

当使用 numeric: 'auto' 且值有自然语言替代时,formatToParts() 会返回一个单一的文本部分:

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

const parts = rtf.formatToParts(-1, 'day');

console.log(parts);
// [
//   { type: 'literal', value: 'yesterday' }
// ]

这种行为允许您检测自然语言与数字格式化的使用情况,从而根据输出类型应用不同的样式或行为。

性能优化

创建 Intl.RelativeTimeFormat 实例需要加载区域设置数据并初始化格式化规则。这一操作的开销较大,应避免不必要的重复操作。

缓存格式化器实例

创建格式化器一次并重复使用:

const formatterCache = new Map();

function getFormatter(locale, options = {}) {
  const cacheKey = `${locale}-${JSON.stringify(options)}`;

  if (!formatterCache.has(cacheKey)) {
    formatterCache.set(
      cacheKey,
      new Intl.RelativeTimeFormat(locale, options)
    );
  }

  return formatterCache.get(cacheKey);
}

// 重用缓存的格式化器
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "昨天"

当需要格式化大量时间戳(例如渲染活动动态或评论线程)时,这种缓存策略尤为重要。

最小化计算开销

存储时间戳,而不是反复计算相对时间:

// 存储创建日期
const comment = {
  text: "Great article!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// 仅在渲染时计算相对时间
function renderComment(comment, locale) {
  const rtf = getFormatter(locale, { numeric: 'auto' });
  const units = {
    day: 24 * 60 * 60 * 1000,
    hour: 60 * 60 * 1000,
    minute: 60 * 1000,
    second: 1000
  };

  const diffInMs = comment.createdAt - new Date();
  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);
    }
  }
}

这种方法将数据存储与展示分离,允许在用户的区域设置更改或渲染更新时重新计算相对时间,而无需修改底层数据。

实际实现

将计算逻辑与格式化结合起来,生成适用于生产环境的可重用工具函数:

class RelativeTimeFormatter {
  constructor(locale = 'en', options = { numeric: 'auto' }) {
    this.formatter = new Intl.RelativeTimeFormat(locale, options);

    this.units = [
      { name: 'year', ms: 24 * 60 * 60 * 1000 * 365 },
      { name: 'month', ms: 24 * 60 * 60 * 1000 * 365 / 12 },
      { name: 'week', ms: 24 * 60 * 60 * 1000 * 7 },
      { name: 'day', ms: 24 * 60 * 60 * 1000 },
      { name: 'hour', ms: 60 * 60 * 1000 },
      { name: 'minute', ms: 60 * 1000 },
      { name: 'second', ms: 1000 }
    ];
  }

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

    for (const unit of this.units) {
      if (absDiff >= unit.ms || unit.name === 'second') {
        const value = Math.round(diffInMs / unit.ms);
        return this.formatter.format(value, unit.name);
      }
    }
  }
}

// 使用示例
const formatter = new RelativeTimeFormatter('en');

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatter.format(fiveMinutesAgo));
// "5 分钟前"

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatter.format(tomorrow));
// "明天"

此类封装了格式化器和单位选择逻辑,提供了一个干净的接口,接受 Date 对象并返回格式化的字符串。

与框架的集成

在 React 应用中,可以创建一次格式化器,并通过上下文或 props 传递:

import { createContext, useContext } from 'react';

const RelativeTimeContext = createContext(null);

export function RelativeTimeProvider({ locale, children }) {
  const formatter = new RelativeTimeFormatter(locale);

  return (
    <RelativeTimeContext.Provider value={formatter}>
      {children}
    </RelativeTimeContext.Provider>
  );
}

export function useRelativeTime() {
  const formatter = useContext(RelativeTimeContext);
  if (!formatter) {
    throw new Error('useRelativeTime 必须在 RelativeTimeProvider 内使用');
  }
  return formatter;
}

// 组件使用
function CommentTimestamp({ date }) {
  const formatter = useRelativeTime();
  return <time>{formatter.format(date)}</time>;
}

这种模式确保了每个语言环境只创建一次格式化器,并在所有需要相对时间格式化的组件之间共享。

浏览器支持

Intl.RelativeTimeFormat 在所有现代浏览器中均可使用,覆盖率达 95%:

  • Chrome 71+
  • Firefox 65+
  • Safari 14+
  • Edge 79+

Internet Explorer 不支持此 API。如果需要支持 IE,可以使用 polyfill,但原生实现提供了更好的性能和更小的包体积。

何时使用此 API

Intl.RelativeTimeFormat 最适合用于:

  • 在信息流和时间线中显示内容的时间
  • 显示评论或帖子时间戳
  • 格式化事件安排相对于当前时间的时间
  • 构建带有相对时间戳的通知系统
  • 创建具有可读时间的活动日志

此 API 不适用于:

  • 绝对日期和时间格式化(请使用 Intl.DateTimeFormat
  • 需要精确到毫秒的时间跟踪
  • 每秒更新的倒计时计时器
  • 日期算术或日历计算

对于需要同时显示相对时间和绝对时间的应用,可以结合使用 Intl.RelativeTimeFormatIntl.DateTimeFormat。对于最近的内容显示相对时间,对于较早的内容切换为绝对日期:

function formatTimestamp(date, locale = 'en') {
  const now = new Date();
  const diffInMs = Math.abs(date - now);
  const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;

  if (diffInMs < sevenDaysInMs) {
    const rtf = new RelativeTimeFormatter(locale);
    return rtf.format(date);
  } else {
    const dtf = new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    });
    return dtf.format(date);
  }
}

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(formatTimestamp(yesterday));
// "昨天"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "2025年9月14日"

这种混合方法为最近的内容提供了自然语言的相对时间优势,同时为较早的时间戳保持了清晰的绝对日期显示。