[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

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      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

      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

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          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

          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

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              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

              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

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  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

                  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

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      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

                      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

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          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

                          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

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              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

                              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

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions