#179: negotiate the SPA shell's lang attribute in nginx #315
Merged
opus-5
merged 1 commits from 2026-08-01 20:24:27 +02:00
issue-179-spa-lang into main
1 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 8752cf0c5a |
#179: negotiate the SPA shell's lang attribute in nginx
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m13s
CI / Build container images (pull_request) Successful in 1m21s
CI / Auth e2e pack (pull_request) Successful in 9m25s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CD / Build and push images (push) Successful in 21s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m40s
CD / Promote to Int (push) Successful in 15s
CI / Lint, typecheck, test (push) Successful in 7m9s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m45s
CI / Import/export fidelity gate (push) Successful in 55s
`apps/web/index.html` carries a hard `lang="en"`. The app corrects it at runtime (#163), but nginx answers every SPA route with that same file, so a crawler or a no-JS visit — `/public/...` on prod is exactly that — saw `en` for German content, permanently. WCAG 3.1.1 is about the delivered document, not the one JavaScript later fixes. nginx-only, no backend involved: a `map` on `Accept-Language` and a `sub_filter` in the index.html path. Only the FIRST tag decides, which is what "the browser's preferred language" means and mirrors #163 — `de-CH` counts as German, `en-US,de` does not. `Vary: Accept-Language` is new. The response now genuinely depends on a request header, and without it a shared cache could hand one language's copy to the other. Everything else in the location is untouched: same CSP, same `Cache-Control: no-cache`, same `nosniff`. The known limit is documented in the config rather than worked around: nginx cannot read `instance.defaultLocale`, so an unlisted or absent Accept-Language yields `en` even on a German instance. For public content that is not the authoritative rendering anyway — the api's server shell (`/api/v1/public/...`) already renders those with the instance locale. Verified against a real nginx 1.27 (the image the stage runs) with the config mounted as-is: `de-DE,de;q=0.9,en;q=0.8` and `de` yield `lang="de"`; `en-US,en;q=0.9`, `en-US,en;q=0.9,de;q=0.8`, `fr-FR,fr` and a request with no header yield `lang="en"`; an SPA route (`/public/teich/seite`) negotiates the same way; the gzipped response is rewritten too, and a JavaScript asset comes through byte-identical. |