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.
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-Language → en. 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.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?