Extends the Fase 10.8 Accept-Language parser to surface `'eu'` as a third
known primary subtag, gated on the same q ≥ 0.5 threshold the existing
`'es'` path uses. `'en'` continues to short-circuit because it's the
source language. `AppLanguage` widens to `'en' | 'es' | 'eu'`.
Tests:
* Unit (apps/web): LANG-U-EU-01 bare 'eu', LANG-U-EU-02 'eu-ES',
LANG-U-EU-03 'eu-FR;q=0.5,en;q=0.4' (borderline q wins), LANG-U-EU-04
navigator.languages array — 4 new specs, all green.
* Integration (packages/test-utils): LANG-INT-EU-01 user updates own
language=eu, LANG-INT-EU-02 user attempts language='fr' and gets the
Postgres 22P02 from the ENUM (cast through `as unknown as` so the
narrow TS type doesn't block the test).
The integration negative case lands on the language_code ENUM gate from
migration 028, not RLS — it would still fail even from a service-role
client.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New $lib/utils/accept-language.ts parses Accept-Language strings
(server-style "es;q=0.9,en;q=0.8") and navigator.languages arrays,
returns 'en' or 'es'. Rule (plan §10.8.1): the highest-q-scored 'es'
entry wins if q >= 0.5; otherwise fall back to 'en'. Unknown
languages (e.g. fr) → 'en'. Pure function, 12 unit tests covering
bare tags, regional variants, q-score ordering, malformed input,
navigator.languages arrays, and the borderline q=0.5 case.
The root +layout.svelte calls maybeBootstrapLanguage() after each
SIGNED_IN: it only fires for genuinely new users (public.users row
created within the last 60s) whose language is still the default
('en'); it reads navigator.languages, runs the parser, and
UPDATEs public.users.language if the result is 'es'. setLanguageTag()
flips the live Paraglide runtime so the user lands on the next
page already in Spanish — no reload needed.
Established users keep their explicit choice (plan §10.8.5 / Riesgo
5). Server-side Accept-Language sniffing was rejected: the OIDC
dance lives outside SvelteKit (Keycloak ↔ GoTrue), so a hooks.server.ts
would never see those requests. navigator.languages is the
client-side equivalent and survives Paraglide's compile-time
tree-shaking.
LANG-01/02 (vitest integration): UPDATE public.users RLS contract
the bootstrap relies on — own row OK, other user's row rejected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>