---
title: Internationalization
source: 'https://academia.sh/en/courses/frontend-architecture/internationalization'
course: 'Application Architecture: Routing, State and Data'
language: en
updated: '2026-08-17T18:11:04+00:00'
license: 'CC BY-SA 4.0'
---

# Internationalization

Separating text from code, translation catalog and key design, plural rules changing by language, leaving formatting to the platform, direction support, and selecting the locale.

The identity flow is complete: the observer is recognized, and their session carries on by
itself. But the recognized observer also has a language. Every piece of text produced
throughout this course was written for a single language — error messages, the "session
dropped" notice, empty-result explanations. Numbers and dates were the same way: the
measurement value was written as "−4.2", the date was shown in day-month-year order.

None of this is correct for an observer watching the North Slope station from another
country. This lesson builds the structure that makes the interface adaptable to language and
region.

## Two Separate Jobs

**Internationalization** makes the application adaptable: separating text from code, not
hardcoding formatting, and having the layout hold up under a direction change. It is done
once, and it is independent of language.

**Localization** is producing content for a specific language and region: writing the
translations, choosing dates and currency to fit that region. It is done separately for each
language.

The practical value of this distinction follows directly. Adding a translation to an
application that has not been internationalized requires touching every part of the code.
Adding a second language to one that has means writing nothing more than a catalog file. This
is why separating text from code is a cheap investment even in applications expected to stay
single-language.

## Translation Catalog and Key Design

A **translation catalog** is a structure that maps keys to messages. The code knows the key,
not the text.

The key itself is a design decision. Using the source sentence as the key — "2 fields need
correction" — looks easy, but every translation breaks whenever that text is edited. A
structured name that defines the meaning — `form.summary` — protects that link. Because the
same word may need to be translated differently in two places, the key also carries context:
`button.save` and `menu.save` are separate keys, even though both are "Save" in English.

```js
// translation-catalog.mjs — keys, placeholders, and plural rules
const CATALOG = {
  tr: {
    "form.summary": { one: "{count} alan düzeltilmeli", other: "{count} alan düzeltilmeli" },
    "measurement.range": "{min} ile {max} arasında bir değer girin.",
    "session.expired": "Oturumunuz sona erdi.",
  },
  en: {
    "form.summary": { one: "{count} field needs attention", other: "{count} fields need attention" },
    "measurement.range": "Enter a value between {min} and {max}.",
  },
};

const DEFAULT = "tr";

function message(locale, key, vars = {}) {
  const entry = CATALOG[locale]?.[key] ?? CATALOG[DEFAULT]?.[key];
  if (entry === undefined) return `!${key}`;   // a missing key is not hidden
  const pattern = typeof entry === "string"
    ? entry
    : entry[new Intl.PluralRules(locale).select(vars.count)] ?? entry.other;
  return pattern.replace(/\{(\w+)\}/g, (whole, name) =>
    name in vars ? String(vars[name]) : whole);
}

console.log("plural categories (Intl.PluralRules)");
console.log("count".padEnd(6) + ["tr", "en", "ru"].map((d) => d.padEnd(8)).join(""));
for (const count of [0, 1, 2, 5, 11, 21, 100]) {
  console.log(String(count).padEnd(6) +
    ["tr", "en", "ru"].map((d) => new Intl.PluralRules(d).select(count).padEnd(8)).join(""));
}

console.log("\nmessages");
for (const locale of ["tr", "en"]) {
  for (const count of [1, 2]) {
    console.log(`  ${locale} count=${count}: ${message(locale, "form.summary", { count })}`);
  }
  console.log(`  ${locale} range  : ${message(locale, "measurement.range", { min: -60, max: 60 })}`);
  console.log(`  ${locale} session: ${message(locale, "session.expired")}`);
  console.log(`  ${locale} missing: ${message(locale, "measurement.unknown")}`);
}
```

```
plural categories (Intl.PluralRules)
count tr      en      ru      
0     other   other   many    
1     one     one     one     
2     other   other   few     
5     other   other   many    
11    other   other   many    
21    other   other   one     
100   other   other   many    

messages
  tr count=1: 1 alan düzeltilmeli
  tr count=2: 2 alan düzeltilmeli
  tr range  : -60 ile 60 arasında bir değer girin.
  tr session: Oturumunuz sona erdi.
  tr missing: !measurement.unknown
  en count=1: 1 field needs attention
  en count=2: 2 fields need attention
  en range  : Enter a value between -60 and 60.
  en session: Oturumunuz sona erdi.
  en missing: !measurement.unknown
```

The plural table shows why a hand-written plural rule is not enough. Turkish and English have
two categories, and the test `count === 1` gives the right answer in both. Three of Russian's
four categories show up in this table, and the choice depends not just on the number itself
but on its last digit: 21 falls into the "one" category, while 11 stays in "many." Code that
writes the rule by hand gets rewritten the moment a third language is added.
`Intl.PluralRules` reads the rule from the language; the catalog only says what to write for
each category.

Turkish's two forms carrying the same text is not an inconsistency. The catalog preserves the
**structure**; how many forms a given language uses is that language's own decision. When the
same catalog gets Russian added to it, `few` and `many` keys are written too, and the code
does not change.

