Skip to content

i18n: _views.VIEW.description is declared, walked and counted as covered but never rendered — plus three surfaces a bundle cannot address at all #16735

Description

@hotlong

Filed from an app repo (objectstack-ai/hotclm, @objectstack/* 17.3.0, @objectstack/console as shipped) after translating it end to end into en + zh-CN — objectstack-ai/hotclm#32, PR objectstack-ai/hotclm#33. ⛔ Unassigned, ungraded; routing is triage's.

Four findings, filed together because they are one seam — the gap between what the i18n coverage walk addresses and what the console renders. Finding 1 is the serious one and would justify its own card; the other three are the same seam in its benign direction (nothing to address, so nothing claims coverage). Split if triage prefers.

Provenance. All four were measured in a browser by the card-11 implementation run, each with a stated repro. I reviewed the evidence and the reasoning and merged the PR on it; I did not personally re-drive these four, and I say so rather than implying otherwise. (I did independently re-measure a fifth, unrelated finding from the same run and found it inverted — see #16695 — so these are not offered as unexamined.)

Dedup: not possible from that session. search_issues returned total_count: 0 for the app's own repo even on a control term certainly present in an open issue's title, so the index was not answering. The run correctly reported these rather than filing them blind, and I am filing them with that caveat attached rather than claiming a duplicate check I did not get.


Finding 1 — a key that makes the coverage report read 100% while the screen stays English

objects.OBJECT._views.VIEW.description is:

  • declared by ObjectTranslationDataSchema,
  • walked by the coverage extractor (it is one of the keys os i18n check expects),
  • authorable, and counted as covered once authored,
  • and never read by the console.

Two legs in one session, zh-CN:

authored as tab strip (the view label, one key over in the same group) the description line under the header
bundle entry 我发起的 ✅ Contracts I launched, grouped by where each one has got to. ❌
inline { en, 'zh-CN' } map 我发起的 ✅ 我发起的合同,按各自进行到哪一步分组。 ✅

The label resolving from the same group is the control: the bundle is loaded, the object and view are addressed correctly, and the neighbouring key works. Only description is dropped on the floor.

Why this is worse than an untranslated string. It is a false negative in the platform's own gate. An app can author every declared key, watch os i18n check report zero missing, ship, and still have English descriptions on every list view in a zh-CN console — with no signal anywhere. The whole value of a coverage gate is that its zero means something, and this key makes the zero lie.

15 view descriptions in that app are affected. It worked around them with inline maps and kept the bundle entries, so a fixed resolver wins over the map and the duplication unwinds itself — but the next app will not know to do that.


Finding 2 — pages.NAME.components.* addresses nothing on a kind: 'slotted' page, and a tab label has no key on any page kind

let n = 0;
walkAddressedPageComponents(ContractDetailPage, (c) => (n++, c));   // -> 0

against 21 authored components and tab labels (@objectstack/spec 17.3.0). walkAddressedPageComponents is the traversal translatePage itself runs, so this is unresolvable rather than merely ungated.

Two independent causes, either sufficient on its own:

  • its roots are regions[].components[], and a slotted page authors slots;
  • its descent is properties.children, and a page:tabs carries properties.items[].children.

⇒ pages.contract_detail carries exactly two addressable keys, label and description, while 7 tab labels, 7 path stages, 7 related-list titles and 1 block label cannot be reached from a bundle at all. Worked around with 22 inline locale maps.

Finding 3 — dashboards.NAME has no group for globalFilters

The group is label · description · actions · widgets. A dashboard's global-filter labels and their option labels are not addressable — and the filter bar draws directly above the widget titles the bundle does translate, so the mixed-language result is maximally visible:

类别: 全部        Requesting Department: 全部
[six Chinese widget titles below]

5 filter labels + 29 option labels in that app. Worked around with inline maps.

Finding 4 — chart category labels from select options ignore the bundle on same-object dimensions, and there is no workaround

On dataset-backed charts, category and series labels drawn from a select field's option values render the inline English option label, while the identical values render the translation in the list grid on the same console.

The correlation is exact and is the useful part:

dataset dimension declared as renders
contract_metrics.direction field: 'direction' (same object) Purchase / Sales ❌
payment_metrics.direction field: 'contract.direction' (dotted, cross-object) 采购 / 销售 ✅

Same option values, same console, same session. Also affects a status funnel (Draft/Submitted/In Review/…) and a currency_code bar (EUR — Euro).

A widget has no authoring surface for category labels, so unlike 1–3 this one cannot be worked around. It is the residue left visible on two of that app's three dashboards.


Also worth knowing, not a finding

  • authorWarnedTranslationGroups() returns exactly ['flows'] on spec 17.3.0, so flow screen copy is skipped by the extractor by design and is not translatable today. An app cannot gate on it; ours states that rather than trimming its requirement to fit.
  • The coverage extractor emits objects.OBJECT._actions.* twice (once via config.actions, once via the object), so os lint reported 793 i18n/missing-* rows for 752 distinct keys. Cosmetic double-count.
  • In a zh-CN console the Studio metadata-type list renders Dataset untranslated while every sibling type is translated (对象/视图/操作/仪表板/流程/页面/岗位/权限集/钩子). That is the metadataForm baseline the platform packages own, not an app string.

Boundaries

  • One app, one console build, one dialect. Re-confirm before acting.
  • ⛔ Nothing was patched in the platform; findings 1–3 are worked around with inline { en, 'zh-CN' } maps documented at each site, and finding 4 is left visible.
  • Finding 1 is the one with a gate consequence. If only one of these is picked up, it should be that one.

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:本卡拆为三张,本卡保留发现 1

    repo:objectui / Bug / priority:p2 / pm:queue

    上报方写「Split if triage prefers」并指出发现 1 是唯一带闸门后果的那个。采纳拆分,刀口按修复落在哪个仓/哪个包划,不按「严重 vs 良性」划:

    发现 去处 车道 级别
    1 —— _views.VIEW.description 声明了、走查了、算作已覆盖,却从不渲染 本卡 repo:objectui p2
    2 + 3 —— 两个 bundle 根本寻址不到的面(slotted 页 slots / tabs items[].children;dashboard 无 globalFilters 组) #16772 domain:spec p2
    4 —— 图表分类标签取 select 选项时忽略 bundle #16773 domain:services p3

    ⛔ 三张互不吞并;发现 1 与 4 都能被看成「控制台没翻译」,但它们的修复面在不同仓的不同包,合成一张就一定会变成一个跨仓 PR。


    ⭐ 域判定:发现 1 是 objectui,不是 spec —— 有两条独立证据

    上报把发现 1 描述为「declared / walked / counted / never read by the console」。读代码之后,packages/spec 这一侧是完整的:

    packages/spec/src/system/i18n-resolver.ts:12   *   objects.<object>._views.<view_key>.label
    packages/spec/src/system/i18n-resolver.ts:13   *   objects.<object>._views.<view_key>.description
    :412   const candidate = data?.objects?.[objectName]?._views?.[key]?.label;
    :433         data?.objects?.[objectName]?._views?.[key]?.description;
    

    ⇒ description 不只是被声明,它在 spec 里是被真正解析出来的(:433),和上报当作对照的 label(:412)在同一个解析器里、隔了二十行。⇒ 掉在地上的位置在 spec 下游。

    下游是谁:

    packages/console/package.json
      "description": "Prebuilt Console SPA pinned to this framework release …
                      Source of truth: @object-ui/console (https://github.com/objectstack-ai/objectui)."
      files: ["dist", "README.md", "CHANGELOG.md"]
    

    该目录在 origin/main 上共 4 个文件,无任何源码。⭐ 而且本仓的类型检查闸门自己也这么记着:

    scripts/check-type-check-coverage.mjs  const EXEMPT = {
      '@objectstack/console':
        'Published objectui build artifact -- package.json/README/CHANGELOG plus a dist/ … 
         No TypeScript sources, no tsconfig; the sources are type-checked in the objectui repo.'
    

    ⇒ 两条独立证据同向:spec 侧解析完好,且 console 在本仓只是一个发布壳、源码在 objectui。⇒ repo:objectui。

    ⚠️ 本席没有去 objectui 仓定位那个渲染点 —— 这是本席读数的边界。认领席在 objectui 侧从「view 的 label 走的是哪条路、description 为什么没跟着走」开始,上报给的同组对照(同一个 view,label ✅ / description ❌)就是现成的最强定位线索。

    等级 p2

    上报把「更坏之处」说得很准,本席复核后原样保留并加权:

    It is a false negative in the platform's own gate. An app can author every declared key, watch os i18n check report zero missing, ship, and still have English descriptions on every list view in a zh-CN console — with no signal anywhere.

    ⇒ 这与本席在 #16630 上处理的形状同类(✓ Server is ready 打在一次降级启动上):一个不可能失败的读数,与一个通过了的读数无法区分。那张卡定 p2,本卡按同一把尺子也定 p2。

    • 不到 p1:无运行时失败、无鉴权面、无数据面;且有绕法(内联 { en, 'zh-CN' } map),上报应用已用它顶住 15 条 view description。
    • 高于 p3:受害的是平台自己的覆盖闸门的可信度,不是一条漏译。⇒ 每一个后来的应用都会踩,而且踩了不知道。

    ⭐ 上报还留了一条对认领席很有价值的事实:那个应用内联 map 与 bundle 条目都保留着,所以解析器一修好,bundle 就赢过 map,重复自动解开。⇒ 修复不需要应用侧配合回滚,但下一个应用不会知道要这么写 —— 这也是该修而不是该写进文档的理由。

    交给认领席

    • ⚠️ 溯源边界,原样转达:四个发现都由报告应用的实现轮次在浏览器里测得,上报方明说「I did not personally re-drive these four」。同一轮次的第五个发现被独立复测后结论反转(record:details: the #13855 sections[].group reference form crashes the renderer, and the enumerated fields form renders raw field keys instead of declared labels #16695)—— 所以这一轮的产出既不是未经审视的,也不是已被确认的。认领时先自己复现发现 1,一个 view + 一条 bundle description 即可。
    • ⛔ 去重没做成:报告会话里 search_issues 对该应用自己的仓返回 total_count: 0,连一个确定存在于某开门 issue 标题里的对照词也返回 0 ⇒ 索引没在应答。上报方正确地选择了带着这个说明上报而不是盲报。认领时重跑去重。
    • 一个应用、一个 console 构建、一种方言。

    ⛔ 未立卡的三条(上报列在「Also worth knowing, not a finding」),本席同意不立卡,记录在此以免丢失

    1. authorWarnedTranslationGroups() 在 spec 17.3.0 上恰好返回 ['flows'] ⇒ flow 屏幕文案按设计被抽取器跳过、今天不可翻译。应用无法对它设闸。这是既定设计,不是缺陷。
    2. 覆盖抽取器把 objects.OBJECT._actions.* 发两遍(一次经 config.actions,一次经对象),于是 os lint 对 752 个不同的键报了 793 行 i18n/missing-*。纯计数重复,无功能后果。
    3. zh-CN 控制台里 Studio 元数据类型列表中 Dataset 未翻译,而九个同级类型全部已翻译。上报判定这属于平台包拥有的 metadataForm 基线、不是应用字符串 —— 本席同意这个判定。⚠️ 这一条是可立卡的(形状清楚、单点);本席按上报方的划界不立,若有人认为该立,直接照本条另开一张 domain:spec 的小卡即可。

    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

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

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions