Effortless Internationalization in JavaScript with the Intl API

7 min read

1678 words

When creating apps and websites, we often need to display data in different languages. Be it currencies, speeds, times or dates. Sorting words in different languages or dynamically setting the singular and plural forms of a word are also such use cases. For this there is the Intl API in JavaScript, which in my opinion still gets far too little attention.

In the past, I have written custom functions or used libraries to do the above in different languages.

So let's take a look at some of the functions of the Intl API and try to understand why it can make our lives easier.

The examples use either English or German so that I can check the results for accuracy as much as possible.

We can see that the Intl API is good at presenting information in a format that feels completely natural across different languages.

Browser support

Most functions are available in all browsers and server JavaScript runtimes (NodeJS and Deno). For example DateTimeFormat, NumberFormat or PluralRules. Other methods and classes such as DurationFormat are currently making their way into the various JavaScript environments. Detailed information on availability can be found on the Mozilla Developer Network (MDN) page: Intl - JavaScript.

Formatting Numbers

Let's start with the formatting of numbers. An example of this is the separators used for decimal numbers. In German, a comma , separates the number, whereas in English it is .. Let's look at this in an example.

const number = 123456.789;

// German formatting
const formatterDE = new Intl.NumberFormat("de-DE");
console.log(formatterDE.format(number));
// Output: 123.456,789

// English formatting
const formatterEN = new Intl.NumberFormat("en-US");
console.log(formatterEN.format(number));
// Output: 123,456.789

Another example is the use of currencies. In particular, the difference between the euro and the US dollar symbol. In the latter, the symbol is placed before the number, whereas in the euro it is placed after the number.

const number = 123456.789;

// Currency formatting
const price = 123456.789;
// German Euro formatting
const priceDE = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR",
});
console.log(priceDE.format(price));
// Output: 123.456,79 €

// US Dollar formatting
const priceEN = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});
console.log(priceEN.format(price));
// Output: $123,456.79

However, we can also create different units without knowing exactly how they are written. For example, I personally didn't know that miles per hour is abbreviated to 'mi/h' in English.

// Unit formatting
const speed = 50;
// German
const speedDE = new Intl.NumberFormat("de-DE", {
  style: "unit",
  unit: "kilometer-per-hour",
});
console.log(speedDE.format(speed));
// Output: 50 km/h

// English
const speedEN = new Intl.NumberFormat("en-US", {
  style: "unit",
  unit: "mile-per-hour",
});
console.log(speedEN.format(speed));
// Output: 50 mph

// German
const milesDE = new Intl.NumberFormat("de-DE", {
  style: "unit",
  unit: "mile-per-hour",
});
console.log(milesDE.format(speed));
// Output: 50 mi/h

Date and Time Formatting

Now let's go further and look at formatting dates and times. We will see that the Intl.DateTimeFormat functions can work with date objects, but also with Temporal objects.

Let's look at some examples of what we can do with this API. The advantage of this is that we don't need to know how times or dates are represented for different languages.

// For Temporal, ensure you have a polyfill (if needed)
// Example using the @js-temporal/polyfill
// import { Temporal } from '@js-temporal/polyfill'; // Uncomment if using polyfill

const date = new Date("2024-01-06T12:00:00Z");
const temporal = Temporal.Instant.from("2024-01-06T12:00:00Z")
  .toZonedDateTimeISO("UTC");

// Basic date formatting
const dateFmtDE = new Intl.DateTimeFormat("de-DE");
console.log(dateFmtDE.format(date)); // 6.1.2024
console.log(dateFmtDE.format(temporal)); // 6.1.2024, 13:00:00 MEZ

const dateFmtEN = new Intl.DateTimeFormat("en-US");
console.log(dateFmtEN.format(date)); // Output: 1/6/2024
console.log(dateFmtEN.format(temporal)); // 1/6/2024, 1:00:00 PM GMT+1

