feat: add localization support for en/pt/es/fr/de
From the PR description
Summary
- Adds end-to-end localization for
en,pt,es,fr,de- UI chrome, auth pages, sidebar nav, common labels. English is canonical fallback at every layer. - Thin custom runtime (no library): server-side resolution in root layout (
cookie → Accept-Language → en), clientI18nProviderfor runtime switching, render-onlyProfileLocaleSyncfor one-way profile→client sync. - New auth-gated
user_profiles.localecolumn with named CHECK;PATCH /user/profileaccepts/rejects via shared allowlist.
Classification
trust-boundary - schema change on user_profiles, new auth-gated user-mutable field, coupled allowlist triple (schema CHECK / backend / frontend).
Linked issue
Closes #4.
Verification
npm run lint --prefix frontend: ✗ - 44 errors / 67 warnings, all pre-existing in unrelated files (privacy/page.tsx,support/page.tsx,convert-courts-to-ts.js, etc.). Stash-comparison againstmainconfirms PR's net delta is -2 problems. Zero new lint failures from this PR's files. Tracked separately.npm run build --prefix frontend: ✓ (Next 16 + Turbopack, 16 routes generated)npm run build --prefix backend: ✓ (tsc)npm run test --prefix backend: ✓ - 34/34 (5 new inlocales.test.ts)- Frontend tests (
tsx --test): ✓ - 46/46 across 11 suites (resolveLocale, translate, format, validateCatalogs, validateRealCatalogs) npm run lint:catalogs --prefix frontend: ✓ - 54 keys × 5 locales
Trust-boundary checks
Auth gate:
PATCH /user/profilemountsrequireAuth(line 310 ofbackend/src/routes/user.ts); identity fromres.locals.userIdonly.Tenant scoping: Locale write reuses unchanged
.update(parsed.update).eq("user_id", userId).user_profiles.user_idisuuid(FK toauth.users(id)) per existing convention - different from text-baseduser_idinprojects/documents/etc; correct for this table.Storage paths / download links: N/A - no document mutation, no download URLs.
Frontend exposure: Zero new
NEXT_PUBLIC_*. No model SDK imports anywhere underfrontend/. Cookiemike_localeis non-httpOnly by design (client must write on switch); carries locale code only - no PII or secret material.Model registry: N/A - not touched.
Built-in workflows: N/A - not touched.
Provider quirks: N/A -
lib/llm/{claude,gemini,openai}.tsnot touched.Coupled allowlist triple flagged for
/architecture-guard:backend/schema.sql-user_profiles_locale_checkCHECK valuesbackend/src/lib/i18n/locales.ts-SUPPORTED_LOCALESfrontend/src/lib/i18n/types.ts-LOCALESconst +SupportedLocaleunion
All three carry
["en","pt","es","fr","de"]in matching literal order.
Rollout
Three SQL blocks in backend/schema.sql (additive, no new table → no new RLS policy or revoke grant required; existing user_profiles policies cover the new column):
- Inside
create table if not exists public.user_profiles-locale text not null default 'en'. - Idempotent ADD COLUMN IF NOT EXISTS - mirrors
openai_provider_settingsprecedent. - Named CHECK via conditional do-block - one stable constraint name (
user_profiles_locale_check) for fresh + existing DBs.
Existing rows receive 'en' through the default clause on column add. No backfill step needed.
Deploy order (single PR): apply schema, deploy backend, deploy frontend. Both layers tolerate skew (frontend treats missing locale as no-op; backend tolerates payloads without locale).
Rollback:
alter table public.user_profiles drop constraint if exists user_profiles_locale_check;
alter table public.user_profiles drop column if exists locale;
Env vars: none added.
Dependency added: tsx to frontend devDependencies (mirrors backend's tsx --test pattern). Used for npm run test and npm run lint:catalogs.
Notes for reviewer:
frontend/tsconfig.jsonadds"scripts"toexclude- needed because Next 16 typechecker rejects.ts-extension imports insidescripts/. Trivial build fix; no runtime impact.frontend/scripts/package.json(just{"type":"module"}) localizes ESM to the scripts dir without affecting Next.
Planning artifacts
- Spec: https://github.com/manueljpconde/mikeEU/issues/4#issuecomment-4414976968
- Plan: https://github.com/manueljpconde/mikeEU/issues/4#issuecomment-4414987693
- GO: https://github.com/manueljpconde/mikeEU/issues/4#issuecomment-4415157201
- Architecture-guard (pre-implementation): APPROVE - all 7 checks PASS. Trust-boundary invariants intact, scope discipline preserved, verification appropriate for v1 scope.
Test plan
Manual verification owed before merge (no live SSR exercise possible from automation):
- Apply schema delta to a test Supabase project; confirm
select column_name, data_type, column_default from information_schema.columns where table_name = 'user_profiles' and column_name = 'locale'returnstext / 'en'::text - Confirm
select conname from pg_constraint where conname = 'user_profiles_locale_check' and conrelid = 'public.user_profiles'::regclassreturns the constraint -
update public.user_profiles set locale = 'xx' where user_id = '<test>'- expect CHECK violation - Existing rows:
select locale, count(*) from user_profiles group by locale- no NULL or out-of-set values - Fresh browser, no cookie,
Accept-Language: pt-BR→<html lang>ispt; UI in Portuguese on/login - Click switcher to
Deutsch→ UI swaps without reload; cookiemike_locale=depresent in DevTools - Reload → UI stays in
de - Sign in fresh user → switcher in sidebar dropdown shows current locale; switch to
Français→ network panel showsPATCH /user/profile {locale:"fr"}succeeds - Reload in different browser logged into same account → loads in
fr(cross-device profile sync) - Switch with backend stopped → local UI swaps;
window.alertsurfaces "Couldn't save preference. Applied on this device only." - Sign out → cookie locale persists for next guest visit
-
npm run lint:catalogs --prefix frontend- passes after this branch lands
Out of scope (follow-ups)
- Complete frontend hardcoded string sweep for i18n coverage.
- Localize backend-emitted user-facing strings (API errors + transactional emails).
- Catalog export/import workflow for translators.
- Wire
lint:catalogsinto broaderlint/buildchain (CI gate). - Component-level testing infra (Vitest + RTL).
- Pre-existing frontend lint debt (
privacy/page.tsx,support/page.tsx,convert-courts-to-ts.js).
Our analysis
Ship five-locale UI with a custom i18n runtime and profile-synced preference — read the full analysis →
Think the analysis missed something the PR description covers?
Commits in this PR (2)
| SHA | Subject | Author | Date | |
|---|---|---|---|---|
346b8197 | feat: add localization support for en/pt/es/fr/de (#4) | Manuel Conde | 2026-05-10 | ↗ GitHub |
commit bodyAdds i18n infrastructure (thin custom + native Intl, no library) covering auth pages, sidebar nav, language switcher, and common labels across the five supported locales. English is canonical fallback at every layer. - Schema: user_profiles.locale text column with named CHECK constraint - Backend: PATCH /user/profile accepts/persists locale with allowlist validation - Frontend: server-side locale resolution (cookie -> Accept-Language -> en) in root layout; client I18nProvider for runtime switching; ProfileLocaleSync applies authenticated profile.locale one-way - Cookie 'mike_locale' (non-httpOnly, Path=/, Max-Age=1y, SameSite=Lax) - Catalog validator with structural + placeholder + plural-shape checks - 51 automated tests (46 frontend + 5 backend) on pure logic; React surface covered manually - Coupled allowlist triple flagged: schema CHECK / backend SUPPORTED_LOCALES / frontend SupportedLocale must move together Spec: https://github.com/manueljpconde/mikeEU/issues/4#issuecomment-4414976968 Plan: https://github.com/manueljpconde/mikeEU/issues/4#issuecomment-4414987693 GO: https://github.com/manueljpconde/mikeEU/issues/4#issuecomment-4415157201 Closes #4 | ||||
cb6b9a2f | fix(i18n): four review findings on PR #10 | Manuel Conde | 2026-05-10 | ↗ GitHub |
commit body1. npm test scripts: replace quoted-glob args (which tsx received literally and didn't expand on bash 3.2) with a Node-based test discovery script per repo. Both `npm test --prefix frontend` and `npm test --prefix backend` now run the suites correctly. 2. Validator now rejects duplicate top-level JSON keys via a state-machine raw-text scan (findDuplicateTopLevelKeys), restoring the contract promised in spec rule 4. JSON.parse silently dedupes; this catches editor mistakes before runtime sees the deduped catalog. 3. Signup persists the active i18n locale alongside name/organisation so a guest who selected pt/es/fr/de doesn't get snapped back to en when ProfileLocaleSync applies the (default 'en') profile after signup. 4. Client setLocale now syncs document.documentElement.lang via useEffect on locale change. SSR sets it once; the effect keeps it in sync after user-driven switches. Tests: frontend 55/55 (added 9 in findDuplicateTopLevelKeys.test.ts), backend 34/34. Both `npm test` and `npm run lint:catalogs` runnable without manual glob expansion. | ||||
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-10.md from
inside the repo you want the changes in.