What is the JavaScript Internationalization API (I18n)?

    Craig Buckler
    Share

    English is the world’s most widely used language, yet only one in seven people speak it. It’s the first (native) language of 379 million people, but 917 million speak Mandarin Chinese, 460 million speak Spanish, and 341 million speak Hindi.

    Many non-English speakers reside in emerging markets with exponential internet growth. If your web app can be globally translated, your potential target market could increase by 700%!

    The JavaScript Internationalization API (also known as i18n) allows you to design web pages and applications in such a way that they can be easily adapted to support the needs of users that speak different languages.

    In this article, we’ll look at the various methods the API offers and how you can implement them in your code to reach a wider, more international audience.

    Internationalization (I18n) Can Be Tricky

    Internationalization looks easy … until you try to do it.

    Latin-based languages can be superficially similar. For example, a form requesting a name, email, and date translates like this:

    • Spanish: nombre, email, fecha
    • French: nom, e-mail, date
    • German: name, email, datum

    The Gettext internationalization and localization system has been around for several decades, and libraries are available for most programming languages.

    In simpler cases, you could use some form of tokenization. For example, take an HTML template containing the following:

    <label for="name">{{ NAME }}</label>
    

    This is dynamically replaced by ‘name’ when a user has English set as their primary language. Unfortunately, that’s where the problems start for your user interface:

    1. There can be different variations of the same language. The Spanish spoken in Spain is not identical to that spoken in South America.
    2. Words in one language can be considerably longer in others. For example, “email” translates to “электронное письмо” in Russian.
    3. Text isn’t always oriented from left to right. Some is written from right to left — such as Arabic, Hebrew, Kurdish, and Yiddish. Others can be written from top to bottom, such as Chinese, Korean, Japanese, and Taiwanese.

    Many issues can be addressed by keeping text to a minimum and adopting CSS properties such as direction, writing-mode, and logical dimensions for layout.

    Terminology Turmoil

    Further confusion will arise when your application needs to display dates, times, numbers, currencies, or units.

    Consider a date shown as “12/03/24”. It will be read as:

    • “3 December 2024” by US residents who use the MDY format
    • “12 March 2024” by European, South American, and Asian residents who use the DMY format, and
    • “24 March 2012” by Canadian, Chinese, Japanese, and Hungarian residents who opt for the considerably more practical YMD format.

    (Be aware that date delimiter slashes are not common in all languages!)

    The number “1,000” will be read as:

    • “one thousand” by those in the US, UK, Canada, China, and Japan, and
    • “one (point zero)” by those in Spain, France, Germany, and Russia where a number’s decimal fraction is separated by a comma.

    The situation can even be complex in English alone. The term “1,000 meters” means:

    • 1 kilometer (or 0.62 of a mile) to US residents
    • a collection of one thousand measuring instruments to those in the UK, Canada, and Australia!

    The JavaScript Intl API

    The little-known JavaScript Intl object implements the ECMAScript Internationalization API in most modern browsers and runtimes. Support is generally good, and even IE11 has many of the more useful methods. For older browsers, there’s a polyfill, and the API can be detected like so:

    if (window.Intl) {
      // Intl supported
    }
    

    The API is slightly unusual. It provides several object constructors for dates, times, numbers, and lists, which are passed a locale and an optional object containing configuration parameters. For example, here’s a DateTime object specifying US English:

    const dateFormatter = new Intl.DateTimeFormat('en-US');
    

    This object can be used any number of times to call various methods which are passed a Date() value (or an ES6 Temporal when available). The format method is usually the most practical option. For example:

    const valentinesDay = dateFormatter.format( new Date('2022-02-14') );
    // returns US format "2/14/2022"
    
    const starwarsDay = dateFormatter.format( new Date('2022-05-04') );
    // returns US format "5/4/2022"
    

    Alternatively, you can create the Intl object and run a method in one line of code:

    const starwarsDay = new Intl.DateTimeFormat('en-US').format( new Date('2022-05-04') );
    

    As well as the format() method, some objects support these:

    • formatToParts(): returns an array of objects containing formatted strings, such as { type: 'weekday', value: 'Monday' }
    • resolvedOptions(): returns a new object with properties reflecting the locale and formatting options used, such as dateFormatter.resolvedOptions().locale.

    Defining Locales

    All Intl objects require a locale argument. This is a string which identifies:

    • a language subtag
    • a script subtag (optional)
    • a region (or country) subtag (optional)
    • one or more variant subtags (optional)
    • one or more BCP 47 extension sequences (optional)
    • a private-use extension sequence (optional)

    The language and region is often enough. For example, "en-US", "fr-FR", and so on.

    As well as using a string, an Intl.locale object can be used to construct locales, such as English US with 12-hour time format:

    const us = new Intl.Locale('en', {
      region: 'US', hourCycle: 'h12', calendar: 'gregory'
    });
    

    This can be used in another Intl constructor. For example:

    new Intl.DateTimeFormat(us, { timeStyle: 'medium' })
      .format( new Date('2022-05-04T13:00:00') );
    
    // "1:00:00 PM"
    

    If no locale is defined, the device’s current language and region settings are used. For example:

    new Intl.DateTimeFormat().format( new Date('2022-05-04') );
    

    This returns "5/4/2022" on a device with US settings and "04/05/2022" on a device with UK settings.

    Dates and Times

    The following tool shows examples of dates and times formatted using Intl.DateTimeFormat() (apologies if your language or region isn’t listed!):

    See the Pen
    i18n date and time formatting tool
    by SitePoint (@SitePoint)
    on CodePen.

    The constructor is passed the locale and an options object. This has many possible properties, although you rarely require more than dateStyle and/or timeStyle:

    property description
    dateStyle the date style: "full" "long" "medium" "short"
    timeStyle the time style: "full" "long" "medium" "short"
    calendar options include: "chinese" "gregory" "hebrew" "indian" "islamic" etc.
    dayPeriod period expressions: "narrow" "short" "long"
    numberingSystem numbering system: "arab" "beng" "fullwide" "latn" etc.
    localeMatcher locale matching algorithm: "lookup" "best fit"
    timeZone time zone: "America/New_York" "Europe/Paris" etc.
    hour12 set true to use 12-hour time notation
    hourCycle hour cycle: "h11" "h12" "h23" "h24"
    formatMatcher format matching algorithm: "basic" "best fit"
    weekday weekday format: "long" "short" "narrow"
    era era format: "long" "short" "narrow"
    year year format: "numeric" "2-digit"
    month month format: "numeric" "2-digit" "long" "short" "narrow"
    day day format: "numeric" "2-digit"
    hour hour format: "numeric" "2-digit"
    minute minute format: "numeric" "2-digit"
    second second format: "numeric" "2-digit"
    timeZoneName either: "long" "short"

    Examples:

    // Japanese short date, no time: "2022/05/04"
    new Intl.DateTimeFormat("ja-JP", { dateStyle: "short" })
      .format( new Date("2022-05-04T13:00") );
    
    // US short date and time: "5/4/22, 1:00 PM"
    new Intl.DateTimeFormat("en-US", { dateStyle: "short", timeStyle: "short" })
      .format( new Date("2022-05-04T13:00") );
    
    // UK long date, short time: "4 May 2022 at 13:00"
    new Intl.DateTimeFormat("en-GB", { dateStyle: "long", timeStyle: "short" })
      .format( new Date("2022-05-04T13:00") );
    
    // Spanish full date and time (dependent on your local time zone)
    // "miércoles, 4 de mayo de 2022, 13:00:00 (hora de verano británica)"
    new Intl.DateTimeFormat("es-ES", { dateStyle: "full", timeStyle: "full" })
      .format( new Date("2022-05-04T13:00") );
    

    Date Ranges

    A formatRange() method takes two dates and formats the period in the most concise way depending on the locale and options. For example:

    // result: "4 May 2022, 13:00–14:00"
    new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeStyle: "short" })
      .formatRange(new Date("2022-05-04T13:00"), new Date("2022-05-04T14:00"))
    

    This method has more limited browser support but was implemented in Chrome 76.

    Relative Periods

    The Intl.RelativeTimeFormat() object can display periods relative to this moment in time. The options object has fewer options:

    property description
    localeMatcher locale matching algorithm: "lookup" "best fit"
    numeric either "always", e.g. "1 day ago" or "auto", e.g. "yesterday"
    style format: "long" "short" "narrow"

    The format() method is passed a numeric value and a unit: "year", "quarter", "month", "week", "day", "hour", "minute", or "second". Examples:

    // US 1 day ago, numeric: "1 day ago"
    new Intl.RelativeTimeFormat("en-US")
      .format( -1, "day" );
    
    // US in one 1 day, auto: "tomorrow"
    new Intl.RelativeTimeFormat("en-US", { numeric: "auto" })
      .format( -1, "day" );
    
    // German, next month auto: "nächsten Monat"
    new Intl.RelativeTimeFormat("de-DE", { numeric: "auto" })
      .format( 1, "month" );
    

    Numbers, Currencies, Percentages, and Units

    The following tool shows examples using Intl.NumberFormat() to format numbers, currencies, percentages, and measurement units:

    See the Pen
    i18n number and currency formatting tool
    by SitePoint (@SitePoint)
    on CodePen.

    The constructor is passed the locale and an options object:

    property description
    numberingSystem options include "arab" "beng" "deva" "fullwide" "latn" etc.
    notation type: "standard" "scientific" "engineering" "compact"
    style formatting: "decimal" "currency" "percent" "unit" — this determines which other options can be set
    currency currency code: "USD" "EUR" "GBP" etc.
    currencyDisplay currency formatting: "symbol" "narrowSymbol" "code" "name"
    currencySign for negative currency values, "standard" a minus sign or "accounting" for parenthesis
    unit a unit type: "centimeter" "inch" "hour" etc.
    unitDisplay unit format: "long" "short" "narrow"
    useGrouping set false to disable thousands separators
    minimumIntegerDigits minimum number of integer digits
    minimumFractionDigits minimum number of fraction digits
    maximumFractionDigits maximum number of fraction digits
    minimumSignificantDigits minimum number of significant digits
    maximumSignificantDigits maximum number of significant digits

    Examples:

    // US number rounded to 2 decimal places: "12,345.68"
    new Intl.NumberFormat("en-US", { maximumSignificantDigits: 2 })
      .format( 12345.6789 );
    
    // French number rounded to 3 decimal places: "12 345,689"
    new Intl.NumberFormat("fr-FR", { maximumSignificantDigits: 3 })
      .format( 12345.6789 );
    
    // US compact number, 0 decimal places: "12K"
    new Intl.NumberFormat("en-US", { notation: "compact", maximumSignificantDigits: 0 })
      .format( 12345.6789 );
    
    // Spanish US dollar value: "12.345,68 US$"
    new Intl.NumberFormat("es-ES", {
      style: "currency",
      currency: "USD",
      currencyDisplay: "symbol"
    })
      .format( 12345.6789 );
    
    // UK meters in long format, 0 decimal places: "12,346 metres"
    new Intl.NumberFormat("en-GB", {
      maximumSignificantDigits: 0,
      style: "unit",
      unit: "meter",
      unitDisplay: "long"
    })
      .format( 12345.6789 );
    

    Lists

    A Intl.ListFormat() object can format an array of items into a language-sensitive list. In English, that typically requires an “and” or an “or” before the last item.

    The options object can set the following properties:

    property description
    type output format: "conjunction" for and-based lists, "disjunction" for or-based lists
    style formatting: "long" "short" "narrow"

    Examples:

    const browsers = ['Chrome', 'Firefox', 'Edge', 'Safari'];
    
    // US English: "Chrome, Firefox, Edge, and Safari"
    new Intl.ListFormat("en-US", { type: "conjunction" }).format(browsers);
    
    // US English: "Chrome, Firefox, Edge, or Safari"
    new Intl.ListFormat("en-US", { type: "disjunction" }).format(browsers);
    
    // French: "Chrome, Firefox, Edge, et Safari"
    new Intl.ListFormat("fr-FR", { type: "conjunction" }).format(browsers);
    
    // French: "Chrome, Firefox, Edge, ou Safari"
    new Intl.ListFormat("fr-FR", { type: "disjunction" }).format(browsers);
    

    Plurals

    The slightly bizarre Intl.PluralRules() object enables plural-sensitive language rules where you have a number of items. The options object can set a type property to either:

    • cardinal: the quantity of things (the default), or
    • ordinal: the ranking of things, such as 1st, 2nd, or 3rd in English

    The select() method returns an English string representing the pluralization category of the number: either zero, one, two, few, many, or other.

    Examples:

    // US English zero cardinal: "other"
    new Intl.PluralRules("en-US", { type: "cardinal" }).select(0);
    
    // US English zero ordinal: "other"
    new Intl.PluralRules("en-US", { type: "ordinal" }).select(0);
    
    // US English 1 cardinal: "one"
    new Intl.PluralRules("en-US", { type: "cardinal" }).select(1);
    
    // US English 1 ordinal: "one"
    new Intl.PluralRules("en-US", { type: "ordinal" }).select(1);
    
    // US English 2 cardinal: "other"
    new Intl.PluralRules("en-US", { type: "cardinal" }).select(2);
    
    // US English 2 ordinal: "two"
    new Intl.PluralRules("en-US", { type: "ordinal" }).select(2);
    
    // US English 3 cardinal: "other"
    new Intl.PluralRules("en-US", { type: "cardinal" }).select(3);
    
    // US English 3 ordinal: "few"
    new Intl.PluralRules("en-US", { type: "ordinal" }).select(3);
    

    String Comparison

    Finally, the Intl.Collator() object enables language-sensitive string comparison. Its options object can set the following properties:

    property description
    collation variant collation for certain locales
    numeric set true for numeric collation where “1” < “2” < “10”
    caseFirst either "upper" or "lower" case first
    usage either string "sort" (default) or "search"
    sensitivity "base" "accent" "case" "variant" comparisons
    ignorePunctuation set true to ignore punctuation

    The compare() method compares two strings. For example:

    // German: returns 1
    new Intl.Collator('de').compare('z', 'ä');
    

    Profit!

    It should be straightforward to show information using the user’s local format if you’re using JavaScript to display data. For example, the following code defines a dateFormat() function which uses the Intl short date format or falls back to YYYY-MM-DD when that’s not supported:

    // date formatting function
    const dateFormat = (Intl && Intl.DateTimeFormat ?
      date => new Intl.DateTimeFormat({ dateStyle: 'short' }).format(date) :
      date => date.toISOString().slice(0, 10)
    );
    
    // insert today's date into DOM #today element
    document.getElementById('today').textContent = dateFormat( new Date() );
    

    This alone won’t make your app easy for an international audience, but it’s one step closer to global distribution.