# ADR-002: Multi-Language Architecture (i18n) **Status:** Accepted **Date:** 2026-06-04 **Context:** Issue #71 (Epic: Multi-Language: UK/RU/EN — DE bleibt für Rechtliches) **Deciders:** Martin (Product Owner), Hermes (Implementation) ## Context and Problem Statement The landing page `landingpage-haus-schleusingen.de` is currently German-only. It must support 4 languages: **DE** (default, for legal content), **EN** (UK English), **UK** (Ukrainian), **RU** (Russian). The site is SEO-critical (real estate listing), has no build step, and runs on stock PHP 8.x on shared hosting. The challenge: ship server-side rendering for SEO + no FOUC, without dragging in a heavy framework or build pipeline. ## Considered Options 1. **PHP-Server-Side-Rendering with `app/Locales/*.php` arrays + `t()` helper** (chosen) 2. JSON translation files + JS-driven i18n (rejected — FOUC, bad SEO) 3. Full Symfony/translation component (rejected — overkill for 1-page site) 4. Static-site per language (`/de/`, `/en/`, `/uk/`, `/ru/` directories) (rejected — duplicates routes/forms, harder to maintain) ## Decision **Option 1: PHP SSR with `app/Locales/*.php` and a `t()` helper.** ### Components - **`App\Core\Locale`** — locale resolution (priority: query-param `?lang=` → cookie → `Accept-Language` header → fallback `de`). Immutable, no globals. Available locales: `['de', 'en', 'uk', 'ru']`. - **`App\Core\I18n`** — translation loader + `t(string $key, array $params = [])` function. Loads `app/Locales/{locale}.php` lazily, caches in static array. Supports `{placeholder}` interpolation. Falls back to `de` if a key is missing in the current locale, then to the key itself. - **`App\Controllers\LocaleController`** — `GET /locale/{locale}` sets a one-year `locale` cookie and 302-redirects to `Referer` (or `/`). - **`app/Locales/{de,en,uk,ru}.php`** — flat `key => 'text'` arrays. Keys use dotted notation (`nav.gallery`, `hero.cta`, `form.error.email`). - **Layout** — `app/views/layouts/main.php` reads current locale from `Locale::current()` and renders dynamic `` + `og:locale`. - **Switcher UI** — in `app/views/partials/locale_switcher.php`, embedded in navbar. Inline SVG flag icons (no external assets). ### Locale Resolution Order 1. Query parameter `?lang=xx` (one-shot, sets cookie) 2. Cookie `locale` (1 year, `SameSite=Lax`, no `Secure` flag for HTTP test hosts, `Secure` flag in prod via env check) 3. `Accept-Language` header — first matching language from `['en-US', 'en', 'uk', 'ru']` (BCP-47 → ISO 639-1 mapping) 4. Fallback: `de` ### Translation File Format ```php // app/Locales/de.php return [ 'nav.gallery' => 'Galerie', 'hero.cta' => 'Jetzt anfragen', 'form.label.email' => 'E-Mail', ]; ``` `{placeholder}` interpolation, e.g.: ```php 'greeting' => 'Hallo, {name}!', echo t('greeting', ['name' => 'Martin']); // "Hallo, Martin!" ``` ### Out of Scope (this MVP) - Right-to-left languages (Arabic, Hebrew) - Plural forms (`{n,plural,one{...}other{...}}` ICU syntax) — flat strings only - Admin UI for editing translations (POEditor, Crowdin, etc.) - Per-page translation overrides - URL-based locale (`/en/`, `/uk/`) — cookie + query only for MVP ### Trade-offs Accepted - **No URL-based locale** → weaker SEO signal for non-default languages. Mitigation: `og:locale` + `` + hreflang tags (TODO post-MVP). - **No ICU plural** → manual `{n} Zimmer` strings. Acceptable: page has fixed numbers (`227 m²`, `6 Zimmer`). - **Flat key namespace** (`nav.gallery` vs nested arrays) → slightly more verbose but trivially diff-able in PRs and avoids PHP array-merging surprises. ## Consequences ### Positive - **Zero new dependencies** (no Composer additions, no JS framework) - **SEO-perfect** — fully server-rendered, no FOUC - **Trivially testable** — pure PHP, no globals, no I/O at request time (files loaded once, cached) - **Diff-friendly** — translation files are flat PHP arrays - **Fast** — locale detection is in-memory; translation load happens once per request, per locale ### Negative - Adding a new key requires touching all 4 files (mitigated by `missing key → fallback to DE → fallback to key string`) - No URL canonicalization for non-DE locales (mitigated post-MVP with hreflang) - Manual translation review (no professional translator for UK/RU in MVP) ## Implementation Plan Issue #71 (Epic) → #72–#77 (6 sub-issues, dependency-ordered). ## References - Issue #71: https://git.home.kies-media.de/greggy/landingpage-haus-schleusingen/issues/71 - Issue #72–#77: sub-issues, all in Milestone "Multi-Language MVP" - W3C i18n tutorials: https://www.w3.org/International/tutorials/ - BCP-47 language tags: https://datatracker.ietf.org/doc/html/rfc5646