Two more behaviors show up in the output. **A missing key is not hidden**: a display marked
with an exclamation point is produced for a key that does not exist. Returning an empty
string, or silently skipping the key, would make the missing translation invisible, and it
would only be noticed after release. **A missing translation falls back to the default
language**: the session message, absent from the English catalog, came back in Turkish. This
is better than the missing text not appearing at all, but it is a warning sign; it should be
logged during development.

One last rule: **messages are not built with string concatenation**. Text written as "Total "
+ count + " records" cannot be translated into a language with a different word order. The
parts stay together in a single pattern, with placeholders.

## Leaving Formatting to the Platform

Everything besides text — number, date, duration, list, sorting — changes by language and
region, and every one of these rules is already available on the platform.

```js
// formatting.mjs — how numbers, dates, lists, and sorting depend on locale
const LOCALES = ["tr", "en-US", "de-DE"];
const MEASUREMENT = -4.2;
const INSTANT = new Date("2026-01-14T06:00:00Z");     // fixed instant: output is reproducible

console.log("temperature (with unit)");
for (const l of LOCALES) {
  console.log("  " + l.padEnd(6) +
    new Intl.NumberFormat(l, { style: "unit", unit: "celsius",
      minimumFractionDigits: 1 }).format(MEASUREMENT));
}

console.log("large number");
for (const l of LOCALES) {
  console.log("  " + l.padEnd(6) + new Intl.NumberFormat(l).format(1840.5));
}

console.log("date and time (UTC)");
for (const l of LOCALES) {
  console.log("  " + l.padEnd(6) + new Intl.DateTimeFormat(l, {
    dateStyle: "long", timeStyle: "short", timeZone: "UTC" }).format(INSTANT));
}

console.log("relative time (-3 hours)");
for (const l of LOCALES) {
  console.log("  " + l.padEnd(6) +
    new Intl.RelativeTimeFormat(l, { numeric: "auto" }).format(-3, "hour"));
}

console.log("list");
const stations = ["North Slope", "East Ridge", "West Valley"];
for (const l of LOCALES) {
  console.log("  " + l.padEnd(6) +
    new Intl.ListFormat(l, { style: "long", type: "conjunction" }).format(stations));
}

// --- Two Turkish-specific traps ---------------------------------------------
const CITIES = ["Ordu", "Ödemiş", "Osmaniye", "Sivas", "Şile", "Isparta", "İstanbul"];
console.log("\nsorting");
console.log("  tr    " + [...CITIES].sort(new Intl.Collator("tr").compare).join(", "));
console.log("  en    " + [...CITIES].sort(new Intl.Collator("en").compare).join(", "));
console.log("  raw   " + [...CITIES].sort().join(", "));

console.log("uppercasing");
console.log("  tr    " + "ilk ölçüm".toLocaleUpperCase("tr"));
console.log("  en    " + "ilk ölçüm".toLocaleUpperCase("en"));
```

```
temperature (with unit)
  tr    -4,2°C
  en-US -4.2°C
  de-DE -4,2 °C
large number
  tr    1.840,5
  en-US 1,840.5
  de-DE 1.840,5
date and time (UTC)
  tr    14 Ocak 2026 06:00
  en-US January 14, 2026 at 6:00 AM
  de-DE 14. Januar 2026 um 06:00
relative time (-3 hours)
  tr    3 saat önce
  en-US 3 hours ago
  de-DE vor 3 Stunden
list
  tr    North Slope, East Ridge ve West Valley
  en-US North Slope, East Ridge, and West Valley
  de-DE North Slope, East Ridge und West Valley

sorting
  tr    Isparta, İstanbul, Ordu, Osmaniye, Ödemiş, Sivas, Şile
  en    Isparta, İstanbul, Ödemiş, Ordu, Osmaniye, Şile, Sivas
  raw   Isparta, Ordu, Osmaniye, Sivas, Ödemiş, İstanbul, Şile
uppercasing
  tr    İLK ÖLÇÜM
  en    ILK ÖLÇÜM
```

This output **depends on the environment**: the details of the formats can vary with the
version of locale data the runtime carries. The lines above were produced on an installation
where `node --version` reports v24.18.0. What can change is the detail of the format; the
principles do not.

Five observations follow.

**The decimal separator and the thousands separator swap places by language.** Turkish and
German write the decimal with a comma, English with a period. Code that formats a number by
hand misses this, and would show "1.840,5" as "1840.5"; worse, it would misread a "1,5" a
user typed. This is why the validation schema converted between comma and period.

**Even whether the unit sticks to the number depends on the language.** In the German output
there is a space between the number and the degree sign; in Turkish there is not.

**Date order and the month name change; so does the time format.** The English output uses a
twelve-hour display, the others a twenty-four-hour one. The time zone has to be supplied
separately; in the example, UTC is fixed, so the output is reproducible.

**Joining a list is grammar.** Turkish puts "ve" before the last item, English a comma and
"and," German "und." Placing commas by hand and adding a conjunction before the last item
means a separate rule for every language.

