jsguides

The Intl API: Formatting and Localization

Introduction

JavaScript’s built-in Intl API provides language-sensitive formatting and processing for dates, numbers, and text. Instead of manually formatting strings based on locale conventions, you can use the Intl namespace to handle the complexity.

The API is available in all modern browsers and Node.js without any external dependencies. It supports:

  • Date and time formatting
  • Number formatting (including currency and percentages)
  • Relative time formatting
  • String comparison and sorting
  • Text segmentation

This guide walks through each API with practical examples you can use in your projects.

Date and time formatting with Intl.DateTimeFormat

The Intl.DateTimeFormat API formats dates according to locale conventions.

Basic Usage

const date = new Date("2026-03-15T14:30:00Z");

const usFormatter = new Intl.DateTimeFormat("en-US");
console.log(usFormatter.format(date));
// "3/15/2026"

const deFormatter = new Intl.DateTimeFormat("de-DE");
console.log(deFormatter.format(date));
// "15.3.2026"

Customizing the Output

The options object gives you full control over which date and time components appear, in what format, and in which timezone. Each option like weekday, year, or hour accepts values such as "long", "numeric", or "2-digit". The formatter builds the output string by combining the parts you request in a locale-appropriate order:

const date = new Date("2026-03-15T14:30:00Z");

const formatter = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  timeZone: "America/New_York"
});

console.log(formatter.format(date));
// "Sunday, March 15, 2026, 10:30 AM"

Manually listing every field works, but for common patterns the dateStyle and timeStyle presets are shorter. These presets select a sensible combination of fields based on the locale, so you get a correctly formatted date without specifying each component individually:

Preset Styles

Instead of specifying each component, use dateStyle and timeStyle:

const date = new Date("2026-03-15T14:30:00Z");

const shortFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short"
});
console.log(shortFormatter.format(date));
// "3/15/26"

const longFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "long",
  timeStyle: "medium"
});
console.log(longFormatter.format(date));
// "March 15, 2026, 10:30:00 AM"

Supported styles: full, long, medium, short.

number formatting with Intl.NumberFormat

Format numbers, currency, and percentages with Intl.NumberFormat.

Basic number formatting

const number = 1234567.89;

const usFormatter = new Intl.NumberFormat("en-US");
console.log(usFormatter.format(number));
// "1,234,567.89"

const deFormatter = new Intl.NumberFormat("de-DE");
console.log(deFormatter.format(number));
// "1.234.567,89"

The base constructor handles grouping separators and decimal marks automatically. For financial or e-commerce use cases, switch to style: "currency" and specify the ISO 4217 currency code. The formatter places the symbol, handles negative amounts, and adjusts decimal places per the locale:

Currency Formatting

const amount = 1999.99;

const usdFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});
console.log(usdFormatter.format(amount));
// "$1,999.99"

const eurFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});
console.log(eurFormatter.format(amount));
// "1.999,99 €"

Currency formatting treats the number as a monetary value and applies appropriate rounding. For ratios and rates, switch to style: "percent". Percentages multiply the input by 100 and append the percent sign, and you can control the number of decimal digits shown:

Percentages

const ratio = 0.756;

const percentFormatter = new Intl.NumberFormat("en-US", {
  style: "percent",
  minimumFractionDigits: 1
});
console.log(percentFormatter.format(ratio));
// "75.6%"

Percent formatting is ideal for completion rates, tax rates, and any value naturally expressed as a fraction of 100. Pass a decimal like 0.756 and it becomes "75.6%" automatically. For very large numbers like view counts or file sizes, notation: "compact" produces human-readable abbreviations like “1.2K” or “3.5M”:

Compact Notation

const large = 1234567;

const compact = new Intl.NumberFormat("en-US", {
  notation: "compact",
  maximumSignificantDigits: 3
});
console.log(compact.format(large));
// "1.23M"

Compact notation picks the appropriate suffix (K, M, B) based on the magnitude of the number and the locale’s conventions. For time-based expressions like “3 days ago” or “in 2 hours”, switch to the Intl.RelativeTimeFormat API instead:

relative time with Intl.RelativeTimeFormat

Format relative times like “3 days ago” or “in 2 hours”. This API requires ES2020 or later.

const rtf = new Intl.RelativeTimeFormat("en", { style: "long" });

console.log(rtf.format(-1, "day"));
// "1 day ago"

console.log(rtf.format(3, "week"));
// "in 3 weeks"

console.log(rtf.format(-2, "hour"));
// "2 hours ago"

The style: "long" option produces full phrases. For more compact output, use "short" or "narrow". The narrow style abbreviates the unit to a single character, which is useful for space-constrained UIs like notification badges or timeline labels:

Short and narrow styles

const rtfShort = new Intl.RelativeTimeFormat("en", { style: "short" });
console.log(rtfShort.format(-1, "day"));
// "1 day ago"

const rtfNarrow = new Intl.RelativeTimeFormat("en", { style: "narrow" });
console.log(rtfNarrow.format(-1, "day"));
// "1d ago"

string sorting with Intl.Collator

The Intl.Collator API provides language-sensitive string comparison and sorting.

Basic Sorting

const names = ["ä", "z", "a", "b"];

const enCollider = new Intl.Collator("en");
console.log(names.sort(enCollider.compare));
// ["a", "b", "ä", "z"]

const svCollider = new Intl.Collator("sv");
console.log(names.sort(svCollider.compare));
// ["a", "ä", "b", "z"]

The Collator.compare function plugs directly into Array.sort(). The Swedish collator sorts “ä” after “a” because Swedish treats “ä” as a distinct letter at the end of the alphabet, while the English collator treats it as a variant of “a”. For strings that contain numbers, enable numeric: true so “file10” sorts after “file2” instead of between “file1” and “file2”:

Numeric Sorting

const files = ["file1.txt", "file10.txt", "file2.txt"];

const numericCollator = new Intl.Collator("en", { numeric: true });
console.log(files.sort(numericCollator.compare));
// ["file1.txt", "file2.txt", "file10.txt"]

Without numeric: true, the default lexicographic sort would produce ["file1.txt", "file10.txt", "file2.txt"]. For case-insensitive comparisons, the sensitivity option controls whether the collator treats uppercase and lowercase as identical. "base" ignores case and diacritics entirely; "variant" considers every difference:

Sensitivity Options

const collatorBase = new Intl.Collator("en", { sensitivity: "base" });
console.log(collatorBase.compare("a", "A"));
// 0 (equal)

const collatorVariant = new Intl.Collator("en", { sensitivity: "variant" });
console.log(collatorVariant.compare("a", "A"));
// -1 (not equal)

text segmentation with Intl.Segmenter

The Intl.Segmenter API splits text into segments by character, word, or sentence. This API requires ES2020 or later.

Word Segmentation

const segmenter = new Intl.Segmenter("en", { granularity: "word" });

const text = "Hello world, how are you?";
const segments = segmenter.segment(text);

for (const segment of segments) {
  if (segment.type === "word") {
    console.log(`"${segment.segment}" [word]`);
  }
}
// "Hello" [word]
// "world" [word]
// "how" [word]
// "are" [word]
// "you" [word]

Word segmentation is useful for word counting, text analysis, and building read-time estimators. For larger structures like full sentences — important when you need to split a paragraph into individual statements for translation or summarization — switch the granularity to "sentence":

Sentence Segmentation

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });

const text = "Hello! How are you? I hope you're well.";
const segments = segmenter.segment(text);

for (const segment of segments) {
  console.log(`"${segment.segment}" [sentence]`);
}
// "Hello!" [sentence]
// "How are you?" [sentence]
// "I hope you're well." [sentence]

Common Pitfalls

Creating formatters inside loops

Avoid creating new formatter instances inside loops. Formatters are expensive to create:

// Bad
const dates = [new Date(), new Date(), new Date()];
dates.forEach(date => {
  const formatter = new Intl.DateTimeFormat("en-US");
  console.log(formatter.format(date));
});

// Good
const formatter = new Intl.DateTimeFormat("en-US");
dates.forEach(date => {
  console.log(formatter.format(date));
});

Conflicting Options

When using dateStyle or timeStyle, individual component options are ignored. The preset style already selects its own set of components, so any explicit fields you pass are silently dropped. If you need fine-grained control over individual fields, skip the presets and configure each component yourself:

const date = new Date("2026-03-15T14:30:00Z");

const formatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short",
  day: "numeric"  // This option is ignored
});
console.log(formatter.format(date));
// "3/15/26"

Locale string format

Use BCP 47 language tags for locales. These follow the pattern language-region (like en-US or de-DE). Passing an arbitrary string like "english" may fall back to the runtime’s default locale instead of throwing an error, which means you might not notice the problem until users report incorrect formatting:

