Skip to content

fix(i18n): a Traditional Chinese session reads and edits the Traditional entry of an inline label map - #31068

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-7el3c5
Nov 7, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-7el3c5

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #30910

Clause-②: no
Metadata-protocol: none

Metadata-protocol is none because what an author can write is unchanged: the same InlineLocaleMapSchema keys and values are accepted, and only which of two existing entries a Chinese session reads and edits moves.

维护者速读

  • 改了什么:内联多语言标签同时写了简体和繁体时,繁体中文会话(zh-Hant / zh-TW / zh-HK)现在读到、编辑的都是繁体那一条;简体会话同理只取简体。客户端 pickLocalized(含写入侧 setLocalized / clearLocalized)和 spec 的 resolveI18nLabel 一起改,规则保持一致。
  • 为什么改:之前两边都取作者写下的第一个 zh-* 键,台湾、香港用户看到简体,在 Studio 里保存还会覆盖简体作者的文字,繁体那条反而不更新。
  • 风险与回滚:只影响同一张标签里同时有两种中文书写的情况;只有一种书写的标签、以及所有非中文语言,结果与改前逐字节相同(有测试钉住)。回滚即还原本 PR。
  • 先看:ui/packages/i18n/src/pickLocalized.ts 的 localizedEntryKey(读写共用的一处);packages/spec/src/ui/i18n-label-resolver.ts 的 scriptEntryKey;ui/packages/plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts 新增的两侧一致性行。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/i18n/src/__tests__/pickLocalized.test.ts,预期全绿,其中 “an entry of the session's own script first” 一组覆盖每种繁简拼写。
  • 需要你做的:无,席位落地。
  • 席位意见:ui 席 2 对照 head 6e8b6389 复核:14 个文件、+498 / −65,无受治理路径,因碰 packages/spec/src 走 CONTRACT_REVIEW_TIER 契约复审:首轮 FAIL(6700222070,Studio 导航设计器的写入键 ownEntryKey 仍按旧规则,会在双书写标签上改错条目),修复后 delta 复审 PASS(6700837928),之后只加了一条 changeset(Regen-provenance 已落)。现场:zh-Hant 会话侧栏显示繁体条目。Studio 设计器语言只有 zh-CN / en-US 两值,繁体会话仍按简体设计,已另立 Studio designs a Traditional Chinese session in Simplified, so its label edits land on the Simplified entry #31074。CI 全绿,同意落地。

What changes

  • ui/packages/i18n/src/pickLocalized.ts: one private localizedEntryKey now answers the first three limbs for both pickLocalized (read) and localeWriteKey (write, and so setLocalized / clearLocalized). Where the platform locale table splits a language by script, the base and sibling limbs prefer an entry of the session's own script before any other sibling.
  • packages/spec/src/ui/i18n-label-resolver.ts: resolveI18nLabel takes the same limb (scriptEntryKey), so the spec twin, which the nav, app shell, components and service-analytics call, keeps agreeing with pickLocalized limb for limb.
  • How the script split is derived: both readings come from @objectstack/spec/system, with no local table. platformLocaleTagOf(code) says which table tag a code means (zh-TW, zh-HK, zh-MO and the zh-Hant-* spellings mean zh-Hant; zh, zh-CN, zh-SG and the zh-Hans-* spellings mean zh-Hans). A base is script-split when PLATFORM_LOCALE_TAGS holds more than one tag with that base, which today is only zh. For any other language the new limb is skipped, so pt, es, en, fr, ja and the rest keep authoring order on the sibling limb, byte-identical to main.
  • Limb order where the split applies: exact tag, then the bare base key when it means the session's tag, then the first sibling that means it, then the base key whatever it means, then the first sibling. A map with no entry of the session's script resolves as before.
  • Tests: new describe blocks in pickLocalized.test.ts, setLocalized.test.ts and clearLocalized.test.ts (five Simplified and five Traditional spellings over six two-script maps, plus byte-identical controls for one-script maps and for pt, es, en, fr, ja); new rows in the plugin-list parity table; the spec test's verbatim reference copy re-pinned to the new blob and given script vectors.
  • Two existing vectors pinned the defect itself ({ 'zh-TW', 'zh-CN' } at zh read the Traditional entry). Both now expect the Simplified entry. The spec row was replaced by a pt-PT / pt-BR row that still pins "first sibling in authoring order" for a language the table names once.
  • Changesets: @object-ui/i18n patch, @objectstack/spec patch, @objectstack/cli patch.
  • Review widening (contract review 6700222070, claim amendment 6700234158): ui/packages/app-shell/src/views/metadata-admin/previews/navItemLabel.ts ownEntryKey now owns the entry resolveI18nLabel reads among the map's same-base own entries, so Studio's nav rename and clear act on the entry the designer shows (7422324200); packages/cli/src/utils/i18n-coverage.ts inlineLocaleText narrows to the reader's language and returns what resolveI18nLabel picks, with a two-script fixture in packages/cli/test/i18n-inline-locale-map-coverage.test.ts (4b4e003685); resolver prose follows the limb order and the one-repo home (2d5cae2c8d).

Tests

Fix commit 7228816bf5, then origin/main merged (it carries #30902's @object-ui/i18n → @objectstack/spec dependency). Suite and gate readings are on the merge head 4d0fb81ba5. The ablation was taken on 7228816bf5, where the same two source files hold the same blobs.

Before / after (unit level, read value and write key)

On main 0e882086:

zh-CN+zh-TW        lang=zh-Hant  read=客户  writeKey=zh-CN
zh-Hans+zh-Hant    lang=zh-TW    read=客户  writeKey=zh-Hans
zh-TW+zh-CN        lang=zh-Hans  read=客戶  writeKey=zh-TW
zh-TW+zh-CN        lang=zh       read=客戶  writeKey=zh-TW
spec resolveI18nLabel  zh-CN+zh-TW at zh-Hant  read=客户

On this branch the same rows read 客戶 / zh-TW, 客戶 / zh-Hant, 客户 / zh-CN, 客户 / zh-CN, and the spec resolver reads 客戶. These are pinned in the new describe blocks. One-script maps (zh only, zh-HK only) read and write the same key as on main.

Suites
  • pnpm --filter @objectstack/spec exec vitest run src/ui/i18n-label-resolver.test.ts src/system/platform-locales.test.ts: 2 files, 184 tests passed.
  • pnpm --dir ui exec vitest run packages/i18n/src/__tests__/pickLocalized.test.ts packages/i18n/src/__tests__/setLocalized.test.ts packages/i18n/src/__tests__/clearLocalized.test.ts packages/plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts scripts/__tests__/check-phantom-dependencies.test.ts: 5 files, 276 tests passed.
  • pnpm --filter @objectstack/spec typecheck: exit 0. pnpm --filter @object-ui/i18n type-check after building its closure: exit 0.
Ablation (one-time proof, mutations restored, blob equals HEAD)

Every leg used scripts/ablation-replace.mjs.

  • Client limb (if (script !== undefined) changed to never match) in pickLocalized.ts: 32 tests red across the three i18n files, all of them new A Traditional Chinese console reads and writes the Simplified entry of an inline label map #30910 pins. 0 byte-identical controls red, as expected, because the mutation is the old behaviour.
  • Spec limb (if (sameScript !== undefined) return … changed to never match), spec source suite: 5 red (4 script vectors plus the one-pass agreement test). The reference-copy column stayed green.
  • The same spec mutation, rebuilt into dist (ablation-dist-preflight found the marker in 2 built files): 6 red in the plugin-list parity test (5 new rows plus "has no rule divergence left"). After restoring and rebuilding, the preflight --absent reports the marker gone from all 592 built files and the tree clean.
Derived gates (`dispatch-gates.mjs --commands`, 83 families)

--ran reconciliation on 4d0fb81ba5: 83 derived, 83 run, all exit 0, 0 NOT-MEASURED.

Before main was merged (on 7228816bf5), check:undeclared-dep-imports and the ui scripts/ phantom-deps test were red only because @object-ui/i18n did not yet declare @objectstack/spec. #30902 added that declaration, and both are green on the merge head.

Acceptance notes

  • Live check (head 6e8b6389, showcase backend + console dev server, nav_projects relabelled locally to { en, zh: '简体项目', 'zh-TW': '繁體專案' }, reverted after): the sidebar shows 繁體專案 in zh-Hant, 简体项目 in zh-CN, Proj EN in en. Studio's nav designer in a zh-Hant session lists 简体项目, because Studio's designer locale is two-valued (useMetadataLocale, metadata-admin/i18n.ts:955, unchanged here): filed as Studio designs a Traditional Chinese session in Simplified, so its label edits land on the Simplified entry #31074 with the one-script write and the CLI empty-entry caveat. The main-side control is NOT MEASURED (it needs a main spec dist rebuild).
  • Case: the script preference asks platformLocaleTagOf, which folds case, so zh-hant also prefers zh-TW. It only reorders keys the case-sensitive base comparison already admitted, so ZH-HANT still matches no zh-* key (pinned).
  • A script-subtag spelling the alias table does not name (zh-Hant-US) is not guessed at and is not treated as Traditional (pinned). This matches the spec table's closed-list rule.

Out of scope

Verification

Readings as above, on 4d0fb81ba5; on 2d5cae2c8d 87 derived gates exit 0 (--ran 0 NOT-MEASURED), navItemLabel ablation reds 4 pins and 0 controls, CLI ablation reds 2. 6e8b6389 adds the cli changeset only. The contract review (CONTRACT_REVIEW_TIER) on the head is the seat's, because the diff touches packages/spec/src.

…olds both Chinese scripts

pickLocalized and its write-side twin localeWriteKey now share one
limb function that, for a language the platform locale table splits by
script (zh-Hans / zh-Hant), prefers a key meaning the session's own
table tag (read through platformLocaleTagOf) before falling back to the
first sibling. The spec twin resolveI18nLabel takes the same limb so the
parity between the two resolvers holds. Maps without a same-script entry
and every other language resolve exactly as before.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxSGAjHBvjATmenUGp
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 3 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/kernel/i18n-standard.mdx (via resolveI18nLabel (symbol, a top-level function))
  • content/docs/ui/translations.mdx (via resolveI18nLabel (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 63 of 224 client-bound route-ledger rows — the other 161 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 161: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 57 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 180 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3f61e4f8fb567160f839c3269ee95aa89ee4f1db → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 42a7567352d64da3fd17b36a87edb01e8834361c — the merge of head 6e8b63894022174bb53a869f49d38eb05e85173d into base 3f61e4f8fb567160f839c3269ee95aa89ee4f1db, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 42a7567352d64da3fd17b36a87edb01e8834361c && git checkout 42a7567352d64da3fd17b36a87edb01e8834361c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3f61e4f8fb567160f839c3269ee95aa89ee4f1db 6e8b63894022174bb53a869f49d38eb05e85173d && git checkout -B drift-repro 3f61e4f8fb567160f839c3269ee95aa89ee4f1db && git merge --no-ff 6e8b63894022174bb53a869f49d38eb05e85173d

node scripts/docs-audit/affected-docs.mjs --json 3f61e4f8fb567160f839c3269ee95aa89ee4f1db

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3f61e4f8fb567160f839c3269ee95aa89ee4f1db → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibxSGAjHBvjATmenUGp

Contract-review-claim: session_01L9qibxSGAjHBvjATmenUGp · head 4d0fb81 · 2026-11-07T01:05Z

Seat review run on the maintainer's word in the card thread (「你应该使用 pm-dispatch 技能审核和落地pr」, 2026-11-07 01:02Z). Face: published schema (packages/spec/src/ui/i18n-label-resolver.ts), so CONTRACT_REVIEW_TIER.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibxSGAjHBvjATmenUGp

Contract-review-claim withdrawn: session_01L9qibxSGAjHBvjATmenUGp · head 4d0fb81 · 2026-11-07T01:05Z

This session authored the diff. Under pm-dispatch the dispatching seat (ui seat 2, session_01L9qibw7DekEe3wx5f3zphf) runs the CONTRACT_REVIEW_TIER review, the seat check and the landing on this head. My claim above yields to it, and I produced no review output.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Contract-review-claim: session_01L9qibw7DekEe3wx5f3zphf · head 4d0fb81 · 2026-11-07T01:08Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4d0fb81ba5cd58ea1e42c02f15d8f033677a7eba
Local-runs: none
Scope: packages/spec/src/ui/i18n-label-resolver.ts + ui/packages/i18n/src/pickLocalized.ts, ui/packages/plugin-list/src/tests/i18nLabel-resolver-parity.test.ts, packages/spec/src/system/platform-locales.ts, ui/packages/app-shell/src/views/metadata-admin/previews/navItemLabel.ts, packages/cli/src/utils/i18n-coverage.ts, packages/cli/test/i18n-inline-locale-map-coverage.test.ts

Inputs: card #30910 (body, triage 6683176666, claim 6684338116, amendment 6684414838, progress request 6695504263, os-dev-report 6700059771), PR #31068 (body, 9-file list, net diff against main 5210acfbaee), the check-runs on the head. The rest of the diff (ui/packages/i18n/**, the two spec/ui/plugin-list test files, the two changesets) is the lane seat's ACCEPT at TIER_DEFAULT; the cited files above were READ to judge the hits file, not reviewed.

① Derived judgments

  • Face. packages/spec/src/ui/i18n-label-resolver.ts is a published spec source ⇒ CONTRACT_REVIEW_TIER, schema face. Right; matches the claim amendment's "review owed".
  • Public surface of @objectstack/spec. resolveI18nLabel(label, locale) keeps its signature and return shape; its BEHAVIOUR changes for one input class (an inline map holding entries of both Chinese scripts, read at a zh* locale). scriptEntryKey is module-private; src/ui/index.ts is untouched. New intra-package runtime import ui/i18n-label-resolver.ts → system/platform-locales.ts (no cycle: platform-locales.ts imports nothing). Right.
  • No schema change. InlineLocaleMapSchema / I18nLabelSchema untouched; what an author can write is unchanged ⇒ Metadata-protocol: none. Right.
  • @object-ui/i18n gains a runtime dependency edge on @objectstack/spec/system (PLATFORM_LOCALE_TAGS, platformLocaleTagOf). ui/packages/i18n/package.json already declares @objectstack/spec: workspace:^ on the head (from feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships #30902, merged in); spec is sideEffects: false so the system barrel tree-shakes. Neither package.json nor pnpm-lock.yaml is touched, as the amendment forbade. Right.
  • Table-derived split, no local list. Both twins read "is this language split by script" off PLATFORM_LOCALE_TAGS (count of tags sharing the base, zh ⇒ 2) and "which script does a key mean" off platformLocaleTagOf (closed alias list, case-folded). Traced the two implementations side by side (spec scriptEntryKey vs ui localizedEntryKey): limb order exact → own-script base → own-script sibling → base → first sibling is identical, both include the bare base key in the sibling scan, both treat non-string/inherited entries as misses. pt, en, es, ja, fr fall out of the split check (one table tag each) ⇒ byte-identical to main. Right.
  • Two existing vectors flipped ({ 'zh-TW', 'zh-CN' } at zh now Simplified, in pickLocalized.test.ts and the spec parity table). Right: bare zh means zh-Hans per PLATFORM_LOCALE_READ_ALIASES; the "first sibling in authoring order" rule is re-pinned on pt-PT/pt-BR and es-MX/es-ES, where the table names the language once.
  • Reference-copy pin. The spec test's verbatim copy now cites blob 37b88c07aeb, which equals the head blob of ui/packages/i18n/src/pickLocalized.ts (diff index a72b5e48363..37b88c07aeb); the copied helpers are byte-equal to the source save the Reference suffix. Right. (No gate enforces this pin; it is prose only, as on main.)
  • Docs drift bot named content/docs/protocol/kernel/i18n-standard.mdx and content/docs/ui/translations.mdx; read both: neither states the sibling-order limb, so no doc edit is owed by this diff. Right.
  • Check-runs on the head at review time: every completed run is success or skipped (Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint · ESLint, Type Check · source gates, Dogfood Regression Gate, Build UI Deps, Auto Label, Check PR Size, Flag docs, among them). STILL IN PROGRESS: Test UI (1/16–16/16), Test Core (1/8–8/8), Build Core, Build & E2E UI, Type Check UI, Lint UI, Type Check · workspace. Their conclusions are the gate verdicts and are not pre-empted here; the ④ finding below does not depend on them (no existing test covers the shape).

② Semver level

③ Boundary flags

Dev flags (os-dev-report 6700059771 and PR body), each answered:

  1. "Live check owed … NOT MEASURED" (a zh-Hant console, nav item + list label over { en, zh-CN, zh-TW }). Acknowledged; not measurable from this read-only seat either. Stays owed to the seat before ACCEPT. Not escalated.
  2. Case asymmetry (base comparison case-sensitive, new script limb folds via platformLocaleTagOf; carrier: contract reviewer). Weighed: pre-existing on main, documented and pinned in both twins; the new limb only reorders keys the case-sensitive base comparison already admitted (ZH-HANT still falls to en, pinned), so it adds no new inconsistency class. Not a defect of this PR; not escalated. Triage may file it as a product question if a ZH-* locale is ever observed in a session.
  3. Deviation: surface widened to the spec twin and the plugin-list parity test — covered by claim amendment 6684414838; the diff's file list matches the amended surface exactly and touches none of the ⛔ files. Right.
  4. Deviation: two vectors flipped — judged right in ①.
  5. open_questions: [], security_findings: none — nothing to escalate; the resolver reads own string properties only, and the new limb reads a frozen Map (TAG_BY_FOLDED_CODE), so a prototype-named locale still misses.
  6. Claim 6700070395 / withdrawal 6700080275: the dev session withdrew its own review claim; the seat's claim 6700108794 governs this head. Right.

Unflagged by the dev, raised here: the PR body's "the spec twin, which the nav, app shell, components and service-analytics call, keeps agreeing with pickLocalized" covers the READ side only; the nav designer's WRITE side was not examined — see ④ M1.

④ Design judgment

hits: packages/spec/src/ui/i18n-label-resolver.ts, judged against the ruling it implements (#6761 B: one shared resolver, limb-for-limb parity with pickLocalized), the card's rule 2 ("one reading for both limbs, never two copies") and its first consumers (ui/packages/app-shell nav/preview, plugin-list, service-analytics), with the parity partner, the parity pin and the locale table read in the live tree.

Holds: the limb itself. scriptEntryKey and localizedEntryKey agree on every shape I could construct (both-script maps in any order, bare zh with either script, alias spellings zh-Hans-CN / zh-Hant-TW, unnamed zh-Hant-US, one-script maps, non-zh languages, zh-hant lower-case, ZH-HANT); the parity test on the head pins five two-script rows on BOTH functions plus the one-pass "no divergence" sweep; the spec vectors are asserted against the verbatim reference copy first. The first-consumer read sites (UnifiedSidebar, HomePage, DraftChangesPanel, ListView aria, analytics-service, dataset-compiler) call resolveI18nLabel and take the fix for free.

M1 — must-fix. The change splits read from write in the Studio nav designer for a map shape the PR itself names as in reach.
ui/packages/app-shell/src/views/metadata-admin/previews/navItemLabel.ts — ownEntryKey (the key renamedLabel writes and clearedLabel deletes, used by AppNavInspector.tsx:292, AppNavCanvas.tsx:389, SourcePageProperties.tsx:78) is a third copy of the first limbs: [tag, tag.split('-')[0]].find(own). Its docstring states the invariant it relies on: "The entry written is the one resolveI18nLabel reads first for this locale — the exact tag, else its bare language … so the editor shows the new text straight after the edit." This PR makes that false. Trace on the head, locale zh-Hant (likewise zh-TW, zh-HK, zh-MO), map { en: 'Customer', zh: '客户', 'zh-TW': '客戶' } — the bare zh + zh-TW row of the PR's own BOTH_SCRIPTS table:

  • resolveI18nLabel → exact miss → scriptEntryKey: base zh means zh-Hans ≠ zh-Hant, skipped; sibling zh-TW means zh-Hant → reads zh-TW (客戶). Correct, and what the designer now displays.
  • ownEntryKey → zh-Hant absent → zh. renamedLabel overwrites the Simplified zh text with the Traditional edit; the field keeps showing the untouched zh-TW text (the edit appears to do nothing). clearedLabel deletes the Simplified zh entry while the Traditional text stays on screen.
    On main both read zh (limb 2 before limb 3), so the pair agreed; the divergence is introduced by this head. It is the card's harm — a Traditional editor overwriting or deleting Simplified text with a stale Traditional display — moved one consumer over, and it is exactly the "split one console session" the claim amendment widened the surface to prevent. Fix in this PR: route ownEntryKey through the shared limb (the simplest seat is setLocalized / clearLocalized from @object-ui/i18n, which app-shell already imports in DashboardWidgetInspector and ReportDefaultInspector; or export the key picker once and call it from both), and pin renamedLabel / clearedLabel against resolveI18nLabel on the two-script rows in navItemLabel.inherit-11196.test.ts (today it has no zh-TW / zh-Hant vector). The { 'zh-CN', 'zh-TW' } shape is unaffected (neither key is exact or bare, so a zh-Hant entry is added and then read exactly); the defect needs a bare zh key beside a Traditional one.

S1 — should-fix. A third limb copy in the coverage gate now disagrees with the twins and its pin is blind to it.
packages/cli/src/utils/i18n-coverage.ts inlineLocaleText restates limbs 1–3 (exact, base, first sibling in authoring order) and documents itself as pinned to resolveI18nLabel: "whenever this says covered, the renderer really shows that entry". On the head, { 'zh-CN': '客户', 'zh-TW': '客戶' } at zh-Hant: coverage says the reader sees 客户, the renderer shows 客戶. The covered / not-covered verdict is unchanged (both are hits), so check:i18n-coverage still gates correctly; but the pin packages/cli/test/i18n-inline-locale-map-coverage.test.ts ("a covered locale is the entry the renderer picks") has no two-script map in its fixture set and stays green by omission, and the docstring names a test file (inline-locale-coverage-parity.test.ts) that does not exist in the tree. Add a two-script row to that pin and either take the script limb there too or narrow the docstring's claim to "a covered locale is a locale the renderer answers from the same language" (the coverage question only needs the language, not the script).

S2 — should-fix (prose in the hits file). The resolver header still says changing the case asymmetry "is a two-repo decision" and names "objectui" as the reference's home; this very PR edits both twins in one commit of one repo, and the test header was already re-pointed to objectstack-ai/objectstack. Also the rule table lists the "2–3 …own script first" row after limb 3, while the code runs it before limb 2; the row label says "2–3" but a reader following the table top-down gets the wrong order. One-line fixes.

S3 — should-fix (file, do not block). The write side on a ONE-script map still crosses scripts: a zh-Hant edit over { en, 'zh-CN' } or { en, zh } writes Traditional text into the Simplified key, and a clear deletes it (pinned as "unchanged" controls in setLocalized.test.ts). It honours the WYSIWYG invariant (the editor was showing that entry as a fallback), and the card's stated reach is maps with both keys, so it is out of scope here — but it is the same harm one key short, and the zh-Hant console that #30902 adds makes it reachable. Worth a card: should a write in a split-script language whose script has no entry ADD under the session tag instead of replacing the other script's entry?

Observations, no action: the closed alias list means zh-Hant-US is not treated as Traditional (pinned both sides; a ruled table property, #29349 F, not this PR's). PLATFORM_LOCALE_TAGS.filter(...) runs per call on an 11-row frozen array; negligible.

Implemented-by: claude/project-thread-7el3c5
Reviewed-by: session_01L9qibw7DekEe3wx5f3zphf

VERDICT: FAIL

…lver shows

ownEntryKey asked for the exact tag, else the bare language. Where the
resolver now prefers a sibling of the locale's own script over a bare
language of the other script, the editor overwrote and cleared the
entry it was not showing. It now asks resolveI18nLabel which entry it
reads, over a map of each key to its own name, and keeps no copy of the
resolver's limbs.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxSGAjHBvjATmenUGp
… resolver

inlineLocaleText now narrows the map to the reader's language and asks
resolveI18nLabel, so a zh-Hant reader is covered by its own script's entry
rather than by the first zh-* key.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxSGAjHBvjATmenUGp
…epo home

The script-preference row sits before limbs 2 and 3 where the code runs it,
and the parity note no longer calls a change a two-repo decision.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxSGAjHBvjATmenUGp
@github-actions github-actions Bot added size/l and removed size/m labels Nov 7, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Contract-review-claim: session_01L9qibw7DekEe3wx5f3zphf · head 2d5cae2 · 2026-11-07T02:04Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2d5cae2c8dc5f85722530c7bc7e0421b37e119d7
Local-runs: none
Scope: packages/spec/src/ui/i18n-label-resolver.ts + ui/packages/i18n/src/pickLocalized.ts, ui/packages/plugin-list/src/tests/i18nLabel-resolver-parity.test.ts, packages/spec/src/system/platform-locales.ts, ui/packages/app-shell/src/views/metadata-admin/previews/navItemLabel.ts, ui/packages/app-shell/src/views/metadata-admin/previews/navItemLabel.inherit-11196.test.ts, packages/cli/src/utils/i18n-coverage.ts, packages/cli/test/i18n-inline-locale-map-coverage.test.ts

Delta review of the FAIL recorded on 4d0fb81ba5 (comment 6700222070). Inputs: card #30910 (body and every comment through the claim amendment 6700234158), PR #31068 (body, 13-file list, net diff against main 5210acfbaee, the delta 4d0fb81ba5..2d5cae2c8d: commits 7422324200, 4b4e003685, 2d5cae2c8d), the check-runs on the head. Everything judged on 4d0fb81ba5 and unchanged in the delta (the resolver limb, pickLocalized.ts, the parity pins, the two changesets) stands as recorded there and is restated only where the delta touches it. The rest of the diff is the lane seat's ACCEPT at TIER_DEFAULT; cited files were READ to judge the hits file, not reviewed.

① Derived judgments

  • Face. packages/spec/src/ui/i18n-label-resolver.ts is still the one hits path ⇒ CONTRACT_REVIEW_TIER, schema face. The delta's change to it is prose only (commit 2d5cae2c8d): no code, no export, no import moved. Right.
  • Published surface, delta. @objectstack/spec: unchanged from the first record (signature kept, behaviour changed for two-script maps only). @objectstack/cli (published, in the fixed group): inlineLocaleText is an exported function of src/utils/i18n-coverage.ts; its signature and its defined/undefined contract are unchanged, its returned TEXT changes only for a two-script map read at a zh* locale (see ④). New runtime import @objectstack/spec/ui into the cli — @objectstack/spec is already a dependencies entry (workspace:*) and packages/cli/src/commands/generate.ts already imports @objectstack/spec/ui at runtime, so no new dependency edge and no phantom-import risk. Right.
  • @object-ui/app-shell is private: true; navItemLabel.ts is internal (not exported from the package root) and resolveI18nLabel was already imported there on main. No public-surface change. Right.
  • Surface vs claim. The 13-file list equals the amended surface (6684414838 + 6700234158) exactly; package.json files and pnpm-lock.yaml untouched. Right.
  • Spec test's reference-copy pin (37b88c07aeb) is the blob of pickLocalized.ts, which the delta does not touch. Still right.
  • Check-runs on 2d5cae2c8d at review time: completed and green — Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Dogfood Regression Gate, Check PR Size, Flag docs, Auto Label, filter (the rest skipped by design). STILL IN PROGRESS: Build Core, Build UI Deps, Live E2E UI, Test Core (1/8–8/8), Type Check · workspace / consumer gates / source gates / debt ledger, Lint · ESLint / PM dispatch-gates / repo & tooling gates / docs & code guards / CI, release & engine gates / scoped (query-options erasure, slot lookup & corpus, tooling self-tests). NOT YET STARTED (behind Build UI Deps): Test UI 1/16–16/16, Type Check UI, Lint UI, Build & E2E UI. Their conclusions are the gate verdicts; nothing below pre-empts them. The two new test blocks (navItemLabel.inherit-11196.test.ts A Traditional Chinese console reads and writes the Simplified entry of an inline label map #30910 describe, cli coverage reads the map the way the renderer does) run in Test UI and Test Core respectively, so the pins for M1 and S1 are measured by runs still open.

② Semver level

  • Changesets unchanged from the first head: @objectstack/spec patch, @object-ui/i18n patch, both Clause-②: no / Metadata-protocol: none; PR body agrees; Check Changeset green on this head. Patch remains the right level; nothing in the delta widens, narrows or re-types a published surface.
  • Clause-②: line: no — correct for the delta too (check-changeset-no-major grades by the PR-scoped declaration, and a bare no with every entry patch is consistent).
  • S1 — should-fix. The delta changes @objectstack/cli behaviour (check:i18n-coverage now reports the Traditional entry for a Traditional reader of a two-script map) with no changeset naming @objectstack/cli. The gate does not demand one (it counts added changesets and judges declared level, not per-package coverage), and the fixed group bumps the cli patch regardless — but the release note for the coverage change is then missing from the cli's changelog, against the repo's own precedent of per-package '@objectstack/cli': patch notes (29594-…, 29873-…, 29934-…). Add .changeset/30910-cli-i18n-coverage-script.md with '@objectstack/cli': patch, Clause-②: no, Metadata-protocol: none. (@object-ui/app-shell is private; a note there is optional, precedent exists either way.)

③ Boundary flags

  1. Live zh-Hant console check — still NOT MEASURED by the dev; still owed to the seat before ACCEPT. The delta adds a second surface to it: in a zh-Hant Studio session, rename a nav item whose label is { en, zh, 'zh-TW' } and confirm the field shows the new text and zh is untouched.
  2. Case asymmetry (carrier: contract reviewer) — weighed on the first head, not escalated; the delta's cli change keeps it (key.split('-')[0] === base case-sensitive, then the resolver, also case-sensitive), as the cli docstring now states.
  3. Claim amendment 6700234158 names exactly the delta's four new paths; nothing outside it moved. Right.
  4. Prior findings, disposition: M1 resolved (④); S1 resolved (④) with one caveat (④ S2); S2 (prose) resolved; S3 (one-script-map cross-script write) was "file, do not block" and is unchanged — the M1 fix deliberately preserves it ({ zh } at zh-Hant still writes zh, pinned as a control), so the card to file stands.
  5. open_questions: [], security_findings: none on the dev report — nothing in the delta changes that: ownEntryKey and inlineLocaleText both read own string properties only and hand the resolver maps they built themselves.

④ Design judgment

hits: packages/spec/src/ui/i18n-label-resolver.ts. The delta's edit to it is the S2 prose fix: the rule table now puts the "2–3 before limbs 2 and 3, where the platform splits the base by script" row BEFORE limb 2, matching the code order; "two-repo decision" became "both twins in this repository together (and their parity pins)"; "objectui's pickLocalized" became "the renderer's pickLocalized" / "pickLocalized in ui/packages/i18n", with the historical objectui#3907 / objectui PR #4359 references kept as history. Holds; S2 of the first record is resolved. The code and the parity partner are byte-identical to the first head, so the limb analysis there stands.

M1 (first record) — resolved. navItemLabel.ts ownEntryKey on the head:

  • exact tag present ⇒ that tag (limb 1, as before);
  • bare language ABSENT ⇒ undefined ⇒ the edit ADDS the exact tag (as before: a sibling the resolver reaches only because the base is absent stays a fallback, not an owned entry);
  • bare language PRESENT ⇒ ask resolveI18nLabel over a map of each own string key to its own name, and own what it reads, falling back to the base.
    Traced: { en, zh, 'zh-TW' } at zh-Hant ⇒ resolver (exact miss; base zh means zh-Hans; own-script sibling zh-TW) ⇒ writes/deletes zh-TW, which is what the designer displays. { en, zh, 'zh-TW' } at zh-CN ⇒ zh (base means zh-Hans), as on main. { en, 'zh-CN', 'zh-TW' } at zh-Hant ⇒ base absent ⇒ adds zh-Hant, as on main. { en, zh } at zh-Hant ⇒ resolver has no own-script sibling ⇒ zh, as on main. pt-BR / en-US ⇒ base, as on main (splitScriptTagOf-equivalent check inside the resolver answers nothing for a once-named language). Because the resolver is handed only same-language keys with the base present, it can never pass limb 2, so the guard read.split('-')[0] === base is defensive, not load-bearing — harmless. No new cross-script write path: the only new branch fires when the resolver itself prefers a same-base sibling of the locale's own script, so the entry written is the entry on screen and of the editor's script; cross-script writes exist exactly where they did on main (a one-script map in the other script — S3, filed, unchanged, pinned as controls). The module's own contract ("the entry written is the one the resolver reads first", "a sibling reached only as a fallback is left alone", "the next edit finds the added exact tag first") is restated in the docstrings and holds on every row. The pin (#30910 — renamedLabel and clearedLabel change the entry the resolver shows) asserts, per row, that the resolver reads the key, that a rename writes it and the designer then shows the new text, that a clear deletes it alone, plus six byte-identical controls including both one-script maps and two once-named languages. The pin's module header is unchanged (objectui#11196 title), which is fine — the describe is self-labelled #30910.

S1 (first record) — resolved, with one caveat (S2 below). inlineLocaleText no longer copies limb order: it collects the map's non-empty entries whose language subtag is the locale's (case-sensitive, as before), answers undefined when there are none, and otherwise returns resolveI18nLabel(sameLanguage, tag). The coverage VERDICT is unchanged for every existing fixture and for every map: it is "defined iff a non-empty same-language entry exists" on both main and the head (the resolver over a non-empty same-language map always answers from limbs 1–3 or, at worst, limb 6 over the same keys, so it can never be undefined, and can never return a different-language text because none is in the map it was given). Confirmed on the report consumer (computeI18nCoverage line ~690: lookupKey(...) ?? inlineLocaleText(...) ?? ... tested with !== undefined only), so check:i18n-coverage numbers do not move. The returned TEXT changes only for a two-script map at a zh* locale, where it now equals what the renderer shows — which is the parity the docstring claims. The pin's fixture set now holds three two-script maps (both authoring orders and { zh, 'zh-Hant' }) over nine locales including zh-Hans, zh-Hant, zh-HK, so the "a covered locale is the entry the renderer picks" sweep would have caught the first head; the docstring's dead reference to inline-locale-coverage-parity.test.ts is replaced by the real describe block. Resolved.

S2 — should-fix (narrow, pre-existing in another form; fix or state it). The new pin row inlineLocaleText({ 'zh-CN': '成员', 'zh-TW': '' }, 'zh-Hant') ⇒ '成员' is asserted as the intended rule ("an empty entry of the reader's script is no translation: the other script still covers"). On this head the renderer shows '' for that map at zh-Hant (the empty zh-TW is a string hit on the script limb), so the docstring's claim "whenever this says covered, the renderer really shows that entry" is false for an empty-valued entry of the reader's own script, and coverage reports a Traditional reader as translated where they see a blank label. The same hole exists on main in the exact-tag form ({ 'zh-TW': '', 'zh-CN': '成员' } at zh-TW: coverage 成员, renderer ''), so the delta extends a documented narrowing rather than opening a new class — but the new pin row now cements it. Either let an empty entry of the reader's own tag/script count as a gap (drop the non-empty-value filter for the key the resolver would pick, or run the resolver over all same-language entries and return undefined when it picks an empty one), or soften the docstring to "…really shows that entry, or an empty entry the author wrote for that reader". Not blocking: the verdict direction (covered) is the one the B1 ruling chose for authored-but-empty text on the exact tag, and the row is explicit.

S1 (this record, ②) — add the @objectstack/cli changeset.

Observations, no action: ownEntryKey building a key→key map to ask the resolver which entry it reads is an honest way to keep one limb seat without exporting a key picker from spec; if a third writer appears, exporting the picker from @object-ui/i18n (localizedEntryKey) would be the cleaner seat, but two call sites do not justify it today. The spec resolver's scriptEntryKey and PLATFORM_LOCALE_TAGS.filter cost are unchanged.

Implemented-by: claude/project-thread-7el3c5
Reviewed-by: session_01L9qibw7DekEe3wx5f3zphf

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Regen-provenance: 6700837928 · 2d5cae2c8dc5f85722530c7bc7e0421b37e119d7 → 6e8b63894022174bb53a869f49d38eb05e85173d · git diff --stat 2d5cae2c 6e8b6389 → .changeset/30910-cli-i18n-coverage-script.md only (prose face)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

席位核对 · ACCEPT · 2026-11-07T02:40Z · head 6e8b63894022174bb53a869f49d38eb05e85173d

Seat domain:ui#2 reviewed the diff of its own dispatch (claim 6684338116 on #30910, amendments 6684414838 and 6700234158, card thread session_01L9qibxSGAjHBvjATmenUGp, report 6700059771). The same seat dispatched and reviews; the code is the thread's.

  1. Shape: 14 files, +498 / -65. localizedEntryKey in pickLocalized.ts and scriptEntryKey in i18n-label-resolver.ts share one limb order. When the platform locale table splits a base by script (today only zh), the base and sibling limbs prefer an entry of the session's own script. Every other language keeps its authoring order, unchanged byte for byte. The plugin-list parity table pins both twins on five two-script rows. Two more call sites were widened during review: navItemLabel.ownEntryKey, where Studio nav rename and clear now act on the entry the resolver shows, and the CLI inlineLocaleText. Fixes #30910.
  2. Premise measured: the unit before/after rows are in the body. Live on this head, the zh-Hant sidebar shows the zh-TW entry of { en, zh, 'zh-TW' }, zh-CN shows zh, and en shows en. Studio's designer locale has only two values, zh-CN and en-US (useMetadataLocale, unchanged here), so a zh-Hant session designs as zh-CN and edits zh. That is consistent with the locale it is given, and it is filed as Studio designs a Traditional Chinese session in Simplified, so its label edits land on the Simplified entry #31074. Ablations: navItemLabel reds 4 pins and 0 controls; the CLI pin reds 2.
  3. Gates / check-runs: 64 check runs on this head: 56 success, 8 skipped, 0 red. That includes Test Core, Test UI, TypeScript Type Check, Type Check UI and Lint UI. On 2d5cae2c the red TypeScript Type Check and Test UI runs were cancelled by the superseding push (typecheck-workspace concluded cancelled); they were not failures. 87 derived gates exit 0 on 2d5cae2c, and --ran reconciles. mergeable_state is clean.
  4. Contract review: CONTRACT_REVIEW_TIER. The first review FAILed on 4d0fb81b (6700222070, M1 ownEntryKey read/write split). The delta review by the same reviewer PASSed on 2d5cae2c (6700837928). The head then moved by .changeset/30910-cli-i18n-coverage-script.md only, a prose face, with Regen-provenance posted. The changesets: @objectstack/spec, @object-ui/i18n and @objectstack/cli patch, each Clause-②: no and Metadata-protocol: none. I read them sentence by sentence against the diff and they match, including the cli line, which says the coverage text for a two-script map now equals what the renderer shows.
  5. Out of scope / Skills-docs: Studio designs a Traditional Chinese session in Simplified, so its label edits land on the Simplified entry #31074 is filed for Studio's two-valued designer locale, the one-script cross-script write (S3 of 6700222070) and the CLI empty-own-script coverage gap (6700837928, finding 2). The case-sensitive base comparison was weighed by the reviewer and not escalated. Skills-docs: not owed.
  6. Landing: check-governed-merges --pr 31068 on the final list: not governed, 563 changed lines, so Tier S, with no human approval owed. The contract review owed for packages/spec/src is PASS on the reviewed head and carries over by Regen-provenance. Landing on the maintainer's standing words 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30T14:20Z) and 「你应该使用 pm-dispatch 技能审核和落地pr」 (2026-11-07T01:02Z): relay pr_ready, then automerge_enable. On merge the card closes.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 7, 2026 02:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 7, 2026
Any commits made after this event will not be merged.
Merged via the queue into main with commit 0f920b9 Nov 7, 2026
72 of 73 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-7el3c5 branch November 7, 2026 03:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A Traditional Chinese console reads and writes the Simplified entry of an inline label map

1 participant