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つのパラメータを受け取ります。最初のパラメータは時間の量を表す数値です。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のような複数形の両方を受け入れます。どちらも同じ出力を生成します。四半期単位は、会計期間を扱うビジネスアプリケーションに便利です。

numeric autoを使用した自然言語の使用

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年の12分の1として計算されます。この近似は、正確な精度よりも近似値の方が有用な相対時間表示に適しています。

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

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

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'));
// Output varies by user's locale
// For en-US: "yesterday"
// For es-ES: "ayer"
// For fr-FR: "hier"
// For 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 minutes ago
  new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
  new Date(Date.now() - 24 * 60 * 60 * 1000) // 1 day ago
];

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);

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