Table of contents
Skip table of contentsIf you want to be kept up to date with new articles, CSS resources and tools, join our newsletter.
Chances are you've used Moment.js, date-fns, Luxon, or numeral.js at some point. Developers have relied on these libraries for years to format dates, numbers, and currencies. Those are all very useful libraries, but they also come with a cost: they add kilobytes to your download size and they require client-side code parsing:
| Library | Size |
|---|---|
| Moment.js | 295 kB |
| date-fns | 77 kB |
| luxon.js | 82 kB |
| numeral.js | 11 kB |
The Intl API is baseline widely available (except for Intl.DurationFormat. It works in all current evergreen browsers, but hasn't been around long enough to qualify for "widely available".) and can handle almost all of your formatting requirements directly in the browser, with zero kB of download and no JS bundle parsing required. It also knows your users' locale preferences so you can format dates and numbers in a way that feels natural to them without any extra work.
Prefer video?
This article is a written version of a talk by Kilian Valkhof, Polypane's creator. You can watch the video here:
Interested in having this talk at your conference or meetup? Get in touch!
Why you need an internationalization API
You might be thinking: "My site is only in one language. Why would I need an internationalization API?"
That's the most common reason developers skip over Intl. But Intl is less about translating your UI and more about formatting data correctly for your users.
Consider a date like 09/12/2025. If you're from the US, you'd read it as September 12th. Almost anywhere else in the world, you'd read it as December 9th. That's quite a big difference, and if a carelessly (un)formatted date leads to someone missing an appointment or even just confusion, that reflects very poorly on your site.
The Intl API formats dates, numbers, currencies, lists, and text in a way that fits the user's locale. Even if your site is just in English, users from different English speaking countries still have different formatting conventions. Intl handles all of that for you, so you don't have to worry about it.
What Intl gives you
- 0 kB added to your bundle: it's built into the browser
- Baseline widely available: works in every major browser
- Locale-aware: knows not just languages but country-specific conventions
- Fast: native implementations are significantly faster than userland libraries
The APIs we'll cover:
Intl.DateTimeFormat: format dates and timesIntl.RelativeTimeFormat: "yesterday", "in 3 days"Intl.DurationFormat: "2 hours, 5 minutes and 30 seconds"Intl.NumberFormat: format numbers, currencies, and unitsIntl.ListFormat: "Apples, Oranges, and Bananas"Intl.PluralRules: figure out plural forms correctlyIntl.Segmenter: break text into words, sentences, or graphemesIntl.Collator: sort strings in a locale-aware way
What Intl doesn't do
We'll get into plenty of examples later on in this article but a common misunderstanding, or maybe wish, is that Intl also handles data wrangling. Unfortunately, Intl is a formatting API, not a calculation or measurement API. It just takes objects (like dates, numbers and arrays) and turns them into strings.
For example if you want to format a relative time compared to now, like to show when a blog post was released compared to the current time, you need to calculate the difference yourself and then pass that to Intl.RelativeTimeFormat. We have an example of how to do that in the section on RelativeTimeFormat.
Later on in the article you'll also see that it can format numbers with units (like degrees Celsius or miles) but it has no conception of what those units mean. So while it knows to put a °C after a number when you ask for Celsius, it doesn't know how to convert from Fahrenheit to Celsius. It's just takes a number and returns a string with an unit in it.
How the Intl API works
All Intl functions have roughly the same shape:
- Pick a locale.
- Pick some options.
- Create a formatter.
- Reuse that formatter with your data.
Here's the general setup with Intl.DateTimeFormat:
const locale = 'en-US';
const options = {
dateStyle: 'full',
timeStyle: 'short',
};
const formatter = new Intl.DateTimeFormat(locale, options);Then, once you have the formatter, you pass it the value you want to format:
const publishedAt = new Date('2026-04-07T16:45:00Z');
formatter.format(publishedAt);
// → "Tuesday, April 7, 2026 at 4:45 PM"
formatter.format(new Date('2026-06-01T09:00:00Z'));
// → "Monday, June 1, 2026 at 9:00 AM"That same pattern shows up throughout Intl:
new Intl.DateTimeFormat(locale, options).format(date);
new Intl.NumberFormat(locale, options).format(number);
new Intl.ListFormat(locale, options).format(items);
new Intl.PluralRules(locale, options).select(number);
new Intl.Collator(locale, options).compare(a, b);The constructor arguments are consistent too:
- A locale string such as
'en-US','nl-NL', or'fr-FR'. If you omit it, the browser uses the user's system locale. You can also passundefinedto use the browser's default locale explicitly, which you need if you also want to set options. - An options object that controls the output. The available options differ per formatter, but the setup pattern stays the same.
Creating the formatter is the expensive part, because that loads the locale data and sets up the internal structures. Calling .format(), .select(), or .compare() afterwards is cheap. That means you should create the formatter once and reuse it when you're handling a lot of values.
// Good: create once, reuse many times.
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
});
dates.map((date) => formatter.format(date));
// Less good: create a new formatter every time.
dates.map((date) => new Intl.DateTimeFormat('en-US', { dateStyle: 'short' }).format(date));That matters most with Intl.Collator, the API that lets you sort strings in a locale-aware way. Sorting can call the comparison function many times, so you definitely want to create the collator once and reuse it.
Setting the locale
For locales, you can use any valid BCP 47 language tag, such as 'en', 'en-US', 'nl-NL', or 'fr-FR'. You can also specify a list of locales, and the browser will pick the first one it supports. In practice, all major browsers support at least seven thousand locales, so you can be pretty confident that the browser will find a match for your users' preferences.
If you don't specify a locale, the formatter uses the browser's locale. You can set it to undefined to get the same effect. That means you can create a formatter that automatically adapts to the user's locale without any extra work. Alternatively you can explicitly ask for navigator.language or navigator.languages to get the user's language preferences and pass those to the formatter:
// Choose a specific locale
const formatter = new Intl.DateTimeFormat('nl-NL');
// Pass a list of locales, the browser picks the first one it supports
const formatter = new Intl.DateTimeFormat(['en-US', 'en-GB']);
// Use the browser's locale
const formatter = new Intl.DateTimeFormat();
const formatter = new Intl.DateTimeFormat(undefined);
// Use the browser's locale but specify options too
const formatter = new Intl.DateTimeFormat(navigator.language);
// Use the user's full language preference list
// (the browser picks the first one it supports)
const formatter = new Intl.DateTimeFormat(navigator.languages);navigator.languages is an array like ['en-US', 'en', 'nl'], so works like a list of locales you can pass directly to the formatter.
What's the difference between a language and a locale? A locale is a language as spoken in a specific country. For example, both the US and the UK speak English, but they have different formatting conventions for dates and numbers. So you have
en-USfor American English anden-GBfor British English. If you just specifyen, the browser will pick a default locale for that language, which is usually the most common one.
Dates & Times
Let's start with the APIs that have the clearest naming: the ones for formatting dates and times.
Intl.DateTimeFormat
Intl.DateTimeFormat is the API for formatting dates and times.
const formatter = new Intl.DateTimeFormat(undefined, {
dateStyle: "full",
timeStyle: "short",
timeZone: "UTC"
});
formatter.format(new Date("2026-04-07T16:45:00.000Z"));At its simplest:
const formatter = new Intl.DateTimeFormat();
formatter.format(new Date());
// → "4/7/2026" (in en-US)The default output is a bit terse. Most of the time you'll want to specify a style. The easiest way is with dateStyle and timeStyle:
const formatter = new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'full', // 'full' | 'long' | 'medium' | 'short'
timeStyle: 'short', // 'full' | 'long' | 'medium' | 'short'
});
formatter.format(new Date());
// → "mercredi 8 avril 2026 à 09:19"What's nice here is that you didn't have to know that "mercredi" is Wednesday in French, or that "avril" is April, or have arrays of days and months somewhere. You ask the browser for French, and the browser already knows.
When you need more control, you can specify exactly which parts of the date to show and how to format them:
const formatter = new Intl.DateTimeFormat(undefined, {
weekday: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
month: "long",
year: "numeric"
});
formatter.format(new Date());If you specify individual parts like this, only those parts will appear in the output. Mixing dateStyle/timeStyle with individual part options throws an error, you can use one or the other but not both.
You might have seen them in the interactive example, but there's a few more options that are worth mentioning:
const options = {
hour12: true | false, // override the locale's 12/24-hour preference
timeZone: 'UTC' | 'Europe/Amsterdam' | ..., // specify a time zone
timeZoneName: 'short' | 'long', // "CET", "Central European Time", etc.
dayPeriod: 'narrow' | 'short' | 'long', // "in the morning", "AM", etc.
};formatToParts()
All Intl formatters that have a .format() method also have a .formatToParts() method. Instead of returning one big string, it returns an array of { type, value } objects. This makes it easy to wrap individual parts in markup for styling, such as making the day bold or adding a tooltip to the time zone name.
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'long',
timeStyle: 'short',
});
formatter.formatToParts(new Date('2026-04-07T16:45:00Z'));
// → [
// { type: 'month', value: 'April' },
// { type: 'literal', value: ' ' },
// { type: 'day', value: '7' },
// { type: 'literal', value: ', ' },
// { type: 'year', value: '2026' },
// { type: 'literal', value: ' at ' },
// { type: 'hour', value: '4' },
// { type: 'literal', value: ':' },
// { type: 'minute', value: '45' },
// { type: 'literal', value: ' ' },
// { type: 'dayPeriod', value: 'PM' },
// ]Intl.RelativeTimeFormat
Intl.RelativeTimeFormat gives you human-friendly relative time strings like "yesterday" and "in 3 days".
const formatter = new Intl.RelativeTimeFormat(undefined, {
numeric: "auto"
});
formatter.format(-3, "day");Unlike DateTimeFormat, it works with a number and a unit, not a Date:
const relative = new Intl.RelativeTimeFormat('nl-NL', {
numeric: 'auto', // 'auto' | 'always'
});
relative.format(-3, 'month'); // → "3 maanden geleden"
relative.format(1, 'day'); // → "morgen"
relative.format(-1, 'day'); // → "gisteren"With numeric: 'auto', the formatter uses words like "tomorrow" and "yesterday" when it can. With numeric: 'always', it always uses a number: "in 1 day", "1 day ago".
Units can be: 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', or 'year'.
Since you need to calculate the difference yourself, here's a helper pattern for calculating days between two dates:
const date = new Date('2026-05-01T00:00:00Z');
const diffInMs = date - new Date();
const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));
const relative = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });
relative.format(diffInDays, 'day');
// → "in 24 days" (or however far away May 1st is)Dates are in milliseconds. So you can convert it by dividing it to the unit you want. Instead of a really big number, like 86400000 for the number of milliseconds in a day, your code is easier to read if you do it step by step: 1000 milliseconds in a second, 60 seconds in a minute, 60 minutes in an hour and then 24 hours in a day.
RelativeTimeFormat also has .formatToParts(), which breaks the output into 'integer', 'literal', and 'unit' pieces. This lets you wrap each part and style it separately.
const relative = new Intl.RelativeTimeFormat('en-US', { numeric: 'always' });
relative.formatToParts(-3, 'month');
// → [
// { type: 'integer', value: '3', unit: 'month' },
// { type: 'literal', value: ' months ago' },
// ]Intl.DurationFormat
Intl.DurationFormat formats time durations.
const formatter = new Intl.DurationFormat(undefined, {
style: "long"
});
formatter.format({
"hours": 2,
"minutes": 45,
"seconds": 30
});It lets you choose between an output like 2:05:30 and one like "2 hours, 5 minutes, 30 seconds":
const duration = new Intl.DurationFormat('en-US', {
style: 'long', // 'long' | 'short' | 'narrow' | 'digital'
});
duration.format({ hours: 2, minutes: 5, seconds: 30 });
// → "2 hours, 5 minutes, 30 seconds"
const clock = new Intl.DurationFormat('en-US', { style: 'digital' });
clock.format({ hours: 2, minutes: 5, seconds: 30 });
// → "2:05:30"While the other date and time formatters use options to control which parts of the date to show, by default the DurationFormat just shows all the parts you give it in the formatter. So if you pass it hours, minutes and seconds, it will show all three. If you only pass it minutes and seconds, it will only show those two.
You can however force it to always show certain parts, even if they're zero by setting the *Display options:
const options = {
yearsDisplay: 'auto' | 'always',
monthsDisplay: 'auto' | 'always',
daysDisplay: 'auto' | 'always',
hoursDisplay: 'auto' | 'always',
minutesDisplay: 'auto' | 'always',
secondsDisplay: 'auto' | 'always',
millisecondsDisplay: 'auto' | 'always',
microsecondsDisplay: 'auto' | 'always',
nanosecondsDisplay: 'auto' | 'always',
};auto will only show the part if it's specified in the object, while always will always show it even if it's omitted.
The default option for all of these is almost always auto, except when you set style to digital, in which case the default for hours, minutes and seconds becomes always.
When you use digital, hours, minutes, and seconds are all shown by default even if they're zero, so you get 00:45:00 rather than just 45.
You can also format much finer durations, down to nanoseconds, which is useful for displaying performance measurements:
const perf = new Intl.DurationFormat('en-US', { style: 'narrow' });
perf.format({ milliseconds: 423, microseconds: 195 });
// → "423ms 195μs"Numbers & Currencies
Intl.NumberFormat
Intl.NumberFormat formats plain numbers, currencies, and units.
const formatter = new Intl.NumberFormat(undefined, {
style: "decimal",
notation: "standard",
useGrouping: "auto"
});
formatter.format(1234567.89);Those are all the same API, but they solve different problems, so it helps to keep them separate.
Plain numbers and percentages
For plain numbers, you normally use the default decimal style. That gives you locale-aware grouping and decimal separators without any extra work.
const formatter = new Intl.NumberFormat('nl-NL');
formatter.format(1000000); // → "1.000.000"
formatter.format(123456.789); // → "123.456,789"Even for plain numbers, locale matters: Dutch uses . for thousands and , for decimals, while US English does the opposite.
Percentages use the same API with style: 'percent':
const pct = new Intl.NumberFormat('fr-FR', { style: 'percent' });
pct.format(0.1); // → "10 %"
pct.format(0.753); // → "75 %"The general number formatter also supports different notations, which lets you set compact numbers like 1M :
const compact = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short',
});
compact.format(1007800); // → "1M"
compact.format(1534); // → "1.5K"
// For a longer version, use 'long' instead of 'short'
const compactLong = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'long',
});
compactLong.format(1007800); // → "1 million"
compactLong.format(1534); // → "1.5 thousand"Note how the compact notation rounds the number to fit the format. 1,007,800 becomes "1M", and 1,534 becomes "1.5K". If you want to control the rounding, you can use the fraction digit options or switch to significant digits.
You can control how many decimal places appear with the fraction digit options, or switch to significant digits for scientific or measurement data:
// Always show exactly two decimal places
const price = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
price.format(9.9); // → "9.90"
price.format(9.999); // → "10.00"
// Cap to three significant digits
const approx = new Intl.NumberFormat('en-US', {
maximumSignificantDigits: 3,
});
approx.format(123456); // → "123,000"
approx.format(0.001234); // → "0.00123"Currencies
A big feature in the number formatting API is formatting currencies, which even in its simplest form has quite a few differences between countries like if the symbol goes before or after the number, and getting them wrong in your UI will easily make your site look unprofessional or untrustworthy.
const formatter = new Intl.NumberFormat(undefined, {
style: "currency",
currency: "EUR",
currencyDisplay: "symbol"
});
formatter.format(123456.789);Currency formatting is more than putting a symbol in front of a number. The symbol position, thousands separator, decimal separator, and rounding rules all vary by locale even when using the same currency (see how different euros are formatted compared between the Netherlands and France). Intl.NumberFormat handles all of that for you without you having to know the specifics across locales.
const moneyNL = new Intl.NumberFormat('nl-NL', {
style: 'currency',
currency: 'EUR',
});
const moneyFr = new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
});
moneyNL.format(123456.789); // → "€ 123.456,79"
moneyFr.format(123456.789); // → "123 456,79 €"You can also customize how the currency itself is displayed, such as showing the currency code instead of the symbol or even the full, localized name of the currency:
const options = {
style: 'currency',
currency: 'USD',
currencyDisplay: 'name', // 'symbol' | 'narrowSymbol' | 'code' | 'name'
currencySign: 'accounting', // 'standard' (default) | 'accounting'
};
new Intl.NumberFormat('nl-NL', options).format(123456.789);
// → "123.456,79 Amerikaanse dollar"The difference between symbol and narrowSymbol is that narrowSymbol can be "lossy". For example, the symbol for US dollars is US$ because other countries also use dollars and it needs to differentiate between US dollars and, say, Canadian dollars, which is CA$. The narrow symbol for both is just $, which is more compact and works when you operate in a single country, but can be confusing if you have an international audience.
currencySign controls how negative currency values are displayed. Normal people expect a minus sign in front of the number, but accountants, non-conformists that they are, prefer to put negative numbers in parentheses.
const standard = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencySign: 'standard',
});
const accounting = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencySign: 'accounting',
});
standard.format(-123.45);
// → "-$123.45"
accounting.format(-123.45);
// → "($123.45)"Units
The unit style is one of the most underused parts of Intl.NumberFormat. It formats a number with a physical unit (or units!) in a locale-appropriate way:
const formatter = new Intl.NumberFormat(undefined, {
style: "unit",
unit: "kilometer-per-hour",
unitDisplay: "short"
});
formatter.format(100);Supported units cover length, mass, temperature, speed, volume, digital size, and time. To get all of them, run Intl.supportedValuesOf("unit") in your console.
const boiling = new Intl.NumberFormat('nl-NL', {
style: 'unit',
unit: 'celsius',
unitDisplay: 'short', // 'short' | 'long' | 'narrow'
});
boiling.format(100);
// → "100°C"
new Intl.NumberFormat('nl-NL', {
style: 'unit',
unit: 'celsius',
unitDisplay: 'long',
}).format(100);
// → "100 graden Celsius"The unit style also lets you combine any two units together when you need to display something like "miles per hour" or "kilometers per hour". The notation for that is unit1-per-unit2:
const speed = new Intl.NumberFormat('nl-NL', {
style: 'unit',
unit: 'kilometer-per-hour',
unitDisplay: 'short', // 'short' | 'long' | 'narrow'
});
speed.format(100);
// → "100 km/u"
new Intl.NumberFormat('nl-NL', {
style: 'unit',
unit: 'kilometer-per-hour',
unitDisplay: 'long',
}).format(100);
// → "100 kilometer per uur"Breaking the output into parts
NumberFormat also has .formatToParts(). It's particularly useful for currencies, where you might want to wrap the different parts of the number and style them:
const money = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
money.formatToParts(1234.56);
// → [
// { type: 'currency', value: '$' },
// { type: 'integer', value: '1' },
// { type: 'group', value: ',' },
// { type: 'integer', value: '234' },
// { type: 'decimal', value: '.' },
// { type: 'fraction', value: '56' },
// ]Lists & Text
There are two APIs for working with natural language text: one for formatting lists and one for finding plural forms.
Intl.ListFormat
Intl.ListFormat turns arrays into natural-language lists.
const formatter = new Intl.ListFormat(undefined, {
type: "conjunction",
style: "long"
});
formatter.format([
"Apples",
"Oranges",
"Bananas"
]);When you need to turn an array into a human-readable list, use it instead of Array.join(', '). A comma-joined list is usually not how lists work in natural language. When you list things, you don't say "a, b, c", you say "a, b and c". ListFormat handles that for you, so you don't have to break out an Array.reduce and keep track of whether you're on the last item or not.
// ✗ Not natural language
['Apples', 'Oranges', 'Bananas'].join(', ');
// → "Apples, Oranges, Bananas"
// ✓ Natural language
const formatter = new Intl.ListFormat('en-US', { type: 'conjunction' });
formatter.format(['Apples', 'Oranges', 'Bananas']);
// → "Apples, Oranges, and Bananas"The three list types each fit a different kind of UI copy.
Use conjunction when the items belong together and you mean "and":
new Intl.ListFormat('en', { type: 'conjunction' }).format(['Cowboy', 'Hat', 'Horse']);
// → "Cowboy, Hat, and Horse"This is what you want for summaries like supported features or anything else where all listed items apply.
Use disjunction when the items are alternatives and you mean "or":
new Intl.ListFormat('en', { type: 'disjunction' }).format(['Cowboy', 'Hat', 'Horse']);
// → "Cowboy, Hat, or Horse"This works for option lists, fallback choices, or messaging like "Choose email, SMS, or push notifications."
Use unit when you want a compact list without an explicit conjunction:
new Intl.ListFormat('en', { type: 'unit' }).format(['Cowboy', 'Hat', 'Horse']);
// → "Cowboy, Hat, Horse"This is useful for tight UI labels, specs, measurements or lists of ingredients.
The style option ('long', 'short', 'narrow') affects how the connector words are shown. In English, short uses an ampersand: "Cowboy, Hat, & Horse".
In American English (en-US), a conjunction list uses the Oxford comma ("...Hat, and Horse"). Switch to British English (en-GB) and the Oxford comma disappears. So you can stop arguing about the Oxford comma in your team and just let the browser handle it for you.
Intl.PluralRules
Intl.PluralRules tells you which plural form to use for a given number. A plural form is the "s" when you go from "1 bird" to "2 birds", but it can be more complex than that in other languages.
const count = 1;
const rules = new Intl.PluralRules(undefined, { type: 'cardinal' });
rules.select(count);What's weird about PluralRules is that, unlike the other formatting APIs, it doesn't actually do the formatting, it just gives you consistent categorization for the plural forms in the language.
So for bird/birds, it would tell you that for 1 bird the plural form is "one", and for 0, 2, 3, etc. the plural form is "other". You can then use that information to display the correct word form in your UI: "one" gets nothing appended, and "other" gets an "s".
const plurals = new Intl.PluralRules('en-US');
plurals.select(0); // → "other"
plurals.select(1); // → "one"
plurals.select(2); // → "other"
const count = 5;
const label = `${count} bird${plurals.select(count) === 'other' ? 's' : ''}`;
// → "5 birds""Cardinal" is the default plural type, and it basically means "count". English has just two cardinal plural forms: 'one' (for the number 1) and 'other' (everything else, including 0). Other languages have more. Arabic, for example, has six plural forms: zero, one, two, few, many, and other.
Along with cardinal, you also have ordinal, which is used for ranking or ordering things: "1st", "2nd", "3rd", "4th", etc. In English, the plural forms for ordinals are one (for 1), two (for 2), few (for 3), and other (for everything else). So you can use that to get the correct suffix for any number:
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' };
const ordinal = (n) => `${n}${suffixes[rules.select(n)]}`;
ordinal(1); // → "1st"
ordinal(2); // → "2nd"
ordinal(3); // → "3rd"
ordinal(4); // → "4th"
ordinal(21); // → "21st"As you can see from these two examples, it's your job to provide the right suffix or, in the case of other language, the right word form.
For example, 3 in Dutch is "drie", but 3rd is "derde". You can't easily append a suffix to get from "drie" to "derde", so you need to provide the full word forms for each plural category.
Segmentation
Intl.Segmenter
Intl.Segmenter breaks text into meaningful segments.
const segmenter = new Intl.Segmenter('default', {
granularity: "word"
});
const segments = Array.from(segmenter.segment(text));Breaking up sentences into parts might sound simple, but it quickly becomes complicated when you consider all the edge cases in natural language. For example, how would you count the number of words in a sentence? You might be tempted to do this:
'How many words are in this sentence?'.split(' ').length; // → 7 ✓Splitting on a space and then calling .length works well for English and other western languages, but it relies on the assumption that words are separated by spaces. That is not a universal rule in natural language. It falls apart here:
'これは日本語のテキストです'.split(' ').length; // → 1 ✗Japanese doesn't use spaces between words. So splitting on a space doesn't work and you're out of luck.
But even String.length to get the number of characters isn't as straightforward as it seems. How long is this single emoji in a string?
'👨👩👧👦'.length; // → 11Not one, but eleven! That's because .length doesn't count visible characters, it counts code units. And this emoji happens to be a ligature of four individual emojis joined by zero-width joiners, which are invisible characters that connect them together. So while you see one emoji, JavaScript sees eleven code units and .length returns 11.
Detecting sentences gets even trickier. Since formatted numbers, abbreviations, and contractions all contain punctuation, splitting on a period also doesn't always work:
`This article could be 100.000 words long if
you don't look out. Do you really need
that many examples?`.split('.').length; // → 3 ✗
// (splits on the period in "100.000" too)Of course, some sentences might also end with a question mark or an exclamation point, so splitting on a period isn't a reliable way to count sentences either.
Intl.Segmenter handles all of these correctly and it has three different levels of granularity so you can get characters, words or sentences in a text string.
'grapheme' (default): user-perceived characters (what you see as a single character, even if it's made up of multiple code points)
const graphemes = new Intl.Segmenter('en', { granularity: 'grapheme' });
Array.from(graphemes.segment('👨👩👧👦')).length; // → 1 ✓
Array.from(graphemes.segment('café')).length; // → 4 ✓'word': words, plus non-word segments such as spaces and punctuation. Each segment has an isWordLike boolean to help you filter out the non-word segments.
const words = new Intl.Segmenter('ja-JP', { granularity: 'word' });
Array.from(words.segment('これは日本語のテキストです')).filter((s) => s.isWordLike).length; // → 6 ✓ (the browser knows Japanese word boundaries)'sentence': complete sentences, with abbreviation and number handling:
const sentences = new Intl.Segmenter('en', { granularity: 'sentence' });
Array.from(sentences.segment('I went to the dr. smith yesterday. How are you?')).map((s) => s.segment);
// → [
// "I went to the dr. smith yesterday. ",
// "How are you?"
// ]One caveat: the sentence segmenter handles dr. as an abbreviation when followed by lowercase, but if the next word is capitalized (like you would expect for a proper name), it may be treated as a sentence boundary. The segmenter is smart, but not perfect.
The segment() method returns an iterable, which is why we wrap it in Array.from(). Each item is an object with at minimum:
segment: the text of the segmentindex: where it starts in the original stringinput: the original string
For the 'word' granularity, items also have a boolean property:
isWordLike:trueif the segment is a word rather than whitespace or punctuation
For more, check out our article on just the Segmenter API.
Collation
Intl.Collator
Intl.Collator sorts strings in a locale-aware way.
const items = ["ñ","n","o","á","A","ß","z"];
const collator = new Intl.Collator(undefined, {
sensitivity: "base"
});
items.sort(collator.compare);Built-in Array.sort() sorts strings by their Unicode code points, which doesn't match the alphabetical order humans expect in many languages:
['ñ', 'n', 'o'].sort();
// → ['n', 'o', 'ñ'] ✗ (ñ has a higher code point, sorts after z)
const collator = new Intl.Collator('es'); // Spanish
['ñ', 'n', 'o'].sort(collator.compare);
// → ['n', 'ñ', 'o'] ✓ (ñ correctly sits between n and o in Spanish)This matters anywhere you show an index, a directory, or any other sorted list.
You might already know String.prototype.localeCompare(), which also does locale-aware string comparison. Under the hood it uses Intl.Collator too, but it creates and destroys a new collator instance every time you call it. This is pretty slow (remember, creating the formatter is the expensive part) so what you can do instead is set up a collator once and then reuse its compare method.
// Works, but slow:
items.sort((a, b) => a.localeCompare(b));
// Better: creates the collator once and reuses it
const collator = new Intl.Collator('en');
items.sort(collator.compare);Intl.Collator has a handful of useful options. The most important one is sensitivity, which controls what differences in characters are considered:
// 'base': only base letters differ ('a' ≠ 'b', but 'a' = 'á' = 'A')
// 'accent': base letters and accents differ, but not case
// 'case': base letters and case differ, but not accents
// 'variant': all differences matter (default for sort order)
const collator = new Intl.Collator('en', { sensitivity: 'base' });
collator.compare('a', 'á'); // → 0 (considered equal)Other useful options:
usage:'sort'(default) or'search'. Use'sort'for ordering lists. If you want to use collator to filter lists based on language-specific logic, you can usesearchand test if the result is0to check for a match. for the collator,cafe,caféandCAFEare all considered equal withsensitivity: 'base', so using search means you have a more natural search experience for users in languages with accents and case differences.caseFirst:'upper','lower', or'false'(default). Controls whether uppercase or lowercase sorts first when case differences matter.ignorePunctuation:trueorfalse(default). Whentrue, punctuation differences are ignored during comparison.collation: requests a specific collation variant. Common values are:'phonebk': phonebook-style sorting, used in German where ä sorts as ae, ö as oe, ü as ue'pinyin': pinyin ordering for Latin and CJK characters, common for Chinese'stroke': stroke-count ordering for CJK characters, used in Traditional Chinese'trad': traditional ordering, for example treating ch and ll as distinct letters in Spanish'emoji': recommended ordering for emoji characters'standard': the locale's standard ordering (the default)
You can get the full list your browser supports with Intl.supportedValuesOf('collation').
Numeric sorting
The numeric option is worth a special mention. By default, string sorting is done character-by-character. That means 10 sorts before 9 because 1 is less than 9. This is almost never what you want when sorting anything with numbers in it.
const files = ['chapter10.txt', 'chapter9.txt', 'chapter2.txt', 'chapter1.txt'];
// Default sort per-character
files.sort();
// → ['chapter1.txt', 'chapter10.txt', 'chapter2.txt', 'chapter9.txt'] ✗
// Numeric sort
const collator = new Intl.Collator('en', { numeric: true });
files.sort(collator.compare);
// → ['chapter1.txt', 'chapter2.txt', 'chapter9.txt', 'chapter10.txt'] ✓This comes up frequently with file names, version numbers, and any list where users use numbers in names: image1, image2, … image10. Without numeric: true, image10 sorts right after image1 and before image2. With it, they sort the way users expect.
Conclusion
When you first look at Intl, it can feel like a loose collection of unrelated APIs. It does something with dates, currencies and also sorts lists or counts words? What an API! If you only look at the constructor names, it can seem like a lot to memorize.
But Intl is built on one core idea: you have a type of data, and you want an easy way to format it in natural language. You pick a locale. You pick some options. You create a formatter, parts formatter, comparator, or segmenter. Then you reuse it with your data. Whether you're formatting dates, numbers, currencies, lists, plurals, words, or sorted strings, the API shape stays similar.
Understanding that shared foundation makes the whole API much easier to reason about and easier to reach for when you come across a string formatting task. It means less hand-rolled formatting code, fewer dependencies, and fewer subtle bugs where a value looks correct to you but confusing or wrong to someone in another locale.
The browser already knows how this stuff works and did the hard work for you. Intl gives you a way to use that knowledge directly.

