Intl.RelativeTimeFormat handles the pluralization and locale text; the function just needs to pick the right unit and value. Divisions are ordered from largest to smallest and stop at the first unit under its threshold.
typescript
const units: { unit: Intl.RelativeTimeFormatUnit; ms: number }[] = [
{ unit: 'year', ms: 1000 * 60 * 60 * 24 * 365 },
{ unit: 'month', ms: 1000 * 60 * 60 * 24 * 30 },
{ unit: 'week', ms: 1000 * 60 * 60 * 24 * 7 },
{ unit: 'day', ms: 1000 * 60 * 60 * 24 },
{ unit: 'hour', ms: 1000 * 60 * 60 },
{ unit: 'minute', ms: 1000 * 60 },
{ unit: 'second', ms: 1000 },
]
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
export function formatRelativeTime(date: Date | string) {
const target = typeof date === 'string' ? new Date(date) : date
const diffMs = target.getTime() - Date.now()
for (const { unit, ms } of units) {
if (Math.abs(diffMs) >= ms || unit === 'second') {
return rtf.format(Math.round(diffMs / ms), unit)
}
}
return rtf.format(0, 'second')
}Negative diffs read naturally as past tense, positive diffs as future. "numeric: auto" lets Intl swap in words like "yesterday" and "tomorrow" where the locale supports it.
typescript
formatRelativeTime(new Date(Date.now() - 1000 * 60 * 5)) // "5 minutes ago"
formatRelativeTime(new Date(Date.now() + 1000 * 60 * 60 * 24)) // "tomorrow"