•CASE STUDY

Runtime Localization System (i18n)

4 min read·764 words·Intermediate

Asked at

1 candidate report in Apr 2026

How to use this case study

SDE-2 / Mid

  • Explain string keys with translations per locale
  • Variables in messages
  • Fallback when a translation is missing

SDE-3 / Senior

  • Go deeper on plural and gender rules (ICU MessageFormat)
  • Locale fallback chains
  • Delivering translation bundles via CDN with versioning
  • Updates without redeploying

Staff / Principal

  • Discuss the translation workflow (extraction, translators, review)
  • Pseudo-localization testing
  • RTL layouts
  • Performance on clients

Problem RestatementProblem

Adobe asked: design a localization system for a product released in dozens of countries. Every user-facing string must resolve by locale at runtime, support variables ("Hello, {name}"), follow language-specific plural rules ("1 file" / "2 files", and languages with more plural forms like Polish or Arabic), fall back safely when a translation is missing, and allow updating translations without redeploying the app.

Core Concepts

  • Message keys: code never contains English text directly. It uses keys: t("files.deleted", { count: 3 }).
  • Catalogs: per locale, a map key → message:

  en:    files.deleted = "{count, plural, one {# file deleted} other {# files deleted}}"
  pl:    files.deleted = "{count, plural, one {Usunięto # plik} few {Usunięto # pliki} many {Usunięto # plików} other {Usunięto # pliku}}"
  

These use ICU MessageFormat, the standard syntax for variables, plurals, gender and selects. The plural categories (one, few, many, other) come from CLDR data per language.

  • Formatting: dates, numbers and currencies use locale-aware formatters (Intl.DateTimeFormat, Intl.NumberFormat), never string concatenation.

Deep Dive — When a string has no translationDeep dive

A new feature ships in English. A user in Quebec opens it before the French translation lands. What they see is a product decision, not an accident.

Weak

Show whatever the lookup returns

Look up the key in the user's locale and render the result.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  K["t('checkout.confirm') in fr-CA"] --> MISS["Key not translated yet"]
  MISS --> EMPTY["Returns empty string"]
  EMPTY --> BLANK["A button with no label"]
  BLANK --> STUCK["The user cannot tell what it does - the flow is unusable"]

A missing translation becomes a blank control. This is worse than showing English: the user is not inconvenienced, they are blocked, and nothing anywhere records that it happened.

Good

Fall back to the default locale

If the key is missing, use English.

The interface stays usable, which is the important fix. Two gaps remain. fr-CA falls straight to English even when a perfectly good fr translation exists — so a Quebec user sees English for a string France already has. And the fallback is silent, so nobody learns which keys are missing.

Best

A fallback chain, and treat every fallback as a signal

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  REQ["t('checkout.confirm') for fr-CA"] --> L1{"fr-CA?"}
  L1 -->|"hit"| OUT["Render"]
  L1 -->|"miss"| L2{"fr?"}
  L2 -->|"hit"| OUT
  L2 -->|"miss"| L3{"en - default"}
  L3 -->|"hit"| OUT
  L3 -->|"miss"| KEY["Render the key itself - never a blank"]
  L1 -->|"miss"| LOG["Log the missing key + locale"]
  L2 -->|"miss"| LOG
  LOG --> DASH["Missing-translation dashboard - feeds the translation queue"]
  • Narrow to broad, then default: fr-CA → fr → en. Regional variants differ in a minority of strings, so falling back to the base language gives a far better experience than jumping to English.
  • Never render nothing. If even the default is missing — which means a bug, not a translation gap — show the key. An untranslated label is a nuisance; an empty one is a broken interface.
  • Log every fallback with the key and locale. This turns missing translations from something discovered by a user complaint into a queue the localisation team works from, and it is nearly free to add.

Two things that belong in the same conversation: never concatenate translated fragments — word order differs between languages, so the whole sentence must be one key with placeholders — and use the platform's plural and gender rules rather than an if count == 1, because many languages have more than two plural forms.

ArchitectureArchitecture

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    DEV["Developers - add keys in code"] --> EXT["Extraction in CI - new/changed keys"]
    EXT --> TMS["Translation management system"]
    TMS --> TR["Translators / vendors + review"]
    TR --> TMS
    TMS -->|"publish"| BLD["Bundle builder - per locale, validated"]
    BLD --> CDN[("CDN - versioned bundles")]
    APP["Apps - web, mobile, desktop"] -->|"fetch bundle for locale"| CDN
    APP --> LIB["i18n runtime - ICU format + fallback"]
  • Extraction: CI scans the code for keys and source strings, and sends new or changed ones (with context screenshots and comments) to the translation management system.
  • Bundles: per locale (and per app module, to keep them small), with a content hash in the name. The published manifest lists the latest bundle per locale.
  • Runtime loading: the app ships with a built-in default bundle (so it works offline and at first launch), then fetches the latest bundle version from the CDN, caches it, and uses it on the next screen or launch. That's how translations update without redeploying.
  • Validation before publish: ICU syntax is valid, variables match the source (a translation mustn't drop {name}), and length limits are respected for UI elements.

Quality and UX

  • Pseudo-localization in testing: replace strings with accented, longer text ("[Ŝéţţîñĝš !!!]") to catch hard-coded strings and layout overflow early.
  • Right-to-left languages (Arabic, Hebrew): mirror the layouts (CSS logical properties), and test them.
  • Locale detection: the user's setting first, then the OS or browser language, then region. Keep the locale separate from the country (prices and legal content may depend on the country).
  • Performance: load only the needed locale and modules, and cache compiled message formatters.

Wrap-UpWrap-up

Replace UI text with message keys and resolve them at runtime from per-locale catalogs written in ICU MessageFormat (variables, CLDR plural and gender rules), with locale-aware date and number formatting and a fr-CA → fr → en fallback chain that logs misses. Run a CI extraction to translation management to validated bundle pipeline, publish versioned bundles to a CDN that apps fetch and cache (with a built-in default), and ensure quality with pseudo-localization, RTL support and variable checks.

More Case Studies

Frequently Asked Questions

What is the Runtime Localization System (i18n) system design question?

Runtime Localization System (i18n) is a system design interview question asked at FAANG companies. It covers frontend, api design, cdn, caching and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Runtime Localization System (i18n) question?

Adobe have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Runtime Localization System (i18n) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Runtime Localization System (i18n) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →