Repository navigation
Every screen flow is English-only in a Chinese UI — TranslationData.flows exists and all four bundles omit it #1210
Description
Activity
- addedbugSomething isn't workingSomething isn't workingmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfaces
on Aug 20, 2026 - addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Aug 23, 2026 Claim: PM loop round 1 (hotcrm seat, session-型执行 PM)
Session:session_61f28fb9-8e31-4a6f-b4d8-10f749a092d2
Branch:claude/issue-1210-screen-flow-i18n
Worktree:hotcrm-issue-1210
Domain: (hotcrm has nodomain:*taxonomy — repo-wide seat)
File surface:src/translations/*.ts(4 locale bundles); read-only onsrc/flows/*.flow.tsto source screen/field ids (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus
Clause-②: no — app-side metadata only; no contract accept/reject behaviour change and no public-surface widening.
Serial constraints cleared: none — no in-flight claim touchessrc/translations/. Adjacent upcoming work: #1209/#1207 onsrc/pages/lead_detail.page.ts, disjoint.Authorization — maintainer direct-dispatch channel. This card was not routed by triage; the maintainer commanded it in session, verbatim (未译, per the seat protocol):
你测试出的问题,使用 epic 项目管理技能负责派发处理
他下班了,你继续
The second line releases the round-mutual-exclusion hold that the
os-samseat's live claim on #856 would otherwise impose.Premise re-verified against
origin/main@a0bc5e8dbefore dispatch — see the dispatch note below.OS-DEV-REPORT — #1210
{ "issue": 1210, "status": "done", "branch": "claude/issue-1210-screen-flow-i18n", "pr": null, "premise_still_valid": false, "summary": "Half the premise holds and half is dead. The carrier exists exactly as described — @objectstack/spec@17.1.0's TranslationDataSchema.flows accepts flows.<flow_name>.screens.<node_id>.title / .fields.<field_name>.{label,placeholder}, keyed by Flow.name / FlowNode.id / ScreenFieldConfig.name — but NOTHING READS IT. The spec's own liveness ledger shipped in node_modules (@objectstack/spec/liveness/translation.json) grades the flows row status:'planned', authorWarn:true, authorHint:'No shipped screen-flow runner reads this group yet — until the objectui half of #7646 lands, a type:screen flow renders the strings authored on the flow in every locale', while every sibling group (objects/apps/dashboards/pages/settings/metadataForms) is 'live'. Upstream #7646 delivered only the spec half in Aug 2026 and its report named two follow-ups it deliberately did not file; neither exists as a card, and the blocker on one of them (#7634) closed 2026-08-11. So authoring the four bundle sections today would parse, validate and load, and render nothing — the suggested route is a no-op. No PR opened, branch pushed empty as the write-route probe and left at BASE a0bc5e8d. The platform card is filed upstream as objectstack#11287 with the full measurement and a back-link; hotcrm#1210 should stay OPEN, blocked by it, and the four bundle sections written the day a runner reads them.", "tests": "No repo file was changed, so no gate is owed on a diff; what was run is measurement. (1) MEASUREMENT, foreground, `pnpm exec tsx` in the worktree, real AutomationEngine + installBuiltinNodes running the real src/flows/lead-conversion.flow.ts, with the real zh-CN bundle CARRYING the proposed flows section loaded into a real FileI18nAdapter registered as the 'i18n' service on the engine ctx and locale:'zh-CN' on the execution context — exit 0, verbatim: 'safeParse(zh-CN + flows).success = true' / 'flows.status = planned | flows.authorWarn = true | children.label = planned | children.screens = planned | (sibling control) objects.status = live | dashboards.status = live | pages.status = live' / 'any resolver naming flow/screen? -> NONE' (35 resolve*/translate* exports enumerated from @objectstack/spec/system at runtime) / POSITIVE CONTROL 'translateObject(crm_lead).label = 线索' and 'i18n.t(\"flows.lead_conversion.screens.screen_1.title\",\"zh-CN\") = 转化详情' (the string IS in the tree) / THE MEASUREMENT 'run status = paused, screen.nodeId = screen_1, screen.title = \"Conversion Details\", screen.fields[].label = [\"createOpportunity: Create Opportunity?\",\"opportunityName: Opportunity Name\",\"opportunityAmount: Opportunity Amount\"]'. (2) `pnpm lint:i18n-gate` on the unchanged tree, EXIT captured before any pipe = 0, gate's own verdict line: '✓ i18n lint gate: 0 `i18n/missing-*` issues (91 total lint issue(s) reported, unaffected by this gate)' — the CLI's COVERAGE_SOURCE taxonomy has no flow/screen bucket, so the gate is green because the surface is invisible to it. (3) ABLATION, trap-guarded restore, direction PREDICTED as 'one new authorWarn advisory' and MEASURED as 'no change at all' — falsified: injected a flows: section into src/translations/zh-CN.ts, confirmed on disk anchored on the injected text ('injected-marker count: 1', 'flows-key count: 1', non-zero required or readings void), ran `objectstack lint --json` → exit 0, 'total issues: 91 | errors: 0 | warnings: 81 | liveness findings naming flows: 0 | i18n/missing-* count: 0', identical to the 91 of the unmutated baseline; trap restored the file and `git status --porcelain` is empty. Root cause of that miss traced and filed as objectstack#11288. No build/dist involved (nothing was rebuilt; the ablation is over source read by the CLI, not over a dist artifact). NOTE: the shared verify lock could not be used — `os-verify-lock.sh --status` on this host prints 'CANNOT OPERATE THIS LOCK ... a stock macOS does not ship flock'; all runs were foreground and single, under NODE_OPTIONS=--max-old-space-size=4096.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack#11287: nothing reads TranslationData.flows — the runner half #7646 deferred; this is the platform card hotcrm#1210 is blocked by (spec resolver helper + console/objectui screen-dialog application + CLI i18n coverage bucket + ledger flip)", "filed as objectstack#11288: lintLivenessProperties walks stack.translations items as TranslationItems, but that collection is z.array(TranslationBundleSchema) (locale-keyed), so every warned-property lookup misses and the whole translation liveness ledger warns nobody — measured as a zero-delta ablation" ] }
Generated by Claude Code
PM review — ACCEPT the stop. Zero files changed is the correct delivery. →
pm:blocked, unassigned.Blocked-by: objectstack-ai/objectstack#11287
Reviewed against the installed spec package and against GitHub, not against the report.
I verified the load-bearing claim myself
The report's whole case rests on one assertion: the carrier exists but nothing reads it. That assertion turns a "just author four bundle sections" card into a no-op and generates two upstream platform cards, so taking the dev's word for it was not an option.
Read directly from the shipped
@objectstack/spec@17.1.0in this repo'snode_modules—liveness/translation.json:props.flows status=planned authorWarn=true props.flows.children.label status=planned props.flows.children.screens status=planned props.objects status=live ← sibling control props.dashboards status=live ← sibling control props.pages status=live ← sibling controlprops.flows.authorHint, verbatim from the package:No shipped screen-flow runner reads this group yet — until the objectui half of #7646 lands, a
type: 'screen'flow renders the strings authored on the flow (config.title,fields[].label,fields[].placeholder) in every locale.The only flow-named resolver anywhere in the shipped dist is
resolveFlowNodeExpressions, which resolves node expressions, not translations. Confirmed.So authoring
flows:in the four bundles today would parse, validate, load — and render nothing. The route this card's own body suggested is dead, and the dev was right to refuse to walk it. A PR here would have shipped four locales of dead metadata plus a passing i18n gate, which is worse than the current honest English: it would have looked fixed.The ablation is the part that makes this trustworthy
The dev predicted its injected
flows:section would raise "one new authorWarn advisory", measured "no change at all" (91 lint issues before and after, 0 findings naming flows), and filed the miss as objectstack#11288 rather than quietly moving on. A stated prediction that gets falsified and reported is the shape of an honest measurement; the same number arrived at without a prediction would have proved nothing.Disposition
pm:dispatched→pm:blocked, assignee cleared,Blocked-by:line above is the machine-readable exit. This card returns to the queue when #11287 closes.- This card stays open. The user-visible defect is real — a zh-CN session still reads "Conversion Details" / "Submit". Only the fix location moved upstream, which is this repo's charter working as intended: 「平台相关的功能应该在平台中实现」.
- Both upstream cards verified on GitHub: open, unassigned, no pm-state labels (awaiting triage), and #11287 back-links here.
- Branch
claude/issue-1210-screen-flow-i18nwas pushed empty as the write-route probe and left at BASE. No PR, nothing to clean up.
PM note against myself
My dispatch words called the carrier's existence and its shape a mechanism assumption and invited falsification — that part worked. But I also wrote a suggested route that presumed the carrier was live, and I had not checked the liveness ledger before dispatching. The ledger is in
node_modules; it cost one file read. Next dispatch that turns on "the spec has a slot for this" reads the liveness row first.- added and removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Aug 23, 2026 huangyiirene commented
on Aug 25, 2026 CollaboratorMore actionsCorroboration from a second, independent encounter — and this card's blast radius just grew by one string
Recording rather than filing: #1207's dev independently hit this gap while shipping the duplicate-conversion warning (PR #1286, landed
413d9640), and reported it as an out-of-scope finding for the PM to file. It is not a new card — it is this one, so the evidence is being attached here instead.Its framing was slightly narrower than the truth, and the difference is worth keeping: it read the gap as "flows carry no entry in the locale packs and the i18n extractor never walks them." Both halves are true, but the measurement on this card is stronger and explains why — the carrier exists and is graded
planned:@objectstack/spec@17.1.0 liveness/translation.json props.flows status=planned authorWarn=true props.objects / dashboards / pages status=live ← sibling controls⇒ "no locale channel" would suggest authoring one. The measured truth is that authoring it renders nothing, which is why this card is blocked rather than queued, and why a well-meaning fix here would have been worse than the honest English.
⚠️ The concrete cost, now one string largerPR #1286 added a per-run warning on the lead-conversion screen naming the record a suspected duplicate repeats. It ships English-only, like every other user-visible string in
src/flows/— and it lands on exactly the surface this card is about:- A zh-CN seller now sees a Chinese record page, a Chinese banner (that copy is localised —
record:alertresolves throughpickLocalized, so the four locales ship inline)… - …and then an English sentence on the conversion dialog, immediately below it.
The contrast is sharper than before this card was filed, because #1207 localised the surface next to the flow. That is not an argument against #1286 — the warning is worth having in any language, and the alternative was shipping nothing — but it is a measured increase in what #11287 is holding up, and it belongs on the record.
State unchanged
pm:blockedon objectstack#11287 stands, and the block is correct: the route this card's own body suggested is dead, measured twice now. ⛔ Nothing to do here until the runner half lands. The four bundle sections get written the day something reads them.⚠️ For whoever unblocks it: the scope is now six screen-bearing surfaces to cover, not five —lead_conversion's screen gained a conditionaldescriptionin PR #1286, and that string needs a slot too.
Generated by Claude Code
- A zh-CN seller now sees a Chinese record page, a Chinese banner (that copy is localised —
半状态修复(R5 开轮巡查)—— 上游卡已关闭,但仍然阻塞。且这一张差点骗过探针。
⚠️ 本条取代本卡正文里的Blocked-by: objectstack#11287。 那张卡 2026-08-24T00:03Z 已 closedcompleted,由 PR objectstack#11484(feat(spec): FLOW_SCREEN_COPY_KEYS resolver family for screen-flow copy(#7646 recommendation B))合并关闭。⇒ 按
Blocked-by:的字面读法,本卡现在「已解除阻塞」。两层都不成立。第一层:关闭它的 PR 只交付了那张卡四项范围里的第一项
objectstack#11287 的
## Suggested scope是四项:FLOW_SCREEN_COPY_KEYS解析器族 ← PR #11484 只做了这一项- runner 应用(server 端本地化
ScreenSpec,或 console/objectui 屏幕对话框端) @objectstack/clii18n coverage/extract 加 flow/screen 桶- 把
flowsliveness 行翻成live、去掉authorWarn
本卡要的是第 2 项。 没有 runner 端的读取方,一份
flows译文写下去仍然一个字都不会上屏 —— 那正是本卡当初立卡的原因。⇒ 上游completed是对它自己第一项的完成,不是对本卡前提的解除。这与 #510 是同一形状(那张的阻塞卡被一个 lint PR 关掉)。第二层:连第一项也没有发布,而朴素的 grep 会说它发布了
@objectstack/spec 最高已发布版本 → 17.2.0(2026-08-23T07:00:38Z) objectstack#11484 合并时间 → 2026-08-24T00:03Z ← 晚 1 天拆开已发布的
spec@17.2.0tarball 亲验:控制词 PAGE_COMPONENT_COPY_KEYS 34 · resolveViewLabel 85 · translateObject 51 目标词 FLOW_SCREEN_COPY_KEYS 1 ← ⚠️ 非零 translateFlow 0 resolveFlowScreenTitle 0⚠️ FLOW_SCREEN_COPY_KEYS有 1 次命中 —— 而那一次命中是一句「它没被做进来」。 命中位于liveness/translation.json:80的一条 ledger 注释里,逐字:…the resolver-side helper (a
FLOW_SCREEN_COPY_KEYSsibling ofPAGE_COMPONENT_COPY_KEYSinpackages/spec/src/system/i18n-resolver.ts) is deliberately NOT in this change — #7634 was in flight on that file.同一份 ledger 里
flows行仍是status: planned。⭐ 由此得到一条新机制,值得写进座位贴
零命中纪律有一面镜子:非零命中同样不构成结论 —— 一个词可以出现在一句否认它的散文里。
本席一直执行的是「零结果在控制词证明搜到代码之前不算发现」。这一次探针给出 1,若就此收工,结论会是「解析器已发布 ⇒ 可以解锁」,而真相恰好相反。⇒ 判据必须是看命中的上下文,不是看命中的条数。
Restart-when:(取代Blocked-by:)两个条件同时成立:
- 发布一个高于
17.2.0的@objectstack/spec,其dist/里导出(不是散文提及)FLOW_SCREEN_COPY_KEYS与translateFlow/resolveFlowScreenTitle一族;且 - 存在一个已发布的 runner 端读取方(console 的屏幕流对话框,或 service-automation 在上线前本地化
ScreenSpec)。
判据 2 的最省事读法:
liveness/translation.json里flows行翻成status: live并带上 evidence 指针 —— 那正是上游第 4 项的定义,而第 4 项按其自身措辞要等 runner 落地才做。⛔ 复核时必须读命中的上下文,不能只数命中条数(见上)。⛔ 更不得以 objectstack#11287 的 issue 状态为准 —— 它已经是 closed 了,而本卡照旧阻塞。
状态维持
pm:blocked。
Generated by Claude Code
阻塞复核(平台 pin 移到 17.3.0 后的定期重扫)—— 条件 1 已成立,条件 2 未成立 ⇒ 维持
pm:blockedrepo:hotcrmPM 席 · sessionsession_018xtjdpZFjgWh4Ad9Wcx68J· R58。
⚠️ 本次重扫的起因:本轮发现 #1206(prio:p0)的解锁谓词其实早已满足却无人求值,躺了 13 天。⇒ 本席现对所有pm:blocked卡逐张复核Restart-when:。本卡是其中一张。R5 写死的两个条件,逐条求值
条件 1 —— 已发布的
@objectstack/spec> 17.2.0,其dist/导出(不是散文提及)FLOW_SCREEN_COPY_KEYS一族⭐ 成立(相对上次复核已翻转)。 在
objectstack-ai/objectstackorigin/main(53cbad9)上:packages/spec/src/system/i18n-resolver.ts:2932 export const FLOW_SCREEN_COPY_KEYS = ['title'] as const; :2934 export type FlowScreenCopyKey = typeof FLOW_SCREEN_COPY_KEYS[number]; :2979 for (const key of FLOW_SCREEN_COPY_KEYS) { …⇒ 这是真正的
export,不是 R5 那次抓到的、藏在 ledger 注释里说「它没被做进来」的那条散文命中。⭐ R5 立的规矩(看命中的上下文,不看命中的条数)这次给出了相反方向的答案 —— 同一条规矩,这次证明它真的在了。 本仓现 pin 17.3.0 > 17.2.0,版本半边亦成立。条件 2 —— 存在已发布的 runner 端读取方(最省事读法:
flowsliveness 行翻成status: live)⛔ 不成立。
packages/spec/liveness/translation.json:95-98 "flows": { "status": "planned", "verifiedAt": "2026-08-11", "authorWarn": true,⭐ 而且这个否定是不对称地强的:上面读的是
origin/main,它领先于任何已发布版本。⇒ 若连 main 都还把flows判为planned,那么没有任何已发布版本会把它判为live。⛔ 因此不需要去解包 17.3.0 的产物来确认 —— 结论在源头就定了。结论
维持
pm:blocked。 剩余缺口现在恰好只有一件事:runner 端的读取方(console 的屏幕流对话框,或 service-automation 在上线前本地化ScreenSpec),以及随之而来的 ledger 翻牌。⭐ 下次复核因此变得极便宜:只需读
packages/spec/liveness/translation.json的flows行是否为live。⛔ 条件 1 已成立,不必再查。Restart-when:更新为:Restart-when: `packages/spec/liveness/translation.json` 的 `flows` 行 status 变为 `live` 且带 evidence 指针(该行按上游自己的措辞要等 runner 落地才翻)。 ⛔ 条件 1(FLOW_SCREEN_COPY_KEYS 导出)已于 2026-09-06 复核成立,不必再查。 ⛔ 仍不得以 objectstack#11287 的 issue 状态为准。未变
用户可见缺陷依旧真实:zh-CN 会话仍读到 "Conversion Details" / "Submit"。
⚠️ 覆盖面仍是 六个载屏表面(#1207 的 PR #1286 给lead_conversion加了条件性description)。⛔ 在 runner 落地前写译文仍是无效功 —— 那正是本卡当初被阻塞而不是入队的原因。
Generated by Claude Code
📌 两条新事实(来自 #1214 第 4 项的 dev,R64)—— ⛔ 卡仍
pm:blocked,但其中一条会改变将来的修法repo:hotcrmPM 席 · sessionsession_018xtjdpZFjgWh4Ad9Wcx68J。#1214 第 4 项的 dev 独立测到了与本卡相关的东西,顺手记在这里,免得将来重测。① 独立佐证:gate 结构上看不见漏译,所以「verify 全绿」永远不能当作本卡已修的证据
authorWarnedProperties('translation')精确返回['flows']⇒ 需求侧的i18n/missing-flow被刻意压制。该 dev 的改动前后lint:i18n-gate都报0 issues,而它本来就抓不到。- 现在若去四个语言包里写
flows.*键,会存下一批没有任何已发布运行时会读的字符串,并触发liveness-planned-property。 - ⭐ 新事实:该 dev 测到 console bundle 完全不解析 flow 的翻译键 —— 本卡此前记录的是「carrier ships and is unused」(载体已发布但没人用),这条把它收紧成「消费端也确实没有」。
⇒ 本卡的两个条件仍然是:(1) resolver 家族导出 —— 已在 R6x 测得成立;(2)
packages/spec/liveness/translation.json的flows行从planned翻成 live —— 仍未成立。⛔ 维持pm:blocked,不改状态。②
⚠️ 新约束:本卡表里点名的那个字段,标签已经不再是一个字面字符串了本卡的表里
quote_generation一行写着Discount %。PR #1711(已入队)把它改成了插值:label 与 placeholder 都由
QUOTE_DISCOUNT_CEILING插值而成(该常量同时被两条校验规则引用),⇒ 渲染出来是Discount % (≤ 60),而字面 60 在 flow 文件里一次都没出现。⇒ 将来修本卡的人会撞上一个本卡原文没有预料到的问题:一个由常量插值出来的标签,要怎么进翻译包? 直接把
Discount % (≤ 60)抄进 zh-CN,就等于把上限的字面值复制到了第四个地方,而 PR #1711 的全部意义正是让它只存在于_thresholds.ts一处 —— ⛔ 那会把一个刚修好的漂移源原样重建在翻译层。⭐ 记在这里作为修法约束,不作裁决:本卡解冻时,这一条必须先有答案(翻译包是否支持插值 / 是否改用占位符参数 / 还是这一个字段例外),否则修本卡会撤销 #1711。
⚠️ 本席未复核上面任何一条 —— 它们是 #1214 第 4 项 dev 的实测,原样转记,来源写明。
Generated by Claude Code
Half-state repair — R61 board audit (
repo:hotcrmseat,session_0132iDHq4FLW2zS9VPXqbf9f, 2026-09-11T01:4xZ)⚠️ State corrected:pm:blocked→pm:on-hold(single replace; read-back confirmsbug, metadata, pm:on-hold, assignee empty).This card's exit condition stopped being an upstream card on 2026-08-26: comment
5421140725found objectstack#11287 closed while the constraint still bound, and explicitly replacedBlocked-by:with aRestart-when:. Comment5558911919then updated that predicate. So for two weeks the card has carried aRestart-when:predicate under apm:blockedlabel — the two halves of the state machine disagreeing.Per the state model,
pm:blocked+Blocked-by:waits on a card; a predicate that waits on a published artifact ispm:on-hold+Restart-when:. Now aligned, with the predicate canonical in the body:Restart-when: `packages/spec/liveness/translation.json` 的 `flows` 行 status 变为 `live`⛔ The hold is unchanged in substance — this is a spelling correction to the board, not a re-decision.
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsrepo:hotcrmseat,session_01X8U3asekbiC7yWoEPWR4Dg· stock re-triage group 2 (maintainer-confirmed ten-card group; maintainer reply verbatim: 「同意。」) · 2026-09-24T23:25Zpm:on-hold→pm:blocked,Blocked-by: objectstack-ai/objectui#5920Re-pointed at the real carrier: objectui#5920 (Screen-flow runner applies the flows translation overlay — the runner half of objectstack#11287), OPEN. Earlier seats could not reach objectui, so they never saw it. The old
Restart-when:read the objectstack source tree's liveness ledger, which is merge-level; this lane's unlock criterion is install-level.Measured today: no
flows:section in anysrc/*/translations/**pack; the installed@objectstack/spec@17.4.0liveness/translation.jsonflowsrow is stillplanned; objectuiFlowRunner.tsx:325rendersscreen.titledirectly. Writing translations here now would have no reader.On unlock (objectui#5920 closes AND the console this repo installs carries it AND the installed spec's
flowsrow readslive): translate the 5 screen flows, including #1724'sClose Datefield. The interpolatedDiscount % (≤ N)label from #1711 needs a placeholder-parameter answer before it can be translated.
Generated by Claude Code
Blocked-by: objectstack-ai/objectui#5920
Every screen flow is English-only in a Chinese UI —
TranslationData.flowsexists and all four bundles omit itWhat a zh-CN user sees
Convert a lead in a fully Chinese session — Chinese nav, Chinese record page, Chinese confirm dialog (
确认要转化此线索吗?) — and then the flow's own screen opens:Same for the other screen-bearing flows:
lead_conversionquote_generationescalate_caseschedule_followupcampaign_enrollmentThe completion toast is
Done.These are not incidental surfaces. Lead conversion and quote generation are the two moments the whole sales story runs through, and both drop the user into English at exactly that point. The contrast is sharp because the surface next to them is translated: the
更新阶段bulk action renders更新阶段 / 新阶段 / 取消 / 确认in Chinese, because actions have a translation section and flows do not.The bundles simply omit the section
@objectstack/spec'sTranslationDataSchemaalready carries it:and none of the four bundles has one:
so the authored English in
src/flows/*.flow.ts(label: 'Conversion Details',label: 'Quote Details',label: 'Escalate Case', …) is what every locale renders. Five flows declaretype: 'screen'nodes:lead-conversion,quote-generation,case-actions,schedule-followup,campaign-enrollment.Unlike #1185 (notify-node localisation, which is genuinely blocked on a platform template channel), nothing blocks this one — the carrier ships and is unused.
Suggested
Add a
flows:section to all four bundles covering the five screen flows: flowlabel, each screen'stitle, and each screen field'slabel(plusplaceholderwhere one is authored). A coverage check that fails when a flow declares a screen node with no bundle entry would keep the sixth flow from shipping English by default.Found by driving the app as a seller in a browser (zh-CN session) against a fresh
pnpm devon17.1.0.Generated by Claude Code