1f695d3932
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.8 KiB
3.8 KiB
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/andno/; 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
anchorIdper 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)
- 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.
- 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: truefor this state; CI allows it but the sitemap/hreflang still emit correctly. - 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-NOper page. Date formatting:2015–2023style is locale-neutral; long dates use locale month names viaIntlat 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).
hreflangemitted 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.