---
title: 'Internationalization API'
source: 'https://academia.sh/en/courses/browser-platform/internationalization-api'
course: 'The Browser and the Web Platform'
language: en
updated: '2026-08-17T18:09:11+00:00'
license: 'CC BY-SA 4.0'
---

# Internationalization API

Doing number, date, and sorting according to locale; the locale tag's structure, splitting formatted output into parts, the time zone's role, sorting's dependence on the alphabet, and the formatter's setup cost.

Part of the work done inside a frame is producing text: writing a
unit next to a measurement value, converting a timestamp into a
readable date, sorting the list by name. None of these operations are
universal.

The decimal separator is not the same character everywhere, the
order of date fields varies by language, letter sorting depends on
the alphabet, and in Turkish the dotted and dotless letter are
separate letters. Hand-written formatting code cannot carry these
differences. For this, the browser offers a family of interfaces
that carry their own language data.

This lesson's outputs got produced on one runtime. The formatted
text's **exact character sequence** depends on the language data the
runtime carries, and can change between versions; what does not
change is the operation itself. This distinction turns into a rule at
the end of the lesson.

## The Locale Tag

All these interfaces take a **locale** tag. The tag consists of
hyphen-separated subtags: language, an optional script, an optional
region. `tr` declares only the language, `tr-TR` the language and the
region, `sr-Latn-RS` all three at once.

The region subtag chooses **rules**, not the language: currency,
measurement system, and date order can diverge between two regions
of the same language. Extensions added at the end of the tag can also
request a calendar, a numbering system, and a sort order.

The user's preference gets read from the browser and arrives as a
**list**: if the first preference is not supported, the second gets
tried. A list can also get given to the interfaces instead of a
single tag; the choice gets made by intersecting with the provided
data. Which setting got chosen and which options are in effect can
get asked back from the formatter.

## Number Formatting

```js
// number.mjs — formatting the same number according to locale
const value = -1234.567;

for (const locale of ["tr-TR", "en-US", "de-DE"]) {
  const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
  console.log(locale, "->", format.format(value));
}

const temperature = new Intl.NumberFormat("tr-TR", {
  style: "unit", unit: "celsius", unitDisplay: "short", maximumFractionDigits: 1,
});
const ratio = new Intl.NumberFormat("tr-TR", { style: "percent", maximumFractionDigits: 1 });
const compact = new Intl.NumberFormat("tr-TR", { notation: "compact" });
const compactText = compact.format(48210);

console.log("temperature:", temperature.format(-4.23));
console.log("ratio      :", ratio.format(0.0731));
console.log("compact    :", compactText, "| code points:",
  [...compactText].map((c) => "U+" + c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")).join(" "));

// Splitting into parts makes it possible to pick a field out of the formatted text.
console.log(JSON.stringify(temperature.formatToParts(-4.23)));
```

```
tr-TR -> -1.234,57
en-US -> -1,234.57
de-DE -> -1.234,57
temperature: -4,2°C
ratio      : %7,3
compact    : 48 B | code points: U+0034 U+0038 U+00A0 U+0042
[{"type":"minusSign","value":"-"},{"type":"integer","value":"4"},{"type":"decimal","value":","},{"type":"fraction","value":"2"},{"type":"unit","value":"°C"}]
```

The first three lines show the decimal and thousands separators
swapping places; the same number appears in three writings. Code that
concatenates a number with a period and writes it to the screen can
run into a period the user takes for a thousands separator.

The percent line carries a second difference: in Turkish, the sign
comes **before** the number. Where the unit and the sign get placed
also comes from the language data; concatenating by hand loses this
information.

The compact notation's code points carry a third warning. The
character between the number and the abbreviation is not an
ordinary space, it is the **non-breaking space** defined in the Text
Labels lesson; it shows up as `U+00A0` in the code point list. Code
that tries to recover the number by splitting the text on whitespace
runs into this character. This is concrete proof that formatted text
should not get parsed.

