Relative Time Labels With Intl

Format timestamps as localized labels such as yesterday or in 2 hours, reusing cached Intl.RelativeTimeFormat instances.

JavascriptData Integrity

Code

snippet
const DIVISIONS = [
  { unit: "year", seconds: 365 * 24 * 60 * 60 },
  { unit: "month", seconds: 30 * 24 * 60 * 60 },
  { unit: "day", seconds: 24 * 60 * 60 },
  { unit: "hour", seconds: 60 * 60 },
  { unit: "minute", seconds: 60 },
  { unit: "second", seconds: 1 },
];

const formatters = new Map();

function getFormatter(locale) {
  let formatter = formatters.get(locale);
  if (formatter === undefined) {
    formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
    formatters.set(locale, formatter);
  }
  return formatter;
}

export function formatRelativeTime(target, options = {}) {
  const { now = Date.now(), locale = "en" } = options;
  const targetTime =
    target instanceof Date ? target.getTime() : typeof target === "string" ? Date.parse(target) : target;
  if (!Number.isFinite(targetTime)) throw new TypeError("target must be a valid Date, ISO string or timestamp");
  if (!Number.isFinite(now)) throw new TypeError("now must be a finite timestamp");
  const deltaSeconds = Math.round((targetTime - now) / 1000);
  const magnitude = Math.abs(deltaSeconds);
  const division =
    DIVISIONS.find((item) => magnitude >= item.seconds) ?? DIVISIONS[DIVISIONS.length - 1];
  const value = Math.round(deltaSeconds / division.seconds);
  return getFormatter(locale).format(value, division.unit);
}

const now = Date.parse("2026-10-21T12:00:00Z");
console.log(formatRelativeTime("2026-10-21T10:00:00Z", { now, locale: "en" }));
console.log(formatRelativeTime(now + 90_000, { now, locale: "de" }));
console.log(formatRelativeTime(now - 45 * 24 * 60 * 60 * 1000, { now, locale: "zh-CN" }));