Skip to content

feat(spec): translate a settings map specifier's column headers - #31484

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-b3hqev
Nov 8, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-b3hqev

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #31453

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:平台设置翻译包里,每个设置项可以翻译 map 表格的表头了:keyColumn(键列)和 columns.列名(各值列),各含 label(表头)与 help(悬停说明)。数据生命周期三张表的 5 个表头已写进 10 个非英文语言包,控制台表格读取这些翻译。
  • 为什么改:用中文等非英文界面打开「数据生命周期」时,保留期覆盖、行数配额、按类别默认配额三张表的表头一直是英文,翻译包里没有地方能放它们。
  • 风险与回滚:只放宽平台翻译包的解析(两个新的可选键,形状严格),存储、保存校验和 manifest 都不变;应用自带的翻译包仍然整组拒绝 settings。直接 revert 即可回滚。
  • 先看:packages/spec/src/system/translation.zod.ts(keys 条目新增 keyColumn / columns 与 settingsMapColumnTranslation);ui/apps/console/src/pages/settings/SettingsMapField.tsx(表头读翻译);packages/services/service-settings/src/translations/settings-translation-coverage.test.ts(表头纳入逐语言覆盖校验)。
  • 两分钟验证:pnpm --dir ui exec vitest run apps/console/src/pages/settings/__tests__/SettingsView.mapRows-31391.test.tsx -t 31453,预期 2 条通过:中文下表头显示「对象 / 行保留时长」,没翻译的列保留英文。
  • 需要你做的:无,席位落地。

What changes

  • Spec (packages/spec/src/system/translation.zod.ts). The platform bundle's settings.NAMESPACE.keys.KEY entry gains two optional groups, spelled as the map specifier spells its headers:
    • keyColumn: the key column, { label, help };
    • columns: a record keyed by the column name, each { label, help }.
      label translates the specifier column's label; help translates its description, the spelling the setting entry already uses for a specifier's description. Both are built with the same strictObject({ surface, history: TRANSLATION_HISTORY, aliases }) helper as their siblings. On the setting entry, keyHeader is answered keyColumn and headers / columnLabels are answered columns. On a column, title / header / heading are answered label, description / helpText / hint are answered help, and a specifier column pasted in whole gets a prescription for name (it is the record key, columns.NAME) and kind (not translatable), never a rename. A bundle written before this change parses unchanged. ServedTranslationDataSchema spreads the same shape, so the served document carries the groups with no other change. SpecifierSchema's keyColumn / columns TSDoc names the translation address, as confirmOffText does.
  • Bundles (@objectstack/service-settings). zh-CN, zh-Hant, ja-JP, ko, es-ES, pt-BR, fr, de, ru and ar translate the five headers of lifecycle.retention_overrides, lifecycle.quotas and lifecycle.quota_defaults (Object, Keep rows for, Expire rows after, Max rows, Lifecycle class). The coverage test holds every map header to the same per-locale coverage as the setting's label, and refuses header copy on a setting that is not a map or for a column the map does not declare.
  • Console overlay (cross-lane ui/ files, named in the claim's file surface). keys.KEY.label is overlaid in the console, by useSettingsLabel, not on the server; the new leaves are read there too: mapKeyColumnLabel / mapKeyColumnHelp / mapColumnLabel / mapColumnHelp, handed to SettingsMapField as headerText. A miss keeps the manifest's text in the page language (pickLocalized), as every other settings string does. help is the header's hover text; the key column now shows its declared description there too, as the value columns already did, so both declared leaves are read.

Served Data Lifecycle page in zh-CN, retention_overrides headers (measured by the new console test against the shipped zh-CN strings):

key column maxAge expireAfter
before Object Keep rows for Expire rows after
after 对象 行保留时长 行过期时长

Tests

  • packages/spec/src/system/translation.test.ts, a map setting's column headers: the leaves parse on the platform face, the platform bundle and the served document; the four entry-level aliases name their targets; an undeclared leaf on a column and on the key column is refused at its path with the alias named; a bare string is refused as invalid_type at its path; a pasted specifier column gets the columns.NAME prescription and no name rename.
  • settings-translation-coverage.test.ts: both new directions. Mutation check: renaming zh-CN's expireAfter entry to expireAt reds both missing for lifecycle: key:retention_overrides.columns.expireAfter and undeclared settings copy: lifecycle.key:retention_overrides.columns.expireAt.
  • SettingsView.mapRows-31391.test.tsx, new top-level describe in the existing harness: zh-CN headers and hover text through the real SettingsView render path, an untranslated column keeps its literal, the translated header still names the row's cells and key picker, and a one-column map reads its own key. Reverse check: with headerText unwired, both cases red ([ 'Object', 'Keep rows for', … ]).

Acceptance notes

  • Shape vs the order. The order proposed keyColumn as a plain string because the specifier's key column "carries only label". Measured: SpecifierMapKeyColumnSchema declares label and description, and SpecifierMapColumnSchema declares label, description, kind and name. description is user-visible text (the value columns render it as the header's title). So both groups are { label, help } objects, which also leaves no string-to-object change owed later.
  • Changeset level. Metadata-protocol: none grades every entry patch (scripts/check-changeset-no-major.mjs), so @objectstack/spec is patch, not the minor the order suggested. none holds because settings is platform-only: both application doors refuse it by name, and authorable-surface/ does not move.
  • ui exhaustive copies. This is a nested leaf under settings, not a new top-level group. SpecTranslationData in @object-ui/i18n forwards settings opaquely, and nothing in ui/ enumerates settings-key leaves except useSettingsLabel, which this PR extends. @object-ui/console type-check is green.
  • Not added. No resolveSettings* helper in i18n-resolver.ts: that family has no in-repo caller for settings keys, and the confirmOffText precedent added none.
  • Serial. feat(console): draw the map settings specifier as a table of keyed rows #31451 (draws these tables) is merged. No open PR on these files was found.
  • Review. An independent review of the spec diff and the code implementing it: round 1 PASS with should-fix (the name alias, a detached TSDoc block, one loose test assertion, wording in four locales), all fixed; delta re-review PASS.

Verification

dispatch-gates: 97 derived, 97 run, 0 NOT-MEASURED

All 97 derived commands exit 0 on this head's tree after git merge origin/main. Nine first exited 3 (prerequisite: a stale spec dist/, unbuilt packages, the teardown fixture commit missing from the shallow clone) and exit 0 after turbo run build --filter=./packages/** and fetching the fixture commit. check:test-typecheck needs a larger heap locally (NODE_OPTIONS=--max-old-space-size=12288); the debt file was not regenerated. Also: pnpm --filter @objectstack/spec check:generated exit 0, check:doc-authoring exit 0, eslint on every touched file exit 0, @object-ui/console type-check exit 0, spec translation.test.ts + src/shared 1224 pass, service-settings translations 135 pass, console map/label suites 22 pass.

🤖 Generated with Claude Code

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fxVwyhFFqT657pqUh
The platform bundle's settings.<ns>.keys.<key> entry gains keyColumn and
columns.<name>, each { label, help }, spelled as the map specifier spells
its headers. The shipped bundles translate Data Lifecycle's five headers,
the coverage test holds map headers to label coverage, and the console's
map table reads the two groups.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fxVwyhFFqT657pqUh
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fxVwyhFFqT657pqUh

Independent spec review (Fable, isolated subagent)

Scope: the spec contract (packages/spec/src/system/translation.zod.ts and its test) and the code implementing it (console overlay in useSettingsLabel.ts / SettingsField.tsx / SettingsMapField.tsx, the service-settings bundles and coverage test, the changeset). The reviewer was given the diff and the card, not the author's conclusions, and re-ran the spec, service-settings and console suites plus check:docs and check:strictness-ledger.

Round 1 (head 6a232f72): PASS with should-fix. No must-fix.

  • should-fix: the column entry's name alias to label advised overwriting the header with the column identifier. Fixed: name and kind now carry prescriptions (name is the columns.NAME record key; kind is not translatable), pinned by a test that also asserts no rename.
  • should-fix: the new helper's TSDoc detached platformSettingsShape's doc block. Fixed: the helper now sits above it.
  • should-fix: the bare-string test passed on any issue. Fixed: it asserts exactly one invalid_type at the keyColumn and columns.maxAge paths.
  • nit: key_column alias redundant with the distance fallback. Removed.
  • nit: SpecifierSchema.keyColumn / columns TSDoc did not name their translation address. Added.
  • nit: wording in zh-CN, zh-Hant, ru and ja-JP for the duration columns. Applied.
  • Confirmed without change: Metadata-protocol: none with patch (both application doors refuse settings; authorable-surface/ does not move); every declared leaf is read by the console; no other in-repo consumer enumerates settings-key leaves.

Round 2 (delta 7d137518): PASS. Each fix landed as described; the delta adds no leaf, export or .describe() text. One optional nit (parallel wording of the two prescriptions), not taken.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 64 of 227 client-bound route-ledger rows — the other 163 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 163: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 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 — 173 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 c65599fae600ab24d7cf517dfee9e33a8d132b0c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6ebb680482c16e4116ded5b665fea85626d8b039 — the merge of head 7d137518f17b648f28560b3f2d859c964cf2d176 into base c65599fae600ab24d7cf517dfee9e33a8d132b0c, 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 6ebb680482c16e4116ded5b665fea85626d8b039 && git checkout 6ebb680482c16e4116ded5b665fea85626d8b039
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c65599fae600ab24d7cf517dfee9e33a8d132b0c 7d137518f17b648f28560b3f2d859c964cf2d176 && git checkout -B drift-repro c65599fae600ab24d7cf517dfee9e33a8d132b0c && git merge --no-ff 7d137518f17b648f28560b3f2d859c964cf2d176

