feat: add localization support for en/pt/es/fr/de

✅ merged · #10 · manueljpconde/mikeEU ← manueljpconde/mikeEU · opened 16d ago by manueljpconde · merged 16d ago by manueljpconde · self · +2,660-110 across 40 files · ↗ on GitHub

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), client I18nProvider for runtime switching, render-only ProfileLocaleSync for one-way profile→client sync.
  • New auth-gated user_profiles.locale column with named CHECK; PATCH /user/profile accepts/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 against main confirms 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 in locales.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/profile mounts requireAuth (line 310 of backend/src/routes/user.ts); identity from res.locals.userId only.

  • Tenant scoping: Locale write reuses unchanged .update(parsed.update).eq("user_id", userId). user_profiles.user_id is uuid (FK to auth.users(id)) per existing convention - different from text-based user_id in projects/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 under frontend/. Cookie mike_locale is 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}.ts not touched.

  • Coupled allowlist triple flagged for /architecture-guard:

    • backend/schema.sql - user_profiles_locale_check CHECK values
    • backend/src/lib/i18n/locales.ts - SUPPORTED_LOCALES
    • frontend/src/lib/i18n/types.ts - LOCALES const + SupportedLocale union

    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):

  1. Inside create table if not exists public.user_profiles - locale text not null default 'en'.
  2. Idempotent ADD COLUMN IF NOT EXISTS - mirrors openai_provider_settings precedent.
  3. 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.json adds "scripts" to exclude - needed because Next 16 typechecker rejects .ts-extension imports inside scripts/. Trivial build fix; no runtime impact.
  • frontend/scripts/package.json (just {"type":"module"}) localizes ESM to the scripts dir without affecting Next.

Planning artifacts

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' returns text / 'en'::text
  • Confirm select conname from pg_constraint where conname = 'user_profiles_locale_check' and conrelid = 'public.user_profiles'::regclass returns 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> is pt; UI in Portuguese on /login
  • Click switcher to Deutsch → UI swaps without reload; cookie mike_locale=de present in DevTools
  • Reload → UI stays in de
  • Sign in fresh user → switcher in sidebar dropdown shows current locale; switch to Français → network panel shows PATCH /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.alert surfaces "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)

  1. Complete frontend hardcoded string sweep for i18n coverage.
  2. Localize backend-emitted user-facing strings (API errors + transactional emails).
  3. Catalog export/import workflow for translators.
  4. Wire lint:catalogs into broader lint/build chain (CI gate).
  5. Component-level testing infra (Vitest + RTL).
  6. 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 body
Adds 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 body
1. 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.

⬇ Download capture-pull-10.md