// Complex formatting with options
const detailedFmtDE = new Intl.DateTimeFormat("de-DE", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "long",
});
console.log(detailedFmtDE.format(date));
// Samstag, 6. Januar 2024 um 13:00 Mitteleuropäische Normalzeit
console.log(detailedFmtDE.format(temporal));
// Samstag, 6. Januar 2024 um 13:00:00 Mitteleuropäische Normalzeit

const detailedFmtEN = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "long",
});
console.log(detailedFmtEN.format(date));
// Saturday, January 6, 2024 at 01:00 PM Central European Standard Time
console.log(detailedFmtEN.format(temporal));
// Saturday, January 6, 2024 at 1:00:00 PM Central European Standard Time

Important Note about Time Zones: The examples use UTC ("2024-01-06T12:00:00Z"). Intl.DateTimeFormat uses the system's time zone by default. If you format a date/time without specifying a time zone, the output will be in the user's local time zone. If you format a date/time with a time zone (like in the Temporal example), the output reflects that time zone.

Relative Time Formatting

One feature I like is the formatting of relative times. For example 'today', 'tomorrow' or 'last week'. We can create all this with the Intl.RelativeTimeFormat function. Let's have a look at the following example.

// German relative time
const rtfDE = new Intl.RelativeTimeFormat("de-DE", {
  numeric: "auto",
});

console.log(rtfDE.format(-1, "day")); // gestern
console.log(rtfDE.format(2, "day")); // übermorgen
console.log(rtfDE.format(-1, "week")); // letzte Woche
console.log(rtfDE.format(0, "quarters")); // dieses Quartal

// English relative time
const rtfEN = new Intl.RelativeTimeFormat("en-US", {
  numeric: "auto",
});

console.log(rtfEN.format(-1, "day")); // yesterday
console.log(rtfEN.format(2, "day")); // in 2 days
console.log(rtfEN.format(-1, "week")); // last week
console.log(rtfEN.format(0, "quarter")); // this quarter

Pluralization

I am glad that such an API now exists, as in the past I had to build it myself or use libraries like pluralise. Having this implemented directly in the browser makes another library obsolete.

// German pluralization
const pluralDE = new Intl.PluralRules("de-DE");

const mappingDE = {
  one: "Buch",
  other: "Bücher",
};

function formatBooksDE(n) {
  return `${n} ${mappingDE[pluralDE.select(n)]}`;
}

console.log(formatBooksDE(0)); // 0 Bücher
console.log(formatBooksDE(1)); // 1 Buch
console.log(formatBooksDE(2)); // 2 Bücher
console.log(formatBooksDE(5)); // 5 Bücher

// English pluralization
const pluralEN = new Intl.PluralRules("en-US");

const mappingEN = {
  one: "book",
  other: "books",
};

function formatBooksEN(n) {
  return `${n} ${mappingEN[pluralEN.select(n)]}`;
}

console.log(formatBooksEN(0)); // 0 books
console.log(formatBooksEN(1)); // 1 book
console.log(formatBooksEN(2)); // 2 books
console.log(formatBooksEN(5)); // 5 books

// More complex example with ordinal numbers
const ordinalDE = new Intl.PluralRules("de-DE", { type: "ordinal" });
const ordinalEN = new Intl.PluralRules("en-US", { type: "ordinal" });

// German ordinal markers
const ordinalMarkersDE = {
  other: ".", // German uses the same marker for all ordinals
};

// English ordinal markers
const ordinalMarkersEN = {
  one: "st",
  two: "nd",
  few: "rd",
  other: "th",
};

function formatOrdinalDE(n) {
  const marker = ordinalMarkersDE[ordinalDE.select(n)];
  return `${n}${marker}`; // e.g., "1.", "2.", "3."
}

function formatOrdinalEN(n) {
  const marker = ordinalMarkersEN[ordinalEN.select(n)];
  return `${n}${marker}`; // e.g., "1st", "2nd", "3rd"
}

// German ordinals
console.log(formatOrdinalDE(1)); // 1.
console.log(formatOrdinalDE(2)); // 2.
console.log(formatOrdinalDE(3)); // 3.
console.log(formatOrdinalDE(4)); // 4.

