JavaScript Intl API: Formatting Dates and Numbers
The Intl API formats dates, numbers, and currency using a user's locale without a library. How Intl.DateTimeFormat and Intl.NumberFormat work.
The Intl API is a built-in JavaScript namespace for locale-aware formatting — dates, numbers, currency, lists, and plurals rendered the way a reader in a given region actually expects to see them. It ships in every modern JavaScript engine, which means most of the date-and-number formatting that used to require a library is now a runtime feature.
Why locale-aware formatting matters
A date like 2026-03-04 is ambiguous outside its own context: is that March 4th or April 3rd? Number formatting has the same problem — 1.234 means one-point-two-three-four in the US and one thousand two hundred thirty-four in much of Europe, where the comma and period swap roles. Hardcoding a format string works until the app has users outside one region, at which point every date and number needs to adapt to the reader’s locale and, often, their currency.
Before Intl matured, teams reached for libraries to handle this. Intl moved the same behavior into the language itself, backed by the Unicode CLDR (Common Locale Data Repository), so formatting stays consistent with what other locale-aware software does.
Intl.DateTimeFormat
Intl.DateTimeFormat formats a Date object according to a locale and a set of style options:
const formatter = new Intl.DateTimeFormat("en-GB", {
year: "numeric",
month: "long",
day: "numeric",
});
formatter.format(new Date(2026, 2, 4)); // "4 March 2026"
Swap the locale to "en-US" with the same options and the output reorders to "March 4, 2026" — no manual string manipulation. The formatter also accepts a timeZone option, which matters for anything that has to render the same instant differently depending on where the viewer is.
Intl.DateTimeFormat handles display formatting; it doesn’t do date arithmetic or parsing. For working with the underlying date and time values themselves, the newer Temporal API is the better fit — the two are complementary, not competing.
Intl.NumberFormat
Intl.NumberFormat handles thousands separators, decimal points, percentages, and currency symbols:
new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(1234.5); // "1.234,50 €"
new Intl.NumberFormat("en-US", {
style: "percent",
}).format(0.42); // "42%"
The style: "currency" option requires an ISO 4217 currency code ("USD", "EUR", "JPY") — Intl formats the amount according to the locale but doesn’t do currency conversion; the numeric value you pass in is what gets displayed with the right symbol and grouping.
Intl.RelativeTimeFormat and Intl.ListFormat
Two smaller formatters solve problems that used to require hand-rolled logic. Intl.RelativeTimeFormat turns a numeric offset into phrasing like “3 days ago” or “in 2 hours”:
new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-3, "day");
// "3 days ago"
Intl.ListFormat joins an array into a grammatically correct sentence fragment:
new Intl.ListFormat("en", { style: "long", type: "conjunction" }).format(
["Node", "Deno", "Bun"]
); // "Node, Deno, and Bun"
Both save you from maintaining locale-specific pluralization and conjunction rules by hand — rules that vary more than most developers assume across languages.
Intl.Collator for sorting
Default JavaScript string sorting (Array.prototype.sort()) compares strings by UTF-16 code unit, which is not how any human alphabetizes text. It capitalizes incorrectly, mishandles accented characters, and gets numeric-looking strings wrong. Intl.Collator fixes this:
["Öl", "of", "öz"].sort(new Intl.Collator("de").compare);
For anything user-facing — a sorted list of names, a searchable table — passing Intl.Collator("locale").compare into sort() is worth doing even for apps that only ship in one language, since default sort order is still surprising for accented characters and mixed case.
When you still need a library
Intl covers formatting and comparison well, but it isn’t a complete internationalization toolkit. It doesn’t manage translation strings, pluralization message catalogs beyond Intl.PluralRules, or right-to-left layout — those still belong to an i18n framework layered on top. What Intl replaces specifically is the formatting logic that used to justify pulling in a dependency for date and number display alone. If you’re only formatting dates, currency, and lists, Intl is usually sufficient on its own; if you’re translating UI copy into multiple languages, you’ll still want a dedicated i18n library, with Intl handling the locale-sensitive formatting underneath it.
Because Intl is a runtime feature rather than a library, there’s no bundle-size cost to reach for it, and it’s worth knowing when picking a TypeScript project’s dependencies whether a date library is actually still needed. It also composes cleanly with toLocaleDateString(), toLocaleString(), and toLocaleTimeString(), which are thin convenience wrappers around Intl.DateTimeFormat and Intl.NumberFormat for one-off formatting without instantiating a formatter object.
The takeaway
The Intl API gives JavaScript native, locale-aware formatting for dates, numbers, currency, relative time, and lists, backed by the same Unicode CLDR data other platforms use. Reach for Intl.DateTimeFormat and Intl.NumberFormat before adding a formatting library, use Intl.Collator any time you sort user-facing strings, and remember that Intl formats values — it doesn’t translate copy or convert currency amounts, so a full i18n setup still needs a translation layer on top.
Keep reading
Takina · · 4 min read What Is a Lockfile? Reproducible Dependency Installs
A lockfile records the exact dependency versions your package manager resolved, so every install — from your laptop to CI — reproduces the same tree.
Takina · · 5 min read Turbopack vs Webpack: Choosing a JS Bundler
Turbopack is a Rust-based bundler built for incremental speed; Webpack is the mature, plugin-heavy standard. How they differ and when to pick each.
Takina · · 4 min read Import Maps Explained: Bare Specifiers Without a Bundler
Import maps let browsers resolve bare module specifiers like "react" to real URLs, enabling native ES module imports without a bundler.