Lesson 23 / 25
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.
Contents
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.
// 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.