「3日前」や「2時間後」のような相対時間をどのようにフォーマットしますか?

Intl.RelativeTimeFormatを使用して、「3日前」や「2時間後」のような時間を、自動的な複数形処理とローカライゼーションを備えたあらゆる言語で表示します

はじめに

ソーシャルメディアのフィード、コメントセクション、アクティビティログには「5分前」、「2時間前」、または「3日後」のような相対的なタイムスタンプが表示されます。これらの相対的なタイムスタンプは、ユーザーが絶対的な日付を解析する必要なく、何かが起きた時間を素早く理解するのに役立ちます。

英語でこれらの文字列をハードコードすると、すべてのユーザーが英語を話し、英語の文法規則に従うと仮定することになります。異なる言語では相対的な時間を表現する方法が異なります。スペイン語では「3 days ago」の代わりに「hace 3 días」と言います。日本語では「3日前」と完全に異なる構造を使用します。また、各言語には単数形と複数形をいつ使用するかを決定する独自の複数形ルールがあります。

JavaScriptはIntl.RelativeTimeFormat APIを提供し、相対的な時間の書式設定を自動的に処理します。このレッスンでは、この組み込みAPIを使用して任意の言語の相対的な時間を正しく書式設定する方法を説明します。

相対的な時間の書式設定が国際化を必要とする理由

異なる言語では相対的な時間をさまざまな方法で表現します。英語では過去の時間には時間単位を「ago」の前に置き、未来の時間には「in」の後に置きます。他の言語では、異なる語順、異なる前置詞、または完全に異なる文法構造を使用します。

const rtfEnglish = new Intl.RelativeTimeFormat('en');
console.log(rtfEnglish.format(-3, 'day'));
// "3 days ago"

const rtfSpanish = new Intl.RelativeTimeFormat('es');
console.log(rtfSpanish.format(-3, 'day'));
// "hace 3 días"

const rtfJapanese = new Intl.RelativeTimeFormat('ja');
console.log(rtfJapanese.format(-3, 'day'));
// "3 日前"

各言語は、それぞれの慣習に従った自然な響きの出力を生成します。これらの慣習を知る必要も、翻訳ファイルを維持する必要もありません。APIがすべての書式設定の詳細を自動的に処理します。

複数形のルールも言語によって大きく異なります。英語では「1 day」と「2 days」を区別します。アラビア語では数に応じて6つの異なる複数形があります。日本語は数量に関係なく同じ形式を使用します。Intl.RelativeTimeFormat APIは各言語に対して正しい複数形ルールを適用します。

Intl.RelativeTimeFormat API

Intl.RelativeTimeFormatコンストラクターは、数値と時間単位をローカライズされた文字列に変換するフォーマッターを作成します。最初の引数としてロケール識別子を渡し、その後format()メソッドを値と単位で呼び出します。

const rtf = new Intl.RelativeTimeFormat('en-US');

console.log(rtf.format(-1, 'day'));
// "1 day ago"

console.log(rtf.format(2, 'hour'));
// "in 2 hours"

format()メソッドは2つのパラメータを取ります。1つ目は時間量を表す数値です。2つ目は時間単位を指定する文字列です。

負の数値は過去の時間を示し、正の数値は未来の時間を示します。この規則を理解すれば、このAPIの使用は直感的になります。

過去と未来の時間のフォーマット

値の符号によって、時間が過去か未来かが決まります。負の値は過去形を生成し、正の値は未来形を生成します。

const rtf = new Intl.RelativeTimeFormat('en-US');

console.log(rtf.format(-5, 'minute'));
// "5 minutes ago"

console.log(rtf.format(5, 'minute'));
// "in 5 minutes"

console.log(rtf.format(-2, 'week'));
// "2 weeks ago"

console.log(rtf.format(2, 'week'));
// "in 2 weeks"

このパターンはすべての時間単位とすべての言語で一貫して機能します。APIは値が正か負かに基づいて、正しい文法構造を自動的に選択します。

利用可能な時間単位

このAPIは、ほとんどの相対時間フォーマットのニーズをカバーする8つの時間単位をサポートしています。単数形または複数形のどちらも使用でき、両方とも同じように機能します。

const rtf = new Intl.RelativeTimeFormat('en-US');

console.log(rtf.format(-30, 'second'));
// "30 seconds ago"

console.log(rtf.format(-15, 'minute'));
// "15 minutes ago"

console.log(rtf.format(-6, 'hour'));
// "6 hours ago"

console.log(rtf.format(-3, 'day'));
// "3 days ago"

console.log(rtf.format(-2, 'week'));
// "2 weeks ago"

console.log(rtf.format(-4, 'month'));
// "4 months ago"

console.log(rtf.format(-1, 'quarter'));
// "1 quarter ago"

console.log(rtf.format(-2, 'year'));
// "2 years ago"

APIはdayのような単数形とdaysのような複数形の両方を受け付けます。どちらも同じ出力を生成します。quarter単位は会計期間を扱うビジネスアプリケーションに役立ちます。

数値オプションによる自然言語の使用

numericオプションは、フォーマッタが数字を使用するか自然言語の代替表現を使用するかを制御します。デフォルト値はalwaysで、常に数字を表示します。

const rtfAlways = new Intl.RelativeTimeFormat('en-US', {
  numeric: 'always'
});

console.log(rtfAlways.format(-1, 'day'));
// "1 day ago"

console.log(rtfAlways.format(0, 'day'));
// "in 0 days"

console.log(rtfAlways.format(1, 'day'));
// "in 1 day"

numericautoに設定すると、特定の値に対してより自然な表現が生成されます。

const rtfAuto = new Intl.RelativeTimeFormat('en-US', {
  numeric: 'auto'
});

console.log(rtfAuto.format(-1, 'day'));
// "yesterday"

console.log(rtfAuto.format(0, 'day'));
// "today"

console.log(rtfAuto.format(1, 'day'));
// "tomorrow"

このオプションによりインターフェースがより会話的になります。ユーザーは「1日前」ではなく「昨日」を見ることができ、より自然に読めます。autoオプションはすべての時間単位とすべての言語で機能し、各言語が独自の慣用的な代替表現を提供します。

フォーマットスタイルの選択

styleオプションは出力の詳細度を制御します。利用可能な3つのスタイルはlongshortnarrowです。

const rtfLong = new Intl.RelativeTimeFormat('en-US', {
  style: 'long'
});
console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

const rtfShort = new Intl.RelativeTimeFormat('en-US', {
  style: 'short'
});
console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

const rtfNarrow = new Intl.RelativeTimeFormat('en-US', {
  style: 'narrow'
});
console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

longスタイルはデフォルトであり、ほとんどのインターフェースに適しています。shortスタイルはモバイルレイアウトやテーブルでスペースを節約します。narrowスタイルは、非常にスペースが制限されたデザインに対して最もコンパクトな出力を生成します。

時間差の計算

Intl.RelativeTimeFormat APIは値をフォーマットしますが、計算は行いません。時間差を自分で計算し、その結果をフォーマッタに渡す必要があります。

時間差を計算するには、現在の日付から対象の日付を引き、結果をミリ秒から目的の単位に変換します。

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

function formatDaysAgo(date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));

  return rtf.format(diffInDays, 'day');
}

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

console.log(formatDaysAgo(yesterday));
// "yesterday"

const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

console.log(formatDaysAgo(tomorrow));
// "tomorrow"

この関数は対象日と現在の間の日数の差を計算します。計算では、ミリ秒を1日のミリ秒数で割り、最も近い整数に丸めます。

date - nowの減算は、過去の日付に対しては負の値を、未来の日付に対しては正の値を生成します。これはformat()メソッドが期待する符号の規則と一致します。

完全なユーティリティ関数の構築

汎用的な相対時間フォーマッターを作成するには、時間差の大きさに基づいて最も適切な時間単位を選択する必要があります。

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

const units = {
  year: 24 * 60 * 60 * 1000 * 365,
  month: 24 * 60 * 60 * 1000 * 365 / 12,
  week: 24 * 60 * 60 * 1000 * 7,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
};

function formatRelativeTime(date) {
  const now = new Date();
  const diffInMs = date - now;
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatRelativeTime(fiveMinutesAgo));
// "5 minutes ago"

const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(threeDaysAgo));
// "3 days ago"

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(tomorrow));
// "tomorrow"

この関数は、最大の単位から最小の単位まで時間単位を反復処理し、絶対差がその単位のミリ秒値を超える最初の単位を選択します。秒へのフォールバックにより、関数が常に結果を返すことが保証されます。

単位の定義は概算値を使用しています。月は異なる月の長さを考慮するのではなく、1年の1/12として計算されます。この近似は、正確な精度よりも概算値の方が有用である相対時間表示に適しています。

ユーザーのロケールに合わせたフォーマット

特定のロケールをハードコーディングする代わりに、ブラウザからユーザーの優先言語を使用することができます。

const userLocale = navigator.language;
const rtf = new Intl.RelativeTimeFormat(userLocale, { numeric: 'auto' });

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(rtf.format(-1, 'day'));
// 出力はユーザーのロケールによって異なります
// en-USの場合: "yesterday"
// es-ESの場合: "ayer"
// fr-FRの場合: "hier"
// de-DEの場合: "gestern"

このアプローチでは、手動でロケールを選択する必要なく、各ユーザーの言語設定に従って相対時間を表示します。ブラウザが言語設定を提供し、APIが適切なフォーマット規則を適用します。

同じ時間を異なる言語で表示する

同じ相対時間の値でも、異なるロケールでは異なる出力が生成されます。各言語は語順、文法、複数形などに関して独自の規則に従います。

const threeDaysAgo = -3;

const rtfEnglish = new Intl.RelativeTimeFormat('en-US');
console.log(rtfEnglish.format(threeDaysAgo, 'day'));
// "3 days ago"

const rtfSpanish = new Intl.RelativeTimeFormat('es-ES');
console.log(rtfSpanish.format(threeDaysAgo, 'day'));
// "hace 3 días"

const rtfFrench = new Intl.RelativeTimeFormat('fr-FR');
console.log(rtfFrench.format(threeDaysAgo, 'day'));
// "il y a 3 jours"

const rtfGerman = new Intl.RelativeTimeFormat('de-DE');
console.log(rtfGerman.format(threeDaysAgo, 'day'));
// "vor 3 Tagen"

const rtfJapanese = new Intl.RelativeTimeFormat('ja-JP');
console.log(rtfJapanese.format(threeDaysAgo, 'day'));
// "3 日前"

const rtfArabic = new Intl.RelativeTimeFormat('ar-SA');
console.log(rtfArabic.format(threeDaysAgo, 'day'));
// "قبل 3 أيام"

各言語はネイティブスピーカーが会話で使用するような自然な表現を生成します。APIは異なる文法構造、異なる書記体系、異なるテキスト方向などの複雑さをすべて処理します。

パフォーマンス向上のためのフォーマッターの再利用

Intl.RelativeTimeFormatインスタンスの作成には、ロケールデータの読み込みとオプションの処理が含まれます。複数のタイムスタンプをフォーマットする場合は、フォーマッターを一度作成して再利用することをお勧めします。

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

const timestamps = [
  new Date(Date.now() - 5 * 60 * 1000),      // 5分前
  new Date(Date.now() - 2 * 60 * 60 * 1000), // 2時間前
  new Date(Date.now() - 24 * 60 * 60 * 1000) // 1日前
];

timestamps.forEach(date => {
  const diffInMs = date - new Date();
  const diffInMinutes = Math.round(diffInMs / (60 * 1000));
  console.log(rtf.format(diffInMinutes, 'minute'));
});

このアプローチは、タイムスタンプごとに新しいフォーマッターを作成するよりも効率的です。アクティビティフィードやコメントスレッドで数百または数千のタイムスタンプをフォーマットする場合、パフォーマンスの差は顕著になります。

インターフェースでの相対時間の使用

ユーザーにタイムスタンプを表示するあらゆる場所で相対時間フォーマットを使用できます。これにはソーシャルメディアフィード、コメントセクション、アクティビティログ、通知システム、そして何かが起きてからの経過時間を表示することでユーザーがコンテキストを理解するのに役立つあらゆるインターフェースが含まれます。

const rtf = new Intl.RelativeTimeFormat(navigator.language, {
  numeric: 'auto'
});

function updateTimestamp(element, date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInMinutes = Math.round(diffInMs / (60 * 1000));

  element.textContent = rtf.format(diffInMinutes, 'minute');
}

const commentDate = new Date('2025-10-15T14:30:00');
const timestampElement = document.getElementById('comment-timestamp');

updateTimestamp(timestampElement, commentDate);

フォーマットされた文字列は他の文字列値と同様に機能します。テキストコンテンツ、属性、またはユーザーに情報を表示するその他のコンテキストに挿入することができます。