SNS やブログの投稿一覧でよく見る「3 日前」「5 分後」といった表記。自分で作ろうとすると、差分の計算に加えて単位の切り替えや「1 日前なら昨日にする」といった分岐が必要になり、思いのほか散らかったコードになりがちです。Intl.RelativeTimeFormat を使えば、数値と単位を渡すだけでその言語にふさわしい相対表記を組み立ててくれます。この記事では基本の format() から、numeric や style といったオプション、formatToParts()、そして2つの Date の差を相対表記に変換する実用的な関数までを解説します。
目次
相対表記を自前で書くと何が面倒なのか
「3 日前」という文字列そのものは、差分の日数と「日前」をつなぐだけで作れます。問題はその先です。単位を日だけで済ませるわけにはいかないので秒・分・時間・月・年の分岐が要りますし、日本語なら「1 日前」ではなく「昨日」と出したくなります。さらに英語にも対応するとなると、単数形と複数形(1 day ago と 3 days ago)や、過去と未来で語順が変わること(3 days ago と in 3 days)まで自分で面倒を見ることになります。
// 自前でやろうとすると、言語ごとに分岐が増えていく
function toRelative( days ) {
if ( days === -1 ) return '昨日';
if ( days === 1 ) return '明日';
if ( days < 0 ) return `${ Math.abs( days ) } 日前`;
return `${ days } 日後`;
// 英語対応、複数形、時間や分の単位……と際限なく増える
}
こうした「言語ごとの言い回し」は、ブラウザと Node.js に内蔵されている国際化の仕組み(Intl)がすでに知っています。Intl.RelativeTimeFormat はその中の、相対時間の表記を担当するオブジェクトです。
基本の使い方
まず new Intl.RelativeTimeFormat() でフォーマッターを作り、format( value, unit ) を呼びます。第1引数の value が数値、第2引数の unit が単位です。
const rtf = new Intl.RelativeTimeFormat( 'ja', { numeric: 'auto' } );
console.log( rtf.format( -3, 'day' ) ); // 3 日前
console.log( rtf.format( -1, 'day' ) ); // 昨日
console.log( rtf.format( 1, 'day' ) ); // 明日
console.log( rtf.format( 5, 'minute' ) ); // 5 分後
console.log( rtf.format( -2, 'month' ) ); // 2 か月前
ここでいちばん大事なのは、value が負なら過去、正なら未来という約束です。「3 日前」は -3、「5 分後」は 5 です。「前」を表したいのに正の数を渡すと、静かに「3 日後」と表示されてしまうので注意してください。
なお日本語ロケールの既定の表記では、3 日前 のように数値と単位のあいだに半角スペースが入ります。これは CLDR(各言語の表記ルールを集めたデータ)に沿った出力で、後述する style: 'narrow' を指定すると 3日前 とスペースなしになります。
第1引数のロケールを省略すると実行環境の既定ロケールが使われます。サーバー上の Node.js など環境によって結果が変わるため、日本語で出したいなら 'ja' を明示しておくのが安全です。
unit に指定できる値
unit に渡せるのは次の8種類です。いずれも複数形(days、minutes など)でも受け付けられ、意味は単数形と同じです。それ以外の文字列を渡すと RangeError になります。
| unit | 単位 | format( -2, unit ) の出力(ja) |
|---|---|---|
year | 年 | 2 年前 |
quarter | 四半期 | 2 四半期前 |
month | 月 | 2 か月前 |
week | 週 | 2 週間前 |
day | 日 | 2 日前 |
hour | 時間 | 2 時間前 |
minute | 分 | 2 分前 |
second | 秒 | 2 秒前 |
単位の選択は format() がやってくれるわけではありません。「45 分前なのか 1 時間前なのか」を決めるのは呼び出す側の仕事です。その部分はあとで関数にまとめます。
コンストラクタのオプション
第2引数のオプションで表記の細かい調整ができます。指定できるのは次の3つです。
| オプション | 指定できる値 | 既定値 | 内容 |
|---|---|---|---|
numeric | 'always' / 'auto' | 'always' | 'auto' にすると「昨日」「来週」のような数値を使わない語彙を優先する |
style | 'long' / 'short' / 'narrow' | 'long' | 表記の長さ。'narrow' ほど短く省略される |
localeMatcher | 'best fit' / 'lookup' | 'best fit' | 要求したロケールが使えないときの代替ロケールの選び方 |
numeric で「昨日」と「1 日前」を切り替える
既定の 'always' は常に数値を含む表記になります。'auto' にすると、その言語に専用の言い回しがある場合にそちらを使います。日本語なら「昨日」「明日」「明後日」「先週」「昨年」などです。
| 呼び出し | numeric: 'always' | numeric: 'auto' |
|---|---|---|
format( -1, 'day' ) | 1 日前 | 昨日 |
format( 1, 'day' ) | 1 日後 | 明日 |
format( 2, 'day' ) | 2 日後 | 明後日 |
format( -3, 'day' ) | 3 日前 | 3 日前 |
format( -1, 'week' ) | 1 週間前 | 先週 |
format( -1, 'year' ) | 1 年前 | 昨年 |
format( 0, 'second' ) | 0 秒後 | 今 |
英語でも同じように振る舞い、format( -1, 'day' ) は 1 day ago と yesterday、format( 1, 'week' ) は in 1 week と next week に分かれます。-3 のように専用の語彙が無い値では、どちらを指定しても結果は同じです。
style で表記の長さを変える
省略の度合いを3段階で選べます。どこまで省略されるかは言語ごとに決まっているため、同じ style でも言語によって差の出かたが違います。
| style | ja: format( -3, 'day' ) | ja: format( 5, 'minute' ) | en: format( -3, 'day' ) | en: format( 5, 'minute' ) |
|---|---|---|---|---|
'long' | 3 日前 | 5 分後 | 3 days ago | in 5 minutes |
'short' | 3 日前 | 5 分後 | 3 days ago | in 5 min. |
'narrow' | 3日前 | 5分後 | 3d ago | in 5m |
日本語では 'long' と 'short' の出力が同じで、'narrow' だけがスペースの無い詰めた表記になります。英語では 'short' で minutes が min. に、'narrow' ではさらに m まで縮みます。スペースが気になって日本語サイトで 'narrow' を選ぶと、英語圏のユーザーには 3d ago というかなり素っ気ない表示になる点は頭に入れておいてください。
localeMatcher は基本そのままでよい
localeMatcher は、要求したロケールが実行環境にないときに代わりをどう選ぶかの設定です。既定の 'best fit' は環境が「もっとも近い」と判断したものを選び、'lookup' は仕様で決められた手順(ja-JP-u-ca-japanese → ja-JP → ja のように後ろから削っていく方法)で探します。実用上はほぼ違いが出ないので、明示的に切り替えたい理由がなければ指定しなくて構いません。
実際にどのロケールとオプションで動いているかは resolvedOptions() で確認できます。指定したロケールが使えなかった場合にどこへ落ちたかを調べるのに便利です。
const rtf = new Intl.RelativeTimeFormat( 'ja' );
console.log( rtf.resolvedOptions() );
// { locale: 'ja', style: 'long', numeric: 'always', numberingSystem: 'latn' }
formatToParts() で数値部分だけを取り出す
format() が1本の文字列を返すのに対して、formatToParts() は組み立てる前の断片を配列で返します。数値の部分にだけ色を付けたい、太字にしたいといったときに使います。
const rtf = new Intl.RelativeTimeFormat( 'ja', { numeric: 'auto' } );
console.log( rtf.formatToParts( -3, 'day' ) );
// [
// { type: 'integer', value: '3', unit: 'day' },
// { type: 'literal', value: ' 日前' }
// ]
// numeric: 'auto' で語彙に置き換わったときは、数値の断片が無い
console.log( rtf.formatToParts( -1, 'day' ) );
// [ { type: 'literal', value: '昨日' } ]
断片の type が 'integer' なら数値、'literal' ならそれ以外の文字です。数値の断片には unit プロパティも付きます。これを使って、数値だけを <strong> で包む例が次のコードです。「昨日」のように数値を含まない出力では 'integer' の断片が現れないので、その場合は自然とそのまま表示されます。
function toHighlightedHtml( value, unit ) {
const rtf = new Intl.RelativeTimeFormat( 'ja', { numeric: 'auto' } );
return rtf.formatToParts( value, unit )
.map( ( part ) => {
// 数値の断片だけタグで包む
if ( part.type === 'integer' ) {
return `<strong>${ part.value }</strong>`;
}
return part.value;
} )
.join( '' );
}
toHighlightedHtml( -3, 'day' ); // '<strong>3</strong> 日前'
toHighlightedHtml( -1, 'day' ); // '昨日'
2つの日時の差から相対表記を作る
実際の投稿一覧では、渡されるのは「-3」という数値ではなく投稿日時そのものです。そこで、2つの Date の差を見て適切な単位を選び、format() に渡すユーティリティを用意します。秒から始めて、60 秒を超えたら分へ、60 分を超えたら時間へ……と段階的に単位を繰り上げていく作りです。
// 「1つ上の単位に繰り上がるまでの量」と、その単位の組み合わせ
const DIVISIONS = [
{ amount: 60, unit: 'second' }, // 60 秒で分へ
{ amount: 60, unit: 'minute' }, // 60 分で時間へ
{ amount: 24, unit: 'hour' }, // 24 時間で日へ
{ amount: 7, unit: 'day' }, // 7 日で週へ
{ amount: 4.34524, unit: 'week' }, // 1 か月の平均週数で月へ
{ amount: 12, unit: 'month' }, // 12 か月で年へ
{ amount: Infinity, unit: 'year' },
];
/**
* 相対時間の文字列を返す。
*
* @param {Date} target 表示したい日時
* @param {Date} base 基準になる日時(既定は現在時刻)
* @param {string} locale ロケール
* @return {string} 「3 日前」のような文字列
*/
function formatRelativeTime( target, base = new Date(), locale = 'ja' ) {
const rtf = new Intl.RelativeTimeFormat( locale, { numeric: 'auto' } );
// 差をミリ秒 → 秒に。過去なら負、未来なら正になる
let duration = ( target.getTime() - base.getTime() ) / 1000;
for ( const division of DIVISIONS ) {
// 繰り上げの境界に達していなければ、この単位で表示する
if ( Math.abs( duration ) < division.amount ) {
// 符号を保ったまま四捨五入する
const value = Math.sign( duration ) * Math.round( Math.abs( duration ) );
return rtf.format( value, division.unit );
}
// 次の単位に換算して繰り上げる
duration /= division.amount;
}
}
基準時刻を base として引数で受け取れるようにしてあるのは、テストのためです。new Date() を関数の中に直接書いてしまうと、実行するたびに結果が変わってテストが書けなくなります。
四捨五入を Math.round( duration ) ではなく Math.sign() と組み合わせているのには理由があります。JavaScript の Math.round() は「ちょうど半分」のとき常に大きいほうへ丸めるため、Math.round( 1.5 ) は 2 ですが Math.round( -1.5 ) は -1 になります。そのまま使うと「1.5 日後」は「2 日後」なのに「1.5 日前」は「1 日前」という非対称な結果になるので、絶対値で丸めてから符号を戻しています。
実際に動かすと次のような出力になります。
const now = new Date( '2026-09-15T12:00:00+09:00' );
const minutesAgo = ( m ) => new Date( now.getTime() - m * 60 * 1000 );
formatRelativeTime( minutesAgo( 0.5 ), now ); // 30 秒前
formatRelativeTime( minutesAgo( 45 ), now ); // 45 分前
formatRelativeTime( minutesAgo( 180 ), now ); // 3 時間前
formatRelativeTime( minutesAgo( 60 * 24 ), now ); // 昨日
formatRelativeTime( minutesAgo( 60 * 24 * 3 ), now ); // 3 日前
formatRelativeTime( minutesAgo( -60 * 5 ), now ); // 5 時間後
ひとつ気に留めておきたいのは、numeric: 'auto' と週・月の単位の組み合わせです。10 日前は週に繰り上がって -1 週になるため「先週」と表示されます。日付としては正確でも、投稿一覧で「先週」とだけ出るのは曖昧に感じられるかもしれません。その場合は DIVISIONS から week の行を外して日から月へ直接繰り上げる、あるいは一定期間より古いものは相対表記をやめて実際の日付を出す、といった調整をします。
time 要素と組み合わせて正確な日時も残す
相対表記は読みやすい反面、正確な日時の情報が失われます。検索エンジンや支援技術に日時を正しく伝えるために、<time> 要素の datetime 属性に機械可読な日時を入れ、表示テキストだけを相対表記に置き換えるのがおすすめです。
<!-- サーバー側では正確な日時を出力しておく -->
<time class="js-relative" datetime="2026-09-12T10:30:00+09:00">2026年9月12日</time>
document.querySelectorAll( 'time.js-relative' ).forEach( ( el ) => {
const target = new Date( el.dateTime );
// 表示だけを相対表記に差し替える(datetime 属性はそのまま)
el.textContent = formatRelativeTime( target );
// マウスを乗せたときに正確な日時が見えるようにしておく
el.title = target.toLocaleString( 'ja-JP' );
} );
datetime 属性は書き換えないので、JavaScript が動かない環境でもサーバーが出力した日付がそのまま残ります。<time> 要素そのものの書き方については別の記事で詳しく扱っているので、ここでは組み合わせ方だけに留めます。
思ったような表記にならないとき
「昨日」にならず「1 日前」と出る
numeric の既定値は 'always' です。オプションを何も渡さずに new Intl.RelativeTimeFormat( 'ja' ) とすると、format( -1, 'day' ) は「1 日前」になります。「昨日」「明日」「先週」といった言い回しを使いたいなら、{ numeric: 'auto' } を明示してください。逆に一覧の表示を数値で揃えたいときは、既定のままにしておけば「1 日前」「2 日前」と並びます。
過去のはずが「◯日後」と表示される
符号が逆になっています。差分を計算するときに base - target(現在 − 対象)としてしまうと、過去の日時ほど正の値になり「3 日後」と表示されます。target - base(対象 − 現在)の順で引くのが正解です。「過去は負」と覚えておけば間違えません。
同じ理由で、あらかじめ「◯日前」という正の日数を持っている場合はそのまま渡してはいけません。rtf.format( -daysAgo, 'day' ) のように、渡す直前で符号を反転させます。
23 時間前なのに「0 日前」と出る
単位の切り替えを小数の切り捨てで行うと起きる問題です。23 時間を日数に直すと約 -0.958 ですが、Math.trunc() や parseInt() で小数を落とすと 0 になり、numeric: 'always' では「0 日前」、'auto' では「今日」と表示されます。
const days = -23 / 24; // -0.9583...
const always = new Intl.RelativeTimeFormat( 'ja' );
always.format( Math.trunc( days ), 'day' ); // 0 日前 ← 意図しない
always.format( Math.round( days ), 'day' ); // 1 日前
丸め方を Math.round() に変えれば「1 日前」になります。ただし根本的には、23 時間を日として扱うかどうかの判断が先です。前述の DIVISIONS のように「24 時間未満なら hour を使う」と決めておけば「23 時間前」と表示され、こうした 0 の問題自体が起きなくなります。
環境によって言語が変わってしまう
ロケールを省略して new Intl.RelativeTimeFormat( undefined, { numeric: 'auto' } ) と書くと、実行環境の既定ロケールが使われます。日本語設定のブラウザでは「3 日前」でも、ロケールが英語のままになっているサーバー上の Node.js では 3 days ago が返ります。手元では意図どおりなのに本番だけ英語になる、という食い違いはこれが原因のことが多いので、出したい言語が決まっているならロケールは必ず文字列で渡してください。
逆に閲覧者の言語に合わせたいなら、ブラウザでは navigator.language を渡します。指定したロケールのデータが環境に無い場合は代替ロケールにフォールバックするため、事前に確認したいときは Intl.RelativeTimeFormat.supportedLocalesOf() で対応状況を調べられます。
まとめ
Intl.RelativeTimeFormat は、数値と単位を渡すだけで「3 日前」「5 分後」といった相対表記を言語に合わせて組み立ててくれます。value は負が過去・正が未来、unit は year から second までの8種類。ここさえ押さえれば基本は使いこなせます。
「昨日」「先週」のような自然な言い回しにしたいなら numeric: 'auto'、表記を詰めたいなら style を調整します。数値の部分だけ装飾したいときは formatToParts() が使えます。一方で「どの単位を選ぶか」は API の担当外なので、日時の差から単位を決める処理は自分で書く必要があります。この記事の DIVISIONS を使った関数をそのまま土台にして、扱いたい粒度に合わせて閾値を調整してみてください。
そして表示を相対表記にするときは、<time> 要素の datetime 属性に正確な日時を残しておくこと。人には読みやすく、機械には正確に伝わる形が両立します。