Skip to content
academia.sh

Lesson 11 / 24

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.

Contents

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

// 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

// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close