Skip to content

页面组件文案没有翻译位:12 处用户可见字符串(首页占 10)在 zh/ja/es 下只能是英文,而 dashboards.widgets 有同形的翻译面 #1004

Description

@yinlianghui

Blocked-by: objectstack-ai/objectstack#12231 — ⛔ VOID, and it was never load-bearing. Removed from the reverse index at round 15. That upstream card was filed on 2026-08-25 asking for a face that had already shipped in @objectstack/spec 17.0.0-rc.6 (measured on this card 2026-08-11, verified upstream 2026-08-14); it was closed completed on 2026-08-28. This repo is pinned at 17.1.0, so the slot exists in the package actually installed. See the round-15 claim comment.

发现于 #1002 实施过程(改首页 ai_briefing 卡片文案时,按裁定 grep 四个语言包找对应键,
发现根本没有这个键面)。#1002 的文件面是那一张卡片,故单独立单。

现状(基线 origin/main = 86010ff)

翻译契约里 pages 只有四个键,而且是 .strict():

pages: Record( name, { label?, description?, title?, subtitle? } )   // strict

(node_modules/@objectstack/spec/dist/translation.zod-*.d.ts)

⚠️ 上面这段是立单时(2026-08-06)的现状,今天已不成立 —— pages 现已带 components 面,见 2026-08-11 / 08-14 两条评论。保留原文是为了让这张卡的论证链可读,⛔ 不要把它当作今天的契约形状。

test/i18n-references.test.ts 的 every page has translated nav copy and header copy 也照着这四个键写,并且在注释里点明了 title / subtitle 的语义:它们对应
page:header
,因为 header 实例没有稳定 id,页面名是唯一能够到它的键。

于是 page:header 以外的组件文案 —— 卡片标题、卡片描述、指标块 label —— 没有任何
翻译位
。语言包写不进去(strict 会拒),运行时也无处可读,值链就是 *.page.ts 里的
字面量直通 dist/objectstack.json。

实测面(从 dist 产物统计,非源码 grep)

8 个页面,12 处这样的用户可见字符串:

sales_home_page (10)
  quick_create.title        "Quick Create"
  my_recent_items.title     "Recent Items"
  key_metrics.title         "Key Performance Indicators"
  kpi_revenue_won.label     "Revenue (Won)"
  kpi_deals_won.label       "Deals Won"
  kpi_pipeline_value.label  "Pipeline Value"
  kpi_open_leads.label      "Open Leads"
  ai_briefing.title         "Ask the AI Assistant"
  ai_briefing.description   "Open the assistant panel from the right edge …"
  upcoming_events.title     "Today's Schedule"
lead_detail_page (1)
  related_tasks.title       "Open Tasks"
utility_bar_page (1)
  quick_notes.title         "Quick Notes"

sales_home_page 是 isDefault: true、发给 sales_rep / sales_manager 的落地页。
一个 zh-CN 用户登录后看到的首页,页头是「销售看板 / 欢迎回来,…」(有翻译位),而右侧
和左侧四张卡片、四个 KPI 块全是英文 —— 同一屏两种语言。

⚠️ 这一段「同一屏两种语言」是推断,不是实测(见 2026-08-25 评论)。同族 #760 用同样强度的
静态证据立单,实测后判 premise-false。⛔ 不要把 12 这个数字当作已验证的影响面向前引用。

这与 AGENTS.md 的 i18n 约束(「No new feature ships without all 4 locales」)以及
仓里 9 个 locale 相关守卫的一贯口径是冲突的:那些守卫覆盖对象/字段/视图/动作/选项,
唯独页面组件文案覆盖不到,因为契约里没有那个键可查。

为什么这像是契约的不对称,而不是本应用漏写

同一个翻译契约里,仪表盘的组件是有翻译位的:

dashboards: Record( name, { label?, description?, actions?, widgets? } )
widgets:    Record( id,   { title?, description? } )                  // 按 id 定位

widget 用 id 定位就能翻译 title/description;page 的组件同样有稳定 id
(ai_briefing、kpi_revenue_won …),却没有对应的 components 面。两个形状几乎
一样的 UI 组件树,一个能翻一个不能,更像是 spec 侧少了一个面,而不是本应用的用法问题。

处置建议(不预判,交分诊)

