# 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

- `<html lang>`: `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.
