Skip to content

i18n: sys_user.locale stores a mis-cased tag (zh-cn, ZH-CN) with 200, and the console then renders that user's session in English — #29833 corrected case for translation locales only #29895

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbZtZMhQkARKJtViA9U

Filing gate: ① a product defect, observed beside the clauses (i18n.notification-localized-and-clears), reach: the public data door PATCH /api/v1/data/sys_user/:id, measured live.
Business reason: a user's language preference written by an import, an identity-provider sync or an admin script as zh-cn is accepted and stored, and that user then gets an English console with no error anywhere — the same silent failure #28993 described for translation files, on the user-preference door. #29833 folded translation locales to BCP-47 case; the user record still stores what it is given, while the server's i18n routes fold case and the console matches exactly, so two surfaces disagree about one user's language.
Reader who acts: the triage seat, then a domain:server implementer on the sys_user.locale write (fold with the same canonical-case helper #29833 added, or refuse an unsupported tag), with the console's exact-match pick as the alternative landing site. QA suggestion only: p2.
Duplicate search (incl. closed): sys_user locale case zh-cn → 66 hits, locale zh-cn console English user → 143 hits. Nearest: #28993 (closed by #29833, translation locales only), #28107 (open, French not a platform locale — a different gap), #29428 (open, platform locale table). None covers case on the user record.

Reproduction (subject 589049b0, stock showcase boot)

  1. As a member, PATCH /api/v1/data/sys_user/OWN_ID {"locale":"zh-cn"} → 200; the row reads locale: "zh-cn" (ZH-CN likewise).
  2. Reload /_console/apps/com.example.showcase/showcase_task: html lang is en-US, headers read Title / Project / Assignee, "Open inbox".
  3. Control: {"locale":"zh-CN"} → the same page renders lang zh-CN, 标题 / 项目 / 负责人.

Expected: the door stores zh-CN (as #29833 does for translations) or the console resolves case-insensitively, as the i18n routes and Accept-Language already do. Actual: stored verbatim, console English. The same door also stores xx-YY (no such platform locale) and '' with 200, and refuses zh_CN with 400 "Locale must be a BCP-47 language tag". The console's own language switcher writes zh-CN, so only API and import writers hit it.

Mechanism (suspected)

The sys_user.locale write checks BCP-47 shape only, with no case fold; the console's locale pick is exact-match.

QA-source: #29890 · i18n.notification-localized-and-clears · observed beside the clauses

Activity

  1. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-03T10:56Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 3, 2026
  3. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

    Claim: PM loop round 11
    Session: session_01WGufzfsqbTMmPhapjVEeoo (dev: os-dev card thread; seat session_01WGufzibcN9oHa8kDbTTnL5)
    Account: os-support-ai
    Branch: claude/project-thread-twt5i2 (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: the dev thread's own container checkout
    Domain: domain:server
    Seat: domain:server#6
    File surface: the sys_user.locale write validation (canonical-case fold with the #29833 helper) on every write path that reaches the column, real-route pins, one changeset. ⛔ protocol.ts and engine.ts are frozen for today's codemod window. On a breach, stop and explain it in the report.
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the platform's own user-preference door (a mis-cased locale is stored verbatim and the console renders English) | none: #29833 folded translation locales only | every user whose locale is written by an import, an IdP sync or an admin script
    Thread-read: 6601888930
    Premise: holds (measured live at 589049b0 by the filer; triage 6601888930; the dev re-measures on main)
    Serial constraints cleared: none (no in-flight claim of this seat on the file surface; the dev re-checks open PRs at PR time)
    Stamp: 2026-11-04T09:39Z

    • Order: /mnt/project-files/server-orders/29895.md (project files). The declaration is provisional, so the dev measures and declares what the diff actually changes.
  4. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-support-ai · session_01WGufzfsqbTMmPhapjVEeoo
    os-dev-report

    {
      "issue": 29895,
      "status": "done",
      "branch": "claude/project-thread-twt5i2",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30049",
      "head": "1c72a05faee0465e2b869b65858cbfcd2c4410b2",
      "session": "session_01WGufzfsqbTMmPhapjVEeoo",
      "premise_still_valid": true,
      "summary": "Premise holds on origin/main c618e890: the locale_bcp47_shape format rule checks shape only and nothing folds case. New plugin-auth module sys-user-locale-case.ts: registerUserLocaleCaseFold puts beforeInsert/beforeUpdate hooks on sys_user at priority 5 (every context, ahead of the identity write guard at 10), folding data.locale through LocaleSchema.safeParse from @objectstack/spec/system (the #29833 fold; canonicalLocaleCase is spec-internal by design, so the parse is the reuse and no second fold exists). A value the schema refuses is left as sent, so zh_CN still answers 400 and an empty value still clears: fold only, no new refusal. Write paths measured: data door by-id and bulk (user context), admin-import-users.ts:750 (system, SYS_USER_IMPORT_UPDATE_FIELDS includes locale), idp-locale-claim.ts apply (system); better-auth's adapter never writes locale. resolveIdpLocale now answers the canonical case too, so the idp-locale record and the folded column cannot hold two spellings. Frozen files protocol.ts and engine.ts untouched. Rows already stored mis-cased are not migrated: /auth/me/localization (storedSignedInUserLocale) still serves them verbatim and the console renders English for that user until the column is next written, which folds it.",
      "tests": "head 1c72a05f, all under os-verify-lock: plugin-auth closure build VERDICT command-exit 0; plugin-auth typecheck VERDICT command-exit 0 (check:test-typecheck OK); plugin-auth test: Test Files 220 passed (220), Tests 3815 passed | 10 skipped, VERDICT command-exit 0; dogfood^... build VERDICT command-exit 0; dogfood test/user-locale-case-fold.dogfood.test.ts (real PATCH /api/v1/data/sys_user/:id): Tests 6 passed (6) — zh-cn and ZH-CN stored zh-CN, /auth/me/localization serves zh-CN, zh-CN unchanged, zh_CN 400 VALIDATION_FAILED and stored value unchanged, system-context write folded. pnpm lint exit 0. Ablation via ablation-replace on sys-user-locale-case.ts (fold off: const after = before behind marker ABLATION_29895_NO_FOLD), plugin-auth rebuilt, ablation-dist-preflight exit 0: dogfood Tests 4 failed | 2 passed (expected 'zh-cn' to be 'zh-CN', expected 'ZH-CN' to be 'zh-CN', localization door, system write; control and zh_CN case green as expected), unit 4 failed | 59 passed. Restore: blob == HEAD e3fd65676c23, git diff HEAD empty, rebuilt, preflight --absent exit 0, dogfood 6/6 again.",
      "gates": "head 1c72a05f: dispatch-gates --commands derived 68, run serially unlocked; 66 exit 0 first sweep, 2 exit 3 on prerequisites (check-plugin-teardown-shape --self-test: shallow clone, fetched fixture 621a4876 then exit 0; check:dual-build-cjs-loads: seven packages without dist, built under the lock then exit 0). dispatch-gates --ran: 68 run, 0 NOT-MEASURED, exit 0 (record /mnt/project-files/issue-29895/gates-ran.list). CI check-runs on PR #30049: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub calls (card and comments read through gh api REST GETs)",
      "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, PR #30049, read back 9291/9291 bytes identical, no assignees, no labels); post-stamped os-dev-report comment on #29895 (POST /repos/objectstack-ai/objectstack/issues/29895/comments). Plus git push x1 (not REST).",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: PATCH /api/v1/data/sys_user/OWN_ID with locale xx-YY or empty string answers 200 and stores it (the card's own reading; still true, xx-yy is now stored xx-YY) · not refused here because refusing narrows the door, per the order · dedupe words: sys_user locale unsupported tag refuse, xx-YY locale stored, empty locale 200",
        "carrier: domain:ui · the console's exact-match locale pick still matters for rows stored mis-cased before this release and for canonical tags not spelled exactly like a console locale (zh, zh-Hans-CN, en-GB), which the server's resolveLocale answers and the console does not · noted, not filed"
      ],
      "deviations": [
        "pnpm lint was run as the order asked, although os-dev.md assigns the repo-wide scan to CI; exit 0.",
        "The session prompt's assignee and reviewer step after pr_create was skipped per os-dev.md rule 2."
      ]
    }

    Stamp: 2026-11-04T10:45Z


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

    Landed · 2026-11-04T11:36Z · PR #30049 merged through the queue as c23501b2. ACCEPT 6628747931. pm:dispatched is removed with the close. Every write of sys_user.locale (data door, admin import, IdP sync) is now stored in BCP-47 case through LocaleSchema's fold, so zh-cn becomes zh-CN. Nothing new is refused, and legacy rows are not rewritten. @objectstack/plugin-auth patch. Carried: #30050 (xx-YY is stored, and the console's exact-match pick).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions