Intl.RelativeTimeFormat API

JavaScriptで完全な国際化対応を備えた相対時間文字列をフォーマットする

はじめに

「3時間前」や「2日後」のような相対的なタイムスタンプを表示することは、Webアプリケーションでよくある要件です。ソーシャルメディアのフィード、コメントセクション、通知システム、アクティビティログなどでは、コンテンツの経過時間に応じて更新される人間が読みやすい時間表示が必要です。

この機能をゼロから構築するには課題があります。時間差の計算、適切な単位の選択、言語間での複数形ルールの処理、サポートされるすべてのロケールの翻訳の維持が必要です。この複雑さが、開発者が従来、単純なフォーマットに見えるものに対して、かなりのバンドルサイズを追加するMoment.jsのようなライブラリを使用していた理由です。

Intl.RelativeTimeFormat APIはネイティブなソリューションを提供します。このAPIは完全な国際化サポートを備えた相対時間文字列をフォーマットし、複数形ルールや文化的慣習を自動的に処理します。このAPIはすべての主要ブラウザで動作し、グローバルカバレッジは95%に達しており、外部依存関係を排除しながら、数十の言語で自然な出力を生成します。

基本的な使用法

Intl.RelativeTimeFormatコンストラクタは、数値と時間単位をローカライズされた文字列に変換するフォーマッタインスタンスを作成します。

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

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

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

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

format()メソッドは2つのパラメータを取ります:

  • value:時間量を示す数値
  • unit:時間単位を指定する文字列

負の値は過去の時間を示し、正の値は未来の時間を示します。APIは値に基づいて「1日前」または「2日前」のように複数形を自動的に処理します。

サポートされている時間単位

このAPIは8つの時間単位をサポートしており、それぞれ単数形と複数形の両方を受け入れます:

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

// これらは同じ出力を生成します
console.log(rtf.format(-5, 'second'));
// "5 seconds ago"

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

最小から最大までの利用可能な単位:

  • secondまたはseconds
  • minuteまたはminutes
  • hourまたはhours
  • dayまたはdays
  • weekまたはweeks
  • monthまたはmonths
  • quarterまたはquarters
  • yearまたはyears

四半期単位は会計期間を追跡するビジネスアプリケーションで役立ち、他の単位は一般的な相対時間フォーマットのニーズをカバーします。

自然言語出力

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

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

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

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

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

numericautoに設定すると、一般的な値に対してより慣用的な表現が生成されます:

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

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

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

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

この自然言語出力により、より会話的なインターフェースが作成されます。autoオプションはすべての時間単位で機能しますが、その効果は日単位で最も顕著です。他の言語にも独自の慣用的な表現があり、APIが自動的に処理します。

フォーマットスタイル

styleオプションは、異なるインターフェースコンテキストに合わせて出力の詳細さを調整します:

const rtfLong = new Intl.RelativeTimeFormat('en', {
  style: 'long'
});

console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

const rtfShort = new Intl.RelativeTimeFormat('en', {
  style: 'short'
});

console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

const rtfNarrow = new Intl.RelativeTimeFormat('en', {
  style: 'narrow'
});

console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

読みやすさが最も重要な標準インターフェースにはlongスタイル(デフォルト)を使用します。モバイルインターフェースやデータテーブルなどのスペースが限られたレイアウトにはshortスタイルを使用します。文字数が重要な非常にコンパクトな表示にはnarrowスタイルを使用します。

時間差の計算

Intl.RelativeTimeFormat APIは値をフォーマットしますが、計算はしません。時間差の計算と適切な単位の選択は自分で行う必要があります。この関心の分離により、計算ロジックを制御しながら、フォーマットの複雑さをAPIに委任することができます。

基本的な時間差の計算

特定の時間単位について、2つの日付の差を計算します:

