如何在 TanStack Start v1 中格式化相对时间
将时间戳格式化为“2 天前”这样的短语
问题
将时间戳显示为“2 天前”或“3 小时后”这样的相对时间短语,可以让用户更直观地理解时间信息。然而,这些短语在不同语言中遵循复杂的语法规则。英语中,“ago”用于数量之后表示过去,“in”用于数量之前表示将来,但其他语言可能有不同的词序、时间单位的词形变化,甚至完全不同的语法结构。仅靠字符串拼接手动构建这些短语,除了硬编码的那种语言外,在其他语言中都会产生错误的结果。
底层的格式化 API 需要一个数值和一个时间单位,但时间戳通常以 Date 对象或毫秒值的形式传入。要将时间戳转换为合适的单位和数值,需要计算与当前时刻的差值,并选择最自然的单位来表达这个差异。
解决方案
计算时间戳与当前时刻的时间差,选择最合适的单位,然后使用特定于本地语言的相对时间格式化方法进行格式化。这样可以将原始时间戳转换为符合语法规则、适应用户语言的短语。
使用 react-intl 的相对时间格式化功能,并配合一个辅助函数来确定最佳单位。该辅助函数会根据每个单位的阈值比较时间差,并返回数值和单位名称。格式化器随后会根据当前语言环境应用正确的语法规则。
步骤
1. 创建单位选择辅助函数
编写一个函数,计算时间差并选择最自然的单位来表达。
type RelativeTimeUnit =
| "second"
| "minute"
| "hour"
| "day"
| "week"
| "month"
| "year";
interface RelativeTimeValue {
value: number;
unit: RelativeTimeUnit;
}
export function selectRelativeTimeUnit(
timestamp: Date | number,
baseTime: Date | number = Date.now(),
): RelativeTimeValue {
const date = typeof timestamp === "number" ? timestamp : timestamp.getTime();
const base = typeof baseTime === "number" ? baseTime : baseTime.getTime();
const diffMs = date - base;
const absDiff = Math.abs(diffMs);
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
const week = 7 * day;
const month = 30 * day;
const year = 365 * day;
if (absDiff < minute) {
return { value: Math.round(diffMs / 1000), unit: "second" };
}
if (absDiff < hour) {
return { value: Math.round(diffMs / minute), unit: "minute" };
}
if (absDiff < day) {
return { value: Math.round(diffMs / hour), unit: "hour" };
}
if (absDiff < week) {
return { value: Math.round(diffMs / day), unit: "day" };
}
if (absDiff < month) {
return { value: Math.round(diffMs / week), unit: "week" };
}
if (absDiff < year) {
return { value: Math.round(diffMs / month), unit: "month" };
}
return { value: Math.round(diffMs / year), unit: "year" };
}
此函数通过将时间戳与固定阈值进行比较,将其转换为相对值和单位。对于过去的时间返回负值,对于未来的时间返回正值,formatter 会正确解析这些值。
2. 创建相对时间组件
构建一个组件,将单位选择辅助函数与 react-intl 的格式化功能结合使用。
import { FormattedRelativeTime } from "react-intl";
import { selectRelativeTimeUnit } from "./selectRelativeTimeUnit";
interface RelativeTimeProps {
date: Date | number;
numeric?: "always" | "auto";
style?: "long" | "short" | "narrow";
}
export function RelativeTime({
date,
numeric = "auto",
style = "long",
}: RelativeTimeProps) {
const { value, unit } = selectRelativeTimeUnit(date);
return (
<FormattedRelativeTime
value={value}
unit={unit}
numeric={numeric}
style={style}
/>
);
}
该组件接收时间戳和格式化选项,计算相对值和单位,然后交由 react-intl 组件进行本地化渲染。numeric="auto" 选项会在合适的情况下生成如“昨天”这样的短语,而不是“1 天前”。
3. 在路由中使用该组件
在需要显示相对时间戳的地方导入并渲染该组件。
import { createFileRoute } from "@tanstack/react-router";
import { RelativeTime } from "../components/RelativeTime";
export const Route = createFileRoute("/posts/$postId")({
component: PostPage,
});
function PostPage() {
const post = {
title: "Understanding Relative Time",
publishedAt: new Date("2024-11-15T10:30:00Z"),
updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
};
return (
<article>
<h1>{post.title}</h1>
<p>
Published <RelativeTime date={post.publishedAt} />
</p>
<p>
Updated <RelativeTime date={post.updatedAt} style="short" />
</p>
</article>
);
}
该组件可在服务端渲染和客户端渲染环境下工作。在服务端会生成初始的相对时间短语,在客户端则根据用户的 locale(由 IntlProvider 提供)显示相同的短语。
4. 添加命令式格式化选项
如果需要直接获取格式化字符串,可以创建一个基于 hook 的辅助函数。
import { useIntl } from "react-intl";
import { selectRelativeTimeUnit } from "./selectRelativeTimeUnit";
export function useRelativeTime() {
const intl = useIntl();
return (
date: Date | number,
options?: {
numeric?: "always" | "auto";
style?: "long" | "short" | "narrow";
},
) => {
const { value, unit } = selectRelativeTimeUnit(date);
return intl.formatRelativeTime(value, unit, options);
};
}
该 hook 返回一个函数,用于命令式地格式化时间戳,适用于设置文本属性或在 JSX 之外计算值。
5. 在非组件场景下使用该 hook
在需要为属性或逻辑获取格式化字符串的组件中调用该 hook。
import { createFileRoute } from "@tanstack/react-router";
import { useRelativeTime } from "../hooks/useRelativeTime";
export const Route = createFileRoute("/events/$eventId")({
component: EventPage,
});
function EventPage() {
const formatRelativeTime = useRelativeTime();
const event = {
name: "Product Launch",
startTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
};
const timeUntil = formatRelativeTime(event.startTime);
return (
<div>
<h1>{event.name}</h1>
<time dateTime={event.startTime.toISOString()} title={timeUntil}>
{timeUntil}
</time>
</div>
);
}
该 hook 以函数形式提供相同的格式化逻辑,使你可以在属性、计算值或无法渲染组件的场景下使用相对时间字符串。