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 the parts of a locale tag, such as
en-IN, and fix a tag a user typed. - 02Format real dataWrite numbers, prices, dates, phrases like
3 days ago, lists, plural words, and country names for any audience. - 03Get the same output every timeFix the locale, options, date, and time zone, so examples and tests show the same text on every computer.
Same data, many audiences
INTERACTIVEPeople in different countries write numbers, prices, and dates in different ways. In the US, a big number looks like 1,234,567.891. In Germany, the same number looks like 1.234.567,891.
Making a website work for many languages and regions is called internationalization. People often shorten it to i18n: an i, then 18 more letters, then an n.
JavaScript has a built-in object called Intl for this job. It holds a set of formatters. A formatter is a tool that turns raw data into text a person can read. Intl has formatters for:
- numbers and prices;
- dates and times;
- phrases such as
3 days ago; - lists such as
A, B, and C; - the right word for a count, such as
1 itemor3 items; - names of countries, languages, and currencies.
Each formatter needs a locale. A locale is a short code that names your reader's language and region. For example, en-US means English as used in the United States. The rules for each locale come built into the browser and Node, in a large collection called ICU data (ICU stands for International Components for Unicode). You never write these rules yourself.
const price = 1234.5;const label = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(price);console.log(label);Line 1 keeps the price as the number 1234.5. You can add to it and compare it. Line 2 makes a formatter for US English and US dollars. It turns the number into the text $1,234.50. Line 3 prints that text.
So keep raw values, such as the number 1234.5, in your variables and in the data you send to a server. Turn them into text only where a person reads them. The text "$1,234.50" is only for showing. You can't safely add it, compare it, or send it back as a number.
Imagine you send the same bill to readers in several countries. In each country, a local helper writes it out. You never change the amount owed. You only tell each helper two things: who will read the bill, and what kind of amount each line is (money, a percent, or a date). Each helper writes the bill the way their readers expect.
- In real life: Tell the helper who will read the bill
- In JavaScript: Pass a locale such as
en-IN,hi-IN, orja-JP - In real life: Tell the helper what kind of amount it is
- In JavaScript: Pass options such as
style: "currency" - In real life: Explain the job to one helper once, then hand over many bills
- In JavaScript: Make one formatter and use it for many values
The playground below uses fixed data: one number, one date, one list, and a few more. Pick a locale with the buttons. The data stays the same. Only the way it is written changes. Your browser works out the results after the page loads, so the server never has to guess which locale you use.
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);Your browser works out these results after the page loads, so the server never guesses your locale.
Locale tags and Intl.Locale
STEP THROUGHA locale tag is the short code that names an audience. Its parts are joined by dashes. Here are three tags:
hi-INmeans Hindi, as used in India.en-Latn-INmeans English, written in Latin letters, as used in India.ja-JP-u-ca-japanesemeans Japanese, as used in Japan, with the Japanese calendar. The-u-starts extra choices, andcais short for calendar.
en-Latn-IN means English, written in Latin letters, as used in India.An address on an envelope gets a letter to the right home. A locale tag gets text to the right readers. The tag is short, but it says enough for ICU data to pick the right way to write numbers and dates.
- In real life: The country and city on an envelope
- In JavaScript: The language and region in a locale tag
- In real life: The flat number
- In JavaScript: Optional extra parts: a script, a calendar, or a number system
- In real life: The post office writes the address the standard way
- In JavaScript:
getCanonicalLocales()fixes capital letters and old names in a tag
You read a locale tag's parts, filled in and dropped likely parts, fixed the capital letters, and checked the known currencies.
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 turns a tag into an object. Then you can read each part, such as locale.region. Use it when you need to check or change a tag before you format anything. Two of its methods add or drop parts:
maximize()adds the most likely missing parts. Plainzh(Chinese) becomeszh-Hans-CN: Chinese, Simplified characters, China.minimize()drops the parts that the likely guess already gives.zh-Hans-CNbecomeszhagain.
Both guesses come from ICU data. Don't copy them into your own rules by hand.
Numbers: Intl.NumberFormat
STEP THROUGHNever store a price as the text “₹1,234.50”. Store the amount as a number. Turn it into text only where a person reads it. Intl.NumberFormat can write a number in many ways:
- a plain number with the right separators, such as
1,234.5; - money, such as
$12.50; - a percent, such as
12.3%; - a number with a unit, such as
88 kilometres per hour; - a short form, such as
1.2K, or science style, such as1.2E3; - a number with a sign, such as
+5, or a set number of digits; - labelled pieces that you can style one by one, with
formatToParts().
const price = 12.5;console.log(new Intl.NumberFormat("en-US", { style: "currency", currency: "USD",}).format(price));Line 1 keeps the price as a number. Lines 2 to 5 make a formatter for US English and US dollars, and use it at once. The output is $12.50. The formatter adds the dollar sign and a second digit after the dot, only in the text. It does not change price.
Raw numbers became plain number text, money text, a unit phrase, and labelled pieces 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);For money, you pass two separate things: the locale and the currency. The locale says how to write the amount. The currency says which money it is. So a German page can show Japanese yen, and a Japanese page can show euros.
Sometimes you want to style one piece, such as making the currency sign smaller. Use formatToParts() for that. It gives back the text as a list of labelled pieces. Don't cut the text apart at the commas and dots yourself, because each locale uses different marks.
const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1,});console.log(compact.format(1200));console.log(compact.format(1200000));Lines 1 to 4 make one formatter. notation: "compact" asks for the short form. maximumFractionDigits: 1 allows at most one digit after the dot. Lines 5 and 6 use the same formatter twice. They print 1.2K and 1.2M.
Short forms are handy in dashboards. Still keep the exact number, for tooltips, for sorting, and for screen readers.
Making a formatter takes some work. So in a table or a loop, make it once for each locale and set of options. Then call format() as many times as you need. Don't make a new formatter for every row.
Dates: DateTimeFormat and RelativeTimeFormat
STEP THROUGHIntl.DateTimeFormat writes dates and times for an audience. It needs to know the time zone. A time zone is a region where clocks show the same time, such as India. UTC is the world's reference clock.
The same moment shows a different clock time in each time zone. So in examples and tests, always pass a locale and a timeZone. If you don't, another computer may show a different time, or even a different day.
The Date & time lesson used toLocaleString(). Here you make the formatter yourself. That lets you reuse it and choose the exact options.
const instant = new Date("2026-01-15T13:30:00Z");const utc = new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC",});console.log(utc.format(instant));console.log(new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeStyle: "short", timeZone: "Asia/Kolkata",}).format(instant));Line 1 fixes one moment. The Z at the end means UTC. Lines 2 to 6 make a formatter with a medium date, a short time, and the UTC time zone. Line 7 prints 15 Jan 2026, 13:30 in Node 22. Lines 8 to 12 write the same moment in the Asia/Kolkata time zone, which is India's. They print 15 Jan 2026, 19:00.
timeZone option only picks which clock to read it from. At 13:30 in UTC, the clocks in India already say 19:00.Dates and phrases like yesterday also depend on the reader, so fix the time zone 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");Intl.RelativeTimeFormat writes phrases such as 3 days ago. You give it two things:
- a number with a sign: a negative number means the past, and a positive number means the future;
- a unit, such as
"day"or"hour".
The numeric option picks the wording. With numeric: "auto", it may use a word such as yesterday. With numeric: "always", it always uses a number: 1 day ago.
const relative = new Intl.RelativeTimeFormat("en-US", { numeric: "auto",});console.log(relative.format(-1, "day"));console.log(new Intl.RelativeTimeFormat("en-US", { numeric: "always",}).format(-1, "day"));Lines 1 to 3 make a formatter with numeric: "auto". Line 4 prints the word yesterday. Lines 5 to 7 make a second formatter with numeric: "always", so line 7 prints 1 day ago. In both, -1 means one day in the past. A plain 1 would mean one day in the future.
Plurals and lists: PluralRules and ListFormat
In English, you write 1 item but 3 items. The word depends on the number. Some languages have more than two forms.
Intl.PluralRules helps with this. It does not write any words. It looks at a number and gives back a plural category: a key such as one, few, or other. Your own table holds the words for each key.
English mostly uses two keys, one and other. Arabic uses six. For ar-EG (Arabic in Egypt), the numbers 0, 1, 2, 3, 11, and 100 give these keys: 0:zero, 1:one, 2:two, 3:few, 11:many, 100:other.
const messages = { one: "1 item", other: "many items" };const rules = new Intl.PluralRules("en-US");console.log(messages[rules.select(1)]);console.log(messages[rules.select(3)]);Line 1 is your table of words. A translator can fill it in for each language. Line 2 makes plural rules for US English. Line 3 asks for the key for 1, gets one, and prints 1 item. Line 4 asks for the key for 3, gets other, and prints many items.
select() only gives back a key, such as one or other. Your own messages object holds the words for each key.Words for counting, like 1 item and 2 items, are called cardinal. Words for order, like 1st, 2nd, and 3rd, are called ordinal. Ordinals have their own rules. Pass { type: "ordinal" } to get them. In English, you can match each key to an ending:
onegetsst,twogetsnd,fewgetsrd, andothergetsth.- That gives
1st,2nd,3rd,4th, and22nd.
Intl.ListFormat joins whole items into one phrase. It adds the commas and the word and or or, the way each locale writes them. Indian English gives Asha, Ravi and Meera, with no comma before and. Use type: "conjunction" for an “and” list and type: "disjunction" for an “or” list.
const names = ["Asha", "Ravi", "Meera"];console.log(new Intl.ListFormat("en-US", { type: "conjunction",}).format(names));console.log(new Intl.ListFormat("en-US", { type: "disjunction",}).format(["tea", "coffee"]));Line 1 keeps each name as its own array item. Lines 2 to 4 make an “and” formatter for US English. It prints Asha, Ravi, and Meera, with a comma before and. Lines 5 to 7 make an “or” formatter and print tea or coffee. Always give ListFormat an array of items, not one string that already has commas in it.
Names and durations: DisplayNames and DurationFormat
FEATURE DETECTData often holds short standard codes: US for a country, de for a language, JPY for a currency. Intl.DisplayNames turns a code into a name that readers understand, in their own language. These outputs were checked in Node 22:
USas a region, in French:États-Unis.deas a language, in Japanese:ドイツ語.JPYas a currency, in English:Japanese Yen.
const regions = new Intl.DisplayNames(["fr"], { type: "region" });console.log(regions.of("US"));console.log(new Intl.DisplayNames(["en"], { type: "currency" }).of("JPY"));Line 1 makes a formatter for region names in French. Line 2 turns the code US into États-Unis. Line 3 uses a second formatter, for currency names in English. It turns JPY into Japanese Yen. Keep the code in your data, and show the name to the reader.
Intl.DurationFormat writes lengths of time, such as 3 hours and 15 minutes. It is new. Node 22 does not have it, and browsers differ. So check that it exists before you use it. This check is called feature detection. The playground below formats a duration only if your browser really has it.
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 your browser.
Press Check support to ask your browser.
Clean and check locale tags
Users may type a locale tag with odd capital letters, such as EN-in. Intl.getCanonicalLocales() rewrites tags in their standard form. Give it ["EN-in"] and it returns ["en-IN"]. If a tag is not valid, it throws a RangeError. Use it before you save a user's choice.
Intl.supportedValuesOf() lists the values this JavaScript engine knows. You pass it the kind of value you want:
"currency"gives currency codes, such asUSD;"calendar","unit","numberingSystem", and"timeZone"give the other lists.
These lists are handy on a settings page. They change when ICU data is updated. So check that one value is in the list, or that the list is not empty. Don't count on a fixed length.
const locale = Intl.getCanonicalLocales(["EN-in"])[0];console.log(locale);console.log(Intl.supportedValuesOf("currency").includes("USD"));Line 1 turns the typed tag into the standard en-IN. Line 2 prints it. Line 3 prints true, because this engine knows the currency code USD.
The table below lists each Intl tool from this lesson. Read across a row: the tool, the job it does, and a short example.
| Tool | What job does it do? | Example |
|---|---|---|
| Intl.Locale | It reads the parts of a locale tag and can add or drop parts. | new Intl.Locale("en-IN").region gives IN. |
| Intl.NumberFormat | It writes numbers, money, percents, units, and short forms. | 1234.5 in rupees for en-IN is ₹1,234.50. |
| Intl.DateTimeFormat | It writes dates and times for a time zone and calendar. | timeZone: "UTC" gives the same time on every computer. |
| Intl.RelativeTimeFormat | It writes phrases such as yesterday or 3 days ago. | numeric: "auto" lets it use words such as yesterday. |
| Intl.PluralRules | It picks a key, such as one or other, for your own words. | English uses one and other. Arabic uses six keys. |
| Intl.ListFormat | It joins items with commas and and or or. | US English gives A, B, and C. |
| Intl.DisplayNames | It turns codes for countries, languages, and currencies into names. | US in French is États-Unis. |
| Intl.DurationFormat | It writes lengths of time, where it exists. | Node 22 does not have it, so check first. |
Where you’ll use Intl
SORTERUse Intl wherever raw data turns into text for a reader. For example:
- bills and price tags;
- dashboards and summaries;
- travel and meeting calendars;
- file times, such as
3 hours ago; - settings pages where users pick a language;
- labels that screen readers read aloud.
Two Intl tools came up in earlier lessons. Comparing & sorting text used Intl.Collator to sort words. The Unicode lesson used Intl.Segmenter to split text into the letters a reader sees.
const priceFormatter = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR",});console.log(priceFormatter.format(1000));console.log(priceFormatter.format(2500));Lines 1 to 4 make one formatter for Indian rupees. Lines 5 and 6 use it for two prices. They print ₹1,000.00 and ₹2,500.00. If many screens use the same format, keep the formatters in one shared place: one for each locale and set of options.
- Show
1234.5as rupees for India - Show
88as kilometres per hour - Show a meeting time in India's time zone
- Say
3 hours ago - Choose between
itemanditems - Join Asha, Ravi, and Meera into one phrase
- Show
USas a country name in French - Turn
EN-inintoen-IN - Check which currency codes this engine knows
Put each job under the group of Intl tools that should do it.
In tests, fix every input: the locale, the options, the date, and the time zone. The outputs in this lesson were checked in Node 22. A browser may use a different version of ICU data. Then it may use different punctuation, or a special space that looks like a normal one. Two such spaces are U+00A0 (no-break space) and U+202F (narrow no-break space). If your test is not about spacing, replace them with normal spaces before you compare.
Common misconceptions
“Intl translates my whole app.”
No. Intl writes data, such as numbers and dates, and some standard names. Your sentences, buttons, and error messages still need translated text.
“The browser’s default locale is fine everywhere.”
The default is fine for a quick page. But it can differ from one computer to another. In examples, in tests, and when a server builds the page, pass the locale and options yourself.
“A locale is just a country.”
A locale is a bundle of choices. It can hold a language, a script, a region, a calendar, a number system, and more.
“I can turn formatted text back into a number later.”
Formatted text is for people to read. Keep the raw value as well. Use formatToParts() to style the pieces, not to read numbers back.
“Every Intl tool works everywhere.”
Most tools in this lesson work in Node 22. Intl.DurationFormat does not, so check that it exists first.
Practice: format for real readers
5 EXERCISESRun both lines. Type the Indian rupee output below. Then look at 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 prints ¥1,235. Prices in yen normally have no digits after the dot, so it rounds to a whole number.
Use ordinal plural rules to find the right ending 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"));For 22, the ordinal key in Indian English is two. The suffixes table matches two to nd, so the program prints 22nd.
Use RelativeTimeFormat with the locale 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 gets -3 and "hour", so Indian English prints 3 hours ago.
Join Asha, Ravi, and Meera for readers of Indian English.
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 adds the comma and the word and the way Indian English writes them: Asha, Ravi and Meera.
Use DisplayNames to show the name of 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 the French names for regions, 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 tag does this print?
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?
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 tool picks a key such as
one,few,many, orother, so you can choose your 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 a
timeZoneto DateTimeFormat?Choose an answer to see the explanation.
Question 7 of 7What should you do before you use Intl.DurationFormat in Node 22?
Choose an answer to see the explanation.
Key takeaways
- Intl writes data, such as numbers and dates, for a locale. It does not translate your sentences.
- The locale says who reads the text. The options say what kind of value it is.
- Keep raw values in your data. Turn them into text only where a person reads them.
- Make one formatter and reuse it for many values with the same locale and options.
- In tests, fix the locale, options, dates, and time zones.
- Check that newer tools, such as
Intl.DurationFormat, exist before you use them.
Remember the one-liner.
Give Intl a locale and options, and it writes your raw data as text for that audience.
Up next: The JavaScript runtime at a glance.