如何在 React Router v7 中格式化相对时间

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

问题

将时间戳显示为“2 天前”或“3 小时后”这样的相对时间短语,可以让内容更具时效性和上下文关联性。然而,这类短语在不同语言中遵循的语法规则差异极大。例如,英语中“ago”用于数量之后表示过去,“in”用于数量之前表示将来,但其他语言可能会对时间单位进行词形变化、调整词序,甚至采用完全不同的语法结构。如果仅通过字符串拼接手动构建这些短语,除了硬编码的那种语言外,其他语言都会出现语法错误,严重影响国际用户体验。

解决方案

应使用支持本地化的相对时间格式化方法,将时间戳差值转换为符合语法规则的短语。先计算给定时间戳与当前时间的差值,再根据用户的本地化规则进行格式化。这样可以确保过去和将来的时间表达都能遵循各自语言的语法、词序和词形变化,无需手动拼接字符串。

步骤

1. 创建用于计算相对时间值的辅助函数

FormattedRelativeTime 组件需要一个数值型 value 和一个 unit 属性。请编写一个辅助函数,用于计算时间差并选择合适的时间单位。

export function getRelativeTimeValue(date: Date | number) {
  const now = Date.now();
  const timestamp = typeof date === "number" ? date : date.getTime();
  const diffInSeconds = Math.round((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: 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. 使用 FormattedRelativeTime 创建相对时间组件

使用来自 react-intl 的 FormattedRelativeTime 组件,该组件可渲染格式化的相对时间,并支持可选的定时更新。

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface RelativeTimeProps {
  date: Date | number;
  updateIntervalInSeconds?: number;
}

export function RelativeTime({
  date,
  updateIntervalInSeconds,
}: RelativeTimeProps) {
  const { value, unit } = getRelativeTimeValue(date);

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

numeric="auto" 选项允许格式化器在合适的情况下使用“昨天”这样的短语,而不是“1 天前”。可选的 updateIntervalInSeconds 属性用于控制组件重新渲染的频率,以保持相对时间的实时性。

3. 在 React Router 路由中使用该组件

在需要显示时间戳的任何路由组件中渲染相对时间组件。

import { RelativeTime } from "./RelativeTime";

interface Post {
  id: string;
  title: string;
  content: string;
  createdAt: number;
}

export function PostDetail({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={new Date(post.createdAt).toISOString()}>
        <RelativeTime date={post.createdAt} />
      </time>
      <p>{post.content}</p>
    </article>
  );
}

该组件会根据用户的语言环境自动格式化时间戳,生成如英文的“2 days ago”、法文的“il y a 2 jours”或西班牙文的“hace 2 días”等短语。

4. 使用 useIntl 以命令式方式格式化相对时间

如果你需要直接获取格式化后的字符串(例如用于设置元素属性),可以使用 formatRelativeTime 函数,该函数来自 useIntl hook。

import { useIntl } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface CommentProps {
  author: string;
  text: string;
  timestamp: number;
}

export function Comment({ author, text, timestamp }: CommentProps) {
  const intl = useIntl();
  const { value, unit } = getRelativeTimeValue(timestamp);
  const relativeTime = intl.formatRelativeTime(value, unit, {
    numeric: "auto",
  });

  return (
    <div aria-label={`Comment by ${author}, posted ${relativeTime}`}>
      <strong>{author}</strong>
      <p>{text}</p>
    </div>
  );
}

这种方式可以让你获得格式化后的字符串,可用于属性、与其他文本拼接,或传递给非 React API,同时保持完整的本地化支持。