Five-locale UI ships with custom i18n runtime and profile-persisted locale

manueljpconde built end-to-end localization for en, pt, es, fr, and de from scratch - no third-party library, just a thin layer over native `Intl`. Locale is stored on the user profile, validated at every layer, and resolved server-side on the initial request.

i18ninfrastructure

The schema adds a locale text not null default 'en' column to user_profiles with a named CHECK constraint (user_profiles_locale_check) enforcing the five-value allowlist. The same allowlist appears in backend/src/lib/i18n/locales.ts as a TypeScript const, and the frontend has a matching SupportedLocale union type. Three places in sync - the commit message calls this out as a maintenance hazard, which is an honest note worth keeping if you cherry-pick this.

Server-side locale resolution in the root layout walks mike_locale cookie → Accept-Languageen. A client-side I18nContext handles switching without reload. ProfileLocaleSync is a render-only component that pushes the authenticated user's stored locale one-way to the cookie - the signup form was also updated to persist the active locale, so a guest who switched to Portuguese before signing up doesn't get snapped back to English when ProfileLocaleSync first fires.

document.documentElement.lang is kept in sync via a useEffect on locale changes; SSR sets it once but it drifted on client-side switches before the fix.

A custom catalog validator (scripts/validateCatalogs.ts, 244 lines) runs structural checks, placeholder consistency, and plural-shape validation across the five JSON catalogs. The follow-up commit added findDuplicateTopLevelKeys.ts (95 lines + 95 test lines) after a reviewer noted that JSON.parse silently deduplicates keys - the validator now catches those with a state-machine raw-text scan. The npm test glob invocation was also broken on bash 3.2; a Node-based discovery script (scripts/run-tests.mjs) replaced the quoted-glob approach in both backend and frontend.

Final test counts: frontend 55/55, backend 34/34.

So what Worth importing as a near-drop-in if you want EU multi-locale support. The library-free approach keeps the dependency surface small and the code auditable. Two caveats before pulling: the named CHECK constraint has a specific identifier, so migration ordering matters if you have other pending schema changes; and the translations from the catalog expansion PR were machine-generated and should be reviewed by a native speaker before shipping to customers.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

2 commits from manueljpconde/mikeEU, oldest first. Source extracted verbatim from the harvested git log.

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 thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-249.md from inside the repo you want the changes in.

⬇ Download capture-thread-249.md