The last line shows the most useful operation. The formatted text can
get split into parts, and each part's type gets declared. This way,
the integer part can get written large, the decimal part small, or
the unit can get put into a separate element; none of this requires
trying to split the text.

The formatting's input also needs attention. The number formatter
expects a number, not a string; the type loss mentioned in the
Storage APIs lesson becomes visible here.

## Date, Time, and Time Zone

```js
// date.mjs — formatting the same instant by locale and time zone
const instant = new Date("2024-02-11T06:05:00Z");   // a fixed instant in universal time

for (const [locale, zone] of [["tr-TR", "Europe/Istanbul"], ["en-US", "America/New_York"]]) {
  const format = new Intl.DateTimeFormat(locale, {
    dateStyle: "long", timeStyle: "short", timeZone: zone,
  });
  console.log(locale.padEnd(6), zone.padEnd(17), format.format(instant));
}

// Requesting fields separately and splitting into parts
const fielded = new Intl.DateTimeFormat("tr-TR", {
  weekday: "long", day: "numeric", month: "long", timeZone: "Europe/Istanbul",
});
console.log("fielded :", fielded.format(instant));
console.log("parts   :", fielded.formatToParts(instant).map((p) => `${p.type}=${p.value}`).join(" "));

// Relative time: unit and number get given, the text comes from the locale.
const relative = new Intl.RelativeTimeFormat("tr-TR", { numeric: "auto" });
for (const [n, unit] of [[-1, "day"], [-3, "hour"], [0, "day"], [2, "week"]])
  console.log("relative:", relative.format(n, unit));
```

```
tr-TR  Europe/Istanbul   11 Şubat 2024 09:05
en-US  America/New_York  February 11, 2024 at 1:05 AM
fielded : 11 Şubat Pazar
parts   : day=11 literal=  month=Şubat literal=  weekday=Pazar
relative: dün
relative: 3 saat önce
relative: bugün
relative: 2 hafta sonra
```

The first two lines show the same instant. What gets stored in the
measurement record is a single instant in universal time; what gets
shown to the user is that instant's counterpart in the user's time
zone. Giving the time zone explicitly to the formatter leaves no
ambiguity about which zone the record gets read in. When it does not
get given, the device's zone gets used, and two users see the same
record with different times.

The field order also comes from the language: in Turkish, day, month,
weekday name; a completely different order in another language. Even
the separators in the parts list are part of the language data; this
shows why concatenating a date by hand does not work.

The relative time formatter solves a separate problem. Calculating
"how many units ago" is the program's job; turning that calculation
into text is the language data's. The automatic mode uses a word
instead of a number wherever one exists — "dün" (yesterday) and
"bugün" (today) in the output are the result of this.

## Sorting and Comparison

The default form of string comparison compares code points. This
order, defined in the Character Encodings lesson, does not
correspond to any language's alphabet.

```js
// sort.mjs — code point order versus locale-aware sorting
const stations = ["Iğdır", "İzmir", "Isparta", "Şanlıurfa", "Sivas", "Çorum", "Ordu"];

console.log("code point :", stations.toSorted().join(" "));
console.log("tr-TR      :", stations.toSorted(new Intl.Collator("tr").compare).join(" "));
console.log("en-US      :", stations.toSorted(new Intl.Collator("en").compare).join(" "));

// Sensitivity: whether case and accent differences count as equal
const loose = new Intl.Collator("tr", { sensitivity: "base" });
console.log("'Izmir' and 'İZMİR' equal:", loose.compare("Izmir", "İZMİR") === 0);

// Numeric sorting: numbers inside the string get compared as numbers
const codes = ["T-2", "T-10", "T-1", "T-21"];
console.log("as string  :", codes.toSorted(new Intl.Collator("tr").compare).join(" "));
console.log("as number  :", codes.toSorted(new Intl.Collator("tr", { numeric: true }).compare).join(" "));

// Plural rules: which category a number falls into gets asked, the text comes from the locale
const plural = new Intl.PluralRules("tr");
console.log("plural tr  :", [0, 1, 2, 11].map((n) => `${n}=${plural.select(n)}`).join(" "));
console.log("plural en  :", [0, 1, 2, 11].map((n) => `${n}=${new Intl.PluralRules("en").select(n)}`).join(" "));
```

```
code point : Isparta Iğdır Ordu Sivas Çorum İzmir Şanlıurfa
tr-TR      : Çorum Iğdır Isparta İzmir Ordu Sivas Şanlıurfa
en-US      : Çorum Iğdır Isparta İzmir Ordu Şanlıurfa Sivas
'Izmir' and 'İZMİR' equal: false
as string  : T-1 T-10 T-2 T-21
as number  : T-1 T-2 T-10 T-21
plural tr  : 0=other 1=one 2=other 11=other
plural en  : 0=other 1=one 2=other 11=other
```

The first line is the result of code point order: accented letters
get thrown outside the alphabet, `Ç` and `Ş` fall to the end of the
list. The second line follows the Turkish alphabet.

The third line carries a subtle distinction. Turkish and English
sorting diverge only at the letter `Ş`: in Turkish, `Ş` is a separate
letter and comes after `S`; in English sorting, it counts as a
variant of `S`, and `Şanlıurfa` comes before `Sivas`. The same data
gets sorted differently depending on the user's language.

The fourth line shows Turkish's well-known trap. Even at the loosest
sensitivity, the dotted and dotless letter **do not count as equal**,
because in Turkish these are not two forms of the same letter, but
two separate letters. Code doing case-insensitive search has to
account for this distinction.

The fifth and sixth lines show numeric sorting: when measurement
codes get sorted as strings, `T-10` comes before `T-2`. The numeric
option compares number groups inside a string as numbers.

The last two lines show plural rules. When writing interface text,
which categories the "1 measurement / 2 measurements" distinction
gets made by depends on the language; the category name gets asked,
and the application supplies the matching text. Turkish's and
English's categories overlap in this example; other languages have
more than two categories, and a two-branch condition is not enough.

## The Formatter's Cost and Stability

**Constructing** the formatter object **is expensive**; language
data gets resolved and options get reconciled. The formatting
operation itself, though, is cheap. A single rule follows from this:
the formatter gets constructed once and reused. Code that constructs
a new formatter in every cell while drawing a thousand-row table can
consume the previous lesson's frame budget all by itself.

The second rule concerns the output's stability. The produced text
**is meant to get shown to the user**; not to get parsed, compared,
or sent to the server. Language data can change between versions; the
space character, a separator, or an abbreviation can take a
different form. Data always gets stored and carried in its raw form;
formatting only happens at the presentation layer. In tests, too, the
whole of the formatted text does not get compared against a
constant; this is why the splitting-into-parts operation exists.

## Summary

- The locale tag consists of language, script, and region subtags;
  the region chooses formatting rules, not the language, and the
  preference gets given as a list.
- The decimal separator, the thousands separator, and the sign's
  position come from the language data; code that concatenates a
  number by hand loses this information.
- Formatted text can get split into parts; styling fields separately
  does not require splitting the text.
- The measurement instant gets stored in universal time, and the
  time zone gets given explicitly at display time; when it does not
  get given, the device's zone gets used.
- Code point order does not correspond to any alphabet; locale-aware
  sorting follows the alphabet, and in Turkish the dotted and
  dotless letter are separate letters even at the loosest
  sensitivity.
- The formatter gets constructed once and reused; the text it
  produces is for presentation, data always gets stored in its raw
  form, and does not get used as parsing input.

## Next Step

Locale, time zone, and language preference get read from the user's
device **without asking permission**; these are information the
page already knows. There are other things the measurement station
page will want to ask for: the coordinates of the point a
measurement got taken at, a photo to attach to a fault report, a
voice note. None of these get given without asking. The next lesson
takes up the capabilities that require permission, permission's
states, its dependence on user gesture, and how the page keeps
working if the request gets denied.
