Intl.RelativeTimeFormat API

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

はじめに

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

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

Intl.RelativeTimeFormat 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');

// These produce identical output
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スタイルを使用します。1文字1文字が重要な極めてコンパクトな表示には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年の12分の1として計算されます。この近似は、精度よりも可読性が重要な相対時間表示において適切に機能します。

国際化サポート

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

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

// Reuse cached formatters
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "yesterday"

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

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

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

// Store the creation date
const comment = {
  text: "Great article!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// Calculate relative time only when rendering
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);
      }
    }
  }
}

// Usage
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アプリケーションでは、フォーマッターを一度作成し、コンテキストまたはprops経由で渡します。

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

// Component usage
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));
// "yesterday"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "Sep 14, 2025"

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