Formatting Dates and Numbers with Intl
Format dates, times, relative times, numbers, currencies, units and lists correctly in an extension's UI with the Intl APIs: matching the browser UI language, caching formatters, time zones, and testing locales.
Table of Contents
- Which locale to format for
- Step-by-step: locale-correct formats
- 1. Create formatters once, from the UI language
- 2. Format dates and times
- 3. Show relative times for recent events
- 4. Format numbers, sizes and compact counts
- 5. Format currency with the currency’s own code
- 6. Join lists naturally
- 7. Format in the service worker and content scripts too
- 8. Test with several locales
- Common mistakes
- Cross-browser variation
- Verification
- FAQ
- Related
The extension’s popup is fully translated into German, but it still says “Saved 10/2/2026” and “1,234.5 MB”, which a German reader parses as 2 October in the wrong order and a number about a thousand times smaller. Translation covers words; formats cover everything else — dates, times, “3 minutes ago”, thousands separators, currencies, units and lists (“A, B and C”). The JavaScript Intl APIs handle all of these for every locale the browser knows, with no data files to ship, as long as the extension asks for the right locale and reuses formatters instead of rebuilding them. This guide covers the formatters an extension UI typically needs. It belongs to internationalization and accessibility.
Which locale to format for
An extension’s strings come from chrome.i18n, which uses the browser’s UI language. Formats should follow the same locale, so the page reads consistently: chrome.i18n.getUILanguage() returns it (for example de or pt-BR). navigator.language usually agrees but can differ — it reflects the accept-language preference in some setups — and in a content script it describes the browser, not the page you are on. Pages you inject into have their own language (document.documentElement.lang), which matters only if your UI is mimicking the page. For time zones, Intl uses the system zone by default; dates stored as UTC timestamps are formatted into the user’s local time automatically.
Step-by-step: locale-correct formats
1. Create formatters once, from the UI language
1// format.js
2export const LOCALE = chrome.i18n.getUILanguage();
3
4export const fmt = {
5 date: new Intl.DateTimeFormat(LOCALE, { dateStyle: "medium" }),
6 dateTime: new Intl.DateTimeFormat(LOCALE, { dateStyle: "medium", timeStyle: "short" }),
7 time: new Intl.DateTimeFormat(LOCALE, { timeStyle: "short" }),
8 number: new Intl.NumberFormat(LOCALE),
9 percent: new Intl.NumberFormat(LOCALE, { style: "percent", maximumFractionDigits: 0 }),
10 relative: new Intl.RelativeTimeFormat(LOCALE, { numeric: "auto" }),
11 list: new Intl.ListFormat(LOCALE, { style: "long", type: "conjunction" }),
12};
Execution context: a shared module imported by popup, options, side panel and content scripts. Constructing Intl formatters is relatively expensive; calling .format() on a cached instance is cheap. Long lists in a popup — hundreds of saved items with dates — render noticeably faster with cached formatters than with toLocaleDateString() called per row, which builds a formatter each time.
2. Format dates and times
1fmt.date.format(new Date(item.savedAt)); // en: "Oct 2, 2026" de: "02.10.2026" ja: "2026/10/02"
2fmt.dateTime.format(Date.now()); // de: "02.10.2026, 14:05"
3fmt.date.formatRange(start, end); // en: "Oct 2 – 9, 2026"
Execution context: any UI context. dateStyle and timeStyle pick the locale’s conventional formats without hard-coding patterns. Store timestamps as numbers (milliseconds since epoch, UTC) in chrome.storage and format at display time; never store formatted strings, which freeze the locale and time zone of whoever saved them.
3. Show relative times for recent events
1const UNITS = [["year", 31536e6], ["month", 2592e6], ["week", 6048e5], ["day", 864e5], ["hour", 36e5], ["minute", 6e4], ["second", 1e3]];
2
3export function ago(ts, now = Date.now()) {
4 const diff = ts - now;
5 for (const [unit, ms] of UNITS) {
6 if (Math.abs(diff) >= ms || unit === "second") return fmt.relative.format(Math.round(diff / ms), unit);
7 }
8}
9// ago(Date.now() - 180000) → en "3 minutes ago", de "vor 3 Minuten"; with numeric:"auto", -1 day → "yesterday"
Execution context: UI code. RelativeTimeFormat handles plurals and word order for every locale. Switch to an absolute date after a week or so — “5 months ago” is less useful than a date. For a live “next run in 4 minutes” label, see showing the next scheduled run in the UI.
4. Format numbers, sizes and compact counts
1const bytes = new Intl.NumberFormat(LOCALE, { style: "unit", unit: "megabyte", maximumFractionDigits: 1 });
2bytes.format(1234.5); // en "1,234.5 MB" de "1.234,5 MB"
3
4const compact = new Intl.NumberFormat(LOCALE, { notation: "compact" });
5compact.format(15300); // en "15K" de "15.300" ja "1.5万"
6
7fmt.percent.format(0.42); // en "42%" de "42 %" fr "42 %"
Execution context: UI code. style: "unit" supports a fixed list of units (byte, kilobyte, megabyte, gigabyte, second, minute and more); pick the unit yourself based on magnitude. Compact notation suits badges and dense lists. Note the non-breaking spaces some locales insert before % and units — don’t strip them.
5. Format currency with the currency’s own code
1export function price(amount, currency) {
2 return new Intl.NumberFormat(LOCALE, { style: "currency", currency }).format(amount);
3}
4price(9.99, "EUR"); // en "€9.99" de "9,99 €"
5price(1200, "JPY"); // ja "¥1,200" — no decimals for yen
Execution context: UI code. The currency (what the money is) and the locale (how the reader formats numbers) are separate inputs — a German user viewing a US price sees “9,99 $”. Never infer currency from the locale. Use integer minor units for storage and divide only at display time to avoid floating-point surprises.
6. Join lists naturally
1fmt.list.format(["Reddit", "YouTube", "X"]); // en "Reddit, YouTube, and X" de "Reddit, YouTube und X"
Execution context: UI code. ListFormat handles conjunctions, the Oxford comma and the Japanese 、 separator, which hand-rolled join(", ") cannot. Use type: "disjunction" for “A, B, or C”.
7. Format in the service worker and content scripts too
Intl is available in service workers and content scripts, so notification text, badge tooltips and in-page overlays can use the same module. In content scripts, remember the formatters reflect the browser’s language, which is right for your UI but may differ from the page’s language. See plurals and placeholders in messages.json for count-dependent sentences.
8. Test with several locales
1google-chrome --lang=de --user-data-dir=/tmp/de-profile --load-extension=dist/chrome
Execution context: a test machine. Launching with --lang changes the browser UI language, which drives both getMessage and getUILanguage. In unit tests, inject the locale into your format module and assert outputs for en, de, ar and ja — they cover most separator, direction and script differences.
Common mistakes
- Hard-coded date patterns.
MM/DD/YYYYis wrong for most of the world. - Storing formatted strings. They freeze locale and time zone.
- Creating a formatter per row. Cache them.
- Inferring currency from locale. They are independent.
- Mixing
navigator.languageandgetUILanguage(). Strings and formats disagree.
Cross-browser variation
- Chrome / Edge: full
Intlsupport includingListFormat,RelativeTimeFormat,formatRangeand unit styles. - Firefox: full support;
browser.i18n.getUILanguage()returns the Firefox locale. - Safari: supports the same APIs in current versions; check
formatRangeand newerNumberFormatoptions against your minimum Safari version.
Verification
- Launch with
--lang=deand--lang=jaand confirm dates, numbers and lists match local conventions. - Change the system time zone and confirm stored timestamps display in local time.
- Profile a popup listing 500 items and confirm formatter construction does not appear in the hot path.
- Confirm a price in USD displays with the right symbol in a German UI.
FAQ
Should I use toLocaleString?
It is fine for one-off values. For lists, cache an Intl formatter instead.
How do I show the time zone?
Add timeZoneName: "short" to DateTimeFormat options, or format in a specific zone with timeZone: "Europe/Berlin".
Can I let users pick a date format?
Yes — store their preference (for example, “ISO” or “locale”) and choose a formatter accordingly. Most users are best served by the locale default.
Does Intl add to the bundle size?
No. The locale data ships with the browser.
What about calendars other than Gregorian?
Intl.DateTimeFormat follows the locale’s default calendar and accepts a calendar option such as "islamic", "hebrew" or "japanese". Respect the locale default unless your feature is tied to a specific calendar, and keep storage in UTC milliseconds either way.
Related
- Plurals and placeholders in messages.json — count-dependent strings.
- Localising extension UI with the i18n API — strings.
- Supporting RTL locales in extension pages — direction.
- Internationalization and accessibility — the parent topic.