Skip to content

Account settings: a "my language" form item for the user's own sys_user.locale #7501

Description

@os-sales

Blocked-by: objectstack-ai/objectstack#14787

Why

The platform now has a first-class per-user notification language. sys_user.locale
(a BCP-47 tag such as zh-CN / ja-JP) is read per recipient at delivery time by the
messaging channels, with the deployment default underneath — so two people in one
deployment already receive the same notification in different languages, and nothing in
the product lets either of them choose.

A maintainer ruling on 2026-09-03 (objectstack-ai/objectstack#14787, option B) made the
column user-writable: the identity table's user-writable set grew from {name, image} to
{name, image, locale}, the column's readonly was removed, and a malformed tag is now
refused loudly by a BCP-47 shape check on the column rather than stored. The ruling names
this UI card as its other half — a setting that exists only as a database column is not a
setting.

What to build

A "Language" item in the account / profile settings surface, on the user's own record:

  • a picker rather than a free-text box. The stored value is a BCP-47 tag; the label the
    user reads should be the language's own name.
  • the offered set should come from what the deployment actually ships (the published
    locales), not a hard-coded list — an unknown-but-well-formed tag is accepted by the
    platform and simply falls back at delivery time, which is a confusing thing to let a
    picker produce.
  • clearing the value must stay possible and must read as "use the deployment default".
    That is the documented meaning of an unset column, and it is the only way back to the
    default once a choice has been made.
  • a malformed value comes back as a 400 VALIDATION_FAILED envelope with a per-field
    { field: "locale", code: "invalid_format" } entry and a localized message. Surface
    the field error on the item rather than as a bare toast.

⚠️ The write route is not settled yet — check before building

The ruling opened the COLUMN, not a self-service route, and the two are separate
decisions in ADR-0092 (D2 decides which columns, D5 leaves who to the permission layer).
As of the ruling:

  • the identity write guard now admits locale on a user-context update to sys_user;
  • but member_default still declares allowEdit: false on sys_user, so a rank-and-file
    member's PATCH to their own row is refused by the permission layer before the guard is
    consulted;
  • and better-auth's /update-user (what the existing update_my_profile action posts to)
    cannot carry the field: locale is deliberately not a better-auth additionalFields
    entry, so that endpoint does not know the column.

So a platform admin can set it today and an ordinary member cannot. Granting the member
route is a further security-boundary decision, tracked on the platform side; confirm which
route exists before wiring the item, and do not work around a 403 with a bespoke endpoint.

Acceptance

  • the item appears on the signed-in user's own account settings and nowhere else;
  • picking a language persists and survives a reload;
  • clearing it returns the account to the deployment default;
  • a refused value renders the server's per-field message on the item;
  • the item is hidden (or read-only with a reason) when the deployment's write route is not
    available to the signed-in user, rather than rendering a control that answers 403.

Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requesti18npm:blockedpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions