← Articles

Understanding the Intl Javascript API

Matías Hernández
author
Matías Hernández
Published
2 years ago

Personalizing the user experience is becoming more critical every day, even more so if your application or content is consumed by users in different parts of the world who most likely use other languages, date formats, currency, etc.

There are multiple solutions to make your content adapt to the location or language of your users. Still, many of these methods have become outdated, complex, or dependent on a particular framework.

Javascript also offers an internationalization solution, the Intl object.

What is Internationalization?

The concept of internationalization, or i18n, is the process of supporting different languages, and countries in your application.

i18n is commonly confused with Localization or even translation, but i18n refers to developing a product, a process focused on supporting different languages and formats based on locality — a common code base.

Providing internationalization support is critical for many products but often overlooked.

Localization refers to the generation of a specific product to target a market or region, including translating content and even modifying the interface and terminology.

Typically i18n implementation may include:

  • Development of software that is independent of a specific language or cultural convention. (e.g. display of times and dates)
  • Use of localization frameworks
  • Removal of "hard-coded" text in the code
  • Support for bi-directional languages
  • Support for different number formats

The Intl object

According to MDN, "The Intl object is the namespace for the ECMAScript Internationalization API, which provides language sensitive string comparison, number formatting, and the date and time formatting. The Intl object provides access to several constructors and functionality common to the internationalization constructors and other language sensitive functions."

In other words, through the global Intl object, you can access a set of tools to work with content sensitive to the user's language.

Most browsers currently support these methods well based on information available at caniuse.com

Can I Use: Intl

TLDR; A quick example of what this API can achieve:

const value = 1234.39;const today = new Date("2022-09-07");/** * @param locale is a string that represents a location code */function dateFormatter(locale) { const dateFormatter = new Intl.DateTimeFormat(locale) const currencyFormatter = new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }) const date = dateFormatter.format(today) const currency = currencyFormatter.format(value) console.log(locale, date, currency);}dateFormatter("en-US");// expected output: en-US 9/7/2022 $1,234.39dateFormatter("es-ES");// expected output: es-ES 7/9/2022 1234,39 US$

You can see a demo of this code in the following link

In this code, you can briefly see some methods and transformations that Intl can do.

  • Gives you static access to different constructors, in this case, DateTimeFormat and NumberFormat
  • Allows you to "transform" content from one format to another based on the value of locale

It is essential to note that the locale argument is received by all constructors exposed by Intl. This locale is the value you will ideally capture dynamically so you can modify content based on its value.

Browsers offer a method of getting the value of locale based on the user's preferences or location.

const locale = navigator.languageconsole.log(locale); // "es-CL"

What is locale, and how does it work?

When we refer to locale we refer to a string that represents a group of user preferences like:

  • Date and Time
  • numbers and currency
  • Time zones, languages, and countries
  • Measurement units

This locale string must follow a particular format to be used, this consists of:

  • A subtag or language code
  • (optional) a region or country subtag
  • (optional) a subtag script
  • (optional) one or more variation subtags
  • (optional) one or more BCP-47 extension sequences

locale example

Each subtag or sequence used to create the locale string is separated by a hyphen. Also, identifiers identify the case.

Some examples of locale strings

  • "es": Spanish (language)
  • "es-CL": Spanish (language) as it is used in Chile (region)
  • "zh-Hans-CN": Simplified Chinese (language) (script) as used in China (region)

When a locale string is passed as an argument to one of Intl's provided constructors, it is compared to a list of available locales for the best fit. This process is done using one of two possible algorithms: lookup or best-fit.

The lookup algorithm checks if the runtime environment has the locale used by searching from the most specific to the least detailed result. If the exact value of locale is unavailable, it will return the closest one. For example, if you search for de-DE-u-co-phonebk if not found, you can return de-DE, and in case of not seeing de reference will return a default value.

The best-fit algorithm is an improvement of the previous algorithm where a default value is not returned if the search is not found. If not, the one that best "fits" is returned. IF es-CL is searched for but not found, es-AR will be returned instead of just es.

You can learn more about this process by reviewing the documentation on MDN.

What does each subtag of the string mean?

As described above, the string locale is made up of different parts separated by a hyphen, the first section being the language identifier.

What are the rest used for?

Script code

This script code is used to identify in what "format" a particular language is written. For example, in Asian languages Hans means that Simplified Chinese will be used vs. Hant, which indicates that Traditional Chinese will be used.

Variant code

Variant codes represent the different dialect options for a specific language.

Extensions

Extensions include identifiers for different calendars, and numeric or ordering systems. In the example of the previous image, phonebk identifies the variant indicating the ordering used for the letters, in this case phone book style.

Time to code

Formatting dates and time

Let's start by formatting dates and times based on different values of locale.

For this, you need access to the DateTimeFormat constructor.

const date = new Date(); //Today's date// Fecha en formato USAconst usaDate = new Intl.DateTimeFormat('en-US').format(date) // 9/8/2022// Fecha en formato Chileconst clDate = new Intl.DateTimeFormat('es-CL').format(date) // 08-09-2022// Fecha en formato Alemanconst deDate = new Intl.DateTimeFormat('de').format(date) // 8.9.2022// Fecha en formato Arabico egiptoconst arDate = new Intl.DateTimeFormat('ar-eg').format(date) // ٨‏/٩‏/٢٠٢٢

Find a demo of this code in this playground

This is the basic way to format date and time, but each constructor provided by the Intl object accepts a second argument that allows you to modify the result.

For DateTimeFormat the options are:

type Options = { dateStyle: 'full' | 'long' | 'medium' | 'short', timeStyle: 'full' | 'long' | 'medium' | 'short', calendar: 'buddhist' | 'chinese' | 'coptic' | 'dangi' | 'ethioaa' | 'ethiopic' | 'gregory' | 'hebrew' | 'indian' | 'islamic' | 'islamic-umalqura' | 'islamic-tbla' | 'islamic-civil' | 'islamic-rgsa' | 'iso8601' | 'japanese' | 'persian' | 'roc' | 'islamicc', dayPeriod: 'narrow' | 'short' | 'long', numberingSystem: 'arab' | 'arabext' | 'bali' | 'beng' | 'deva' | 'fullwide' | ' gujr' | 'guru' | 'hanidec' | 'khmr' | ' knda' | 'laoo' | 'latn' | 'limb' | 'mlym' | 'mong' | 'mymr' | 'orya' | 'tamldec' | 'telu' | 'thai' | 'tibt', localeMatcher: 'lookup' | 'best fit', year: "numeric" | "2-digit", month: "numeric" | "2-digit" | "long" | "short" | "narrow", day: "numeric" | "2-digit", hour: "numeric" | "2-digit", minute: "numeric" | "2-digit", second: "numeric" | "2-digit", era: "long" | "short" | "narrow", weekday: "long" | "short" | "narrow", hourCycle: 'h11'|'h12'|'h23'|'h24', hour12: boolean, timeZone: string, formatMatcher: 'basic' |'best fit', timeZoneName: 'long' | 'short' |'shortOffset'|'longOffset'|'shortGeneric'| 'longGeneric'}

You have many options to define the best way to display a date and time in your application.

Let's review some examples.

const options = { year: "2-digit", month: "long", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", weekday: "long", hour12: true, timeZone: 'America/Santiago'}// Date in USA formatconst usaDate2 = new Intl.DateTimeFormat('en-US', options).format(date)// Thursday, September 8, 22 at 10:20:54 AMconst clDate2 = new Intl.DateTimeFormat('es-CL', options).format(date)// jueves, 8 de septiembre de 22, 10:20:54 a. m.// Date in German formatconst deDate2 = new Intl.DateTimeFormat('de', options).format(date)//Donnerstag, 8. September 22 um 10:20:54 AM// Date in Arabic Egypt formatconst arDate2 = new Intl.DateTimeFormat('ar-eg', options).format(date)// الخميس، ٨ سبتمبر ٢٢ في ١٠:٢٠:٥٤ ص

Formatting Numbers

Another constructor offered by Intl is NumberFormat. You can use this constructor to change the way numbers are represented on the screen.

The only difference between DateTimeFormat and NumberFormat constructors is the set of options that these contructors receives.

type Options = { compactDisplay?: "short" | "long"; // Only used when notation is "compact" currencyDisplay?: "symbol" | "narrowSymbol" | "code" | "name"; currencySign?: "standard" | "accounting"; localeMatcher?: "lookup" | "best fit"; notation?: "standard" | "scientific" | "engineering" | "compact"; numberingSystem?: 'arab' | 'arabext' | 'bali' | 'beng' | 'deva' | 'fullwide' | 'gujr' | 'guru' | 'hanidec' | 'khmr' | 'knda' | 'laoo' | 'latn' | 'limb' | 'mlym' | 'mong' | 'mymr' | 'orya' | 'tamldec' | 'telu' | 'thai' | 'tibt'; signDisplay?: "auto" | "always" | "exceptZero" | "negative" | "never" ; style?: "decimal" | "currency" | "percent" | "unit"; unit?: string; unitDisplay?: "long" | "short" | "narrow"; useGrouping?: "always" | "auto" | boolean | "min2"; roundingMode?: "ceil" | "floor" | "expand" | "trunc" | "halfCeil" | "halfFloor" | "halfExpand" | "halfTrunc" | halfEven"; roundingPriority?: "auto" | "morePrecision" | "lessPrecision"; roundingIncrement?: 1 | 2 | 5 | 10 | 20 | 25 | 50 | 100 | 200 | 250 | 500 | 1000 | 2000 | 2500 | 5000; trailingZeroDisplay?: "auto" | "stripIfInteger"; minimumIntegerDigits?: number; minimumFractionDigits?: number; maximumFractionDigits?: number; minimumSignificantDigits?: number; maximumSignificantDigits?: number;}

Using this extensive list of options, you can very likely format a number with every possible use case.

Check this quick example.

function currencyFormatter({ currency, value}) { const formatter = new Intl.NumberFormat('en-US', { style: 'currency', minimumFractionDigits: 2, currency }) return formatter.format(value)}const value = 123456const dollar = currencyFormatter({ currency: "USD", value}) //$123,456.00const pound = currencyFormatter({ currency: "GBP", value}) // £123,456.00const peso = currencyFormatter({ currency: "CLP", value}) // CLP 123,456.00const dinar = currencyFormatter({ currency: "DZD", value}) // DZD 123,456.00

In this code snippet, you can see the following

  • A function was created to hold the formatter.
  • The formatter is always using the en-US locale.
  • Three options define what style of number formatting will use style: currency.

Even though we kept the same locale, the values returned by the formatting function are different.

What would happen if you also changed the locale for each currency conversion?

In case these options don't cover your use-case, you can use another method that is provided by the formatter: formatToParts. This method will return an array of objects representing the number as a string in parts so you can use them to customize and solve your particular situation.

const formatToParts = new Intl.NumberFormat('en-US', { currency: 'USD', style: 'currency', minimumFractionDigits: 2}).formatToParts(value) /*[ { type: "currency", value: "$"}, { type: "integer", value: "123" }, { type: "group", value: "," }, { type: "integer", value: "456" }, { type: "decimal", value: "." }, { type: "fraction", value: "00" }]

What if you want to show a number like the social networks? Like 1.2K?

The NumberFormat can solve that.

// Compact format function formatCompact(value) { const result = new Intl.NumberFormat( 'en-US', { notation: "compact"} ).format(value) return result;}const res1 = formatCompact(123) // 123const res2 = formatCompact(1234) // 1.2Kconst res3 = formatCompact(12345) // 12Kconst res4 = formatCompact(123456) // 123Kconst res5 = formatCompact(1234567) // 1.2M

In this case, the solution involves the use of the notation option.

You can find a demo for the number formatting in this playground.

Relative Time

Sometimes, you want to display the dates in a relative form such as: 2 weeks ago. You can do this by doing some math and string concatenation with the dates, but Intl helps you with this.

Intl expose a constructor named RelativeTimeFormat that, same as before, accepts an argument to tell it what language you want to use and a set of options

type Options = { localeMatches: 'best fit' | 'lookup', numeric: 'always' |'auto' // The format of the output style: 'long' | 'short' | 'narrow'}

For this constructor the format method accepts a numeric value (NOT A DATE) like this

const formatter = new Intl.RelativeTimeFormat('en-US');const monthAgo = formatter.format(-1, 'month') // 1 month agoconst futureMonth = formatter.format(1, 'month') // in 1 month

When would you use something like this?

Imagine a list of articles, each article have a publication date, but you don't want to show just that, you want to show how much relative time is between the publication date and today (the day the user is reading the list).

To accomplish this you'll need to do a little math to get the difference between the two dates and use that result as a relative time value.

const publicationDate = new Date('2022/01/05')const currentDate = new Date()const msPerDay = 1000 * 60 * 60 * 24;const diffTime = Math.abs(currentDate - publicationDate);const diffDays = Math.ceil(diffTime / msPerDay);const enRtf = new Intl.RelativeTimeFormat("en-US", { numeric: 'auto',});console.log(enRtf.format(-diffDays, "day")); // 251 days agoconsole.log(enRtf.format(-diffDays/30, "month")); //8.367 months agoconst esRtf = new Intl.RelativeTimeFormat("es-ES", { numeric: 'auto',});console.log(esRtf.format(-diffDays, "day")); // hace 251 díasconsole.log(esRtf.format(-diffDays / 30, "month")); // hace 8.367 meses

Check the demo playground in this link

Pluralization

The Intl object also supports a way to define plural-sensistive content with the PluralRules constructor.

One use case for this feature is to show the number of items that exist in your collection, ideally this should respect the grammatical numbering in each language you choose to use.

You can always get around this requirement by writing your copy avoiding the need of pluralization, but what happen is you need it?

Let's check an example

function pluralize(locale, count, singular, plural) { const pluralRules = new Intl.PluralRules(locale); const numbering = pluralRules.select(count); switch (numbering) { case 'one': return count + ' ' + singular; case 'other': return count + ' ' + plural; default: throw new Error('Unknown: '+ numberig); }}function showItemsQuantity(count) { return pluralize('en-US', count, 'item', 'items')}const zeroItem = showItemsQuantity(0) // 0 itemsconst oneItem = showItemsQuantity(1) // 1 itemconst manyItem = showItemsQuantity(12) // 12 items// Spanishfunction mostrarCantidadCajas(count) { return pluralize('es-ES', count, 'caja', 'cajas')}const ceroItem = mostrarCantidadCajas(0) // 0 cajasconst unoItem = mostrarCantidadCajas(1) // 1 cajaconst muchosItem = mostrarCantidadCajas(12) // 12 cajas

Check the demo in the playground

Note: in a real-woprld sceneario, you wouldn't hardcode plurals in this snippet; they'd be part of your translation files

Maybe this example is too naive, since English and Spanish have just two pluralization rules; however, not every language follow this rule, some have only a single plural form, while other have multiple forms.

This example has been borrowed from v8 blog

const suffixes = new Map([ ['zero', 'cathod'], ['one', 'gath'], // Note: the `two` form happens to be the same as the `'one'` // form for this word specifically, but that is not true for // all words in Welsh. ['two', 'gath'], ['few', 'cath'], ['many', 'chath'], ['other', 'cath'],]);const pr = new Intl.PluralRules('cy');const formatWelshCats = (n) => { const rule = pr.select(n); const suffix = suffixes.get(rule); return `${n} ${suffix}`;};formatWelshCats(0); // '0 cathod'formatWelshCats(1); // '1 gath'formatWelshCats(1.5); // '1.5 cath'formatWelshCats(2); // '2 gath'formatWelshCats(3); // '3 cath'formatWelshCats(6); // '6 chath'formatWelshCats(42); // '42 cath'

This constructor just like the others exposed by Intl accepts a second argument to define options. One option is the type that allows you to define the selection rule. By default it uses the cardinal option. If you want to get the ordinal indicator for a number (for example to create a list ) you can accomplish that as follows:

const pr = new Intl.PluralRules('en-US',{ type: 'ordinal'})const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th'}const formatOrdinals = n => `${n}${suffixes[pr.select(n)]}`formatOrdinals(0) // 0thformatOrdinals(1) //1sstformatOrdinals(2) // //2ndformatOrdinals(40) //40thformatOrdinals(63) // 63rdformatOrdinals(100) // 100nd

List Formatting

Displaying a list is one of the most used ways to showcase information in a web app. But since your users speaks different language you need a way to format the list based on that language convention.

To avoid the hassle of implementing this formatting rules by hand - that could be really hard - Intl offers the ListFormat API.

function getFormatter(locale = 'en-US') { const lf = new Intl.ListFormat(locale); return lf}const enLf = getFormatter()const zfLf = getFormatter('zh')enLf.format(['one']); // OneenLf.format(['one', 'two']); // One and twoenLf.format(['one', 'two', 'three']); // One, two, and threeenLf.format(['one', 'two', 'three', 'four']); // One, two, and threezfLf.format(['one']); // OnezfLf.format(['one', 'two']); // One和twozfLf.format(['one', 'two', 'three']); // One、two和threezfLf.format(['one', 'two', 'three', 'four']); // One、two、three和four

What this formatter does is basically joining an array of string with the correct conjunction or disjunction to create a meaningful phrase.

The default way to format is by using a conjunction unless you pass the type options as disjuntion

const dlf = new Intl.ListFormat('en-US', { type: 'disjunction'})dlf.format(['One']) // Onedlf.format(['One', 'two']) // One or twodlf.format(['One', 'two', 'three']) // One, two, or threedlf.format(['One', 'two', 'three', 'four']) // One, two, three or four

As always, you can check the playground example in this link.

Segmentation

Segmentation refers to the requirement of splitting some text in segments, for example, split a paragraph in words. The Intl.Segmenter constructor offers a locale-sensitive solution to split the text returning meaningful items for the source string.

The most naive and most often used solution to split a string is just using String.prototype.split, for example splitting a text by whitespaces .split(' '). But, what happen if the language does not use whitespace between words? The result of the split action will be wrong, to avoid this let's use the Segmenter constructor.

const str = "吾輩は猫である。名前はたぬき。";const segmenterJa = new Intl.Segmenter('ja-JP', { granularity: 'word' });const segments = segmenterJa.segment(str);console.table(Array.from(segments));// [{segment: '吾輩', index: 0, input: '吾輩は猫である。名前はたぬき。', isWordLike: true},// etc.// ]

Example extracted directly from MDN documentation

String comparison

The last constructor to review is the Intl.Collator, this constructor enables string comparison with locale sensitivity.

This constructor is very useful to sort strings that contain extra letters like German or Swedish. Different languages have different sorting rules, let's check a quick example of this

const letters = ['ä', 'z']const listDE = new Intl.Collator('de')const listSV = new Intl.Collator('sv')// in German, ä sorts with aconsole.log(letters.sort(listDE.compare));// → a negative value// in Swedish, ä sorts after zconsole.log(letters.sort(listSV.compare));// → a positive value

Check the code in the playground

Conclusion

Internationalization is an important piece of an application or website that is meant to be used by a worldwide audience, but getting it right is a complex topic. Luckly there are many building blocks directly available from the Javascript engine that can help you implement a locale sensible UI.

That’s a wrap! If you have any questions or feedback, open an issue on my AMA repo or ping me on Twitter.