// English ordinals
console.log(formatOrdinalEN(1)); // 1st
console.log(formatOrdinalEN(2)); // 2nd
console.log(formatOrdinalEN(3)); // 3rd
console.log(formatOrdinalEN(4)); // 4th
console.log(formatOrdinalEN(11)); // 11th
console.log(formatOrdinalEN(21)); // 21st
console.log(formatOrdinalEN(42)); // 42nd

// Using with different number formats
const numberDE = new Intl.NumberFormat("de-DE");
const numberEN = new Intl.NumberFormat("en-US");

function formatLargeOrdinalDE(n) {
  return `${numberDE.format(n)}${ordinalMarkersDE[ordinalDE.select(n)]}`;
}

function formatLargeOrdinalEN(n) {
  return `${numberEN.format(n)}${ordinalMarkersEN[ordinalEN.select(n)]}`;
}

// Large number formatting with ordinals
console.log(formatLargeOrdinalDE(1234)); // 1.234.
console.log(formatLargeOrdinalEN(1234)); // 1,234th

List Formatting

Last but not least, we can format lists using the Intl API. As in the previous examples, we need to specify the language for which we want to format. We also have options to define how our output should look. Another advantage is the automatic ending of sentences with words such as and (conjunction) or or (disjunction).

const fruits = ["Äpfel", "Birnen", "Orangen"];

// German list formatting
const listFmtDE = new Intl.ListFormat("de-DE", {
  style: "long",
  type: "conjunction",
});
console.log(listFmtDE.format(fruits));
// Output: Äpfel, Birnen und Orangen

// English list formatting
const listFmtEN = new Intl.ListFormat("en-US", {
  style: "long",
  type: "conjunction",
});
console.log(listFmtEN.format(fruits));
// Output: Äpfel, Birnen, and Orangen

// Different types of lists (German)
const disjunctionFmtDE = new Intl.ListFormat("de-DE", {
  style: "short",
  type: "disjunction",
});
console.log(disjunctionFmtDE.format(["lesen", "schreiben", "bearbeiten"]));
// Output: lesen, schreiben oder bearbeiten

// Different types of lists (English)
const disjunctionFmtEN = new Intl.ListFormat("en-US", {
  style: "short",
  type: "disjunction",
});
console.log(disjunctionFmtEN.format(["read", "write", "edit"]));
// Output: read, write, or edit

// If you just want to order it
const narrowListFmtEN = new Intl.ListFormat("en-US", {
  style: "narrow",
  type: "conjunction",
});
console.log(narrowListFmtEN.format(["Apple", "Pear", "Orange"])); // Output: Apple, Pear, Orange

It seems that the style: short in German (de-DE) makes no difference to the output. In English en, however, the short style produces an & symbol instead of an and. See the example below.

const fruits = ["Äpfel", "Birnen", "Orangen"];

// German list formatting
const listFmtDE = new Intl.ListFormat("de-DE", {
  style: "long", // Uses full conjunction word "und"
  type: "conjunction",
});
console.log(listFmtDE.format(fruits)); // Äpfel, Birnen und Orangen

const shortListFmtDE = new Intl.ListFormat("de-DE", {
  style: "short",
  type: "conjunction",
});
console.log(shortListFmtDE.format(fruits));
// should return Äpfel, Birnen u. Orangen but returns Äpfel, Birnen und Orangen in de-DE

const listFmtEN = new Intl.ListFormat("en", {
  style: "long",
  type: "conjunction",
});
console.log(listFmtEN.format(fruits)); // Äpfel, Birnen and Orangen

const shortListFmtEN = new Intl.ListFormat("en", {
  style: "short",
  type: "conjunction",
});
console.log(shortListFmtEN.format(fruits)); // Äpfel, Birnen, & Orangen

Conclusion

In summary, the Intl API gives us some helpers that can make external libraries obsolete. The ability to get spellings for different languages directly, without a lot of custom logic, should speed up the internationalisation of websites and services considerably.