// Correct
const formatter = new Intl.DateTimeFormat("en-US");
const formatter2 = new Intl.DateTimeFormat("de-DE");

// May not work as expected
const badFormatter = new Intl.DateTimeFormat("english");

When Intl helps most

The Intl API is most valuable when your app serves people in more than one locale, or when the format should match the user’s expectations rather than a single hard-coded style. Dates, numbers, relative time, and sorting all have rules that vary by language and region. Intl gives you those rules without forcing you to maintain your own formatting tables.

It is especially useful in interfaces where the same data needs to appear in several places: dashboards, emails, receipts, exports, and admin screens. If the output is user-facing, locale-sensitive, and likely to be reused, Intl is usually the right layer to reach for.

Choosing locale and defaults

The locale string is not just a label. It directly affects punctuation, order, separators, and even the shape of the words that appear. When possible, pass the user’s preferred locale rather than guessing. If you do not have a user preference, let the runtime fallback to its default locale instead of hard-coding one that may not match the audience.

That approach keeps the app flexible. It also makes testing easier because you can intentionally compare one locale against another and see the differences clearly. For applications that ship globally, this is often the difference between output that feels natural and output that feels translated by hand.

Fallbacks and feature detection

Not every environment supports every Intl feature at the same level. DateTimeFormat and NumberFormat are common and broadly available, while newer APIs such as Segmenter may not exist everywhere. A good pattern is to check support for the specific formatter you need, then fall back to a simpler path if it is missing.

That does not mean you need a complicated compatibility layer for every call site. In many cases, a short conditional around the formatter constructor is enough. The goal is not to mimic every locale rule manually. The goal is to keep the app working gracefully when a newer API is unavailable.

Formatting versus parsing

Intl is primarily about presentation, not about validating user input. A number can be formatted for display in a locale-specific way, but parsing that same string back into a number is a different problem. If you need to accept user-entered text, build a separate input pipeline instead of assuming that formatted output can be read back without ambiguity.

That separation helps avoid subtle bugs. Formatting should make text easier for people to read. Parsing should make text safe and predictable for code to consume. Keeping those two concerns apart makes the whole system easier to reason about.

Reusing formatter instances

Formatter constructors are relatively expensive, so create them once and reuse them when you can. This matters most in lists, tables, and repeated renders. Creating a new formatter for every row or every item wastes work that can be avoided by holding onto the instance and calling format() repeatedly.

Caching also makes intent clearer. A named formatter shows that the output is supposed to share the same rules across the page or feature. That is better than rebuilding the same configuration over and over again in a loop or render path.

Building a localization habit

The strongest use of Intl is not one specific API call. It is the habit of treating formatting as a locale-aware concern from the start. Once you do that, you begin to look for places where a hard-coded format might break for another language, another region, or another calendar convention. That mindset keeps internationalization from becoming an afterthought.

If the guide becomes part of your workflow, the same patterns will apply across the rest of the app: use locale-aware display functions, reuse formatters, test a couple of locales, and keep raw values separate from formatted text. That is the core of practical internationalization in JavaScript.

Frequently asked questions

Do I need Intl for every app?

Not every app needs to expose every locale feature, but most user-facing apps benefit from at least a small amount of locale-aware formatting. If you show dates, currency, or counts, Intl usually gives you better output with less code than a hand-built formatter.

Should I format data before storing it?

Usually no. Store raw values when you can, then format them at the edges where the user sees them. That keeps your data easier to search, compare, and reformat later. Formatting is a presentation concern, so it belongs as close to the UI as possible.

What is the easiest API to start with?

For most teams, Intl.NumberFormat and Intl.DateTimeFormat are the first two constructors to learn. They cover the most common display cases and are easy to reuse. Once those feel familiar, RelativeTimeFormat, Collator, and Segmenter become natural next steps.

Format at the edge

The strongest use of Intl is not one specific formatter. It is the habit of keeping raw data separate from the display rules that turn it into something a person reads. If you store a date, a number, or a measurement in a neutral form and format it only when rendering, the rest of the app stays easier to search, compare, and test. That split also makes it easier to switch locales without rewriting the business logic.

It helps to reuse formatter instances wherever possible. A page with a long table or many repeated labels should not rebuild the same formatter for every cell. Creating the formatter once keeps the code simpler and avoids unnecessary work. That is a small habit, but in a UI that formats a lot of values, it can make the output path cleaner and more predictable.

See Also