JavaScript Internationalization

By

A practical guide to JavaScript Intl for formatting dates, numbers, currencies, lists, names, relative time, plural rules, and text segments.

~~~

Intl is JavaScript’s built-in internationalization API. It formats values using the conventions of a locale, so we do not have to maintain our own tables of decimal separators, month names, plural categories, or text boundaries.

The API includes formatters for dates, numbers, lists, display names, relative times, durations, and plural rules. It also includes a collator for sorting and a segmenter for finding grapheme, word, and sentence boundaries.

Let’s start with the core formatters, then combine them into a practical application strategy.

Intl.Collator

This constructor gives you language-sensitive string comparison.

You initialize a Collator object using new Intl.Collator(), passing it a locale, and you use its compare() method which returns a positive value if the first argument comes after the second one. A negative if it’s the reverse, and zero if it’s the same value:

const collator = new Intl.Collator('it-IT')
collator.compare('a', 'c') //a negative value
collator.compare('c', 'b') //a positive value

We can use it to order arrays of characters, for example.

Intl.DateTimeFormat

This property gives you access to language-sensitive date and time formatting.

You initialize a DateTimeFormat object using new Intl.DateTimeFormat(), passing it a locale, and then you pass it a date to format it as that locale prefers:

const date = new Date()
let dateTimeFormatter = new Intl.DateTimeFormat('it-IT')
dateTimeFormatter.format(date) //27/1/2019
dateTimeFormatter = new Intl.DateTimeFormat('en-GB')
dateTimeFormatter.format(date) //27/01/2019
dateTimeFormatter = new Intl.DateTimeFormat('en-US')
dateTimeFormatter.format(date) //1/27/2019

The formatToParts() method returns an array with all the date parts:

const date = new Date()
const dateTimeFormatter = new Intl.DateTimeFormat('en-US')
dateTimeFormatter.formatToParts(date)
/*
[ { type: 'month', value: '1' },
  { type: 'literal', value: '/' },
  { type: 'day', value: '27' },
  { type: 'literal', value: '/' },
  { type: 'year', value: '2019' } ]
*/

You can choose individual fields or use dateStyle and timeStyle. We will also set an explicit time zone later in this guide.

Intl.NumberFormat

Intl.NumberFormat applies locale-sensitive separators, digits, signs, currency rules, and units.

const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
})

formatter.format(1000) // "$1,000.00"
formatter.format(10) // "$10.00"
formatter.format(123233000) // "$123,233,000.00"

The same currency is displayed using Italian conventions when we change the locale:

const formatter = new Intl.NumberFormat('it-IT', {
  style: 'currency',
  currency: 'USD',
})

formatter.format(1000) //uses Italian separators and currency placement

Intl.PluralRules

Intl.PluralRules returns the plural category for a number. One practical use is selecting English ordinal suffixes: 1st, 2nd, 3rd, and 4th.

const pr = new Intl.PluralRules('en-US', {
  type: 'ordinal',
})
pr.select(0) //other
pr.select(1) //one
pr.select(2) //two
pr.select(3) //few
pr.select(4) //other
pr.select(10) //other
pr.select(22) //two

Every time we got other, we translate that to th. If we have one, we use st. For two we use nd. few gets rd.

We can use an object to create an associative array:

const suffixes = {
  one: 'st',
  two: 'nd',
  few: 'rd',
  other: 'th',
}

and we do a formatting function to reference the value in the object, and we return a string containing the original number, and its suffix:

const format = (number) => `${number}${suffixes[pr.select(number)]}`

Now we can use it like this:

format(0) //0th
format(1) //1st
format(2) //2nd
format(3) //3rd
format(4) //4th
format(21) //21st
format(22) //22nd

Locale selection and time zones

Hardcoding en-US is useful in examples, but real applications should start from the user’s language preferences. In a browser, navigator.languages returns an ordered list:

const locales = navigator.languages

Pass that list directly to an Intl constructor. The runtime picks the best supported locale:

const formatter = new Intl.NumberFormat(navigator.languages)
formatter.format(123456.78)

Locale selection and time zone selection are different decisions. A locale controls conventions such as names, separators, and ordering. A time zone controls which local clock time a date represents.

const formatter = new Intl.DateTimeFormat('it-IT', {
  dateStyle: 'full',
  timeStyle: 'short',
  timeZone: 'Europe/Rome',
})

Never infer a time zone from a language. Italian speakers do not all live in Italy, and people travel.

Use resolvedOptions() when you need to inspect what the runtime selected:

formatter.resolvedOptions().locale //'it-IT'
formatter.resolvedOptions().timeZone //'Europe/Rome'

Format date and time ranges

Intl.DateTimeFormat can format a range without repeating unnecessary parts:

const formatter = new Intl.DateTimeFormat('en-GB', {
  dateStyle: 'medium',
})

const start = new Date('2026-08-10T10:00:00Z')
const end = new Date('2026-08-12T10:00:00Z')

formatter.formatRange(start, end)

Use formatRangeToParts() when you need custom markup. It marks which pieces belong to the start, end, or shared portion of the range.

For a complete explanation of parsing, time zones, daylight-saving changes, and the Temporal API, see my JavaScript dates guide.

Format relative time

Intl.RelativeTimeFormat produces text such as “yesterday” or “in 3 days”:

const relative = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto',
})

relative.format(-1, 'day') //'yesterday'
relative.format(3, 'day') //'in 3 days'

The API formats a value you already calculated. It does not compare two dates or choose the right unit for you.

Format lists

Joining a list with commas is not enough. Languages use different punctuation and conjunctions.

const list = new Intl.ListFormat('en', {
  style: 'long',
  type: 'conjunction',
})

list.format(['HTML', 'CSS', 'JavaScript'])
//'HTML, CSS, and JavaScript'

Change type to disjunction when you want “or.” Use formatToParts() when every item needs its own HTML element.

Display language, region, and currency names

Intl.DisplayNames translates codes into names meant for people:

const regions = new Intl.DisplayNames(['it'], {
  type: 'region',
})

regions.of('DK') //'Danimarca'

The type can represent languages, regions, scripts, currencies, calendars, and date-time fields. This is better than maintaining your own incomplete translation object.

Split text at real language boundaries

Intl.Segmenter splits text into graphemes, words, or sentences using locale-aware rules:

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

for (const part of segmenter.segment('JavaScript is fun')) {
  if (part.isWordLike) console.log(part.segment)
}

This matters because whitespace is not a universal word separator, and a visible character can contain several Unicode code points. My Unicode in JavaScript guide explains those layers in detail.

Work with locale identifiers

Intl.Locale lets you inspect and modify a locale without manually parsing its language tag:

const locale = new Intl.Locale('en-US-u-hc-h23')

locale.language //'en'
locale.region //'US'
locale.hourCycle //'h23'

Use Intl.getCanonicalLocales() to normalize locale identifiers. Use each constructor’s supportedLocalesOf() method when you need to know which requested locales the runtime supports.

You can also ask the runtime which values it supports:

Intl.supportedValuesOf('timeZone')
Intl.supportedValuesOf('calendar')
Intl.supportedValuesOf('currency')

Do not hardcode the returned list as a permanent source of truth. Runtime data can change.

Reuse formatter objects

Creating an Intl formatter involves locale negotiation and option processing. When formatting many values with the same options, create the formatter once and reuse it:

const prices = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
})

for (const price of [12, 25, 49]) {
  console.log(prices.format(price))
}

This also keeps formatting rules in one place. In a larger application I would build a small formatting module with one function per product decision: formatPrice(), formatEventDate(), and formatRelativeTime(). I would not spread raw toLocaleString() calls across components.

Intl does not translate application copy

Intl formats data. It does not translate sentences, load message catalogs, or decide which language your interface should use.

You still need translated message strings and a fallback strategy. Intl.PluralRules helps select the correct message variant, but your application supplies the words.

Keep the raw value separate from its presentation. Store a price as a number plus an ISO currency code. Store an instant as a timestamp and keep its intended time zone separately when that matters. Format values only at the interface boundary.

The complete list of constructors and options is defined by the official ECMAScript Internationalization API specification. It is the best reference when you need the exact behavior of a formatter.

Number formatting beyond currency

Intl.NumberFormat also handles percentages, units, compact notation, signs, and rounding.

const percent = new Intl.NumberFormat('en', {
  style: 'percent',
  maximumFractionDigits: 1,
})

percent.format(0.126) //'12.6%'

A percentage formatter expects a ratio, so 1 means 100 percent.

Use unit formatting when the value and unit belong together:

const distance = new Intl.NumberFormat('en-GB', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long',
})

distance.format(12) //'12 kilometres'

Compact notation is useful in dashboards where space is limited:

const compact = new Intl.NumberFormat('en', {
  notation: 'compact',
})

compact.format(1250000) //'1.3M'

Do not assume every currency uses two decimal digits. The formatter knows the normal number of minor units for the selected currency:

new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY',
}).format(1000)
//'¥1,000'

Only override fraction-digit rules when the product requires it.

Sort text with Collator

Default array sorting compares UTF-16 code units. That is rarely the order a reader expects.

const names = ['Åsa', 'Zoë', 'Ana']
const collator = new Intl.Collator('sv')

names.toSorted(collator.compare)

Options change what counts as equal for sorting. sensitivity: 'base' can ignore case and accents, while numeric: true sorts digit sequences as numbers:

const files = ['file2', 'file10', 'file1']
const collator = new Intl.Collator('en', { numeric: true })

files.toSorted(collator.compare)
//['file1', 'file2', 'file10']

Do not use a collator result as if it must be exactly -1 or 1. The contract only promises a negative number, zero, or a positive number.

Use plural rules with message variants

Plural categories are not just singular and plural. The possible categories include zero, one, two, few, many, and other, and languages use different subsets.

const rules = new Intl.PluralRules('en')

const messages = {
  one: '1 book',
  other: count => `${count} books`,
}

function formatBooks(count) {
  const category = rules.select(count)
  const message = messages[category]

  return typeof message === 'function' ? message(count) : message
}

Always provide an other message. A translation system normally manages these variants for you, but Intl.PluralRules is the language-aware decision underneath.

Format durations when the runtime supports it

Intl.DurationFormat formats a duration record without turning it into a date:

const duration = new Intl.DurationFormat('en', {
  style: 'long',
})

duration.format({ hours: 1, minutes: 30 })
//'1 hour and 30 minutes'

This API is newer than the other formatters in this guide. Check the environments you support before using it, and keep a fallback if older browsers matter.

~~~

Related posts about js: