FSD Frontend System Design

Part V β€” The Craft Β· Lesson 10.6

Multi Language Support

In one lineKeys not sentences, one locale bundle at a time, and a layout that survives text growing 35%.

Where you've seen it"Zahlungsmethode hinzufΓΌgen" overflowing a button sized for "Add payment".

  • Time17 min
  • Diagrami18n pipeline + what changes besides words
  • Chapter10 Β· Low Level Design

Diagram

diagram
THE PIPELINE

  source code                     runtime
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ t('cart.empty')  β”‚            β”‚ detect locale          β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚  URL /de/ > cookie >   β”‚
           β”‚ extract              β”‚  Accept-Language       β”‚
           β–Ό                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”‚ lazy-load
  β”‚ en.json (source) β”‚                        β–Ό
  β”‚ de.json  ← TMS   β”‚            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ hi.json  ← TMS   β”‚ ─────────► β”‚ de.json only  (~20KB)  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚ NOT all 30 locales     β”‚
                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                              β–Ό
                                     t('cart.empty')
                                     β†’ "Ihr Warenkorb ist leer"
diagram
WHAT CHANGES BESIDES THE WORDS

  text length      EN β†’ DE +35%, EN β†’ FI +40%   ← never fix widths
  direction        ar, he, fa β†’ RTL             ← logical CSS properties
  numbers          1,234.56  vs  1.234,56  vs 1,23,456 (en-IN)
  dates            03/04 = Mar 4 (US) or 3 Apr (most of the world)
  currency         β‚Ή1,234  $1,234.00  1.234,00 €
  sorting          Γ€ sorts differently per locale
  names/addresses  field order and required fields differ
  images/icons     hand gestures, colours, flags β‰  languages
diagram
PLURALS AND INTERPOLATION β€” never concatenate

  βœ— t('you have') + ' ' + n + ' ' + t('items')
      word order is language-specific; this breaks everywhere

  βœ“ t('cart.count', { count: n })
      en: { one: "{{count}} item", other: "{{count}} items" }
      pl: { one: …, few: …, many: …, other: … }   ← 4 forms
      ar: 6 forms

How it works

  1. Key by meaning (cart.empty), never by the English sentence β€” source text changes, keys shouldn't.
  2. Lazy-load one locale bundle per session and split large namespaces per route (5.7).
  3. Use Intl for dates, numbers, currency, plural rules, and relative time β€” it's built in and correct.
  4. Use logical CSS (margin-inline-start, not margin-left) so RTL works by setting dir="rtl" rather than rewriting styles.
  5. Set lang and dir on <html> β€” screen readers switch pronunciation on it (8.3).
jsx
// interpolation + plurals handled by the library
<p>{t('cart.count', { count: items.length })}</p>

// formatting handled by the platform
new Intl.NumberFormat(locale, { style: 'currency', currency: 'INR' })
  .format(1234.5);                       // "β‚Ή1,234.50"

Trade-offs

DecisionConsequence
Lazy-load per localeSmall bundles; a flash before strings load
Ship all localesNo flash; large bundle
Server-render the localeBest LCP + SEO per language
Translate at build timeFast, but a release per copy change

Pseudo-localisation is the cheap test: render [!!! Δ€Γ©ΔΌΔΌΓΆ Ε΄ΓΆΕ•ΔΌΓ° !!!] β€” 40% longer, accented, bracketed. It surfaces hardcoded strings and fragile layouts before any translator is paid.

Interview angle

"How would you add multi-language support to an existing app?"

  • Extract to keys with a source bundle, wire Intl for all formatting, and lazy-load one locale at a time.
  • Fix layout for text expansion and switch to logical CSS properties so RTL is a dir attribute, not a second stylesheet.
  • Decide URL strategy early β€” /de/path is best for SEO and shareability, and it makes locale a routing concern rather than hidden state.

Recap

  • Keys by meaning, plurals by library, formatting by Intl.
  • Design for +35% text and RTL from the start; retrofitting either is expensive.
  • Pseudo-localise early β€” it finds the bugs before translation does.