const rtf = new Intl.RelativeTimeFormat('en', { 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 nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);

console.log(formatDaysAgo(nextWeek));
// "in 7 days"

このアプローチは、ユースケースに適した単位が分かっている場合に有効です。コメントのタイムスタンプは常に時間や日を使用し、イベントのスケジューリングは日や週に焦点を当てることがあります。

自動単位選択

汎用的な相対時間フォーマットでは、時間差の大きさに基づいて最も適切な単位を選択します:

const rtf = new Intl.RelativeTimeFormat('en', { 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"

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

単位のしきい値は概算値を使用しています。月は月の長さの変動を考慮せず、1年の1/12として計算されます。この近似は、精度よりも読みやすさが重要な相対時間表示に適しています。

国際化サポート

フォーマッターは相対時間表示のためのロケール固有の規則に対応しています。言語によって複数形のルール、語順、慣用表現が異なります。

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

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

const rtfSpanish = new Intl.RelativeTimeFormat('es', {
  numeric: 'auto'
});

console.log(rtfSpanish.format(-1, 'day'));
// "ayer"

const rtfJapanese = new Intl.RelativeTimeFormat('ja', {
  numeric: 'auto'
});

console.log(rtfJapanese.format(-1, 'day'));
// "昨日"

複数形のルールは言語によって大きく異なります。英語では1つと複数を区別します(1 day vs 2 days)。アラビア語では数に応じて6つの複数形があります。日本語では数量に関係なく同じ形式を使用します。このAPIはこれらの複雑さを自動的に処理します。

const rtfArabic = new Intl.RelativeTimeFormat('ar');

console.log(rtfArabic.format(-1, 'day'));
// "قبل يوم واحد"

console.log(rtfArabic.format(-2, 'day'));
// "قبل يومين"

console.log(rtfArabic.format(-3, 'day'));
// "قبل 3 أيام"

console.log(rtfArabic.format(-11, 'day'));
// "قبل 11 يومًا"

フォーマッターは右から左に書く言語のテキスト方向も処理し、文化的に適切なフォーマット規則を適用します。この自動ローカライゼーションにより、翻訳ファイルの管理やカスタム複数形ロジックの実装が不要になります。

formatToPartsを使用した高度なフォーマット

formatToParts()メソッドはフォーマットされた文字列をオブジェクトの配列として返し、個々のコンポーネントのカスタムスタイリングや操作が可能になります。

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

const parts = rtf.formatToParts(-5, 'second');

console.log(parts);
// [
//   { type: 'integer', value: '5', unit: 'second' },
//   { type: 'literal', value: ' seconds ago' }
// ]

各パートオブジェクトには以下が含まれます:

  • type:数値の場合はinteger、テキストの場合はliteral
  • value:このパートの文字列内容
  • unit:時間単位(整数パートに存在)

この構造により、数字とテキストを異なるスタイルで表示したり、特定のコンポーネントを表示用に抽出したりするカスタムレンダリングが可能になります:

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

function formatWithStyledNumber(value, unit) {
  const parts = rtf.formatToParts(value, unit);

  return parts.map(part => {
    if (part.type === 'integer') {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  }).join('');
}

console.log(formatWithStyledNumber(-5, 'hour'));
// "<strong>5</strong> hours ago"

自然言語の代替表現がある値にnumeric: 'auto'を使用すると、formatToParts()は単一のリテラルパートを返します:

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

const parts = rtf.formatToParts(-1, 'day');

console.log(parts);
// [
//   { type: 'literal', value: 'yesterday' }
// ]

この動作により、自然言語が使用されているか数値フォーマットが使用されているかを検出でき、出力タイプに基づいて異なるスタイルや動作を適用できます。

パフォーマンス最適化

Intl.RelativeTimeFormatインスタンスの作成には、ロケールデータの読み込みとフォーマットルールの初期化が含まれます。この処理は不必要に繰り返すことを避けるべき程度にコストがかかります。

フォーマッターインスタンスのキャッシュ

フォーマッターを一度作成して再利用します:

const formatterCache = new Map();

function getFormatter(locale, options = {}) {
  const cacheKey = `${locale}-${JSON.stringify(options)}`;

  if (!formatterCache.has(cacheKey)) {
    formatterCache.set(
      cacheKey,
      new Intl.RelativeTimeFormat(locale, options)
    );
  }

  return formatterCache.get(cacheKey);
}

// キャッシュされたフォーマッターを再利用
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "yesterday"

このキャッシング戦略は、アクティビティフィードやコメントスレッドのレンダリングなど、多くのタイムスタンプをフォーマットする場合に重要になります。

計算オーバーヘッドの最小化

相対時間を繰り返し計算するのではなく、タイムスタンプを保存します:

// 作成日時を保存
const comment = {
  text: "素晴らしい記事!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// レンダリング時にのみ相対時間を計算
function renderComment(comment, locale) {
  const rtf = getFormatter(locale, { numeric: 'auto' });
  const units = {
    day: 24 * 60 * 60 * 1000,
    hour: 60 * 60 * 1000,
    minute: 60 * 1000,
    second: 1000
  };

  const diffInMs = comment.createdAt - new Date();
  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);
    }
  }
}

このアプローチはデータストレージとプレゼンテーションを分離し、ユーザーのロケールが変更された場合やレンダリングが更新された場合に、基礎となるデータを変更せずに相対時間を再計算できるようにします。

実用的な実装

計算ロジックとフォーマットを組み合わせることで、本番アプリケーションに適した再利用可能なユーティリティ関数が生成されます:

class RelativeTimeFormatter {
  constructor(locale = 'en', options = { numeric: 'auto' }) {
    this.formatter = new Intl.RelativeTimeFormat(locale, options);

    this.units = [
      { name: 'year', ms: 24 * 60 * 60 * 1000 * 365 },
      { name: 'month', ms: 24 * 60 * 60 * 1000 * 365 / 12 },
      { name: 'week', ms: 24 * 60 * 60 * 1000 * 7 },
      { name: 'day', ms: 24 * 60 * 60 * 1000 },
      { name: 'hour', ms: 60 * 60 * 1000 },
      { name: 'minute', ms: 60 * 1000 },
      { name: 'second', ms: 1000 }
    ];
  }

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

    for (const unit of this.units) {
      if (absDiff >= unit.ms || unit.name === 'second') {
        const value = Math.round(diffInMs / unit.ms);
        return this.formatter.format(value, unit.name);
      }
    }
  }
}

// 使用例
const formatter = new RelativeTimeFormatter('en');

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

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

このクラスはフォーマッターと単位選択ロジックの両方をカプセル化し、Dateオブジェクトを受け取りフォーマットされた文字列を返すクリーンなインターフェースを提供します。

フレームワークとの統合

Reactアプリケーションでは、フォーマッターを一度作成し、コンテキストまたはプロップスを通して渡します:

import { createContext, useContext } from 'react';

const RelativeTimeContext = createContext(null);

export function RelativeTimeProvider({ locale, children }) {
  const formatter = new RelativeTimeFormatter(locale);

  return (
    <RelativeTimeContext.Provider value={formatter}>
      {children}
    </RelativeTimeContext.Provider>
  );
}

export function useRelativeTime() {
  const formatter = useContext(RelativeTimeContext);
  if (!formatter) {
    throw new Error('useRelativeTime must be used within RelativeTimeProvider');
  }
  return formatter;
}

// コンポーネントでの使用例
function CommentTimestamp({ date }) {
  const formatter = useRelativeTime();
  return <time>{formatter.format(date)}</time>;
}

このパターンにより、フォーマッターはロケールごとに一度だけ作成され、相対時間フォーマットを必要とするすべてのコンポーネント間で共有されます。

ブラウザサポート

Intl.RelativeTimeFormatはグローバルで95%のカバレッジを持つすべての最新ブラウザで動作します:

  • Chrome 71以上
  • Firefox 65以上
  • Safari 14以上
  • Edge 79以上

Internet ExplorerはこのAPIをサポートしていません。IEサポートが必要なアプリケーションの場合、ポリフィルが利用可能ですが、ネイティブ実装の方がパフォーマンスが向上し、バンドルサイズが小さくなります。

このAPIを使用するタイミング

Intl.RelativeTimeFormatは以下の用途に最適です:

  • フィードやタイムラインでのコンテンツ経過時間の表示
  • コメントや投稿のタイムスタンプの表示
  • 現在時刻に対する相対的なイベントスケジュールのフォーマット
  • 相対的なタイムスタンプを持つ通知システムの構築
  • 人間が読みやすい時間表記を含むアクティビティログの作成

このAPIは以下の用途には適していません:

  • 絶対的な日付と時刻のフォーマット(Intl.DateTimeFormatを使用)
  • ミリ秒単位の正確さを要求する精密な時間追跡
  • 毎秒更新するカウントダウンタイマー
  • 日付の計算やカレンダー計算

相対的および絶対的な時間表示の両方を必要とするアプリケーションでは、Intl.RelativeTimeFormatIntl.DateTimeFormatを組み合わせます。最近のコンテンツには相対時間を表示し、古いコンテンツには絶対日付に切り替えます:

function formatTimestamp(date, locale = 'en') {
  const now = new Date();
  const diffInMs = Math.abs(date - now);
  const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;

  if (diffInMs < sevenDaysInMs) {
    const rtf = new RelativeTimeFormatter(locale);
    return rtf.format(date);
  } else {
    const dtf = new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    });
    return dtf.format(date);
  }
}

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(formatTimestamp(yesterday));
// "昨日"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "2025年9月14日"

このハイブリッドアプローチにより、最近のコンテンツには自然言語の利点を持つ相対時間を提供し、古いタイムスタンプには明確さを維持します。