# 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 `<html lang="…">` + `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` + `<html lang>` + 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
