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.