Internationalisation with Intl
An app used across India has to speak to people in Hindi, Tamil, Bengali and English, and its numbers must look right: ₹12,50,000, not ₹1,250,000. Writing that formatting by hand is slow and full of bugs. JavaScript has a built-in object, Intl (short for internationalisation), that already knows the rules for hundreds of languages and regions.
You met Intl.NumberFormat and Intl.DateTimeFormat in Dates & Intl Formatting. This lesson covers the whole family: compact numbers and units, date ranges, "3 hours ago", correct plurals, lists like "Aarav, Priya and Kabir", sorting names properly and splitting text into real characters and words.
Locales and the Intl pattern
Every Intl tool takes a locale: a language code, optionally followed by a region, such as "en-IN" (English, India), "hi-IN" (Hindi, India), "ta-IN" (Tamil, India) or "en-US". These are standard BCP 47 language tags.
All the tools follow the same pattern: create a formatter once with a locale and an options object, then call its method as many times as you like. Creating a formatter does some work, so reuse it instead of creating a new one inside a loop.
const rupees = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" });
rupees.format(1299); // "₹1,299.00"
rupees.format(250000); // "₹2,50,000.00"Some useful details:
- Pass
undefinedas the locale to use the visitor's own browser language. This is usually right for a public website. - Pass an array such as
["ta-IN", "en-IN"]to give fallbacks: the browser uses the first one it supports. - A locale can carry extra settings after
-u-. For example,"hi-IN-u-nu-deva"asks for Devanagari digits (४५,६७,८९०). formatter.resolvedOptions()shows the locale and options the browser actually chose.
Numbers: compact, percent and units
Intl.NumberFormat does far more than currency:
const inr = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" });
console.log(inr.format(4567890.5));
const compact = new Intl.NumberFormat("en-IN", { notation: "compact" });
console.log(compact.format(1500), compact.format(250000), compact.format(12500000));
const percent = new Intl.NumberFormat("en-IN", { style: "percent", maximumFractionDigits: 1 });
console.log(percent.format(0.8725));
const speed = new Intl.NumberFormat("en-IN", { style: "unit", unit: "kilometer-per-hour" });
console.log(speed.format(120));
const litres = new Intl.NumberFormat("en-IN", { style: "unit", unit: "liter", unitDisplay: "long" });
console.log(litres.format(2.5));
const change = new Intl.NumberFormat("en-IN", { signDisplay: "always" });
console.log(change.format(5), change.format(-3));
console.log(new Intl.NumberFormat("hi-IN-u-nu-deva").format(4567890));
console.log(new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(1299.5));₹45,67,890.50 1.5K 2.5L 1.3Cr 87.3% 120 kph 2.5 litres +5 -3 ४५,६७,८९० 1.299,50 €
notation: "compact"gives short forms, and with"en-IN"it uses L for lakh and Cr for crore. Useful for views, followers and prices on small screens.style: "percent"multiplies by 100 for you, so pass0.8725, not87.25.style: "unit"knows units such askilometer,kilogram,liter,celsiusandmegabyte.- The last line shows why you never hard-code separators: Germany uses
.for thousands and,for decimals.
formatToParts() returns the pieces separately, such as { type: "currency", value: "₹" }, which helps when you want to style the symbol differently from the digits.
Dates: styles, ranges and parts
Intl.DateTimeFormat can format in any language with the ready-made dateStyle and timeStyle options, and it can format a range between two dates, writing shared parts only once:
const start = new Date(Date.UTC(2026, 11, 24, 4, 30)); // 24 Dec 2026, 10:00 am in India
const end = new Date(Date.UTC(2027, 0, 2, 4, 30));
const india = { timeZone: "Asia/Kolkata" };
const medium = new Intl.DateTimeFormat("en-IN", { ...india, dateStyle: "medium" });
console.log(medium.formatRange(start, end));
const short = new Intl.DateTimeFormat("en-IN", { ...india, day: "numeric", month: "short" });
console.log(short.formatRange(start, new Date(Date.UTC(2026, 11, 28))));
const hindi = new Intl.DateTimeFormat("hi-IN", { ...india, dateStyle: "full" });
console.log(hindi.format(start));
const parts = new Intl.DateTimeFormat("en-IN", { ...india, weekday: "long", day: "numeric", month: "long" });
console.log(parts.formatToParts(start).map((p) => `${p.type}=${p.value}`).join(" | "));24 Dec 2026 – 2 Jan 2027 24–28 Dec गुरुवार, 24 दिसंबर 2026 weekday=Thursday | literal=, | day=24 | literal= | month=December
The second range is shorter because both dates share the month and year. The separator and spacing come from the locale data (an en dash here, and the exact spaces can differ between browsers and Node.js), so let formatRange() build ranges instead of joining strings with + " - " +.
Relative time: "3 hours ago"
Intl.RelativeTimeFormat turns a number and a unit into words. With numeric: "auto", it uses natural words such as "yesterday" and "tomorrow" where the language has them. It does not calculate the difference for you, so a small helper picks the best unit:
const rtf = new Intl.RelativeTimeFormat("en-IN", { numeric: "auto" });
console.log(rtf.format(-1, "day"), "|", rtf.format(1, "day"), "|", rtf.format(2, "week"));
const hindi = new Intl.RelativeTimeFormat("hi-IN", { numeric: "auto" });
console.log(hindi.format(-1, "day"), "|", hindi.format(3, "hour"));
function timeAgo(date, now = new Date()) {
const seconds = Math.round((date - now) / 1000);
const units = [
["year", 365 * 24 * 3600],
["month", 30 * 24 * 3600],
["week", 7 * 24 * 3600],
["day", 24 * 3600],
["hour", 3600],
["minute", 60],
];
for (const [unit, size] of units) {
if (Math.abs(seconds) >= size) return rtf.format(Math.round(seconds / size), unit);
}
return rtf.format(seconds, "second");
}
const now = new Date(Date.UTC(2026, 9, 30, 12, 0));
console.log(timeAgo(new Date(Date.UTC(2026, 9, 30, 11, 15)), now));
console.log(timeAgo(new Date(Date.UTC(2026, 9, 29, 9, 0)), now));
console.log(timeAgo(new Date(Date.UTC(2026, 10, 2, 9, 0)), now));yesterday | tomorrow | in 2 weeks कल | 3 घंटे में 45 minutes ago yesterday in 3 days
Negative numbers mean the past and positive numbers the future. The helper treats a month as 30 days, which is fine for "posted 2 months ago" labels.
Plurals and ordinals: PluralRules
"1 seats left" looks careless, and plural rules differ between languages. Intl.PluralRules tells you which plural category a number belongs to in a language: "zero", "one", "two", "few", "many" or "other". English only uses "one" and "other" for counting, but uses four categories for ordinals (1st, 2nd, 3rd, 4th):
const plural = new Intl.PluralRules("en-IN");
const seats = (n) => `${n} ${plural.select(n) === "one" ? "seat" : "seats"} left`;
console.log(seats(1), "|", seats(0), "|", seats(5));
const ordinal = new Intl.PluralRules("en-IN", { type: "ordinal" });
const suffix = { one: "st", two: "nd", few: "rd", other: "th" };
const rank = (n) => n + suffix[ordinal.select(n)];
console.log([1, 2, 3, 4, 11, 12, 13, 21, 22, 23, 101, 111].map(rank).join(" "));1 seat left | 0 seats left | 5 seats left 1st 2nd 3rd 4th 11th 12th 13th 21st 22nd 23rd 101st 111th
The ordinal rules handle the tricky cases (11th, 12th and 13th, but 21st, 22nd and 23rd) that hand-written if chains usually get wrong. In a multilingual app, keep a message for each category in each language and let select() choose.
Lists, names and display names
Intl.ListFormat joins items the way each language does. Intl.DisplayNames gives the name of a language, region or currency from its code.
const and = new Intl.ListFormat("en-IN", { type: "conjunction" });
console.log(and.format(["Aarav", "Priya", "Kabir"]));
console.log(and.format(["Meera", "Farhan"]));
console.log(new Intl.ListFormat("en-US").format(["Aarav", "Priya", "Kabir"]));
console.log(new Intl.ListFormat("en-IN", { type: "disjunction" }).format(["UPI", "card", "cash"]));
console.log(new Intl.ListFormat("hi-IN").format(["आरव", "प्रिया", "कबीर"]));
const regions = new Intl.DisplayNames(["en"], { type: "region" });
const languages = new Intl.DisplayNames(["en"], { type: "language" });
const currencies = new Intl.DisplayNames(["en"], { type: "currency" });
console.log(regions.of("AE"), "|", languages.of("ta"), "|", currencies.of("INR"));Aarav, Priya and Kabir Meera and Farhan Aarav, Priya, and Kabir UPI, card or cash आरव, प्रिया, और कबीर United Arab Emirates | Tamil | Indian Rupee
Notice that American English adds a comma before "and" (the serial comma), while Indian English does not. Intl follows each locale's convention so you do not have to.
Sorting and comparing: Collator
The default sort() compares UTF-16 code units, so all capital letters come before all small letters, accented letters go to the end, and "Class 10" sorts before "Class 2". Intl.Collator compares strings the way people expect:
const files = ["Class 10.pdf", "class 2.pdf", "Class 1.pdf", "Zebra.txt", "apple.txt", "éclair.txt"];
console.log([...files].sort());
const collator = new Intl.Collator("en-IN", { numeric: true, sensitivity: "base" });
console.log([...files].sort(collator.compare));
console.log(collator.compare("Rohtak", "ROHTAK"));
console.log(["कबीर", "आरव", "अनन्या", "प्रिया"].sort(new Intl.Collator("hi").compare));[ 'Class 1.pdf', 'Class 10.pdf', 'Zebra.txt', 'apple.txt', 'class 2.pdf', 'éclair.txt' ] [ 'apple.txt', 'Class 1.pdf', 'class 2.pdf', 'Class 10.pdf', 'éclair.txt', 'Zebra.txt' ] 0 [ 'अनन्या', 'आरव', 'कबीर', 'प्रिया' ]
numeric: truecompares runs of digits as numbers, so 2 comes before 10.sensitivity: "base"ignores case and accents, socompare("Rohtak", "ROHTAK")returns0(equal). That makes it a good case-insensitive search check.collator.comparereturns a negative number,0or a positive number, exactly whatsort()expects.
For a single comparison, a.localeCompare(b, "en-IN", options) does the same job, but a reused Collator is faster when sorting long lists.
Splitting text: Segmenter
"नमस्ते".length is 6, but a reader sees three letters: न, म and स्ते. JavaScript strings count UTF-16 code units, and many Indian-language "letters" are built from several of them. Intl.Segmenter splits text into what people actually see as characters (graphemes), into words, or into sentences.
const word = "नमस्ते";
const letters = new Intl.Segmenter("hi", { granularity: "grapheme" });
console.log(word.length, [...letters.segment(word)].map((s) => s.segment));
const words = new Intl.Segmenter("en", { granularity: "word" });
const sentence = "Train 12951 leaves Mumbai at 5:00 pm, doesn't it?";
const onlyWords = [...words.segment(sentence)].filter((s) => s.isWordLike).map((s) => s.segment);
console.log(onlyWords.length, onlyWords.join(" / "));6 [ 'न', 'म', 'स्ते' ] 10 Train / 12951 / leaves / Mumbai / at / 5 / 00 / pm / doesn't / it
Use graphemes for character limits ("max 20 letters") and for cutting text without breaking a letter in half. Word segmentation ignores punctuation and also works for languages such as Thai and Japanese that do not put spaces between words, where split(" ") is useless.
Common mistakes
- Creating a new formatter inside a loop. Create it once and call
.format()many times. - Passing 87 to a percent formatter. It multiplies by 100; pass
0.87. - Expecting identical text in every browser. The locale data comes from the browser and can differ slightly between versions. Never compare formatted strings in your program logic; compare the numbers or dates.
- Sorting with plain
sort()for names shown to users. UseIntl.Collator. - Counting letters with
.length. UseIntl.Segmenterwith"grapheme"for user-visible length.
Key points
Intlformats and compares text for any locale such as"en-IN","hi-IN"or"ta-IN"; passundefinedto use the visitor's language.NumberFormathandles currency, lakh and crore grouping, compact forms (2.5L, 1.3Cr), percentages and units.DateTimeFormathasdateStyle,timeStyle,formatRangeandformatToParts;RelativeTimeFormatwrites "yesterday" and "in 3 days".PluralRuleschooses plural and ordinal forms;ListFormatjoins lists;DisplayNamesnames languages, regions and currencies.Collatorsorts naturally (numeric: true) and compares ignoring case (sensitivity: "base").Segmentersplits text into visible characters, words or sentences.
Exercise
A food delivery app in Kochi shows a summary card. Given orders = 1250000, revenue = 98765432.5 (rupees), rating = 0.946 (as a fraction) and cities = ["Kochi", "Pune", "Jaipur", "Lucknow"], print four lines using Intl: the orders in compact form, the revenue in rupees with no paise, the rating as a percentage with one decimal place, and "Now in Kochi, Pune, Jaipur and Lucknow". Then print the ordinal form of 22 ("22nd").
Show solution
const orders = 1250000;
const revenue = 98765432.5;
const rating = 0.946;
const cities = ["Kochi", "Pune", "Jaipur", "Lucknow"];
const compact = new Intl.NumberFormat("en-IN", { notation: "compact" });
const rupees = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 });
const percent = new Intl.NumberFormat("en-IN", { style: "percent", minimumFractionDigits: 1, maximumFractionDigits: 1 });
const list = new Intl.ListFormat("en-IN", { type: "conjunction" });
const ordinal = new Intl.PluralRules("en-IN", { type: "ordinal" });
const suffix = { one: "st", two: "nd", few: "rd", other: "th" };
console.log(`Orders: ${compact.format(orders)}`);
console.log(`Revenue: ${rupees.format(revenue)}`);
console.log(`Rating: ${percent.format(rating)}`);
console.log(`Now in ${list.format(cities)}`);
console.log(22 + suffix[ordinal.select(22)]);Orders: 13L Revenue: ₹9,87,65,433 Rating: 94.6% Now in Kochi, Pune, Jaipur and Lucknow 22nd
Each formatter is created once with the "en-IN" locale, so the grouping, symbols and list style all follow Indian English. Compact notation rounds 12.5 lakh to "13L", and the rupee amount is rounded to the nearest whole rupee.
Quick Quiz
1. What does new Intl.NumberFormat("en-IN", { notation: "compact" }).format(250000) give?
With the Indian English locale, compact notation uses L for lakh and Cr for crore.
2. Why should you create an Intl formatter once and reuse it?
Constructing a formatter loads and processes locale data; calling .format() on an existing one is cheap.
3. Which option makes Intl.Collator sort "file 2" before "file 10"?
numeric: true compares digit sequences by their numeric value.
4. Why can "नमस्ते".length (6) be misleading?
Many letters are built from several code units; Intl.Segmenter with "grapheme" counts what users see.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