三条路成本差别很大:

  1. 上游补面:给 pages 加 components: Record( id, { title?, description?, label?, help? } ),
    与 dashboards.widgets 对齐 —— 一致性最好,但要动 @objectstack/spec;
  2. 应用侧规避:把这类文案挪到有翻译位的表面(例如能承载它的组件类型),或接受
    首页卡片只有英文并在守卫里把这一点写成明示豁免,免得下一个人再查一遍;
  3. 先量再定:先确认控制台是否真的直接渲染这些字面量(与 closing_this_quarter 的切换器页签标题写作 "Closing Soon" —— 与 label / 四个语言包的第四个拼法,且 tabs[].label 没有翻译位 #760 同样的不确定性,
    只是那一条是 tabs[].label)。本条的静态证据比 closing_this_quarter 的切换器页签标题写作 "Closing Soon" —— 与 label / 四个语言包的第四个拼法,且 tabs[].label 没有翻译位 #760 强(strict schema 直接排除了
    键的存在),但「渲染出来是什么」仍值得在浏览器里看一眼再定路线。

✅ 路线 1 已由上游交付(@objectstack/spec 17.0.0-rc.6,面比本卡所求更宽:
title / description / label / placeholder / emptyText / submitLabel)。
⛔ 路线 2 本车道宪章明令不可用(绕行把平台缺口藏进样板)。
⚠️ 路线 3 仍未做,且它是实施的第一步,不是可选项 —— 见 R15 派发令。

关联

