---
title: 'Localization Requirements'
source: 'https://academia.sh/en/courses/accessible-patterns/localization-requirements'
course: 'Accessible Component Patterns'
language: en
updated: '2026-08-19T05:19:53+00:00'
license: 'CC BY-SA 4.0'
---

# Localization Requirements

Deriving formatting from the locale, plural form count varying by language, short strings growing proportionally more, string concatenation baking in word order, and measuring translation coverage.

The previous two lessons treated interface text in a single language: the guide's rules
and an error message's structure. Once the catalog opens into another language, the
text's surroundings change along with the text itself. The same button label can double
in some languages and stop fitting its box; date, number, and percent formats change;
and a sentence like "records found" does not keep the same word order across languages.

This lesson treats localization not as translation work but as **flexibility the
interface has to carry**. That flexibility has measures, and all of them can be
computed.

## Formatting Is Not Part of the Text

Dates, numbers, percentages, and currency are not written into interface text; they are
derived from the locale. The computation below produces the same value's form across six
locales and counts their plural rules.

```js
// locale.mjs — text growth, formatting, and plural rules derived from the locale

const LOCALES = ["tr", "en", "de", "fr", "ru", "ja"];

// 1. Formatting: the same value, six locales.
const DATE = new Date(Date.UTC(2026, 7, 12, 9, 30));
const NUMBER = 1234567.89;
console.log("locale  date (long)             number            percent");
for (const locale of LOCALES) {
  const date = new Intl.DateTimeFormat(locale, { dateStyle: "long", timeZone: "UTC" }).format(DATE);
  const number = new Intl.NumberFormat(locale).format(NUMBER);
  const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }).format(0.427);
  console.log(`${locale.padEnd(7)} ${date.padEnd(24)} ${number.padEnd(17)} ${percent}`);
}

// 2. Plural rules: how many distinct forms are needed?
console.log("\nlocale  forms needed  1        2        5        11       0.5");
for (const locale of LOCALES) {
  const rules = new Intl.PluralRules(locale);
  const forms = new Set([0, 1, 2, 3, 5, 11, 21, 100].map((n) => rules.select(n)));
  const sample = [1, 2, 5, 11, 0.5].map((n) => rules.select(n).padEnd(8)).join(" ");
  console.log(`${locale.padEnd(7)} ${String(forms.size).padStart(13)}  ${sample}`);
}

// 3. List joining: why concatenating strings breaks.
console.log("\nlocale  list join (three items)");
for (const locale of LOCALES) {
  const list = new Intl.ListFormat(locale, { style: "long", type: "conjunction" })
    .format(["astronomy", "meteorology", "archiving"]);
  console.log(`${locale.padEnd(7)} ${list}`);
}

// 4. Text growth: short strings grow proportionally more.
// Instead of measuring, use a rule: expected growth factor by length band.
const GROWTH = [
  { min: 1,  max: 10,  factor: 2.0 },
  { min: 11, max: 20,  factor: 1.8 },
  { min: 21, max: 30,  factor: 1.6 },
  { min: 31, max: 50,  factor: 1.4 },
  { min: 51, max: 70,  factor: 1.3 },
  { min: 71, max: 999, factor: 1.15 },
];
const factorFor = (n) => GROWTH.find((g) => n >= g.min && n <= g.max).factor;

const INTERFACE = [
  { key: "button.borrow",  text: "Borrow",                                                  box: 120 },
  { key: "button.cancel",  text: "Cancel",                                                   box: 90 },
  { key: "label.shelf",    text: "Shelf code",                                               box: 110 },
  { key: "title.results",  text: "Search results",                                           box: 220 },
  { key: "help.isbn",      text: "ISBN must be 13 digits; it is printed under the barcode.",  box: 420 },
];
const CHAR_WIDTH = 8; // px, average for body text
console.log("\nkey             chars     factor  widest case (px)  box  fits");
for (const i of INTERFACE) {
  const factor = factorFor(i.text.length);
  const width = Math.ceil(i.text.length * factor * CHAR_WIDTH);
  console.log(
    `${i.key.padEnd(15)} ${String(i.text.length).padStart(8)} ${factor.toFixed(2).padStart(5)} ` +
      `${String(width).padStart(17)} ${String(i.box).padStart(5)}  ${width <= i.box ? "yes" : "NO"}`
  );
}

// 5. String concatenation trap: fragments versus a whole sentence.
const fragments = (n) => "Total " + n + " records found";        // fragments translated separately
const whole = (n, locale) =>
  new Intl.NumberFormat(locale).format(n) +
  ({ tr: " kayıt bulundu", en: " records found" }[locale] ?? " records found");
console.log("\nfragmented setup: " + fragments(1234567));
console.log("whole pattern (tr): " + whole(1234567, "tr"));
console.log("whole pattern (en): " + whole(1234567, "en"));
console.log("  -> the fragmented setup never formats the number and freezes word order.");

// 6. Translation coverage: missing-key audit.
const CATALOG = {
  tr: { "button.borrow": "Ödünç al", "button.cancel": "İptal", "label.shelf": "Raf kodu", "title.results": "Arama sonuçları", "help.isbn": "ISBN 13 hane olmalı." },
  en: { "button.borrow": "Borrow", "button.cancel": "Cancel", "label.shelf": "Shelf mark", "title.results": "Search results" },
  de: { "button.borrow": "Ausleihen", "button.cancel": "Abbrechen" },
};
const keys = Object.keys(CATALOG.tr);
console.log("\nlocale  keys  coverage  missing keys");
for (const [locale, table] of Object.entries(CATALOG)) {
  const missing = keys.filter((k) => !(k in table));
  console.log(
    `${locale.padEnd(6)} ${String(Object.keys(table).length).padStart(4)}  ` +
      `${String(Math.round((100 * (keys.length - missing.length)) / keys.length)).padStart(6)}%  ${missing.join(", ") || "-"}`
  );
}
```

```
locale  date (long)             number            percent
tr      12 Ağustos 2026          1.234.567,89      %42,7
en      August 12, 2026          1,234,567.89      42.7%
de      12. August 2026          1.234.567,89      42,7 %
fr      12 août 2026             1 234 567,89      42,7 %
ru      12 августа 2026 г.       1 234 567,89      42,7 %
ja      2026年8月12日               1,234,567.89      42.7%

locale  forms needed  1        2        5        11       0.5
tr                  2  one      other    other    other    other   
en                  2  one      other    other    other    other   
de                  2  one      other    other    other    other   
fr                  2  one      other    other    other    one     
ru                  3  one      few      many     many     other   
ja                  1  other    other    other    other    other   

locale  list join (three items)
tr      astronomy, meteorology ve archiving
en      astronomy, meteorology, and archiving
de      astronomy, meteorology und archiving
fr      astronomy, meteorology et archiving
ru      astronomy, meteorology и archiving
ja      astronomy、meteorology、archiving

key             chars     factor  widest case (px)  box  fits
button.borrow          6  2.00                96   120  yes
button.cancel          6  2.00                96    90  NO
label.shelf           10  2.00               160   110  NO
title.results         14  1.80               202   220  yes
help.isbn             56  1.30               583   420  NO

fragmented setup: Total 1234567 records found
whole pattern (tr): 1.234.567 kayıt bulundu
whole pattern (en): 1,234,567 records found
  -> the fragmented setup never formats the number and freezes word order.

locale  keys  coverage  missing keys
tr        5     100%  -
en        4      80%  help.isbn
de        2      40%  label.shelf, title.results, help.isbn
```

The first section's output gives one date and one number in six separate forms:
separators change, the month's position changes, the percent sign sits before the value
in some locales and after it in others, and even whether a space separates it is
locale-dependent. None of these differences is a designer's choice; every one belongs to
the locale.

The rule is: formatting happens at runtime. Writing "August 12, 2026" into the text
catalog freezes that string in one locale; the catalog should carry only the **pattern**,
and a formatter produces the value.

## Plural Is Not Just Two Forms

The second section overturns a common assumption. Turkish, English, German, and French
get by with two plural forms, Russian needs three, and Japanese does not change form by
number at all. Decimals behave differently too: French treats 0.5 as singular, Turkish
does not.

The consequence is direct: a setup with two branches — "1 record" and "N records" —
produces the wrong text in a language that needs three forms. The correct setup asks the
locale for the plural form and keeps separate text for each one. The same holds for list
joining: the third section's output shows that the comma and the conjunction change from
locale to locale, and Japanese uses no conjunction at all.

## Text Growth Is a Layout Constraint

The fourth section measures growth. The rule is: short strings grow proportionally more.
An eight-character button label can double, while a fifty-character help text grows by
roughly forty percent. The reason is that short text usually cannot be rendered in a
single word in most languages.

The calculation shows that three of the five components do not fit their box at their
widest. This finding belongs to the design stage: giving boxes a fixed width ties the
interface to one language. The fix is boxes that grow with their padding, layout that
allows line wrapping, and — by the rule from the Layout and Spacing topic — deriving
width from content.

The measure itself is an estimate, and it should be labeled as one; real growth is
measured once translation exists. The estimate's job is to show **which components are
at risk** before translation arrives.

## String Concatenation Breaks Translation

The fifth section compares a fragmented setup with a whole one. In the fragmented
version, the number is pasted in raw and the word order is baked into the code: `"Total
" + n + " records found"`. This loses two things at once — the number is never
formatted, and the translator cannot reorder the words.

The correct setup keeps a single pattern and places values into named slots. The
translator can then move the slots to fit their language's order, and the number is
formatted per locale. This is the interface-text side of the rule set up in the
Internationalization lesson.

## Coverage Is Measured

The last section counts translation coverage. A missing key is not a defect but a
**state**: translation always lags behind code. What matters is that the gap is visible
and a fallback behavior is defined — which language's text shows for a missing key, and
how that state is marked in the interface.

Coverage measurement is also a release criterion: a language is not shown to users while
its coverage sits below some threshold. The threshold's number varies by product; the
existence of the measurement does not.

## Summary

- Date, number, and percent formats are not part of the text; they are derived from the
  locale at runtime — the same value takes six different forms across six locales.
- Plural form count depends on the language: one locale in the model needs three forms,
  another makes no distinction at all; a two-branch setup produces wrong text in those
  languages.
- Short strings grow proportionally more; fixed-width boxes tie the interface to one
  language, and in the model three components do not fit their box at their widest.
- String concatenation bakes in word order and leaves the number unformatted; a pattern
  with named slots solves both problems at once.
- Translation coverage is measured, and a fallback behavior is defined for a missing key;
  coverage is a release criterion.

## Next Step

Every example in this lesson used languages written left to right. Once the interface
opens into a right-to-left language, more than the text's direction changes: the layout
itself mirrors, icon direction can flip meaning, and numbers and embedded Latin-script
terms keep their own direction. The next lesson takes on direction-sensitive layout
decisions and shows, with real computation, why the difference between physical and
logical properties is an architectural decision rather than a translation problem.