**Sorting and case conversion are a special case in Turkish.** Raw sorting gives code-point
order and does not match the Turkish alphabet at all. In Turkish sorting, `O` and `Ö`, `S` and
`Ş` are separate letters and are arranged in that order; in English sorting they are counted
as variants of the same letter, so "Ödemiş" sits in a different place in the two lists. In
case conversion, too, the uppercase of `i` is `İ` in Turkish and `I` in other languages; a
conversion done without supplying the locale produces a result that varies with the machine
it runs on.

## Direction Support

Right-to-left languages require not just the text but the layout to change direction too.
The work splits into three levels.

**The document level.** Direction is declared on the root element; direction is a property of
the writing system, not of the language, so it is supplied separately from the language
declaration. When direction changes, text alignment, which side a list's bullet sits on, and
scroll direction all flip on their own.

**The layout level.** The logical properties introduced in the Visual Presentation with CSS
course find their match here: when the start and end edges of the block and inline axes are
written instead of left and right, the layout adapts to a direction change on its own. Every
rule written with a physical direction has to be reviewed one by one when direction support
is added.

**The text level.** Mixed-direction text — a left-to-right station code inside a
right-to-left sentence — raises a special problem: the ordering algorithm can push
punctuation at the boundary to the wrong side. A fragment coming from the user or from data
needs to be isolated from the surrounding text; markup has an element set aside for this, and
this element's default direction guess looks at the fragment's own content.

Some icons are directional too: a forward arrow, a back arrow, indentation markers get
mirrored on a direction change. Icons with no inherent direction, like a clock face or a
volume level, are not mirrored.

## Selecting the Locale and Loading the Catalog

Which language to use comes from three sources, and the priority order is fixed. **The user's
explicit preference** sits at the top, and is stored. Failing that, **the address itself** is
used — a path segment or subdomain carrying the language code — which keeps the link
shareable. If neither is present, the language list the browser reports, the
`Accept-Language` header, is evaluated.

The browser's list is a ranked preference, not a single language. Matching looks for an exact
match first; if none is found, the region is dropped and tried again; if nothing sticks, it
falls back to the default language. A locale contains more than a language — region,
calendar, numbering system — so there are cases where the text language and the formatting
setting can differ.

Loading the catalog is a bundling decision. Shipping every language in a single bundle makes
the user download text they will never use. The right approach is producing a separate chunk
per language and loading only the selected one; this ties directly to the Code Splitting
topic in the Rendering Strategies and Infrastructure course. What the interface shows during
the time it takes to load a new catalog after a language switch — the old language, or a
blank — connects to the state machine from the Loading and Error States lesson.

Finally, translated text **takes up space**. The same sentence can be noticeably longer in
one language than another; fixed-width buttons and labels squeezed onto a single line
overflow once translated. Having the layout hold up under text length is an
internationalization requirement.

## Summary

- Internationalization makes the application adaptable and is done once; localization is
  content produced separately for each language.
- A translation catalog uses structured keys that define meaning; making the source sentence
  the key breaks every translation once the text is edited.
- Plural categories change by language and can depend on a number's last digit; the rule is
  read from the platform, and the catalog only carries what to write for each category.
- Messages are built with a single pattern and placeholders, not string concatenation; a
  missing key is not hidden, and a missing translation falls back to the default language.
- Number, date, relative time, list, and sorting are left to the platform's locale tools;
  sorting in Turkish and converting the letter `i` do not produce a correct result without
  supplying the locale.
- Direction support is handled separately at the document, layout, and text levels; logical
  properties resolve the layout, and an isolation element resolves mixed-direction text.

## Course Wrap-Up

This course built an application's internal architecture around four questions.

**Where is the user?** The Routing topic made the address a part of the application's state:
nested layouts shared a common shell, route parameters carried state in the address, route
guards managed access at the visibility level, and code splitting made every route pay its
own cost.

**Where does information live?** The State Management topic split state into its kinds —
local, shared, server, and address state — and showed a different container for each.
Treating server state separately opened the door to the next topic.

**How does information arrive?** The Data Access topic built the request layer and the error
contract, showed cache normalization for query-based access, consumed streamed data in a way
that resists disconnection, reduced a view's states to a finite set, and absorbed network
instability without it reaching the user.

**How does information get entered, and who enters it?** The Forms and Identity topic built
where a value is held, how rules stay identical on both sides, how an error is made
perceivable, where a proof of identity is stored, how a session keeps going, and how the
interface adapts to language.

What all four have in common is that they all take place **inside the browser**. This course has
covered what happens after the application opens; we never once asked what happens before it
opens. Yet the same application can be built in three different places: in the user's
browser, on the server answering the request, or at build time, before any request arrives.
The choice determines how quickly the first screen arrives, what the search engine sees, how
much work the server does, and how fresh the content can be.

The next course — **Rendering Strategies and Infrastructure** — builds this choice along with
its trade-offs: the costs of rendering on the client, on the server, and at build time,
selectively refreshing static output, gaining interactivity piece by piece, bundling and
asset optimization, and the deployment pipeline that gets the application to the user.
