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 フックから利用してください。
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に渡す場合でも、ロケールサポートを維持したまま利用できます。