node scripts/docs-audit/affected-docs.mjs --json c65599fae600ab24d7cf517dfee9e33a8d132b0c

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2eAG5J8aqAyPZif4BJ

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7d137518f17b648f28560b3f2d859c964cf2d176
Local-runs: none
Scope: packages/spec/src/system/translation.zod.ts, packages/spec/src/system/settings-manifest.zod.ts (TSDoc only), packages/spec/src/system/translation.test.ts; ui/apps/console/src/pages/settings/{useSettingsLabel.ts,SettingsField.tsx,SettingsMapField.tsx,tests/SettingsView.mapRows-31391.test.tsx}, packages/services/service-settings/src/translations/{10 locales,settings-translation-coverage.test.ts}, .changeset/31453-*.md, docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md + packages/spec/src/shared/strict-object.ts, packages/objectql/src/lifecycle/lifecycle-settings.ts, packages/spec/authorable-surface/system.json, packages/spec/scripts/check-generated.ts, scripts/check-changeset-no-major.mjs, AGENTS.md, ui/packages/i18n/src/{pickLocalized.ts,utils/spec-translations.ts}

① Derived judgments

  • Parse/refuse: settingsMapColumnTranslation (translation.zod.ts:2240-2261) is the house strictObject({surface, history: TRANSLATION_HISTORY, aliases, guidance}); guidance is a declared option (strict-object.ts:158). Entry-level aliases keyHeader/headers/columnLabels (translation.zod.ts:2348-2351); leaves optional (2360-2365); only PlatformTranslationDataSchema and ServedTranslationDataSchema spread platformSettingsShape() (2464, 2515), app face unchanged. Tests pin parse, aliases, paths, bare-string refusal, name guidance (translation.test.ts:1850-1911).
  • Keyed by column name, leaves match the specifier: SpecifierMapColumnSchema = name/label/description/kind, SpecifierMapKeyColumnSchema = label/description (settings-manifest.zod.ts:97-116). Deviation 1 (keyColumn as {label, help}) is correct; the order's "carries only label" was false.
  • Enforce-or-remove: all four leaves read. useSettingsLabel.ts:174-180 probes keys.<key>.keyColumn.label/.help, columns.<col>.label/.help; resolve returns the fallback on a miss (:100-125). SettingsField.tsx:818-834 routes column=undefined → keyColumn, passes headerText (:1102); SettingsMapField.tsx:305-310 renders label + title for key and value columns (:322-327). Miss ⇒ literal via pickLocalized (console test asserts Expire rows after keeps its literal, title null).
  • Bundles: 10 locales carry keyColumn + columns.{maxAge,expireAfter} / {max_rows} on retention_overrides, quotas, quota_defaults, matching lifecycle-settings.ts:51-80 names. Coverage test pins presence per locale (:100-105) and refuses headers on non-map keys or undeclared columns (:150-158).

② Semver level

  • Clause-②: yes (widening) correct: a strict platform entry accepts keys it refused.
  • Metadata-protocol: none ⇒ every entry patch is what check-changeset-no-major.mjs enforces (:909-926; AGENTS.md:1024-1027); none beside minor would be refused, so the order's "spec minor" was internally inconsistent with none. Platform-only claim true: PER_APP_SETTINGS_PLATFORM_ONLY on both app doors (translation.zod.ts:917-943). The authorable-surface/system.json shard does not move because it enumerates only top-level def keys, not record-value leaves. nit: the body's "no authorable surface moves" is therefore a gate artefact, not proof the leaf is unauthorable; none stands as the order expected.
  • service-settings and @object-ui/console both patch in the one changeset (:2-4); both are in the fixed group (config.json), so published. Deviation 2 accepted.

③ Boundary flags

  • Outside the order's surface: .changeset/31453-*.md (required); docs/audits/...counts/system.md 399→400 — gate-required regen artifact (check-generated.ts:141-152 registers check:strictness-ledger, run by spec-liveness-check.yml:82 on packages/spec/**); settings-manifest.zod.ts TSDoc only (spec lane, no shape change).
  • Cross-lane ui: useSettingsLabel.ts, SettingsField.tsx, SettingsMapField.tsx, SettingsView.mapRows-31391.test.tsx. The order anticipated SettingsMapField.tsx and "any ui copy enumerating settings-key leaves"; SettingsField.tsx is the necessary wiring. Named in the body. Deviation 3 is correct: no server overlay exists (no translateSettingsManifest in service-settings/objectql/rest).
  • No ui exhaustive copy moves: @object-ui/i18n forwards settings opaquely (spec-translations.ts:95). Other SettingsLabelHelpers users only consume or cast, so the widened interface breaks no literal.
  • Author is objectstack-fleet[bot]; no login note.
  • nit: body says the console test runs "against the shipped zh-CN strings"; it uses an inline fixture copying them (test:386-399), the shipped bundle is tied to the manifest by the coverage test instead.

④ Design judgment

Holds against cited code. Console-side overlay is right: keys.<key>.label is overlaid only in useSettingsLabel (:166), same site confirmOffText (#30500) took. Column record key unconstrained like sibling options while specifier name is an identifier regex (settings-manifest.zod.ts:98) — nit, coverage test catches shipped typos.

Implemented-by: claude/project-thread-b3hqev
Reviewed-by: session_017ihL2eAG5J8aqAyPZif4BJ

VERDICT: PASS

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:system size/m 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.

settings translations: a columns group so a map setting's key and column headers translate (item 7 of #31391)

2 participants