ADR-0005. Per-account locale preference served via canonical /auth/me
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-06-21 |
| Deciders | Phat Nguyen |
| Supersedes | - |
Context
- BANA-970 needs a persistent per-account UI language that follows the user across devices/browsers, overriding the current
navigator.languagefallback. - A
localecolumn already exists onUserProfile(defaulten-US);PATCH /users/profilealready accepts it. The gap is a stable read seam and a constrained value set. @venizia/ignis0.0.10-0adds a framework-native current-user seam:IAuthService.getUserInformation→GET /auth/meandGET /auth/who-am-i?withUserInformation=true(additive,501when unimplemented).- The frontend i18n (
apps/core) keys on short codesen/vi; the stored value is BCP-47en-US/vi-VN.
Decision
Keep locale on UserProfile as BCP-47 (en-US | vi-VN), constrained by a shared Locales constant + LocaleSchema. Implement AuthenticationService.getUserInformation to return the full user profile (UserProfileSelectSchema, incl. emails[], phones[], locale), and make GET /auth/me the canonical current-user read. Writes continue through the existing PATCH /users/profile (profile.locale). The frontend maps BCP-47 ↔ i18n short code at the edge.
| Concern | Resolution |
|---|---|
| Storage | Existing UserProfile.locale, text().$type<TLocale>().default('en-US') - no new column |
| Value set | Locales (EN='en-US', VI='vi-VN') + LocaleSchema; enforced on insert/update + sign-up Zod |
| Read | GET /auth/me via getUserInformation (canonical); /who-am-i?withUserInformation=true free |
| Write | PATCH /users/profile → profile.locale (unchanged) |
| Payload | Full UserProfileSelectSchema (nullable) - emails[], phones[], names, locale, … |
| Legacy rows | Backfill migration normalises non-{en-US,vi-VN}/null → en-US |
Consequences
| Pros | Cons |
|---|---|
| Reuses framework seam + existing column - minimal surface | Two reads exist transitionally (/auth/me vs /users/profile) until FE migrates |
| Single source of truth for current-user shape going forward | BCP-47 ↔ short-code mapping lives in FE, not BE |
| Value set centralised; bad values rejected at write | getUserInformation adds a profile-include fetch per /me call |
| No FE app code in this package; handoff doc drives the client | - |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
New locale column on User/account | Matches ticket's literal wording | Duplicates existing UserProfile.locale | Redundant; violates fold-into-existing |
Store short codes en/vi | No FE mapping | Diverges from existing en-US data + BCP-47 standard | PO chose to keep BCP-47 |
Keep read on GET /users/profile only | No new endpoint | Two parallel "me" reads with no canonical one | /auth/me is the framework-native current-user seam |
UserConfiguration USER_PREFERENCES jValue | Generic prefs bucket | Extra table + plumbing for one field | UserProfile.locale already wired |
References
- BANA-970 -
[FE/client] Add account locale auto-applied on sign-in core/src/common/constants.ts(Locales,TLocale)core/src/models/schemas/identity/user-profile/(schema.tslocale column,model.tsLocaleSchema)identity/src/services/authentication.service.ts(getUserInformation)identity/src/models/responses/auth.response.ts(GetUserInformationResponse)identity/src/application.ts(auth controllerpayload.getUserInformation)@venizia/ignis0.0.10-0- auth user-information endpoint changelog