Skip to content

fix(spec): translateObject serves validations[].message in the reader's language - #27202

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-h0y6db
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-h0y6db

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #27135

Clause-②: no
Metadata-protocol: none
Skills-docs: none — skills/objectstack-i18n/SKILL.md L167 teaches _validations.{rule_name} as "message only — the sentence a rejected write returns", which stays true; no file under skills/** teaches which language /meta serves a rule's message in (grep _validations, validations[].message, objectValidationMessageKey, translat near validat|rule). No skills/** edit, so the PR is not governed.

维护者速读

  • 改了什么:translateObject 现在把对象每条校验规则的 message 按请求语言翻译,读取的正是规则校验器拒绝写入时用的那一个键(objects.OBJECT._validations.RULE.message)。
  • 为什么改:中文或日文部署里,服务器拒绝写入时提示是译文,而 /meta 下发的同一条规则仍是原文;看板上被规则关闭的泳道提示与拒绝提示是两种语言的两句话。
  • 风险与回滚:风险低,只改服务给读者的副本,不动 schema、不动写入判定;没有译文或规则无名时保持原文。回滚即 revert 本 PR。
  • 先看:packages/spec/src/system/i18n-resolver.ts 的 translateObjectValidations(键与回退);packages/objectql/src/engine-validation-locale-negotiation.test.ts 新增的同串对照表;packages/rest/src/meta-object-validation-message-i18n.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/system/i18n-resolver.object-validations.test.ts,预期 12 条全绿。
  • 需要你做的:无,席位落地。

What changes

  • translateObject (packages/spec/src/system/i18n-resolver.ts) overlays each validations[].message through a new private helper, translateObjectValidations, the way it already overlays fields, actions and the embedded listViews.
  • One key. The helper calls objectValidationMessageKey(object, rule) and walks that same dotted key through each locale's data, exactly as the served II18nService resolves it for the ObjectQL rule evaluator. No second spelling and no new key shape: a test pins that the retired top-level validationMessages shape and a bare objects.OBJECT.RULE.message both stay unread.
  • The evaluator's fallbacks. A rule with no name, a bundle with no entry or an empty entry, and no bundle at all each keep the authored message. A conditional rule's branches are translated by their own names; the wrapper's own message is left as authored, because the branch supplies the violation (the extractor scaffolds no key for the wrapper either). Locale negotiation and the fallback chain are the shared pickData / localeChain every other overlay uses.
  • When nothing matches, the authored validations array goes out by reference and a document with no validations gets no key added.
  • No exported type, schema or ObjectLike member changes. Patch changeset for @objectstack/spec; the objectql and rest edits are tests only.
  • The kanban comment this PR makes false, fixed. ui/packages/plugin-kanban/src/ObjectKanban.tsx (the closed-lane tooltip, dropBlockedReason, from fix(plugin-kanban): a lane that refuses the dragged card says why #27133) said the text /meta serves is untranslated because the bundle does not reach validations[].message there. It now says /meta serves the bundle's _validations.RULE.message for the request locale, the same string the refusal uses. Comment only, no behaviour change; @object-ui/plugin-kanban gets a patch line in the same changeset.

Why

Under a translated locale the server's refusal of a write that breaks a rule was translated, while /meta served the same rule's authored message, so a client explaining the rule (the kanban's closed-lane tooltip first) showed a different sentence, in a different language, from the refusal of the same move.

Tests

  • packages/spec/src/system/i18n-resolver.object-validations.test.ts (new, 12 tests). The bundle is parsed by TranslationBundleSchema and every entry is written at the path objectValidationMessageKey spells. Covers: the overlay in zh-CN, bare zh reaching zh-CN, the fallback chain then authored, the /meta door translateMetadataDocument('object'), no input mutation, the four fallbacks above, no other spelling read, and fields / actions output identical with and without validations.
  • packages/objectql/src/engine-validation-locale-negotiation.test.ts, one new case: for zh-CN, zh, en and de, the served rule's message (translateObject with the deployment's fallback as its chain, as /meta calls it) equals the refusal envelope's message for the same write against the same bundle.
  • packages/rest/src/meta-object-validation-message-i18n.test.ts (new, 3 tests): GET /api/v1/meta/:type/:name and GET /api/v1/meta/:type serve the translated message under zh-CN, the authored one for a rule without an entry, and the authored ones under en.

Acceptance notes

  • Measured on origin/main 6866e2e7 first, by ablation: with the one line that puts the overlay on the copy deleted (the resolver then serves exactly what main serves), 8 of the new assertions go red. The objectql cross-path table reads served = the authored English and refusal = the Chinese translation for zh-CN and zh, which is the card's defect, and both rest reads serve the authored message under zh-CN. Restored, all green.
  • Readers of validations[].message from /meta, measured under ui/. The only one that shows it to a user is PR fix(plugin-kanban): a lane that refuses the dragged card says why #27133's kanban closed-lane tooltip and screen-reader description (ui/packages/plugin-kanban/src/ObjectKanban.tsx, dropBlockedReason), which now shows the translated sentence. state-machine-next-values.ts reads transitions only. Studio's validations panel, clientValidation.ts and the deprecated object-validation-engine.ts run on Studio's draft or /layers baseline, which is untranslated, so nothing that edits a rule reads the translated copy and no translated message can be saved back. Nothing else changes.
  • No packaged-base comparison, on purpose. The object's three scalars follow the ADR-0029 D9.2a divergence rule. A rule message is a flat catalog ?? document, like a field label, because the evaluator translates the refusal without any packaged base, and applying the rule here alone would serve one sentence on /meta and another in the refusal of the same write. Consequence: a rule whose message a tenant rewrote still gets the packaged translation in both places, consistently.
  • A second door, measured by the contract review (6467333940). The public form route (packages/rest/src/form-routes.ts ~L317) translates its object schema through the same translateMetadataDocument('object'), so the anonymous form's validations[].message is now translated too. That is the same direction as the refusal of that submit. The tenant-rewrite consequence in the bullet above is filed as i18n: a tenant-rewritten validation rule message loses to the packaged translation under a non-default locale, on both the refusal and /meta — apply ADR-0029 D9.2a to both readers #27221, which applies D9.2a to both readers together.
  • One locale edge, unchanged and shared with field labels. When the requested locale is the deployment's defaultLocale, /meta skips the fallback chain ([Decision] the default locale's text — is the authored label the default-locale source (a default-locale request stops there), or must every supported locale ship a bundle? os i18n check says the former, localeChain does the latter; the resolver's literal ['en'] default rides on the answer #15711) while the i18n adapter behind the refusal still tries its fallback locale. The two can differ only when the default locale's own bundle lacks the entry and the fallback locale's has it.

Verification

Suites and ablation on head ac138e64e (the resolver and its tests); gates rerun on head c8e5fee88, which adds only the kanban comment and its changeset line. All under scripts/pm/os-verify-lock.sh on a shared box. No plugin-kanban test was run: the edit is a source comment.

  • pnpm --filter @objectstack/spec test: 741 files passed, 22241 tests passed, 1 todo. typecheck exit 0.
  • pnpm --filter @objectstack/objectql test: 476 files, 8889 tests passed. typecheck exit 0.
  • pnpm --filter @objectstack/rest test: 333 files passed, 6555 tests passed, 326 skipped. typecheck exit 0.
  • Ablation: scripts/ablation-replace.mjs deleted the anchor line (1 → 0, blob 06ccb909 → a5de27ec), spec rebuilt, ablation-dist-preflight --absent exit 0. Red: spec 5 of 12, objectql 1 of 7, rest 2 of 3. Restore: blob back to 06ccb909, git diff HEAD empty, spec rebuilt, preflight present in 2 dist chunks, objectql 7 of 7 and rest 3 of 3 green.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on c8e5fee88: 85 derived, 85 exit 0 (11 of them rerun after building the @objectstack/rest^... closure and @objectstack/client, having first exited 3 for unbuilt output in the fresh worktree). --ran reconciliation: 85 run, 0 NOT MEASURED, 0 unrun.
  • node scripts/check-nul-bytes.mjs exit 0.
  • pnpm --filter @objectstack/spec run check:generated exit 0 (all 14 generated artifacts up to date against the dist built on this head).

Human Minimum

Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema change; the edit is in the existing i18n resolver.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran. pnpm lint is CI's run.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (check:generated: nothing to regenerate.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 block is in Chinese by the repo's dispatch rule.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…le evaluator's key

The served object now carries each validation rule's message in the
request's locale, read from objectValidationMessageKey, so /meta and the
server's refusal of the same write show one sentence.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziCBhjapdXXqzpW1B2
@github-actions github-actions Bot added size/l 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 Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 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
  • the SDK route bridge reached 56 of 208 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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 — 148 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 9dacccf96127755bd196e1844ce5c4cd61717d57 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9dacccf96127755bd196e1844ce5c4cd61717d57

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

…s the translated rule message

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziCBhjapdXXqzpW1B2
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c8e5fee888af2f7cad5ed0b42cc4813784e0027e
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts + packages/spec/src/system/i18n-resolver.object-validations.test.ts, packages/objectql/src/engine-validation-locale-negotiation.test.ts, packages/rest/src/meta-object-validation-message-i18n.test.ts, ui/packages/plugin-kanban/src/ObjectKanban.tsx, .changeset/27135-object-validation-message-i18n.md; the code those hunks make claims about, read on origin/main 9dacccf9: packages/objectql/src/validation/rule-validator.ts (authoredRuleMessage, checkConditional), packages/objectql/src/validation/record-validator.ts (ValidationMessageContext, resolveFieldLabel), packages/objectql/src/engine-validation-messages.ts (validationMessageContext, negotiatedMessageLocale), packages/services/service-i18n/src/file-i18n-adapter.ts (t, resolveFromLocale, getTranslations), packages/core/src/fallbacks/memory-i18n.ts (t), packages/rest/src/meta-item-read-gate.ts (translateMetaList, translateMetaDocument, translationBundleOf, metaTranslateOptions, metaRequestLocale), packages/rest/src/meta-item-read-routes.ts (the locale-keyed ETag), packages/rest/src/meta-collection-routes.ts (Vary), packages/rest/src/form-routes.ts L300-325, packages/cli/src/utils/i18n-extract.ts (pushValidationMessages), packages/spec/src/system/translation.zod.ts (_validations), ui/packages/react/src/hooks/useSettledSchema.ts (the kanban's objectDef source), skills/objectstack-i18n/SKILL.md L167, content/docs/ui/translations.mdx L388-396, docs/adr/0029 D9.2 and D9.2a; PR #27133 (body, file list, merge ec66ac0d); card #27135 body and comments 6463784251, 6465192030, 6466141109, 6466175269; PR #27202 body, file list, both commits, compare/ac138e64e...c8e5fee88, net diff against main, the one PR comment (docs-drift bot), and the head's check-runs.

Card, PR, diff and cited code read 2026-10-28T11:04Z; the head was c8e5fee88 at every read and had not moved.

① Derived judgments

1. One key — holds. translateObjectValidations (i18n-resolver.ts L3996-4038 at the head) spells the address with objectValidationMessageKey(objectName, rule.name) and walks it as a dotted path through each locale's data; rule-validator.ts L3769 authoredRuleMessage asks II18nService.t for the output of the same function. No second spelling and no second resolver: the locale ladder is the shared localeChain + pickData (resolveBundleLocale) every overlay in this file uses, and the write path negotiates the request tag with the same resolveBundleLocale over getLocales() (negotiatedMessageLocale) before an exact t lookup, so a bare zh reaches zh-CN on both. The spec test pins that the retired top-level validationMessages and a bare objects.OBJECT.RULE.message stay unread, and builds every bundle entry at the path objectValidationMessageKey spells, parsed by TranslationBundleSchema.

2. /meta equals the refusal — holds, with three pre-existing edges shared with field labels. The chains compared: the resolver walks locale → fallbackChain = [getFallbackLocale()] → authored, skipping the chain when locale is the defaultLocale (#15711); FileI18nAdapter.t walks locale exact → fallbackLocale → the key (miss) → the validator keeps the authored text. They agree on: a rule with no name (both authored: validator L3767, resolver L4015); a bundle with no entry (both authored); an inactive or warning-only rule (the overlay translates its message regardless of active/severity; no refusal is emitted for such a rule, so there is no second sentence to disagree with, and the kanban asks the gate before naming a rule); an extended object (both key on the merged object's name, and pushValidationMessages writes the same address for an objectExtensions rule, as i18n-standard.mdx documents); a tenant/org authored translation layer (translationBundleOf reads getTranslations, the merged static+authored view; t probes the same two maps authored-first — one precedence); a conditional rule (checkConditional hands the branch to evaluateRule, so the refusal carries the branch's own name and message; the overlay translates the branches and leaves the wrapper authored, pinned with a wrapper entry present in the bundle).
Where they can differ, none introduced here and each shared with field labels through the same lookupObjectFieldAttr / resolveFieldLabel pair: (a) a request for the default locale whose own bundle lacks the entry while the fallback locale's has it (the dev declared this); (b) an empty-string entry in the requested locale — t returns '' and takes no fallback leg, so the validator keeps the authored text, while localeChain walks on and may serve the fallback locale's string; (c) createMemoryI18n().t reads the DEFAULT locale's bundle before fallbackLocale when the requested locale has no bundle at all. Recorded, not failed: the card's Expected is "the same way it overlays field labels", and that is what this is. The depth bound MAX_VALIDATION_DEPTH = 10 equals the extractor's; the validator has none, so a branch nested deeper than ten is translated by the refusal and not by /meta — no extracted key exists for it either; negligible, noted.

3. Nothing else moves — holds. label/pluralLabel/description, fields, actions and listViews are computed before the new call and are untouched; the test pins fields and actions equal with and without validations, and no validations key added to a document without one. /meta's shape is unchanged: the same array, each rule spread with only message replaced, and the authored array by reference when nothing matched. Caching: the item ETag is simpleHash(locale + JSON.stringify(item)) (meta-item-read-routes.ts L649) and both transports send Vary: Accept-Language (meta-collection-routes.ts L219, L634) — unchanged by this PR, so no locale's translated message can be served from another locale's cached /meta. One door the PR body's reader measurement does not name: form-routes.ts L317 hands the public form's object schema to translateMetadataDocument('object'), so the anonymous form's served validations[].message is now translated too — the same direction as the refusal of the same submit, and the fix's intent; recorded. formula-option-labels.ts L150 and service-analytics L1168 pass { name, fields } only — unaffected.

4. The kanban comment — true at this head; patch honest. objectDef is useSettledSchema(schemaKey, dataSource), the adapter's /api/v1/meta/object/:name read, so ObjectKanban.tsx L1838-1840 now states what /meta serves: the bundle's _validations.RULE.message for the request locale, the refusal's string (edges in ①2 aside). compare/ac138e64e...c8e5fee88 is one commit: the three comment lines and three changeset lines, nothing else. @object-ui/plugin-kanban is published (18.2.0, not in .changeset/config.json ignore), so the patch line is owed and is comment-only, as declared.

5. Declarations — hold. Clause-②: no: no Zod schema, no export, no ObjectLike member moved; no api-surface/ or authorable-surface/ shard is in the file list and check:generated had nothing to write. Metadata-protocol: none: no authorable key, value or meaning changes — the bundle key's meaning (the sentence a rejected write returns) is unchanged, it gains a second reader. patch: kind none on a behaviour fix. Skills-docs: none: skills/objectstack-i18n/SKILL.md L167 ("message only — the sentence a rejected write returns") stays true; no skills/** or .claude/skills/** text teaches that /meta serves the message untranslated (grep _validations, objectValidationMessageKey, untranslated); the row now understates the key's reader set — a docs follow-up, not a false sentence. content/docs/ui/translations.mdx L388-396 likewise stays true. Fixes #27135 is complete: the card's Expected (overlay from the one key; authored on a miss) and its "is a server-side test owed?" question are both answered.

6. Tests — pin the claims; nothing weakened. Spec (12): the overlay in zh-CN; zh → zh-CN; the declared chain then authored; the translateMetadataDocument('object') door; no input mutation; nameless rule, missing entry, empty entry, no bundle; no other spelling read; fields/actions unchanged; no key added. ObjectQL (+1): translateObject(..., { locale, fallbackChain: ['en'] }) as /meta calls it, against the real engine's refusal (ql.update → authoredRuleMessage) for zh-CN, zh, en, de, in one table. REST (3): GET /meta/:type/:name and GET /meta/:type under zh-CN and en through RestServer with i18nServiceProvider as the 14th constructor argument (position verified against rest-server.ts). Not pinned: edges (a)-(c) above and the authored-translation layer — all pre-existing paths. The diff's 3 deletions are comment lines (kanban 2, docblock 1): no existing assertion changed. The ablation (anchor line deleted → 5/12 spec, 1/7 objectql, 2/3 rest red; restored green) is the dev's reading on ac138e64e, read, not re-run.

7. Gates — green on the head. Check-runs on c8e5fee888af… were read four times: at 2026-10-28T10:44Z (44 runs, 23 in_progress), 2026-10-28T10:55Z (65 runs, 23 in_progress), then every two minutes until 2026-10-28T11:03Z: 67 runs, 57 success, 10 skipped (opt-in, matrix-stub and schedule-gate rows), 0 failure, 0 in_progress. The eight required contexts are each success: Lint & Repo Gates, TypeScript Type Check, Test Core (8/8 shards), Build Core, Governed Surface Queue Guard, Lint UI, Type Check UI, Build & E2E UI; Check Changeset, PR-body guards, Spec property liveness, Dogfood Regression Gate and Test UI (16/16) too. The dev's 85/85 derived gates with exit 0 on c8e5fee88 and the suites and ablation on ac138e64e are read as reported, not re-run (Local-runs: none). Head unchanged at every read.

② Semver level

@objectstack/spec patch: a served-document behaviour fix with Clause-②: no and Metadata-protocol: none — kind none takes patch. @object-ui/plugin-kanban patch: a source comment, and a published package whose src/** the diff moves. One changeset, both lines, both declarations present. Correct.

③ Boundary flags

F1 — the dev's open question (ADR-0029 D9.2a): answered — A, a declared deviation, right for this card. D9.2a binds the three scalars and says in its own first paragraph that for fields, validations and indexes the fold "still does" run as before: validations is named as outside the clause, so A contradicts no ADR. What A deviates from is this translator's own convention (translateObject docblock: field description "follows the rule above, as every string this translator added since does" — description, inlineTitle and the embedded listViews are each judged by valueOverridesPackagedBase). The deviation is right because the comparison is a property of the PAIR of readers, not of one: authoredRuleMessage has no packaged base and applies the catalog unconditionally, so B would serve the tenant's rewrite on /meta and the packaged translation in the refusal — the card's split re-opened for exactly the rewritten rules — and field label, the card's named precedent, is already flat catalog ?? document here. The deviation is declared where it lives (the helper's docblock "No packaged-base comparison") and in the acceptance notes. What it leaves, stated plainly: under a non-default locale, a tenant who rewrote a rule's message in a D9 overlay (same rule name) now sees the packaged translation on /meta as well as in the refusal, where before this PR /meta showed the rewrite. The #8284 sentence "an explicit override beats a packaged default" holds on neither surface for rule messages; the refusal half pre-exists. The complete answer is D9.2a on BOTH readers — the validator comparing against the packaged owner's rule (the registry already holds getPackagedObjectBase), then this overlay following the convention — a validator-side card outside this claim's surface. The lane seat files it as a card (the #8284 class, on a surface the 2026-08-13 ruling did not reach); the acceptance note meets Prime Directive 10's floor, so this is owed, not a condition of the verdict. Nit: the docblock declares the deviation without the ADR's id; one ADR-0029 D9.2a token there would make it greppable (Prime Directive 13).

F2 — the default-locale edge (dev-declared): answered in ①2(a). The #15711 contract, pre-existing and shared with every overlay; not this PR's to move.

F3 — a ui/ path in a domain:spec PR: within the amended claim. Comment 6466175269 widened the surface to the one comment and the changeset line; the file list matches it exactly. No governed path (skills/**, docs/adr/**, .claude/**, .github/workflows/**) is in the file list; Governed Surface Queue Guard success.

F4 — branch claude/project-thread-h0y6db, not claude/issue-27135-*: declared in the claim (platform-assigned thread branch, written as measured); the Claim: comment decides ownership and the PR-body guards passed. Not a flag.

F5 — not re-measured here: the dev's reading that Studio's rule editors run on the draft or the /layers baseline, so a translated message cannot be saved back. I did not re-measure it; the class is the ADR-0029 D9.2a "honest edge" already recorded for every translated string the served body carries, unchanged in class by this PR.

Implemented-by: claude/project-thread-h0y6db
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:translateObject 现在也翻译校验规则的提示语 validations[].message。它读翻译包里 objects.<对象>._validations.<规则>.message,和服务器拒绝写入时用的是同一个键(objectValidationMessageKey)。所以 /meta 和服务器的拒绝提示,在同一语言下显示同一句话。看板关闭列的提示(fix(plugin-kanban): a lane that refuses the dragged card says why #27133)随之显示译文,那段过时的代码注释也一并改正。
  • 为什么改:中文、日文等部署里,同一条规则的说明,服务器提示是译文,看板上却是原文,用户会读到两种语言的两句话。
  • 风险与回滚:没有 schema 变化,只是读取时多一层翻译覆盖。没有译文的规则保持原文。fields、actions 的翻译不变,缓存按语言区分也不变。回滚 = revert 本 PR。
  • 先看:packages/spec/src/system/i18n-resolver.ts 里新增的 translateObjectValidations。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run meta-object-validation-message-i18n 预期全绿(同一规则、同一语言下,/meta 与拒绝提示一致)。
  • 需要你做的:无,席位落地。

席位意见:接受。契约复核 PASS(6467333940),确认只有一个键、/meta 与拒绝提示在各种情况下一致、其它翻译不受影响。开发提出的问题(校验提示不做“已打包原文”比较)复核判定为有声明的偏离,而且是对的:只改一边会重新造成两句话不一致。租户改写过的规则提示会被打包译文盖住,这是本 PR 之前就有的问题,另立卡 #27221,两边一起修。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-28T11:07Z

Head: c8e5fee888af2f7cad5ed0b42cc4813784e0027e · card #27135 · claim 6465192030, amendment (the kanban comment) · triage 6463784251 (one key)

  1. 形态: a draft PR to main, Fixes #27135. 6 paths, +535 / −3:

    • i18n-resolver.ts and its test;
    • an objectql and a rest test;
    • the kanban comment;
    • one changeset.

    All are inside the amended claim surface. check-governed-merges --pr 27202 reports NOT governed (538 lines), so the ordinary queue landing applies. The assignee is os-support-ai.

  2. 前提实测: the premise holds on main. translateObject overlays fields and actions but not validations[].message, while the rule validator translates the refusal through objectValidationMessageKey. The review read the head and confirmed four things:

  3. 门禁 check-runs (on this head): 67 runs, 57 success, 10 skipped, none red or pending, read at 2026-10-28T11:03Z. All eight required contexts are green. Dev-side readings:

    • resolver suites and the ablation on ac138e64e;
    • dispatch gates on c8e5fee88: 85 derived, 85 exit 0, 0 NOT-MEASURED.

    c8e5fee88 adds only the kanban comment and its changeset line.

  4. 接受的偏差: (a) Validation messages use a flat catalog ?? document, with no ADR-0029 D9.2a packaged-base comparison. This is declared in the helper docblock and in Acceptance notes, and the review judged it right for this card: D9.2a names validations as outside its clause, and comparing on one reader alone would re-open the split. (b) The branch name is the platform-assigned thread branch.

  5. 范围外去向: a tenant-rewritten rule message loses to the packaged translation under a non-default locale. That is pre-existing on the refusal and now consistent on /meta; it is filed as i18n: a tenant-rewritten validation rule message loses to the packaged translation under a non-default locale, on both the refusal and /meta — apply ADR-0029 D9.2a to both readers #27221 (D9.2a on both readers). The public form door (form-routes.ts ~L317) now serves the translated message too, in the same direction as its refusal; that is named in the body. The review's nit (cite ADR-0029 D9.2a in the helper docblock) is not owed for landing.

  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (with the second door and i18n: a tenant-rewritten validation rule message loses to the packaged translation under a non-default locale, on both the refusal and /meta — apply ADR-0029 D9.2a to both readers #27221 added in this round), the changeset, and the kanban comment. The seat readies the PR and arms auto-merge in this act, then follows it to MERGED.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 11:08
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 11:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit ae16687 Oct 28, 2026
72 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-h0y6db branch October 28, 2026 11:28
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/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants