Repository navigation
fix(spec): translateObject serves validations[].message in the reader's language - #27202
Conversation
…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
📓 Docs Drift Check3 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
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
…s the translated rule message Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufziCBhjapdXXqzpW1B2
Contract reviewServed-tier: Card, PR, diff and cited code read 2026-10-28T11:04Z; the head was ① Derived judgments1. One key — holds. 2. 3. Nothing else moves — holds. 4. The kanban comment — true at this head; 5. Declarations — hold. 6. Tests — pin the claims; nothing weakened. Spec (12): the overlay in 7. Gates — green on the head. Check-runs on ② Semver level
③ Boundary flagsF1 — 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 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 F4 — branch F5 — not re-measured here: the dev's reading that Studio's rule editors run on the draft or the Implemented-by: VERDICT: PASS |
维护者速读 (席位终稿)
席位意见:接受。契约复核 PASS(6467333940),确认只有一个键、 席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #27135
Clause-②: no
Metadata-protocol: none
Skills-docs: none —
skills/objectstack-i18n/SKILL.mdL167 teaches_validations.{rule_name}as "messageonly — the sentence a rejected write returns", which stays true; no file underskills/**teaches which language/metaserves a rule's message in (grep_validations,validations[].message,objectValidationMessageKey,translatnearvalidat|rule). Noskills/**edit, so the PR is not governed.维护者速读
translateObject现在把对象每条校验规则的message按请求语言翻译,读取的正是规则校验器拒绝写入时用的那一个键(objects.OBJECT._validations.RULE.message)。/meta下发的同一条规则仍是原文;看板上被规则关闭的泳道提示与拒绝提示是两种语言的两句话。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 eachvalidations[].messagethrough a new private helper,translateObjectValidations, the way it already overlaysfields,actionsand the embeddedlistViews.objectValidationMessageKey(object, rule)and walks that same dotted key through each locale's data, exactly as the servedII18nServiceresolves it for the ObjectQL rule evaluator. No second spelling and no new key shape: a test pins that the retired top-levelvalidationMessagesshape and a bareobjects.OBJECT.RULE.messageboth stay unread.name, a bundle with no entry or an empty entry, and no bundle at all each keep the authored message. Aconditionalrule's branches are translated by their own names; the wrapper's ownmessageis 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 sharedpickData/localeChainevery other overlay uses.validationsarray goes out by reference and a document with novalidationsgets no key added.ObjectLikemember changes. Patch changeset for@objectstack/spec; the objectql and rest edits are tests only.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/metaserves is untranslated because the bundle does not reachvalidations[].messagethere. It now says/metaserves the bundle's_validations.RULE.messagefor the request locale, the same string the refusal uses. Comment only, no behaviour change;@object-ui/plugin-kanbangets apatchline in the same changeset.Why
Under a translated locale the server's refusal of a write that breaks a rule was translated, while
/metaserved 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 byTranslationBundleSchemaand every entry is written at the pathobjectValidationMessageKeyspells. Covers: the overlay inzh-CN, barezhreachingzh-CN, the fallback chain then authored, the/metadoortranslateMetadataDocument('object'), no input mutation, the four fallbacks above, no other spelling read, andfields/actionsoutput identical with and withoutvalidations.packages/objectql/src/engine-validation-locale-negotiation.test.ts, one new case: forzh-CN,zh,enandde, the served rule'smessage(translateObjectwith the deployment's fallback as its chain, as/metacalls it) equals the refusal envelope'smessagefor 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/:nameandGET /api/v1/meta/:typeserve the translated message underzh-CN, the authored one for a rule without an entry, and the authored ones underen.Acceptance notes
origin/main6866e2e7first, 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 readsserved= the authored English andrefusal= the Chinese translation forzh-CNandzh, which is the card's defect, and both rest reads serve the authored message underzh-CN. Restored, all green.validations[].messagefrom/meta, measured underui/. 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.tsreadstransitionsonly. Studio's validations panel,clientValidation.tsand the deprecatedobject-validation-engine.tsrun on Studio's draft or/layersbaseline, which is untranslated, so nothing that edits a rule reads the translated copy and no translated message can be saved back. Nothing else changes.catalog ?? document, like a fieldlabel, because the evaluator translates the refusal without any packaged base, and applying the rule here alone would serve one sentence on/metaand 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.packages/rest/src/form-routes.ts~L317) translates its object schema through the sametranslateMetadataDocument('object'), so the anonymous form'svalidations[].messageis 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.defaultLocale,/metaskips 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 checksays the former,localeChaindoes 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 headc8e5fee88, which adds only the kanban comment and its changeset line. All underscripts/pm/os-verify-lock.shon 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.typecheckexit 0.pnpm --filter @objectstack/objectql test: 476 files, 8889 tests passed.typecheckexit 0.pnpm --filter @objectstack/rest test: 333 files passed, 6555 tests passed, 326 skipped.typecheckexit 0.scripts/ablation-replace.mjsdeleted the anchor line (1 → 0, blob06ccb909→a5de27ec), spec rebuilt,ablation-dist-preflight --absentexit 0. Red: spec 5 of 12, objectql 1 of 7, rest 2 of 3. Restore: blob back to06ccb909,git diff HEADempty, 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/objectstackonc8e5fee88: 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).--ranreconciliation: 85 run, 0 NOT MEASURED, 0 unrun.node scripts/check-nul-bytes.mjsexit 0.pnpm --filter @objectstack/spec run check:generatedexit 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.
packages/specholds no runtime logic. (No schema change; the edit is in the existing i18n resolver.)pnpm lintpasses. (No formatter ran.pnpm lintis CI's run.)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (check:generated: nothing to regenerate.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code