Repository navigation
feat(i18n): a refused end node's message translates in the run's locale - #22525
Conversation
… run's locale The flows translation group gains flows.FLOW.refusals.NODE_ID.message, judged as the same text slot as the source message. The trigger and action doors forward the request's resolved ExecutionContext.locale as AutomationContext.locale, and the engine picks the translated template through a lazily bridged i18n service before it renders the holes. objectstack validate refuses a refusal key over an unknown flow, an unknown node, or a node that is not a refused end; the extractor emits the skeleton and coverage rows. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…nement sites The translated refusal message carries the text-slot judge as a refinement, which JSON Schema cannot state; the ledger names each published site. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
api-surface, export-origins, references docs, strictness-ledger counts and liveness state counts, from a freshly built packages/spec. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
The trigger and action doors forward the locale; os validate refuses a refusal key over a completed end, an unknown node and an unknown flow; the extractor scaffolds and demands the key; the schema judges a translated refusal as a text slot and accepts the skeleton's empty slot. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…ow-refusal-translation
check:liveness refuses a blanket verdict over an unclassified child; the entry's only key, message, carries the reader and producer evidence. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 149 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 aff780d7e22d2927b0c95990e6a1c2a67a787346 && git checkout aff780d7e22d2927b0c95990e6a1c2a67a787346
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee8751d41e61a18f7819e4d3ad2c340f51ab2418 5b3bf68de58a9a51a71357fc6cbbdaebdb2e0bef && git checkout -B drift-repro ee8751d41e61a18f7819e4d3ad2c340f51ab2418 && git merge --no-ff 5b3bf68de58a9a51a71357fc6cbbdaebdb2e0bef
node scripts/docs-audit/affected-docs.mjs --json ee8751d41e61a18f7819e4d3ad2c340f51ab2418
|
… body 226 published schemas and 688 sites, the 6 schemas and 8 sites the refusal translation's text-slot refinement adds. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22450 (body and all six comments: the triage direction Check-runs on the head, settled: 39 success, 7 skipped, 0 failures, 0 in progress. The seven required contexts are each ① Derived judgmentsEach accept-set or public-surface change the diff implies, named right or wrong.
No accept-set is narrowed, and no author-facing string carries a tracker number. ② Semver level
Per package, against what it publishes:
③ Boundary flagsRound-1 open question (route A / A2 / B / C / D, report Dev deviations (report
Report caveats: the one unnamed cli unit file left NOT MEASURED by a forks-worker crash is answered by the head's Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22450
Clause-②: yes (widening)
A refused
endnode's message now has a translation key,flows.FLOW.refusals.NODE_ID.message. The run renders it in the locale of the person who started it. Before this, a refusal showed in the source language on every console, even when the flow label and screens around it were translated. This PR implements route A of the seat's order on the card (comment 6083596139), in one PR, so the key ships together with its reader.Why the engine translates, not the renderer
The run stores the refusal already rendered:
sys_automation_run.refusal_messageandAutomationResult.refusalMessage. Nothing downstream can translate it after that. So the engine picks the translated TEMPLATE before it fills the double-brace holes. This follows the validation-message precedent:objectValidationMessageKey, read by objectql's rule evaluator through the samei18nchannel.What changed
@objectstack/specTranslationDataSchema.flowsgainsrefusals.NODE_ID.message.textSlotTemplateRefusal) applies to the translation exactly asEndConfigSchemaapplies it to the source message, so a single-brace token is refused.os i18n extractwrites.text/reason/descriptionpoint tomessage, and atitlegets guidance.flowRefusalMessageKey(flowName, nodeId)sits besideobjectValidationMessageKeyinsystem/i18n-resolver.ts.AutomationContext.localeis new and optional. It is documented as the door's already-resolvedExecutionContext.locale.ExecutionContext.localedocblock is corrected. It said the locale came from thelocalizationsettings, but the request'sAccept-Languagewins (assemble-execution-context.ts,requestLocale ?? localization?.locale).translation/flows/refusals(live), which names the engine as its reader.dropped-refinements.baseline.jsondeclares the 8 published sites where the new text-slot refinement cannot be stated in JSON Schema. The build gate requires that declaration (see Acceptance notes).api-surface/system.json,export-origins/system.json,references/system/translation.mdx,references/api/protocol.mdx, the strictness-ledger counts and the liveness state counts.@objectstack/runtime. The two door builders forwardec.locale:buildAutomationContextindomains/automation.ts, which serves the trigger route, the legacy trigger route andtype: 'flow'endpoints;dispatchFlowActioninaction-execution.ts, which serves REST/actionsand MCPrun_action.@objectstack/service-automationendbranch ofexecuteNodecallsrenderRefusalMessage.i18nservice forflowRefusalMessageKey(flow, node)incontext.locale, negotiated byresolveBundleLocaleagainstgetLocales()(objectql's rule).renderTextSlot.warnnaming the key. The refusal stays a refusal; it never becomes a run failure.i18nServiceReader(ctx), atinit(). It resolves at question time, never at boot.RefusalI18nServiceis exported as the type the new publicsetI18nServiceSource()takes.@objectstack/lint.validateTranslationReferencesjudgesflows.FLOW.refusals.NODE_IDbeside the screens judge, astranslation-target-unknown(error). It refuses:end;The universe is the
walkFlowNodesone, so nested nodes count.@objectstack/cli. The extractor emitsflows.FLOW.refusals.NODE_ID.messagefor everyenddeclaringoutcome: 'refused', at any depth, seeded with the authored template, holes included. The coverage bucketflownow reads "flow screens and refusals".Sub-points, as ruled
ResumeSignalgets nolocale. The context is persisted with the suspended run (context_json), and subflow/map children inherit it through their existing spread.Pins
service-automation/src/end-node-refusal-translation.test.tscreateMemoryI18nzhnegotiates to thezh-CNbundleec.locale; an absent or empty locale adds no keyruntime/src/flow-run-locale.test.tsos validaterefuses a key on a completedend, an unknown node, an unknown flow, or a node of another typelint/src/validate-translation-references.test.tsspec/src/system/translation.test.tsTEXT_SLOT_TEMPLATE_REFUSAL'scli/test/i18n-flow-refusal-coverage.test.tsVerification
Final head
5b3bf68de5(the seat's edit, from the dev's report6088248845on #22450).packages/spec/dropped-refinements.baseline.json'smeasuredheader totals to 226 / 688. The speclocalsuite caught the miss at2788f5e6e6, and the re-run ofscripts/dropped-refinements.test.tsandsrc/system/translation.test.tsgave 2 files / 171 tests passed.repo54 / 915.dispatch-gates --commandsderived 119 commands, all 119 exit 0. The artifact-roster block: 48 exit 0, and the 3 PR-context guards exit 0 when wired to this PR.5b3bf68de5is green.The lines below were written at
3b83c95baa.All readings were taken at head
3b83c95baa. That head includes a merge oforigin/main446c8b2a6.pnpm --filter @objectstack/spec buildexited 0. The turbo closure builds of the touched packages exited 0.pnpm --filter PKG run typecheck, exited 0 for each of@objectstack/spec,@objectstack/lint,@objectstack/cli,@objectstack/runtimeand@objectstack/service-automation.@objectstack/lint: 131 files, 5984 tests passed.@objectstack/service-automation: 180 files, 2212 tests passed.@objectstack/cliunit project: 274 of 275 files and 4058 of 4059 tests passed. One forks worker exited unexpectedly ("Worker exited unexpectedly"), and the default reporter does not name its file. The i18n subset is re-run verbose to rule this PR in or out. That result, and the full@objectstack/speclocal andrepoprojects and the full@objectstack/runtimesuite, were still queued on the shared verify lock when this PR was opened. Theos-dev-reportcomment on i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 carries them.translation.test.ts: 144 of 144 passed.validate-translation-references.test.ts: passed.end-node-refusal-translation.test.ts+end-node-refused-outcome.test.ts: 23 of 23 passed.flow-run-locale.test.ts+flow-caller-param-keys.test.ts: 18 of 18 passed.this.renderRefusalMessage(...)inengine.tswas replaced with the oldrenderTextSlot(endConfig.message, variables), usingscripts/ablation-replace.mjs(anchor 1 to 0, blob4a2bcc50to74fefde0).zhnegotiation, the compile-failure warning, the hot resume and the cold resume.git diff HEADempty, 11 of 11 passed.node scripts/pm/dispatch-gates.mjs --commandsprinted 119 commands; all 119 exited 0.--ran: "119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED".check:error-code-provenanceincluded. No new error code is stamped. The other three refuse without PR context (exit 2, NOT WIRED).check:partof-closing-keywordexited 0 with this body asPR_BODY.eslint --no-inline-config --format jsonreports 17 files, 0 errors, 0 warnings..tsfiles. None is ignored, since an ignored file would surface as a warning.eslint.config.mjsenables no type-aware linting (noparserOptions.project) and no import-graph plugin, so this diff cannot move a verdict on an untouched file.Acceptance notes
z.toJSONSchemahas no arm for it, and the closed projection list (src/shared/refinement-projection.ts) has no exact pattern for a single-brace-token rule. So the build gate requires the sites to be declared. The source message's own refinement is already dropped the same way, underflows.element.TranslationDataSchemarefuses a single-brace token. A translation whose double-brace holes do not compile (an unknown formatter, say) passesos validate. At run time the refusal then shows the authored message and awarnnames the key. The source message is compiled atregisterFlowand byos validate's expression pass. Compiling translations atos validatewould need a new finding kind in the translation judge. That is left out, to keep this PR's surface as ordered.supportedLocalesand has a refusingendnow gets onei18n/missing-flowissue per locale until it translates the refusal. The changeset says so.refusalMessageverbatim. No REST, wire orsys_automation_runshape change.Generated by Claude Code