# I18N_SPEC.md ## 1. Architecture summary Three i18n concerns, three mechanisms — never mixed: | Concern | Mechanism | Examples | |---|---|---| | **Routes** | Astro built-in i18n (defaultLocale `en` at root, `no` prefixed) + slug map module | `/projects/jobtrack` ↔ `/no/prosjekter/jobtrack` | | **UI strings** (~60 keys) | Typed dictionary modules per locale + `t(key)` accessor, compile-time checked | `nav.projects`, `cv.download`, `form.error.email`, `a11y.skipLink` | | **Content** (prose, case studies, experience) | Content collections with per-locale entries sharing one zod schema | `projects/en/jobtrack.md` + `projects/no/jobtrack.md` | No runtime i18n library. Locale is resolved from the URL at build time; every page is rendered once per locale into static HTML. ## 2. Dictionary spec - Shape: nested typed object; both locale files implement the same interface → a missing key is a **compile error**, not a runtime fallback. - Keys are semantic (`cv.download`), never English-sentence keys. - Interpolation: simple `{param}` tokens only; no plural engine (the ~3 plural cases are written out per locale). - A11y strings are first-class dictionary entries: skip-link, lang-switch announcement ("Bytt til norsk" labelled *in the target language* per MICRO_INTERACTIONS), image alt patterns, form validation messages. ## 3. Content collection localisation - Each collection folder splits `en/` and `no/`; entries share the same filename (`jobtrack.md`) = the join key. - One schema validates both locales (DATA_MODEL.md); locale-invariant fields (dates, stack chips, links, image refs, status) live in the EN entry and are **inherited** by the NO entry at load time (a small content-loader utility merges them) — this is the anti-duplication mechanism: facts exist once, prose exists twice. - Section anchors: schema field `anchorId` per section, per locale (`architecture`/`arkitektur`) with the pageId-level mapping used by the language switch to land on the equivalent section (J5). ## 4. Fallback rules (build-time, never runtime) 1. **Full parity is the shipping rule.** CI parity test: every EN content entry has a NO twin and vice versa; every dictionary key present in both. 2. **Temporary asymmetry** (a new case study written EN-first): the NO twin must still exist, containing a native-Norwegian summary + link "Full versjon på engelsk" (CONTENT_STRATEGY rule). The schema supports `summaryOnly: true` for this state; CI allows it but the sitemap/hreflang still emit correctly. 3. No silent English fallback ever renders on a `/no/` page — a Norwegian reader must never hit unexplained English mid-page. (Machine translation is banned outright.) ## 5. Locale-sensitive rendering rules - ``: `en-GB` / `nb-NO` per page. Date formatting: `2015–2023` style is locale-neutral; long dates use locale month names via `Intl` at build time. - Quotes: “ ” (EN) / « » (NO) — enforced in content review, not code. - The CV split-button defaults to the *current page locale's* CV; stored language preference overrides on EN pages only if the user previously chose NO (subtle: preference sets default, page locale is tiebreak). - OG/meta per locale (SEO_SPEC). `hreflang` emitted from the slug map: `en`, `nb`, `x-default → en`. ## 6. Translation management workflow - Author EN and NO in the same PR (content files sit side by side; the parity test enforces it). - Norwegian quality gate before launch: native/fluent review pass of all NO content (tracked as a Phase 3 exit criterion; the reviewer reviews *markdown files in a PR*, which keeps review diffs clean — a deliberate benefit of content-in-git). - Glossary (CONTENT_STRATEGY §5.3) lives as a comment header in the NO dictionary file so it's in front of whoever edits translations. - No TMS tooling: two locales, one author, git history is the audit trail.