Repository navigation
docs(i18n): the i18n standard page teaches two incompatible bundle-key conventions, and declares one of them the only one #17768
Description
Activity
Claim: PM loop round 1 (wave 4)
Session:session_017ef78bLdybu3AffehKkhfk
Branch:claude/issue-17768-i18n-bundle-key-convention
Worktree:objectstack-issue-17768
Domain:domain:devx
File surface:content/docs/protocol/kernel/i18n-standard.mdx(stop on breach; explain in the report)
Container & model:S,mode:subagent,model: opus(default judgement tier — no path-derived mandate; deciding which of two taught conventions is the real one is a judgement the card does not settle)
Clause-②: no
Thread-read: the card carries 1 comment; read to its end first
Serial constraints cleared: the page is FREE — no queued PR and no opendomain:devxclaim declares it; the other two cards in this wave touch different pages under the same directory, verified disjoint by path.⚠️ Its lastorigin/maintouch is15805ea32, which is AFTER the card's measurement ⇒ the card's line numbers are STALE by construction and are hints only.bash scripts/pm/os-verify-lock.sh --statusreadlock is free · queue empty.readings taken 2026-09-14T12:01Z
Generated by Claude Code
os-dev-report
{ "issue": 17768, "status": "done", "branch": "claude/issue-17768-i18n-bundle-key-convention", "pr": "https://github.com/objectstack-ai/objectstack/pull/18189", "premise_still_valid": true, "premise_R": "BRANCH 1 — all four named consumers agree on object-first, POSITION-derived keys; the flat dialect is read by nothing. Readings, each with its control: (1) resolvers — packages/spec/src/system/i18n-resolver.ts builds objects.OBJECT.fields.FIELD.label from the object/field NAMES (objectFieldLabelKey, objectLabelKey, viewTranslationKey); the authored string is only the fallback. (2) os i18n extract — collectExpectedEntries pushes segment arrays, pushViewEntries emits ['objects',obj,'_views',view,'label']; the label VALUE becomes sourceValue, never the key. (3) os i18n check — computes no expected set of its own: packages/cli/src/utils/i18n-coverage.ts imports the identical collectExpectedEntries. (4) Studio editor — its door is TranslationItemSchema, the same closed object-first shape plus locale. FIFTH, the card's explicit open question: objectui measured at clone cfcc17d9dd04ebb80c092cacb4f4034425f51b29 — its label reads are objects.{name}.label / objects.{obj}.fields.{f}.label / objects.OBJ._views.VIEW.label / objects.OBJ._sections.KEY.label, and translateLabel is NOT a bundle lookup at all: it is module-private to packages/components/src/renderers/layout/containers.tsx:279 and maps eight fixed English tokens into the objectui UI pack (KNOWN_LABEL_DICT / KNOWN_LABEL_KEYS), leaving anything else untouched. CONTROLS: schema door — the page's three flat bundles through TranslationDataSchema give unrecognized_keys ['label','pluralLabel','fields','industries'] / ['list','actions'] / the twelve file-format keys, while the page's own object-first example PASSES (firing control). Loader door — FileI18nAdapter over a dir holding both layouts loads only en.json, t('objects.account.label')='Account' (control fires) while t('account.fields.name')='account.fields.name', the key returned unchanged; the documented i18n/en/account.json registers no locale. NOTE the ruling's conditional: under branch 1 the 'exactly one shape' sentence is KEPT and made checkable; what made it wrong was the page teaching a second shape beside it, so the repair removes the flat dialect and records what it was.", "summary": "Docs-only repair of content/docs/protocol/kernel/i18n-standard.mdx. The page taught a flat NAMESPACE.PROP bundle key (label: 'account.fields.name' paired with i18n/en/account.json) beside the normative object-first, position-derived convention, and asserted 'The raw label value stored on the metadata is the translation key'. Measurement selected premise-R branch 1, so the flat dialect is corrected across every section that taught it (Directory Structure, Translation File Format, Basic/Pluralization/Context-Based Translation, Using Plugin Translations, ObjectQL Integration, ObjectUI Integration, Best Practices 1-3), a Callout records what it was, and the uniqueness sentence is kept and made checkable. Sections were located by CONTENT, not by the card's line numbers, which are stale by construction (page's last origin/main touch 15805ea32 is later than the card's aaacf1d5c5): heading sweep plus a residue sweep for i18n/LOCALE/NAMESPACE.json shapes and quoted dotted keys, every zero backed by a firing control. Nothing landed by #16251 (PR #17772) is contradicted — its Display Label Forms section already states the key 'comes from where the label sits in the metadata, never from the string the author wrote', which is exactly what the ObjectQL/ObjectUI sections violated. Two incidental corrections the rewrite made unavoidable, both measured: _views.list to _views.default (expandViewContainer names an unnamed default list entry 'default'; objectui pins that _views.list resolves nothing), and two messages ids containing a dot made single-segment (messages is a flat record and t() walks the key segment by segment, so a dotted id is unreachable — plugin-audit states the same rule in-repo). No resolver, CLI command or Studio code touched; no code change was needed, so the ruling's stop-condition never fired. The card was dispatched with the assignee already set by the PM; the newest Claim names this branch.", "tests": "Gate sweep DERIVED by the tool, no hand-written path list: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` -> 40 commands; each run with its exit code captured to a file BEFORE any pipe, then reconciled: '✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3).' 40/40 exit 0 at head b7acd8a8a. FIRST sweep had 5 non-zeros, four of them PREREQUISITE not findings: check:doc-formula-expressions / check:doc-security-posture / check:docs-transcript-drift exit 3 on an unbuilt @objectstack/lint ('PREREQUISITE NOT MET ... this is NOT a pass and NOT a finding: nothing was measured'), check:skill-examples exit 1 on an unbuilt @objectstack/client-react dist. The one real finding was check:doc-anchors exit 1 naming a dead fragment #form-1-is-one-locale-s-text introduced by this diff (github-slugger DROPS the apostrophe: the real id is form-1-is-one-locales-text) — fixed in b7acd8a8a, prerequisites built, and the WHOLE sweep re-run. pnpm lint is INAPPLICABLE here and that is a measurement, three pieces: (1) population read from eslint's own config — every block in eslint.config.mjs is extension-scoped to {ts,tsx,mts,cts,js,jsx,mjs,cjs}, no block names .mdx; (2) count read from --format json — 1 file reported, 0 errors, one warning 'File ignored because no matching configuration was supplied'; (3) invariance — type-aware linting is enabled nowhere in this repo (eslint.config.mjs: 'never enables type-aware linting ... for ANY file'), so an edit to a file eslint does not read cannot move any other file's verdict. No package build or test owed: the diff touches no package, so the affected closure is empty. No ablation applies — this is a docs diff with no gate or guard added. Control-byte scan clean (grep -naP over the file exits 1; a planted tab fires the control).", "line_budget": "n/a — no skills/** path in the diff; content/docs/** carries no line ratchet. Page 1149 -> 1195 lines (+46 net).", "files_changed": [ "content/docs/protocol/kernel/i18n-standard.mdx" ], "gates": "40/40 derived families exit 0, reconciled by dispatch-gates --ran with exit codes; CI not awaited (PM owns convergence). Labels: skip-changeset applied via POST /issues/18189/labels and read back; read-back shows ['size/m','skip-changeset'] with nothing stripped (size/m is the size-labeler's, another actor, left alone).", "changeset": "skip-changeset, verified on the publish surface rather than asserted: of 70 non-private packages declaring a files[] array, ZERO list any content or docs path, and apps/docs — the only consumer of content/docs — is private: true.", "deviations": "None. The commit trailer pair is model-free (check:commit-card-trailers confirmed on both pushes), the card relation lives only in the PR body, and Clause-②: no is a bare token at line start with its reasoning on the next line.", "mcp_calls": "4 — all READ-only (issue_read get, issue_read get_comments, search_pull_requests, pull_request_read get_diff). Zero MCP GitHub writes.", "api_writes": "4 — git push (one lane, 3 pushes: empty-branch routing probe, then one per commit); POST /repos/objectstack-ai/objectstack/pulls (draft); POST /repos/objectstack-ai/objectstack/issues/18189/labels; POST /repos/objectstack-ai/objectstack/issues/17768/comments (this report). No PATCH of any body — the PR body read back carrying the platform's appended session-URL footer block, exactly one rule line and one footer, so it was left alone.", "open_questions": [], "out_of_scope_findings": [ "to file (class c, metadata-authoring trap) — a dotted `messages` id is authorable and unresolvable: `messages` is a flat record of id->string and both t() implementations walk the key segment by segment, so `messages: { 'common.save': 'Save' }` saves, validates and resolves to nothing. packages/spec/src/system/translation.zod.ts teaches the trap in TranslationItemSchema's own @example (`messages: { 'common.save': '保存' }`), the doorway an agent copies from; packages/plugins/plugin-audit/src/translations/messages.ts:3-10 already documents the rule privately for its own bundle; nothing rejects a dotted id at either door. Dedupe words: `messages dotted key`, `translation message id segment`, `resolveKey dot walk`, `TranslationItemSchema example`, `single-segment message id`. Carrier: the spec @example is the one an authoring agent reads, so it will keep minting the trap until fixed.", "noted, not filed: the page's Translation Service Integration, Pluralization and locale-switching sections are design intent and already carry warn Callouts saying so. Carrier: none.", "noted, not filed: Best Practices 4 (Test All Locales) shows renderView('account_list', ...), an API this repo does not export; illustrative test prose, not a bundle-key question. Carrier: none." ] }
Generated by Claude Code
复核裁决:ACCEPT — PR #18189
domain:devx执行席session_017ef78bLdybu3AffehKkhfk· R1 波次 4 · 复核读数 2026-09-14T12:33Z · 对 GitHub 与树核验,⛔ 不对报告自述核验前提 R —— 选中分支 1,且它多测了一个本席没要求的消费者
派发令让它先测四个消费者各自 key 在什么约定上。它测了四个,外加第五个(objectui,在一个 pin 住的 clone 上),结论:四者全部一致于 object-first、位置推导的键;扁平方言没有任何东西读它。
本席独立抽验最承重的那一条 —— 解析器到底 key 在哪:
git show origin/main:packages/spec/src/system/i18n-resolver.ts :394 function viewTranslationKey(view: ViewLike, objectName: string): string 发火对照:同文件 export 出现 84 次 ⇒ grep 确实能看见东西⇒ 键是
(view, objectName)的函数,由位置推导,⛔ 不是作者写下的那个 label 字符串。分支 1 成立。⭐ 它的两道门对照做得对:schema 门(三个扁平 bundle 过
TranslationDataSchema得unrecognized_keys,而该页自己的 object-first 例子 PASS —— 发火对照)与 loader 门(FileI18nAdapter只加载en.json,t('objects.account.label')='Account'发火,而t('account.fields.name')原样返回键)。⇒ 两个方向都证了,不是单侧读数。⭐ 它对本席条件裁决的读法是对的,值得点名
本席的裁决写了三支。它选的处置是:保留「exactly one shape」那句话,删掉与之并存的扁平方言。乍看像没执行"停止宣称不存在的唯一性",但它的理由正确:在分支 1 下那句话是真的 —— 让它变假的不是那句话本身,而是页面在它旁边又教了第二套。⇒ 移除第二套之后,唯一性重新为真,并被做成可核的。这是对条件裁决的正确读法,⛔ 不是绕过它。
抽查读数 —— 扁平方言到底删干净没有
⚠️ 本席先用了错的量具,第二次了。 裸数account.fields.name得 3(main 上 4),差点报成"没删干净"。它是正确键objects.account.fields.name.label的子串 —— 计数分不出对错两形。改用锚定模式:label: '<ns>.<prop>' 这一**作者书写形** origin/main 6 PR head 1 ← 仅存的一处在 <Callout type="warn"> 里,引述"退休前是什么样" 发火对照 PR head 上任意 label: ' 出现 30 次 ⇒ 模式确实能命中 i18n/en/account.json(配套的按命名空间 bundle) 3 → 2(余下的同在该 Callout 内)⇒ 教学位置上一处不剩,只在显式的"已退休"告示里留了记录 —— 这正是文档该有的做法(记下它曾经是什么),⛔ 不是遗漏。
⭐ 记一条方法:当正确形把错误形包含为子串时,计数不可能区分二者,必须锚定。本席本班在这上面栽了两次(上一次是
"changes":嵌在payload里),两次都靠换量具才看清。清单结论
draft ✅ · base
main✅ · 首行Fixes #17768✅ · 正文带Clause-②: no✅ · 1 个文件,content/docs/**,零代码 ✅(裁决的停手条件因此从未触发)·skip-changeset判在发布面上(70 个非私有包的files[]零个列 content/docs;apps/docs是private: true)✅ · 治理面命中 0 ✅CI:2026-09-14T12:33Z 去重后 30 个 distinct check,RED 0,6 个仍 in_progress ⇒ ⛔ 本裁决不含放行。
两处顺带更正,本席判在范围内
_views.list→_views.default(expandViewContainer把未命名的默认 list 记作default,objectui 侧 pin 住_views.list解析不到任何东西)与两个带点的messagesid 改成单段。两者都是重写这些段落时无法回避的:留着就等于新写的示例本身是错的。均已申报并各带读数。⭐ 它自己的门禁红是自己引入的,并且自己逮到了
首轮
check:doc-anchorsexit 1,点名一个本 diff 新引入的死锚#form-1-is-one-locale-s-text—— github-slugger 会丢掉撇号,真实 id 是form-1-is-one-locales-text。它修掉、把先决条件建好、整轮 40 个重跑。另有 4 个 exit 3 被正确读作 PREREQUISITE 而非 finding。⇒ exit 3 与真 finding 分得清,处置正确。偏差
deviations: None,本席核对属实。 trailer pair model-free(两次推送都过check:commit-card-trailers)、卡片关系只在 PR 正文、Clause-②: no是行首裸 token 且理由在下一行。out_of_scope_findings3 条,本席逐条判:第 1 条属 (c) 类,本席另立卡承接(卡号追评于下);后两条不属三类(设计意图段已带 warn Callout;renderView是示意性测试散文)⇒ ⛔ 不立卡。⇒ ACCEPT。 放行前置:去重后全部 check 全绿且 RED 0。
Generated by Claude Code
✅ 已落地 ——
14f274cd3onorigin/maindomain:devx执行席session_017ef78bLdybu3AffehKkhfk· 落地读数 2026-09-14T13:18Zgit show origin/main:content/docs/protocol/kernel/i18n-standard.mdx Retired 2 ← 记载"已退休方言"的 Callout,落地前为 0 objects\. 17 ← 发火对照:object-first 键仍是页面主体 main tip 14f274cd3 docs(protocol): key i18n bundles on metadata position, not on the label value (#18189)⇒ 前提 R 的分支 1 已落地:页面只教 object-first、位置推导的约定,扁平方言只在显式的"已退休"告示里留记录。
承接卡 #18190((c) 类:带点的
messagesid 可写、schema 合法、解析为空,而TranslationItemSchema自己的@example在两处教了这个陷阱)。⚠️ 该卡里t()逐段走键那条腿本席未验证,已在卡上标明,⛔ 接手者不要继承成已测事实。
Generated by Claude Code
github-actions commented
on Sep 14, 2026 on Sep 14, 2026 – with GitHub ActionsContributorMore actionsos-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: docs(protocol): key i18n bundles on metadata position, not on the label's value #18189, merged.
- Closing commit
14f274cd3e, merged intomain. - Left untouched:
priority:p2,domain:devx— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34851083408 · trigger
scheduleGenerated by Claude Code
Found while implementing #16251, which states both authorized
I18nLabelforms oncontent/docs/protocol/kernel/i18n-standard.mdx. Filed unassigned and unlabelled —triage owns
domain:*and the grading. This is not a defect in that card's PR:none of the lines below were edited by it.
The gap
Measured on
origin/mainataaacf1d5c5, the same page describes two different,incompatible bundle-key conventions for the same field, and says of one of them that
it is the only one.
### Object-First Convention(:166), at:174:### Orphan Keys and Option Keysthen tabulates those keys asobjects.{object},objects.{object}.fields.{field},objects.{object}._views.{view}, and so on.## ObjectQL Integration(:730) and## ObjectUI Integration(:784) teach theother convention:
with a bundle file
i18n/en/account.jsonholding{ "label": "Account", "fields": { "name": "Account Name" } }— a flat per-namespace file, not the object-firstobjects.account.labelthe same page declares normative. The prose at:780asserts:
Why that sentence is the load-bearing one
packages/spec/src/system/i18n-resolver.tsderives the key from the label'sposition, never from its value:
The page already states that half itself, at
### Locale Fallback(:155): "theinline
label:authored on the metadata is the default locale's text". So onepage carries both "the authored label IS the default locale's text" and "the raw
label value stored on the metadata is the translation key", about the same field.
What was NOT measured
objectui's
translateLabellives in the sibling repo and was not measured here,so this card does not claim the examples fail at runtime — only that the page
contradicts itself and its own normative sections. Whoever picks it up should measure
translateLabelfirst, and then decide which of the two is true: theObjectQL/ObjectUI examples need rewriting to the object-first convention, or the flat
convention is a second live route and the "exactly one shape" sentence is the one
that is wrong.
Why it was left out of the #16251 PR
That decision needs the objectui measurement and would rewrite two sections plus their
code samples — outside a card whose subject is the missing second label form. The
#16251 PR repaired only the one adjacent sentence its own addition made
self-contradictory (
os i18n check"runs in one direction — which keys the metadataexpects that no bundle carries", which is not how a gap on a map-authored prop is
closed).
Related: #16251, #15984, #14749, #5377.