Relativize.js

Made for use on this website.

See the Pen Relativize Lib by j0lol (@j0lol) on CodePen.

Code
// Relativize library
// Authored by: Josie 
// Licensed: CC0 || Unlicense
// Calculates "N months/days/hours ago" strings with Temporal.

// feel free to tweak this, i guess.
// i doubt you want ms precision, or weeks separate from days
const units = ["years", "months", "days", "hours", "minutes", "seconds"];

// Finds the largest unit (y, m, d, etc.)
function findLargestUnit(duration) {
  return units.find((unit) => (duration[unit] !== 0));
}

// 2 months, 3 days, 1 minute, 5 seconds, ... -> 2 months
function plainSimplify(duration) {
  const largestUnit = findLargestUnit(duration);
  return (new Temporal.Duration(0)).with({
    [largestUnit]: duration[largestUnit]
  });
}

// i don't have the ability to translate this, sorry.
function formatRelString(duration, ...localeArgs) {
  const sign = duration.sign;
  if (sign === 1) {
    const localeString = duration.toLocaleString(...localeArgs);
    return `${localeString} ago`
  } else if (sign === -1) {
    const localeString = duration.abs().toLocaleString(...localeArgs);
    return `in ${localeString}`;
  } else {
    return "now";
  }
}

const defaultRelativizeOptions = {
  locale: "en-US",
  largestFormattableUnit: "years",
  durationFormattingOptions: { style: "long" },
  plainDateFormattingOptions: { dateStyle: "short" }
};

const dRO = defaultRelativizeOptions; // alias

// `locale` is used for `toLocaleString` calls. 
//   (Realistically only English is useful here... weh)
//
// `largestFormattableUnit` is the unit where relativize will stop bothering.
//   (this defaults to a year, set it to null/undefined to disable this behavior)
//
// `durationFormattingOptions` and `plainDateFormattingOptions` are both passed to
// `toLocaleString`, in case you want to change how it looks.
// See `Intl.DateTimeFormat()` and `Intl.DurationFormat()` constructors for more info.
export function relativize({
  locale = dRO.locale, 
  largestFormattableUnit = dRO.largestFormattableUnit,
  durationFormattingOptions = dRO.durationFormattingOptions,
  plainDateFormattingOptions = dRO.plainDateFormattingOptions
} = defaultRelativizeOptions) {
  
	// upgrade your browser!
  // in theory this could be used with a Temporal polyfill.
  // don't do this. or do. i'm not your boss.
	if (!Temporal) { return; }
  
	document.querySelectorAll("time[data-relative]").forEach((el) => {
		el.innerHTML = "processing...";
    
    // we have to make sure both dates are in the same 
    // timezone to do calendar-aware math with Duration
		const then = Temporal.ZonedDateTime.from(
      el.attributes.datetime.value
    ).withTimeZone("UTC");
		const now = Temporal.Now.zonedDateTimeISO().withTimeZone("UTC"); 
    
    // specifying largestUnit years puts the Duration into
    // calendar-aware mode. weird behavior, i know.
    // if not specified, all the time will be put into "hours".
		const dur = now.since(then, { largestUnit: "years" });
    
    if (largestFormattableUnit != null) {
      if (dur.abs()[largestFormattableUnit] >= 1) {
        el.innerHTML = "on " + then.toPlainDate().toLocaleString(locale, plainDateFormattingOptions);
        return;
		  }    
    }
		
    el.innerHTML = formatRelString(plainSimplify(dur), locale, durationFormattingOptions);
	});
}
Last updated: Jun 12, 2026 ()