ADR-017 — Paraglide-js for i18n, locale overlay architecture for content
Status · Accepted Date · 2026-04-28 TA anchor · §stack · §contracts
Context
Orrery targets a global audience. English (US) is the initial locale but the architecture must support translation of both UI chrome and all mission/editorial content. The i18n architecture must not require modifying existing mission JSON files to add a new language.
Decision
Two-part i18n architecture:
1. UI chrome — Paraglide-js. Compile-time i18n for all UI strings (nav labels, button text, HUD labels, tab names, status badges, error messages). Strings live in src/lib/i18n/en-US.json. Each language adds a new file. Paraglide tree-shakes unused locale strings at build time — no runtime overhead for the active locale.
2. Mission and editorial content — locale overlay files. Base mission files (data/missions/mars/curiosity.json) contain only language-neutral data: dates, delta-v, agency, links, orbital elements. Language-specific content lives in separate overlay files:
data/i18n/en-US/missions/mars/curiosity.json ← name, description, first, type, events[].note
data/i18n/fr/missions/mars/curiosity.json
data/i18n/es/missions/mars/curiosity.jsonThe data client merges base + locale overlay at fetch time. Missing translations fall back to en-US.
Rationale
Paraglide is the SvelteKit-native i18n library — it integrates directly with SvelteKit's routing and compiles translations at build time. The locale overlay architecture keeps base data files clean and language-independent: adding a new language means adding new files, not modifying the 28 existing mission files. A translator can work entirely in data/i18n/[locale]/ without touching any JavaScript or base data.
Alternatives considered
- svelte-i18n — runtime i18n; larger bundle; less type-safe; rejected.
- Embedded language keys in mission JSON (
"description_en","description_fr") — simpler structure; requires editing all 28 files per new language; rejected. - Single locale file per language — one big
fr.jsonwith all content; hard to maintain; rejected.
Consequences
Positive: adding a language requires zero code changes; translators work in JSON files; UI strings tree-shaken at build; base mission data is language-independent. Negative: data client merge step adds a small amount of complexity; locale overlay files must be kept in sync with base files when mission data changes.
Implementation notes
src/lib/i18n/ for UI strings. data/i18n/[locale]/ for content overlays. Data client gains a locale parameter, defaults to en-US, falls back to en-US for missing translations. SvelteKit layout reads locale from URL prefix (/fr/explore) or browser Accept-Language header.