Activity

  1. added
    metadataDeclarative metadata — schema, security posture, UI surfaces
    upstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream
    on Aug 6, 2026
  2. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    CollaboratorAuthor

    上游镜像已建:objectstack-ai/objectstack#6080(Blocked-by)

    按上游协议处理:本单症结在 @objectstack/spec 的翻译契约(pages 为 .strict() 四键、无组件文案面),不是 hotcrm 侧可修的——语言包写不进(strict 拒收)、运行时无处读,应用侧任何"修法"都是平台 workaround,⛔ 不做。


    Generated by Claude Code

  3. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    Re-measured on @objectstack/* 17.0.0-rc.6 during the rc.5 → rc.6 upgrade (#1059, PR #1066). The asymmetry this card identified is closed upstream — option 1 in the disposition list ("上游补面") is what shipped.

    pages in the translation contract is no longer four keys. It now carries a components face, keyed by component id, exactly parallel to dashboards.widgets:

    pages: Record(name, {
      label?, description?, title?, subtitle?,
      components: Record(id, {
        title?, description?, label?, placeholder?, emptyText?, submitLabel?
      })   // strict
    })

    The face is wider than this card asked for — beyond title / description / label it adds placeholder, emptyText and submitLabel — so it covers component copy this card did not enumerate. (settings and metadataForms gained comparable faces in the same contract.)

    So all 12 strings listed here now have a translation slot: the 10 on sales_home_page (quick_create.title, the four KPI labels, ai_briefing.title / .description, etc.), lead_detail_page.related_tasks.title, and utility_bar_page.quick_notes.title. A zh-CN user's landing page no longer has to be two languages on one screen.

    Not done here: this PR is the dependency bump only. Actually populating components across the four locale bundles — and extending test/i18n-references.test.ts to walk the new face so the coverage guard can finally see page component copy — is app-side work with a real review surface. Worth reopening this card's scope as that follow-up, or filing it fresh.

    Scope note: static read of the shipped @objectstack/spec translation schema. Not verified in a browser that the console actually resolves pages.<name>.components.<id>.title at render time — the point #1004's option 3 raised, and still the right thing to check before writing 12 strings × 4 locales.


    Generated by Claude Code

  4. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Transfer sweep (#1156, part 1) — outcome 2: the fix lands in hotcrm. Relabelled and kept here.

    The outcome test that was applied

    Where would the fix land today? The upstream:objectstack label was correct when applied on 2026-08-06 — the contract genuinely had no key to write into. It is stale now.

    Verdict: the upstream gap this card identified is closed; what is left is app-side

    Platform half — shipped. The mirror objectstack#6080 ("翻译契约 pages 无组件文案面") is closed / completed, via merged PR objectstack#6466 ("feat(spec): translatable page component copy"). Verified upstream directly, not inferred from the on-card note.

    The contract's pages face is no longer four strict keys. It now carries a component face keyed by component id, parallel to dashboards.widgets — which is precisely the asymmetry argument this card made, and option 1 of its own disposition list (「上游补面」) is what shipped. The delivered face is wider than this card asked for: beyond title / description / label it also carries placeholder, emptyText and submitLabel.

    So all 12 strings enumerated in the body now have a translation slot — the 10 on sales_home_page, lead_detail_page.related_tasks.title, and utility_bar_page.quick_notes.title. The value chain this card described (literals in *.page.ts straight through to dist/objectstack.json, unwritable because .strict() rejects the key) no longer holds.

    App half — outstanding, and it is real work with a review surface. Populate the component copy across the four locale bundles, and extend test/i18n-references.test.ts to walk the new face so the coverage guard can finally see page component copy — today it cannot, which is how this gap stayed invisible until #1002 tripped over it. That is app-side work in this repo, so there is no platform need left to route anywhere.

    Actions taken on this card

    • upstream:objectstack — removed. The upstream ask was made, accepted and shipped.
    • pm:blocked — removed. The blocking condition (no key in the contract) is gone.
    • prio:p2 added. This card carried no priority grade at all. P2 (triaged backlog, no near-term schedule) is the honest grade: the user-visible symptom is real — a zh-CN user's landing page is two languages on one screen, on an isDefault: true page shipped to sales_rep / sales_manager — but it is copy, not function, and nothing is broken.
    • metadata — kept.
    • Left open. Not one of the 12 strings has been written.

    ⚠️ Carried forward: this card's option 3 is still unanswered, and it is the right first step

    The body's option 3 asked whether the console actually renders these literals — i.e. whether it resolves pages.[ name ].components.[ id ].title at render time, or only accepts the key in the schema. Everything established above is a static read of the shipped contract. Nobody has confirmed the render-time resolution in a browser.

    That matters practically: writing 12 strings × 3 locales against a key the console does not read would produce a silently ineffective change that still passes every guard. Check the render path first, then write the bundles.

    ⛔ And note what that check is not grounds for. If the console turns out not to resolve the key, that is a new finding against the console at that point — file it fresh, in objectui, with the measurement. It is not a reason to keep this card labelled upstream:objectstack today on a defect nobody has observed.

    What was NOT done here

    No app boot, no browser check, no code, no locale bundles touched. This sweep is routing and bookkeeping only. The upstream half was verified by reading objectstack#6080's state and its merged closing PR; the render-time question above is exactly the part that remains unmeasured, and it is named rather than assumed either way.

    Related, unchanged

    #760 (tabs[].label has no translation slot) is the sibling case and was cross-linked from the upstream mirror on the expectation that both unlock together. This sweep did not touch it — it is not in part 1's six cards. Whoever picks up #1004 should re-check whether the shipped face covers #760 as well; if it does, #760 needs the same relabelling this card just got.

    Authority

    hotcrm 席位的原则是用平台的能力做元数据应用的开发,平台的需求应该转给平台

    This card is the ruling working as intended: the platform need was routed to the platform, the platform built the capability, and what returns is an app using it. The companion disposition 「关掉,只留指向镜像的指针」 does not apply — the mirror is already closed, and pointing a reader at a closed card would hide the app-side work that is the only thing left.


    Generated by Claude Code


    Generated by Claude Code

  5. added and removed
    upstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream
    on Aug 14, 2026
  6. hotlong commented on Aug 25, 2026

    @hotlong
    Contributor

    Triage (代扫, vacancy clause) → pm:blocked, Blocked-by: objectstack-ai/objectstack#12231 (filed by this seat just now).

    Filed 2026-08-06 with metadata + prio:p2 and no pm-state — nineteen days invisible. Vacancy clause: triage actions only.

    The card asked triage to choose among three routes. Two of them are already decided by the charter.

    • Route 1, upstream — the charter's own instruction: 「平台相关的功能应该在平台中实现」. A missing face in the translation contract is as platform-side as it gets. Taken, filed as objectstack#12231 with the asymmetry, the 12-string surface and the boundary of what is measured.
    • Route 2, app-side workaround — ⛔ not available in this lane. The charter forbids it in terms: 「永不在 hotcrm 内绕行 —— 绕行把平台缺口藏进样板,展品从此示范错误写法」. Moving copy onto whatever surface happens to have a slot, or writing an explicit exemption into a guard, is precisely hiding a platform gap inside the exemplar. Recording that here so the next reader does not re-open it as an option.
    • Route 3, measure first — legitimate, and it is a question about impact, not existence, so it does not gate the filing. Carried into the upstream card as an explicit unestablished claim rather than dropped.

    That leaves nothing this repo can do until the slot exists, which is the definition of pm:blocked.

    ⚠️ The part of this card that is inference, kept labelled as inference

    What is measured and renderer-independent: the key does not exist. pages is .strict() with four keys, so a locale pack is refused permission to carry component copy — this is not an English fallback, it is a closed door.

    What is inferred: that all 12 strings reach the screen as written. The sibling case #760 was filed on comparable static evidence about list.tabs[].label and was closed this week as premise-false — the switcher rendered a different string entirely and there was no user-visible symptom at all. Same family, same kind of static argument, opposite outcome on measurement.

    So the "同一屏两种语言" figure is the strong claim on this card and the least verified one. It is stated as such in objectstack#12231 rather than carried across as fact. ⛔ Whoever eventually implements this should not quote the 12-string count forward as a verified impact figure without a browser read.

    prio:p2 retained.


    Generated by Claude Code

  7. added and removed on Sep 9, 2026
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

    metadataDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions