ArchitectureGENERALBROWSER-SPECIFICFRAMEWORK-SPECIFIC

Internationalization

Not a translation table bolted on later: plural rules, locale-aware formatting, text that grows, and a layout that has to work in both directions.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What has to be true about this interface before it can exist in another language?

The user intent

The product is launching in three more countries. People there expect an interface that reads naturally, not one that reads like it was translated from English by someone in a hurry.

The obvious build

Extract the strings into a JSON file, hand it to translators, and look up by key at render time. Everything else stays as it is.

Why it breaks

Sentences get assembled from fragments — "Deleted " + count + " items" — and word order differs by language, so the translator receives pieces that cannot be reassembled correctly in theirs.

How it breaks in a real browser
  • Sentences get assembled from fragments — "Deleted " + count + " items" — and word order differs by language, so the translator receives pieces that cannot be reassembled correctly in theirs.
  • Pluralization is treated as "one versus many". Many languages have three, four or six plural categories, and several have different rules for 2, for numbers ending in certain digits, or for zero. English is unusually simple, which is why the assumption survives.
  • Dates, numbers and currency are formatted by hand, so 03/04 means March 4th to some users and 3 April to others, and a decimal comma is read as a thousands separator.
  • German and Finnish strings are routinely much longer than the English original, so fixed-width buttons and single-line labels break in exactly the places nobody tested.
  • Right-to-left languages need the layout mirrored, not just the text — and every margin-left and text-align: left in the codebase is now a bug (Fluid Layout First).
  • Sorting uses code-point order, so accented names land in the wrong place and the user concludes the list is broken.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • A locale is not a language. It is a language plus a region plus conventions — de-DE and de-AT share a language and differ in formatting, and en-GB and en-US differ in date order in a way that silently produces wrong answers rather than obviously wrong ones.
  • The platform ships a full internationalization library: Intl.NumberFormat, Intl.DateTimeFormat, Intl.PluralRules, Intl.RelativeTimeFormat, Intl.ListFormat and Intl.Collator. It knows the rules for every locale the browser supports, and it is already in the bundle at zero cost (The Real Cost of JavaScript).
  • `Intl.PluralRules` is the answer to the plural problem: it returns a category for a number in a locale, and the translation supplies a string per category. Your code never decides which form to use.
  • Message formatting — a single translatable string with named placeholders and inline plural/select rules — is what lets a translator control word order. Concatenation is what takes that control away.
  • Logical properties (margin-inline-start, padding-block, text-align: start, inset-inline) are resolved against the writing direction, so one stylesheet works in both directions instead of two that drift apart (The Box Model).
  • The lang attribute on the document, and on any element that switches language, is what tells the browser which hyphenation, font and — critically — which pronunciation a screen reader should use.

What this makes the browser do

And which of it is avoidable.

  • Intl formatters are relatively expensive to construct and cheap to reuse. Constructing one inside a render for every row of a table is a real and commonly-shipped cost (What a Component Costs to Render).
  • Loading only the active locale's messages rather than all of them — otherwise every user downloads every translation (Code Splitting).
  • Font loading for scripts the primary font does not cover, which is an extra critical-path resource for exactly the users you are trying to serve better (Images and Fonts).
  • Re-layout when direction flips, and when longer strings change wrapping (The Box Model).

The sentence is the unit

Almost every serious internationalization defect traces back to one decision: whether the translatable unit is a sentence or a fragment. A translator handed fragments cannot reorder them, cannot agree the adjective, and cannot choose a plural form that depends on a number they were not given.

The fix is a message format — one string, named placeholders, plural and select rules inline — so the translator holds the whole structure and the code supplies only values.

"You have N unread messages"
Assembled from fragments
t('you_have') + ' ' + count + ' ' +
  (count === 1 ? t('message') : t('messages')) + ' ' + t('unread')

// the translator receives four disconnected strings
// and an English assumption about plural forms
One message, with the rules inside it
t('unread_count', { count })

// en: {count, plural,
//        one {You have # unread message}
//      other {You have # unread messages}}
//
// pl: {count, plural,
//        one {Masz # nieprzeczytana wiadomosc}
//        few {Masz # nieprzeczytane wiadomosci}
//       many {Masz # nieprzeczytanych wiadomosci}
//      other {Masz # nieprzeczytanej wiadomosci}}

The second version gives the translator the whole sentence and the plural categories their language actually has, and lets them put the number wherever their grammar requires. The first hardcodes English word order and English plural rules into application logic, where no translator can reach them and no reviewer who reads English will notice.

The platform already knows the rules
1// plural category for this number, in this locale
2new Intl.PluralRules('pl').select(2) // 'few'
3new Intl.PluralRules('en').select(2) // 'other'
4
5// formatting, without inventing separators
6new Intl.NumberFormat('de-DE').format(1234.5) // '1.234,5'
7new Intl.NumberFormat('en-US', { style: 'currency',
8 currency: 'EUR' }).format(1234.5) // '€1,234.50'
9
10// sorting that respects the alphabet
11['Ö', 'Z', 'A'].sort(new Intl.Collator('sv').compare) // A, Z, Ö
12
13// construct once, reuse — building these per row is a real cost
14const money = new Intl.NumberFormat(locale, { style: 'currency', currency })

None of this is a dependency. It is in every browser, it knows more about locale rules than any table you would write, and the last line is the one people miss.

Layout that survives another language

Translation changes the physical shape of the interface. Strings get longer, sometimes dramatically, and in right-to-left languages the whole layout mirrors. Both are cheap to handle up front and expensive to retrofit, because the fix touches every stylesheet.

Logical properties are the mechanism: write margin-inline-start instead of margin-left, and the browser resolves it against the writing direction. Setting dir="rtl" then mirrors the layout without a second stylesheet.

What breaks when the language changes
TriggerSymptomCauseResponse
Translated to GermanButton labels truncate or overflowFixed widths sized to the English stringFluid sizing and a pseudo-locale build in CI that inflates every string (Fluid Layout First).
Switched to ArabicText mirrors, chevrons and progress bars do notPhysical CSS properties and directional icons that do not follow dirLogical properties throughout; flip directional icons explicitly and leave non-directional ones alone.
User in en-GBA date is read as the wrong dayManually formatted MM/DD presented without a localeIntl.DateTimeFormat against the user's locale (Timezones and Locale Formatting).
A locale ships lateRaw keys visible in productionFallback returns the key rather than the source languageFall back to the source locale, and alert on fallback rate per locale.
Screen reader on a mixed-language pageA foreign phrase is unintelligibleNo lang on the element that switches languageSet lang on the document and on any element in a different language.
Names list sortedAccented names sort to the endDefault comparator uses code-point orderIntl.Collator for the active locale.
Physical (breaks in RTL)Logical (works in both)What it resolves to
margin-leftmargin-inline-startLeft in LTR, right in RTL
padding-rightpadding-inline-endRight in LTR, left in RTL
text-align: lefttext-align: startThe reading direction's beginning
left: 0inset-inline-start: 0The reading direction's near edge
border-leftborder-inline-startThe near edge's border
width / heightinline-size / block-sizeAlong and across the text flow

Where the work actually goes

Internationalization is usually described as a feature and behaves like a constraint: it changes how you write strings, how you write CSS, and what you can assume about layout. Adopted early it is nearly free; adopted late it is a sweep through every component in the codebase.

The pipeline below is worth making explicit because the handoffs are where things go wrong — most commonly at extraction, where a string assembled at runtime simply never appears for a translator to translate.

A string's journey, and where it is lost
  1. 1
    Author

    Write a whole sentence with named placeholders and a context note.

    fails by Concatenating fragments, which cannot be reordered in any other language.

  2. 2
    Extract

    Static extraction pulls messages into a catalogue.

    fails by A dynamically-built key, which extraction cannot see and which silently never gets translated.

  3. 3
    Translate

    A human supplies each plural category for their locale.

    fails by No context, so an ambiguous word is translated as the wrong part of speech.

  4. 4
    Load

    Ships only the active locale to the browser.

    fails by Bundling every locale, so all users pay for all languages (Code Splitting).

  5. 5
    Format

    Intl renders numbers, dates and plurals for the locale.

    fails by Hand-formatting, which is wrong somewhere and looks right where it was written.

  6. 6
    Lay out

    Logical properties and fluid sizing absorb length and direction.

    fails by Physical properties and fixed widths, discovered by users rather than by CI.

How to build it

Most important first.

  • Make the translatable unit a whole sentence with placeholders, never a fragment. The translator needs the sentence to reorder it.
  • Use Intl.PluralRules and let the message catalogue carry one form per category. Never write count === 1 ? ... : ... in application code.
  • Format every number, date, currency and list with Intl, against the user's locale, at the moment of display.
  • Design for text that is substantially longer than the source. Test with a pseudo-locale that inflates every string — it finds truncation and overflow before a translator does (Responsive Typography).
  • Use logical properties throughout, and set dir on the document. Then RTL is a configuration change rather than a second stylesheet.
  • Give translators context. A key called submit with no note produces a different word depending on whether it is a button or a heading, and they cannot tell which.
  • Sort with Intl.Collator, never with the default comparator.
  • Never put text in an image, and never rely on word order in a layout — both make translation impossible rather than merely awkward.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • The lang attribute is an accessibility feature before it is anything else: a screen reader uses it to select pronunciation, and text marked with the wrong language is read in the wrong accent and is often unintelligible (Semantics Are Behaviour).
  • A phrase in another language inside a page needs its own lang on that element, or it is read with the surrounding language's rules.
  • Text expansion and accessibility pull in the same direction: a layout that survives a 40% longer string is also a layout that survives a user increasing text size (Responsive Typography).
  • RTL is not only a language question. Mirroring must include focus order expectations, and directional icons must flip while non-directional ones (a clock, a checkmark) must not (Keyboard Operability).
  • Never convey meaning through word order alone in a layout, and never build a sentence from separately-positioned DOM nodes — a screen reader reads them in DOM order, which may not be the visual order in any language (The Accessibility Tree).

What can go wrong

Failure modes
  • Concatenated sentences that cannot be reordered, producing translations that are grammatically wrong in a way engineers never see.
  • A missing key falling back to the raw key, so production shows checkout.confirm.button to a user.
  • A pluralization that is correct in English and wrong everywhere else, which reviews never catch because the reviewer reads English.
  • Hardcoded date parsing of a localized string, which breaks the moment the locale changes the separator.
  • An RTL layout where the text mirrored and the icons, progress direction and chevrons did not, so "next" points backwards.
  • Locale detected once and cached, so a user who changes their preference keeps getting the old one.
Security
  • Translated strings are content, and if any of them are ever user-supplied or community-contributed they are untrusted content. A message catalogue rendered as HTML is an injection sink like any other (Cross-Site Scripting).
  • Locale is user-controlled input. A locale string taken from a URL or header and used to build a file path is a path-traversal risk; validate against a known list.
  • Right-to-left override characters can be used to disguise text — a filename that appears to end in .txt and does not. Where you display untrusted strings, be aware that visual order and logical order can be made to disagree.
Misreads
  • "Internationalization is translation." Translation is one part. Formatting, pluralization, direction, sorting and layout resilience are the rest, and they are the parts that produce wrong answers rather than awkward ones.
  • "We only support English for now." That is a reasonable product decision and it is not a reason to concatenate sentences. Retrofitting is far more expensive than not foreclosing.
  • "toLocaleDateString is enough." It is a good default and it is not a substitute for deciding *which* locale and *which* timezone (Timezones and Locale Formatting).
  • "RTL is just direction: rtl." Text flows; icons, animation direction and physical CSS properties do not follow automatically.
  • "Plurals are singular and plural." That is true of English and a minority of languages.

Measuring it, and what changes in the field

How you would see this
  • Missing-key and fallback-rate telemetry per locale — the fastest signal that a release shipped untranslated strings (Frontend Error Tracking).
  • Layout-overflow detection in a pseudo-locale build, run in CI rather than discovered in a screenshot from a user.
  • Locale distribution in field data, which usually reveals that a locale nobody prioritised has real users (Real User Monitoring).
  • Bundle size per locale, to confirm users are not downloading translations they will never see (Bundle Analysis).
Slow device, slow network, large data, old tab
  • On a slow network, shipping every locale is a direct and entirely avoidable cost to every user.
  • At scale, the translation pipeline becomes the constraint: strings must be extractable, reviewable and shippable independently of code, or releases start waiting on translation.
  • In a product with user-generated content, the interface locale and the content locale differ, and both need marking.
What this costs
  • Message formatting with placeholders is more ceremony than string concatenation, and it is the only form that survives translation.
  • Loading locales on demand saves bytes and adds a loading state on language switch.
  • Logical properties are less familiar than physical ones and remove an entire class of RTL bug permanently.
  • A pseudo-locale build costs a CI step and catches the expansion problems that otherwise reach translators as bug reports.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALPlural categories, locale-dependent formatting and text expansion are properties of human languages, so they hold regardless of framework, platform or library choice.
  • BROWSER-SPECIFICWhich locales Intl supports and how completely differs by browser and by operating system, since engines often defer to system locale data — so a formatted string can differ between two users on the same site, and anything requiring byte-identical output across clients must be formatted server-side.
  • FRAMEWORK-SPECIFICMessage extraction, catalogue loading and locale switching are library concerns and differ substantially between ecosystems; what does not differ is that the message must be a whole sentence with placeholders rather than assembled fragments.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

API Designerror-model
Securityxss
Domains that do not exist yet
  • Software Design — the message catalogue is an interface between engineers and translators, and it fails in the ordinary way interfaces fail: when one side can express things the other cannot act on.