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 day ago"

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

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

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

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

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

支持的时间单位

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

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

// These produce identical output
console.log(rtf.format(-5, 'second'));
// "5 seconds ago"

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

可用的单位从最小到最大依次为:

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

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

自然语言输出

numeric 选项用于控制格式化器是使用数值还是自然语言表达。

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

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

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

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

numeric 设置为 auto 时,对于常见值会生成更符合习惯的表达:

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

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

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

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

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

格式化样式

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

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

console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

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

console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

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

console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

标准界面中建议使用 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 与 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' }
// ]

每个 part 对象包含:

  • type:对于数值类型为 integer,对于文本类型为 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);
}

// Reuse cached formatters
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "yesterday"

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

最小化计算开销

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

// Store the creation date
const comment = {
  text: "Great article!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// Calculate relative time only when rendering
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);
      }
    }
  }
}

// Usage
const formatter = new RelativeTimeFormatter('en');

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

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

该类同时封装了格式化器和单位选择逻辑,提供了一个简洁的接口,接收 Date 对象并返回格式化字符串。

与各类框架集成

在 React 应用中,应只创建一次 formatter,并通过 context 或 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 must be used within RelativeTimeProvider');
  }
  return formatter;
}

// Component usage
function CommentTimestamp({ date }) {
  const formatter = useRelativeTime();
  return <time>{formatter.format(date)}</time>;
}

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

浏览器支持

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));
// "yesterday"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "Sep 14, 2025"

这种混合方式可为最新内容提供自然语言的相对时间优势,同时保证较早时间戳的清晰度。