Skip to content

[Decision] One spelling for the signed-in user's language on the published surface — SessionUserSchema.language (unproduced), sys_user.locale (#13881) and /auth/me/localization.locale (request/deployment) coexist #14788

Description

@claude

Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 2). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). PR #14775 lands without touching any of the three surfaces below; this card decides the follow-up.

背景

登录用户的「语言」在公开面上现在有三种拼法并存:

  1. SessionUserSchema.language(packages/spec/src/api/auth.zod.ts:36,z.string().default('en'),描述 "Preferred language")—— 已发布的 session 契约,但全仓没有生产者也没有消费者(只有自己的 spec 测试);
  2. sys_user.locale —— [Decision] Per-user notification locale —— 2026-08-13 裁决所等的「实测拉力」已到(hotcrm:4 个已发布语言 × 16 个 notify 节点 × 0 可本地化) #13881 裁决新增的一等列(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775),通知投递按收件人读它;
  3. GET /auth/me/localization 返回的 locale(packages/plugins/plugin-auth/src/current-user-endpoints.ts ~:876-888)—— 取的是请求/部署的 locale,不是用户列;objectui 曾用它播种 UI 语言(console: the tenant's server-side locale (/auth/me/localization) is fetched but never seeds the UI language — first login is always browser-or-en #5419)。

#13881 的裁决明确排除「第二种拼法」,却没看见 language 早已躺在契约上。复审判 PR 可以不动它先落地,本卡决定收口方向。

带 re-check 命令的前提

  • language 仍在契约上且无生产者:git grep -n "language" origin/main -- packages/spec/src/api/auth.zod.ts packages/plugins/plugin-auth/src packages/rest/src (只应命中 auth.zod.ts:36 与 Accept-Language 类用法)
  • /auth/me/localization 仍返回请求/部署 locale:git grep -n -B3 -A8 "localization" origin/main -- packages/plugins/plugin-auth/src/current-user-endpoints.ts
  • objectui 是否引用 SessionUser.language(未测,本席只有 objectstack 检出):在 objectui 仓 git grep -n "\.language\b\|SessionUser" origin/main -- packages apps

一句话问题

用户在个人资料里选了中文,前端问服务器「这个用户什么语言」时,契约上有一个永远是 'en'language、一个不看用户只看请求的 locale,和一个刚落地但契约上不可见的用户列 —— 三个答案,没有一个是对的。

选项 × 真实代价

选项 做什么 真实代价
A 不动 language 继续躺在契约上,sys_user.locale 只做存储 前端/AI 读契约会相信 language 有意义,拿到永远的 'en';用户选的语言 UI 永远看不到 —— 「声明 ≠ 兑现」原样留着
B 加 locale 到 session 契约,与 language 并存 两种拼法一个面 正是 #13881 裁决明令排除的形状;每个消费者都要猜读哪个
C 直接改名 language → locale 破坏性改名,ADR-0087 登记 已发布契约破坏;objectui 是否引用未测 —— 可能弄坏 console 登录态
D 退役 language(ADR-0049 enforce-or-remove),只在 session 端点真正产出时再以 locale 投影用户列 先删从未兑现的键;/auth/me/localization 改成「用户列 → 请求 → 部署默认」并成为唯一的读面 一次 ADR-0087 退役登记 + 一个端点改动;需要先测 objectui 有没有读 language

业务上:A = 产品文档里写着「支持用户语言」但设置无效;B = 设置页有两个「语言」下拉;C = 一次升级把老客户端弄坏;D = 像成熟 SaaS 那样,只有一个「我的语言」,后端按它答。

四轴(从业务立场)

  • 实际业务需求:language 零拉动(从未产出、从未消费);用户列的 UI 侧拉动是真的(console: the tenant's server-side locale (/auth/me/localization) is fetched but never seeds the UI language — first login is always browser-or-en #5419 显示 console 曾试图从 /auth/me/localization 播种语言,而它今天不读用户列)。
  • 项目长远合理性(≥50%):一个概念一个拼法、声明即兑现 —— D 把契约收窄到真实生产者,B 扩生特例,A 留着假声明,C 用破坏性动作换一个本可以先删再加的结果。
  • 防 AI 犯错:AI 读契约看到 language 就会用,拿到 'en' 还以为对了 —— 静默错误的温床;退役后读契约只剩一个 locale,错了是响亮的编译期缺键。
  • 创业阶段不扩散:D 是 remove 优于 declare-and-maintain;不设双拼写宽限(2026-08-27 裁:创业阶段不渐进)。

推荐 + 回退 + 置信缺口

  • 推荐 D:先在 ADR-0049 通道退役 SessionUserSchema.language(ADR-0087 登记),/auth/me/localizationlocale 改为「用户列 → 请求 locale → 部署默认」,成为唯一读面;不在 session 契约上另加字段,直到有真实生产者。
  • 回退 A(什么都不动,记录在案)。
  • 本分析看不见什么:objectui / cloud / hotcrm 是否读 SessionUser.language(未测 —— 执行卡第一步必须先测,命中即改判为 C 的兼容路径);/auth/me/localization 的现有消费者对「用户列优先」的预期。

裁后我会怎么执行(你不用管)

Refs

#13881(裁决 5494464459)· PR #14775(复审 5518923117,H3 finding)· #5419(console 播种 UI 语言,已关)· #14762 · ADR-0049 · ADR-0087

os-decision-facets

  • ① 长远合理性:一个概念一个拼法、声明即兑现 —— 退役从未产出的 language,用户语言只经 locale 投影(D);B 扩生特例,A 留假声明,C 破坏性换同一结果。
  • ② 实际业务拉动:language 零拉动(无生产者无消费者);用户列的 UI 播种拉动是真的(console: the tenant's server-side locale (/auth/me/localization) is fetched but never seeds the UI language — first login is always browser-or-en #5419)—— 拉动指向 D 的第二步,不指向保留 language
  • ③ 防 AI 犯错:留着 language = AI 读契约拿到永远的 'en' 还以为对;删掉后错误是响亮的缺键。
  • ④ 创业阶段不扩散:remove 优于 declare-and-maintain,不设双拼写宽限。
  • 推荐:D(回退 A;C 只在 objectui 实测有引用时作为兼容路径)。
  • 置信缺口:objectui / cloud / hotcrm 对 SessionUser.language/auth/me/localization 的引用未测。

Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions