Next.js(Pages Router)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 は時間の経過に伴う更新などの高度な機能を実装し、value、unit、および相対フォーマットオプションのプロップを使用して 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. フォーマットスタイルをカスタマイズする

このコンポーネントは numeric や style プロパティを含む RelativeTimeFormatOptions を受け入れます。これらのオプションを調整して出力形式を制御します。

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" を設定すると、適切な場合に「1 day ago」の代わりに「yesterday」のようなフレーズが生成され、style="narrow" を設定すると「2 months ago」の代わりに「2 mo. ago」のような短い形式が作成されます。