如何在 Next.js(页面路由)v16 中格式化相对时间

将时间戳格式化为“2 天前”这样的短语

问题

将时间戳显示为相对时间短语,例如“2 天前”或“3 小时后”,可以让内容显得更即时且具有上下文关联。然而,这些短语遵循复杂的语法模式,不同语言之间差异显著。英语在过去的时间量后使用“ago”,在未来的时间量前使用“in”,但其他语言可能采用不同的词序、根据数量变化对时间单位进行词形变化,或者使用完全不同的语法结构。通过字符串拼接手动构建这些短语会在除编写语言外的所有语言中产生错误的输出,从而破坏国际用户的体验。

解决方案

使用 react-intl 的相对时间格式化功能,将数值时间差转换为符合用户语言环境的语法正确短语。计算时间戳与当前时间之间的时间差,确定适当的单位(秒、分钟、小时、天),并将这两个值传递给 react-intl 的格式化 API。底层的 Intl.RelativeTimeFormat API 会自动处理特定语言环境的语法、词序和词形变化规则。

步骤

1. 创建一个辅助函数来计算相对时间值

时间差必须转换为数值和适当的单位。构建一个函数,将时间戳与当前时间进行比较,并选择最佳单位。

export function getRelativeTimeValue(date: Date | number) {
  const now = Date.now();
  const timestamp = typeof date === "number" ? date : date.getTime();
  const diffInSeconds = (timestamp - now) / 1000;

  const minute = 60;
  const hour = minute * 60;
  const day = hour * 24;
  const week = day * 7;
  const month = day * 30;
  const year = day * 365;

  const absDiff = Math.abs(diffInSeconds);

  if (absDiff < minute) {
    return { value: Math.round(diffInSeconds), unit: "second" as const };
  } else if (absDiff < hour) {
    return {
      value: Math.round(diffInSeconds / minute),
      unit: "minute" as const,
    };
  } else if (absDiff < day) {
    return { value: Math.round(diffInSeconds / hour), unit: "hour" as const };
  } else if (absDiff < week) {
    return { value: Math.round(diffInSeconds / day), unit: "day" as const };
  } else if (absDiff < month) {
    return { value: Math.round(diffInSeconds / week), unit: "week" as const };
  } else if (absDiff < year) {
    return { value: Math.round(diffInSeconds / month), unit: "month" as const };
  } else {
    return { value: Math.round(diffInSeconds / year), unit: "year" as const };
  }
}

此函数返回一个带符号的数值(负数表示过去,正数表示未来)以及基于差值大小的最适合单位。

2. 使用 useIntl 钩子格式化相对时间

formatRelativeTime 函数返回格式化的相对时间字符串,需要一个数值、一个单位以及符合 Intl.RelativeTimeFormatOptions 的选项。使用第 1 步中的辅助函数获取数值和单位,然后进行格式化。

import { useIntl } from "react-intl";
import { getRelativeTimeValue } from "@/lib/relative-time";

export default function CommentTimestamp({ date }: { date: Date }) {
  const intl = useIntl();
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <time dateTime={date.toISOString()}>
      {intl.formatRelativeTime(value, unit)}
    </time>
  );
}

formatRelativeTime 方法会生成符合本地化的短语,例如英文中的 "2 days ago" 或法语中的 "il y a 2 jours"。

3. 使用 FormattedRelativeTime 进行声明式格式化

FormattedRelativeTime 实现了诸如随时间更新等高级功能,并使用 formatRelativeTime API,通过属性传递数值、单位和相对格式化选项。对于需要自动更新的组件,使用组件形式。

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "@/lib/relative-time";

export default function LiveTimestamp({ date }: { date: Date }) {
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <time dateTime={date.toISOString()}>
      <FormattedRelativeTime
        value={value}
        unit={unit}
        numeric="auto"
        updateIntervalInSeconds={10}
      />
    </time>
  );
}

updateIntervalInSeconds 属性控制组件重新渲染的频率,确保显示的短语随着时间推移保持最新。

4. 自定义格式化样式

该组件接受 RelativeTimeFormatOptions,包括 numeric 和 style 属性。调整这些选项以控制输出格式。

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "@/lib/relative-time";

export default function CompactTimestamp({ date }: { date: Date }) {
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <FormattedRelativeTime
      value={value}
      unit={unit}
      numeric="auto"
      style="narrow"
    />
  );
}

设置 numeric="auto" 会在适当情况下生成类似 "yesterday" 的短语,而不是 "1 day ago",而 style="narrow" 会生成更短的形式,例如 "2 mo. ago" 而不是 "2 months ago"。