How do you format relative times like 3 days ago or in 2 hours?
Use Intl.RelativeTimeFormat to display times like 3 days ago or in 2 hours in any language with automatic pluralization and localization
Introduction
Social media feeds, comment sections, and activity logs display timestamps like "5 minutes ago", "2 hours ago", or "in 3 days". These relative timestamps help users quickly understand when something happened without needing to parse an absolute date.
When you hardcode these strings in English, you assume all users speak English and follow English grammar rules. Different languages have different ways of expressing relative time. Spanish says "hace 3 días" instead of "3 days ago". Japanese uses "3日前" with a completely different structure. Each language also has unique pluralization rules that determine when to use singular versus plural forms.
JavaScript provides the Intl.RelativeTimeFormat API to handle relative time formatting automatically. This lesson explains how to format relative times correctly for any language using this built-in API.
Why relative time formatting needs internationalization
Different languages express relative time in different ways. English places the time unit before "ago" for past times and after "in" for future times. Other languages use different word orders, different prepositions, or entirely different grammatical structures.
const rtfEnglish = new Intl.RelativeTimeFormat('en');
console.log(rtfEnglish.format(-3, 'day'));
// "3 days ago"
const rtfSpanish = new Intl.RelativeTimeFormat('es');
console.log(rtfSpanish.format(-3, 'day'));
// "hace 3 días"
const rtfJapanese = new Intl.RelativeTimeFormat('ja');
console.log(rtfJapanese.format(-3, 'day'));
// "3 日前"
Each language produces natural-sounding output that follows its own conventions. You do not need to know these conventions or maintain translation files. The API handles all formatting details automatically.
Pluralization rules also vary significantly across languages. English distinguishes between "1 day" and "2 days". Arabic has six different plural forms depending on the count. Japanese uses the same form regardless of quantity. The Intl.RelativeTimeFormat API applies the correct pluralization rules for each language.
The Intl.RelativeTimeFormat API
The Intl.RelativeTimeFormat constructor creates a formatter that converts numeric values and time units into localized strings. You pass a locale identifier as the first argument, then call the format() method with a value and unit.
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-1, 'day'));
// "1 day ago"
console.log(rtf.format(2, 'hour'));
// "in 2 hours"
The format() method takes two parameters. The first is a number representing the amount of time. The second is a string specifying the time unit.
Negative numbers indicate past times, while positive numbers indicate future times. This convention makes the API intuitive to use once you understand the sign convention.
Formatting past and future times
The sign of the value determines whether the time is in the past or future. Negative values produce past tenses, while positive values produce future tenses.
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-5, 'minute'));
// "5 minutes ago"
console.log(rtf.format(5, 'minute'));
// "in 5 minutes"
console.log(rtf.format(-2, 'week'));
// "2 weeks ago"
console.log(rtf.format(2, 'week'));
// "in 2 weeks"
This pattern works consistently across all time units and all languages. The API automatically selects the correct grammatical structure based on whether the value is positive or negative.
Available time units
The API supports eight time units that cover most relative time formatting needs. You can use either singular or plural forms, and both work identically.
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-30, 'second'));
// "30 seconds ago"
console.log(rtf.format(-15, 'minute'));
// "15 minutes ago"
console.log(rtf.format(-6, 'hour'));
// "6 hours ago"
console.log(rtf.format(-3, 'day'));
// "3 days ago"
console.log(rtf.format(-2, 'week'));
// "2 weeks ago"
console.log(rtf.format(-4, 'month'));
// "4 months ago"
console.log(rtf.format(-1, 'quarter'));
// "1 quarter ago"
console.log(rtf.format(-2, 'year'));
// "2 years ago"
The API accepts both singular forms like day and plural forms like days. Both produce identical output. The quarter unit is useful for business applications that work with fiscal periods.
Using natural language with numeric auto
The numeric option controls whether the formatter uses numbers or natural language alternatives. The default value is always, which always shows numbers.
const rtfAlways = new Intl.RelativeTimeFormat('en-US', {
numeric: 'always'
});
console.log(rtfAlways.format(-1, 'day'));
// "1 day ago"
console.log(rtfAlways.format(0, 'day'));
// "in 0 days"
console.log(rtfAlways.format(1, 'day'));
// "in 1 day"
Setting numeric to auto produces more natural phrasing for certain values.
const rtfAuto = new Intl.RelativeTimeFormat('en-US', {
numeric: 'auto'
});
console.log(rtfAuto.format(-1, 'day'));
// "yesterday"
console.log(rtfAuto.format(0, 'day'));
// "today"
console.log(rtfAuto.format(1, 'day'));
// "tomorrow"
This option makes interfaces feel more conversational. Users see "yesterday" instead of "1 day ago", which reads more naturally. The auto option works across all time units and all languages, with each language providing its own idiomatic alternatives.
Choosing a formatting style
The style option controls output verbosity. The three available styles are long, short, and narrow.
const rtfLong = new Intl.RelativeTimeFormat('en-US', {
style: 'long'
});
console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"
const rtfShort = new Intl.RelativeTimeFormat('en-US', {
style: 'short'
});
console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"
const rtfNarrow = new Intl.RelativeTimeFormat('en-US', {
style: 'narrow'
});
console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"
The long style is the default and works well for most interfaces. The short style saves space in mobile layouts or tables. The narrow style produces the most compact output for extremely space-constrained designs.
Calculating time differences
The Intl.RelativeTimeFormat API formats values but does not calculate them. You must compute the time difference yourself, then pass the result to the formatter.
To calculate a time difference, subtract the target date from the current date, then convert the result from milliseconds to the desired unit.
const rtf = new Intl.RelativeTimeFormat('en-US', { 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 tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
console.log(formatDaysAgo(tomorrow));
// "tomorrow"
This function calculates the difference in days between the target date and now. The calculation divides milliseconds by the number of milliseconds in a day, then rounds to the nearest whole number.
The subtraction date - now produces a negative value for past dates and a positive value for future dates. This matches the sign convention expected by the format() method.
Building a complete utility function
For a general-purpose relative time formatter, you need to select the most appropriate time unit based on the magnitude of the time difference.
const rtf = new Intl.RelativeTimeFormat('en-US', { 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"
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(tomorrow));
// "tomorrow"
This function iterates through time units from largest to smallest, selecting the first unit where the absolute difference exceeds the unit's millisecond value. The fallback to seconds ensures the function always returns a result.
The unit definitions use approximate values. Months are calculated as 1/12 of a year rather than accounting for different month lengths. This approximation works well for relative time displays where approximate values are more useful than exact precision.
Formatting for the user's locale
Instead of hardcoding a specific locale, you can use the user's preferred language from the browser.
const userLocale = navigator.language;
const rtf = new Intl.RelativeTimeFormat(userLocale, { numeric: 'auto' });
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(rtf.format(-1, 'day'));
// Output varies by user's locale
// For en-US: "yesterday"
// For es-ES: "ayer"
// For fr-FR: "hier"
// For de-DE: "gestern"
This approach displays relative times according to each user's language preferences without requiring manual locale selection. The browser provides the language preference, and the API applies the appropriate formatting conventions.
Seeing the same time in different languages
The same relative time value produces different output for different locales. Each language follows its own conventions for word order, grammar, and pluralization.
const threeDaysAgo = -3;
const rtfEnglish = new Intl.RelativeTimeFormat('en-US');
console.log(rtfEnglish.format(threeDaysAgo, 'day'));
// "3 days ago"
const rtfSpanish = new Intl.RelativeTimeFormat('es-ES');
console.log(rtfSpanish.format(threeDaysAgo, 'day'));
// "hace 3 días"
const rtfFrench = new Intl.RelativeTimeFormat('fr-FR');
console.log(rtfFrench.format(threeDaysAgo, 'day'));
// "il y a 3 jours"
const rtfGerman = new Intl.RelativeTimeFormat('de-DE');
console.log(rtfGerman.format(threeDaysAgo, 'day'));
// "vor 3 Tagen"
const rtfJapanese = new Intl.RelativeTimeFormat('ja-JP');
console.log(rtfJapanese.format(threeDaysAgo, 'day'));
// "3 日前"
const rtfArabic = new Intl.RelativeTimeFormat('ar-SA');
console.log(rtfArabic.format(threeDaysAgo, 'day'));
// "قبل 3 أيام"
Each language produces natural-sounding output that native speakers would use in conversation. The API handles all the complexity of different grammatical structures, different writing systems, and different text directions.
Reusing formatters for performance
Creating an Intl.RelativeTimeFormat instance involves loading locale data and processing options. When formatting multiple timestamps, create the formatter once and reuse it.
const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });
const timestamps = [
new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago
new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
new Date(Date.now() - 24 * 60 * 60 * 1000) // 1 day ago
];
timestamps.forEach(date => {
const diffInMs = date - new Date();
const diffInMinutes = Math.round(diffInMs / (60 * 1000));
console.log(rtf.format(diffInMinutes, 'minute'));
});
This approach is more efficient than creating a new formatter for each timestamp. The performance difference becomes significant when formatting hundreds or thousands of timestamps in activity feeds or comment threads.
Using relative times in interfaces
You can use relative time formatting anywhere you display timestamps to users. This includes social media feeds, comment sections, activity logs, notification systems, and any interface where showing how long ago something happened helps users understand context.
const rtf = new Intl.RelativeTimeFormat(navigator.language, {
numeric: 'auto'
});
function updateTimestamp(element, date) {
const now = new Date();
const diffInMs = date - now;
const diffInMinutes = Math.round(diffInMs / (60 * 1000));
element.textContent = rtf.format(diffInMinutes, 'minute');
}
const commentDate = new Date('2025-10-15T14:30:00');
const timestampElement = document.getElementById('comment-timestamp');
updateTimestamp(timestampElement, commentDate);
The formatted strings work like any other string value. You can insert them into text content, attributes, or any other context where you display information to users.