Intl.RelativeTimeFormat API
Format relative time strings in JavaScript with full internationalization support
Introduction
Displaying relative timestamps like "3 hours ago" or "in 2 days" is a common requirement in web applications. Social media feeds, comment sections, notification systems, and activity logs all need human-readable time displays that update as content ages.
Building this functionality from scratch presents challenges. You need to calculate time differences, select appropriate units, handle pluralization rules across languages, and maintain translations for every supported locale. This complexity explains why developers traditionally reached for libraries like Moment.js, adding significant bundle size for what seems like simple formatting.
The Intl.RelativeTimeFormat API provides a native solution. It formats relative time strings with full internationalization support, handling pluralization rules and cultural conventions automatically. The API works across all major browsers with 95% global coverage, eliminating the need for external dependencies while producing natural-sounding output in dozens of languages.
Basic Usage
The Intl.RelativeTimeFormat constructor creates a formatter instance that converts numeric values and time units into localized strings.
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"
The format() method takes two parameters:
value: A number indicating the amount of timeunit: A string specifying the time unit
Negative values indicate past times, positive values indicate future times. The API automatically handles pluralization, producing "1 day ago" or "2 days ago" based on the value.
Supported Time Units
The API supports eight time units, each accepting both singular and plural forms:
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"
Available units from smallest to largest:
secondorsecondsminuteorminuteshourorhoursdayordaysweekorweeksmonthormonthsquarterorquartersyearoryears
The quarter unit proves useful in business applications tracking fiscal periods, while the others cover typical relative time formatting needs.
Natural Language Output
The numeric option controls whether the formatter uses numeric values or natural language alternatives.
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"
Setting numeric to auto produces more idiomatic phrasing for common values:
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"
This natural language output creates more conversational interfaces. The auto option works across all time units, though the effect is most noticeable with days. Other languages have their own idiomatic alternatives that the API handles automatically.
Formatting Styles
The style option adjusts output verbosity for different interface contexts:
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"
Use long style (the default) for standard interfaces where readability matters most. Use short style for space-constrained layouts like mobile interfaces or data tables. Use narrow style for extremely compact displays where every character counts.
Calculating Time Differences
The Intl.RelativeTimeFormat API formats values but does not calculate them. You must compute time differences and select appropriate units yourself. This separation of concerns gives you control over calculation logic while delegating formatting complexity to the API.
Basic Time Difference Calculation
For a specific time unit, calculate the difference between two dates:
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"
This approach works when you know which unit makes sense for your use case. Comment timestamps might always use hours or days, while event scheduling might focus on days or weeks.
Automatic Unit Selection
For general-purpose relative time formatting, select the most appropriate unit based on the time difference magnitude:
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"
This implementation iterates through units from largest to smallest, selecting the first unit where the time difference exceeds the unit's millisecond value. The fallback to seconds ensures the function always returns a result.
The unit thresholds use approximate values. Months are calculated as 1/12 of a year rather than accounting for variable month lengths. This approximation works well for relative time displays where precision matters less than readability.
Internationalization Support
The formatter respects locale-specific conventions for relative time display. Different languages have different pluralization rules, different word orders, and different idiomatic expressions.
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'));
// "昨日"
Pluralization rules vary significantly across languages. English distinguishes between one and many (1 day vs 2 days). Arabic has six plural forms depending on the count. Japanese uses the same form regardless of quantity. The API handles these complexities automatically.
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 يومًا"
The formatter also handles text direction for right-to-left languages and applies culturally appropriate formatting conventions. This automatic localization eliminates the need to maintain translation files or implement custom pluralization logic.
Advanced Formatting with formatToParts
The formatToParts() method returns the formatted string as an array of objects, allowing custom styling or manipulation of individual components.
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' }
// ]
Each part object contains:
type: Eitherintegerfor numeric values orliteralfor textvalue: The string content of this partunit: The time unit (present on integer parts)
This structure enables custom rendering where you might want to style numbers differently from text, or extract specific components for display:
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"
When using numeric: 'auto' with values that have natural language alternatives, formatToParts() returns a single literal part:
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const parts = rtf.formatToParts(-1, 'day');
console.log(parts);
// [
// { type: 'literal', value: 'yesterday' }
// ]
This behavior lets you detect when natural language is used versus numeric formatting, allowing you to apply different styling or behavior based on the output type.
Performance Optimization
Creating Intl.RelativeTimeFormat instances involves loading locale data and initializing formatting rules. This operation is expensive enough to avoid repeating unnecessarily.
Cache Formatter Instances
Create formatters once and reuse them:
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"
This caching strategy becomes important when formatting many timestamps, such as rendering activity feeds or comment threads.
Minimize Calculation Overhead
Store timestamps rather than calculating relative times repeatedly:
// 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);
}
}
}
This approach separates data storage from presentation, allowing you to recalculate relative times when the user's locale changes or when rendering updates without modifying the underlying data.
Practical Implementation
Combining calculation logic with formatting produces a reusable utility function suitable for production applications:
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"
This class encapsulates both the formatter and the unit selection logic, providing a clean interface that accepts Date objects and returns formatted strings.
Integration with Frameworks
In React applications, create the formatter once and pass it through context or 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>;
}
This pattern ensures formatters are created once per locale and shared across all components that need relative time formatting.
Browser Support
Intl.RelativeTimeFormat works across all modern browsers with 95% global coverage:
- Chrome 71+
- Firefox 65+
- Safari 14+
- Edge 79+
Internet Explorer does not support this API. For applications requiring IE support, polyfills are available, though the native implementation provides better performance and smaller bundle sizes.
When to Use This API
Intl.RelativeTimeFormat works best for:
- Displaying content age in feeds and timelines
- Showing comment or post timestamps
- Formatting event scheduling relative to current time
- Building notification systems with relative timestamps
- Creating activity logs with human-readable times
The API is not suitable for:
- Absolute date and time formatting (use
Intl.DateTimeFormat) - Precise time tracking requiring exact millisecond accuracy
- Countdown timers that update every second
- Date arithmetic or calendar calculations
For applications requiring both relative and absolute time displays, combine Intl.RelativeTimeFormat with Intl.DateTimeFormat. Show relative times for recent content and switch to absolute dates for older content:
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"
This hybrid approach provides the natural language benefits of relative times for recent content while maintaining clarity for older timestamps.