Internationalization with Intl
Format numbers, dates, lists, plurals, display names, and locale-aware labels with JavaScript’s Intl APIs without guessing a reader’s conventions.
- 01Choose a localeRead locale tags with Intl.Locale and canonicalize user input safely.
- 02Format real dataUse NumberFormat, DateTimeFormat, RelativeTimeFormat, ListFormat, PluralRules, and DisplayNames.
- 03Write deterministic UIFix locales, options, dates, and time zones in tests while computing reader-locale demos on the client.
Same data, many audiences
INTERACTIVEInternationalization, often shortened to i18n, is the work of making an interface fit more than one language, region, script, calendar, numbering system, and writing convention. JavaScript’s Intl namespace gives you built-in formatters for the pieces that should not be hand-written: numbers, currency, dates, relative time, lists, plural categories, and names for codes.
Imagine handing the same invoice to several local translators. You do not change the amount owed. You tell each translator who will read it and whether it should look like currency, a percent, a date, or a list. Intl is that formatting team.
- In real life: Tell the translator the audience
- In JavaScript: Pass a locale such as
en-IN,hi-IN, orja-JP - In real life: Tell them the style
- In JavaScript: Pass options such as
style: "currency" - In real life: Hire once for repeated labels
- In JavaScript: Reuse a formatter instead of recreating it in a loop
Where the analogy stops: Intl formats structured data; it does not translate whole paragraphs, product copy, or legal text for you.
The playground uses one number, two currencies, one fixed date, one relative offset, one list, and plural categories. Pick a locale and notice that the data stays the same while the presentation changes. This is computed in the browser after mount; the server-rendered page does not guess the reader’s default locale.
const locale = "en-IN";const number = 1234567.891;const date = new Date("2026-01-15T13:30:00Z");const items = ["mangoes", "bananas", "guavas"]; new Intl.NumberFormat(locale).format(number);new Intl.NumberFormat(locale, { style: "currency", currency: "INR" }).format(number);new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(date);new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(-3, "day");new Intl.ListFormat(locale, { type: "conjunction" }).format(items);The browser computes these examples after mount so server rendering never guesses a reader's locale.
Locales & Intl.Locale
STEP THROUGHA locale tag is an address for an audience. hi-IN says Hindi as used in India. en-Latn-IN says English, Latin script, India. Unicode extension keys can add preferences such as calendars: ja-JP-u-ca-japanese asks for Japanese as used in Japan with the Japanese calendar.
An address gets a letter to the right audience. A locale tag gets text into the right conventions. The tag is compact, but it carries enough information for ICU data to make good formatting choices.
- In real life: Country and city on an envelope
- In JavaScript: Region and language in a locale tag
- In real life: Apartment details
- In JavaScript: Optional script, calendar, numbering system, or collation extensions
- In real life: A postal service normalizes the address
- In JavaScript:
getCanonicalLocales()normalizes casing and aliases
Where the analogy stops: Locales are preferences, not identity. A person may speak several languages or choose a locale that differs from where they live.
Step through a locale tag as an address: language, script, region, and supported data.
script
console.log(locale.language, locale.script, locale.region); const zh = new Intl.Locale("zh");console.log(zh.maximize().toString());console.log(zh.maximize().minimize().toString()); console.log(Intl.getCanonicalLocales(["EN-in"])[0]);console.log(Intl.supportedValuesOf("currency").length > 0);Intl.Locale is useful when you need to inspect or adjust a tag before formatting. maximize() and minimize() use likely-subtag data, so their results come from ICU, not from a rule you should copy by hand.
Intl.NumberFormat
STEP THROUGHNever store a price as “₹1,234.50”. Store the amount as a number or decimal-friendly domain value, then format it for humans at the edge of your UI. Intl.NumberFormat covers decimal numbers, currency, percents, units, compact notation, scientific notation, signs, digit counts, rounding, and formatToParts().
Watch one raw number become decimal text, currency text, unit text, and parts you can style.
script
new Intl.NumberFormat("en-IN").format(amount);new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR",}).format(amount);new Intl.NumberFormat("en-IN", { style: "unit", unit: "kilometer-per-hour", unitDisplay: "long",}).format(88);new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR",}).formatToParts(1234.5);Currency formatting needs both a locale and a currency code because the locale and the currency are different decisions. A German page can show Japanese yen, and a Japanese page can show euros. For custom visual emphasis, use formatToParts() instead of parsing punctuation yourself.
Intl.DateTimeFormat & RelativeTimeFormat
STEP THROUGHDateTimeFormat formats instants and calendar values for an audience. In examples and tests, always pass explicit locales and a timeZone; otherwise the same instant may render as a different day or clock time on another machine. The earlier Date & time lesson introduced toLocaleString; here you are using the formatter directly so you can reuse it and specify exact options.
Dates and relative times are also audience-specific, so fix timeZone in examples and tests.
script
const ended = new Date("2026-01-18T13:30:00Z"); new Intl.DateTimeFormat("en-IN", { dateStyle: "medium", timeStyle: "short", timeZone: "Asia/Kolkata",}).format(started);new Intl.DateTimeFormat("en-IN", { month: "short", day: "numeric", timeZone: "Asia/Kolkata",}).formatRange(started, ended);new Intl.RelativeTimeFormat("en-IN", { numeric: "auto" }).format(-1, "day");new Intl.RelativeTimeFormat("en-IN", { numeric: "always" }).format(-1, "day");RelativeTimeFormat receives a signed number and a unit. Negative values point to the past; positive values point to the future. With numeric: "auto", a locale may use words such as yesterday. With numeric: "always", it stays numeric: 1 day ago.
Intl.PluralRules & ListFormat
PluralRules does not write a sentence. It chooses a category for a number in a locale, and your message table supplies the words. English cardinal plurals mostly use one and other; Arabic has more categories. For ar-EG, a few checked values are: 0:zero, 1:one, 2:two, 3:few, 11:many, 100:other.
Ordinal rules are separate. An English helper can map one to st, two to nd, few to rd, and other to th. That gives 1st, 2nd, 3rd, 4th, and 22nd.
ListFormat joins complete items. Indian English conjunction style gives Asha, Ravi and Meera; other locales may use different punctuation or words. Use type: "disjunction" for “or” lists.
Intl.DisplayNames & Intl.DurationFormat
FEATURE DETECTDisplayNames turns standardized codes into localized labels. Verified examples: US as a French region is États-Unis, de as a language in Japanese is ドイツ語, and JPY as an English currency is Japanese Yen.
Intl.DurationFormat is newer and is not available in Node 22. Browser support can differ, so production code should feature-detect before using it. The playground below shows a duration only if the current browser actually implements the API.
if ("DurationFormat" in Intl) { const formatter = new Intl.DurationFormat("en-IN", { style: "long" }); formatter.format({ hours: 3, minutes: 15 });} else { "Intl.DurationFormat is not available in this runtime yet.";}Press Check support to ask this browser.
Press Check support to ask this browser.
getCanonicalLocales & supportedValuesOf
Intl.getCanonicalLocales(["EN-in"]) returns ["en-IN"]. Use it to validate and normalize user-entered locale tags before saving preferences. Intl.supportedValuesOf("currency") returns supported currency codes; supportedValuesOf("calendar"), "unit", "numberingSystem", and "timeZone" are also useful when building settings UIs. Check membership or that the list is non-empty; do not hard-code counts because ICU data changes.
| API | Purpose | Example |
|---|---|---|
| Intl.Locale | Parse and adjust locale tags | new Intl.Locale("en-IN").region → IN |
| Intl.NumberFormat | Format decimals, currency, percents, units, notation, signs, and parts | en-IN currency → ₹1,234.50 |
| Intl.DateTimeFormat | Format dates, times, ranges, calendars, and time zones | timeZone: "UTC" keeps tests stable |
| Intl.RelativeTimeFormat | Say yesterday, 3 days ago, or localized equivalents | numeric: "auto" can use words |
| Intl.PluralRules | Choose plural categories for text you write | English has one/other; Arabic has more |
| Intl.ListFormat | Join lists with localized conjunctions or disjunctions | A, B, and C vs localized punctuation |
| Intl.DisplayNames | Show names for regions, languages, currencies, scripts, and more | US in French → États-Unis |
| Intl.DurationFormat | Format duration-like records when supported | Feature-detect: not in Node 22 |
Where you’ll use this
SORTERIntl belongs wherever raw data becomes reader-facing text: invoices, dashboards, product cards, travel calendars, analytics summaries, file timestamps, preference screens, and accessibility labels. It also builds on earlier text lessons: use Comparing & sorting text for Intl.Collator, and remember the Unicode lesson’s Intl.Segmenter when splitting user-visible text.
- Show
1234.5as rupees for India - Show
88as kilometers per hour - Show a meeting in the Kolkata time zone
- Say
3 hours ago - Choose whether to say item or items
- Join Asha, Ravi, and Meera naturally
- Show
USas a country name in French - Turn
EN-inintoen-IN - Check which currency codes this engine knows
Sort each job by the Intl tool that should handle it.
Use explicit locales, options, fixed dates, and fixed time zones. ICU data versions can change spaces, especially non-breaking spaces in times and currencies, so tests may normalize U+00A0 and U+202F when spacing is not the point.
Common misconceptions
“Intl translates my whole app.”
No. Intl formats structured data and some standardized names. You still need translated messages for sentences.
“The browser default locale is fine everywhere.”
Defaults are convenient for quick UI, but examples, SSR, and tests need explicit locales and options.
“A locale and a country are the same thing.”
A locale is a preference bundle. It may include language, script, region, calendar, numbering system, and more.
“Formatted numbers are safe to parse later.”
Formatted text is for humans. Keep raw data separately; use parts for styling, not parsing.
“All Intl APIs exist everywhere.”
Most APIs in this lesson are in Node 22 with full ICU. Intl.DurationFormat is not, so feature-detect it.
Practice: format for real audiences
5 EXERCISESRun both lines. Check the Indian rupee answer here, then compare the Japanese yen output yourself.
console.log(new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }).format(1234.5));
console.log(new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY" }).format(1234.5));console.log(new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }).format(1234.5));The Indian formatter prints ₹1,234.50. The Japanese formatter rounds JPY to ¥1,235 because yen has no minor unit in normal currency formatting.
Use ordinal plural rules to return the right suffix for 22.
const rules = new Intl.PluralRules("en-IN", { type: "ordinal" });
const suffixes = { one: "st", two: "nd", few: "rd", other: "th" };
const n = 22;
console.log(String(n) + (suffixes[rules.select(n)] ?? "th"));const rules = new Intl.PluralRules("en-IN", { type: "ordinal" });
const suffixes = { one: "st", two: "nd", few: "rd", other: "th" };
const n = 22;
console.log(String(n) + (suffixes[rules.select(n)] ?? "th"));The ordinal category for 22 in Indian English is two, so the suffix table chooses nd and prints 22nd.
Use RelativeTimeFormat with explicit en-IN and numeric: "always".
console.log(new Intl.RelativeTimeFormat("en-IN", { numeric: "always" }).format(-3, "hour"));console.log(new Intl.RelativeTimeFormat("en-IN", { numeric: "always" }).format(-3, "hour"));RelativeTimeFormat receives -3 and hour, so Indian English prints 3 hours ago.
Join Asha, Ravi, and Meera for Indian English readers.
console.log(new Intl.ListFormat("en-IN", { type: "conjunction" }).format(["Asha", "Ravi", "Meera"]));console.log(new Intl.ListFormat("en-IN", { type: "conjunction" }).format(["Asha", "Ravi", "Meera"]));ListFormat supplies the commas and and for this Indian English locale.
Use DisplayNames to show the United States in French.
console.log(new Intl.DisplayNames(["fr"], { type: "region" }).of("US"));console.log(new Intl.DisplayNames(["fr"], { type: "region" }).of("US"));DisplayNames uses French region names, so US becomes États-Unis.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7What two things do most Intl formatters need?
Choose an answer to see the explanation.
Question 2 of 7What does DisplayNames print here?
Read the code, then predictconsole.log(Intl.getCanonicalLocales(["EN-in"])[0]);Choose an answer to see the explanation.
Question 3 of 7What does this print in Node 22 with full ICU?
Read the code, then predictconsole.log(new Intl.RelativeTimeFormat("en-IN", { numeric: "auto" }).format(-1, "day"));Choose an answer to see the explanation.
Question 4 of 7Which API should choose
one,few,many, orotherbefore you pick words?Choose an answer to see the explanation.
Question 5 of 7What does this print?
Read the code, then predictconsole.log(new Intl.DisplayNames(["fr"], { type: "region" }).of("US"));Choose an answer to see the explanation.
Question 6 of 7Why should tests pass explicit
timeZoneto DateTimeFormat?Choose an answer to see the explanation.
Question 7 of 7What should you do before using Intl.DurationFormat in this lesson’s environment?
Choose an answer to see the explanation.
Key takeaways
- Intl formats structured data for a locale; it does not translate full messages.
- Think audience plus style: locale tags name the audience, options name the style.
- Reuse formatters when formatting many values with the same locale and options.
- Pin locales, options, dates, and time zones in tests; normalize special spaces only when appropriate.
- Feature-detect newer APIs such as
Intl.DurationFormat.
Remember the one-liner.
Intl is JavaScript’s built-in formatting team: give it a locale and options, and it writes human-facing text for that audience.
Up next: The JavaScript runtime at a glance.