Repository navigation
feat(i18n): the engine translates a screen's title and description in the run's locale - #22626
Conversation
…title and description templates in the run's locale Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
…clare its dropped refinements Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
… screen copy Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
…on reference Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
…reen-description-server-pick
📓 Docs Drift CheckThis PR changes 4 package(s): 2 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 — 145 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 617dac8ece6666b0b83f37b62233381ac9e5c9fe && git checkout 617dac8ece6666b0b83f37b62233381ac9e5c9fe
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 83b8b80728e1b1120324abea31b750cb238c3aa4 a42541293c719e1722fede05d5acf13fff0e2c00 && git checkout -B drift-repro 83b8b80728e1b1120324abea31b750cb238c3aa4 && git merge --no-ff a42541293c719e1722fede05d5acf13fff0e2c00
node scripts/docs-audit/affected-docs.mjs --json 83b8b80728e1b1120324abea31b750cb238c3aa4
|
Contract reviewServed-tier: What was read. Card #22507 (body and all 14 comments: triage ① Derived judgmentsEach accept-set or public-surface change the net diff implies, judged against ruling A (
② Semver levelAll four packages publish publicly at 17.7.0 (
The DELIBERATE CORRECTION —
|
Landing pre-checks at
|
Fixes #22507
Clause-②: yes (widening)
This PR implements the maintainer's ruling A on #22507 (comment 6093472708, 「22507 同意」): the engine picks a screen's translated
titleanddescriptiontemplates in the run's locale, then renders them. The ruling states the rule once:It follows the refusing
endnode's pick (#22450, PR #22525,faf6348508) through the same channel. There is no second i18n path. PR #22555 (Part of #22507) keyed the option labels and the terminal toasts. This PR keys the last owed screen slot, so it closes the card. The approval node'sdecisionOutputs[].labelrow staysowed, as the ruling says.What changes
@objectstack/specflowsface gainsflows.FLOW.screens.NODE_ID.description, besidescreens.NODE_ID.title. The address oftitledoes not change.descriptionis judged by the one text-slot judge (textSlotTemplateRefusal), the same ruleScreenConfigSchemaapplies to the source text and therefusalsmessage applies to its translation. A single-brace token is refused, and the message gives its double-brace spelling. An empty string is accepted, because it is the untranslated slotos i18n extractwrites.guidance.descriptionentry is removed. A guidance entry filed under a declared key can never fire (the alias-integrity audit instrict-object.tsholds that). Its message, "the server picks both", now lives in thetitleanddescriptiondescribes and in theflowsdocblock, which replaces the "Not keyed" section with "picked by the engine".flowScreenCopyKey(flowName, nodeId, key)spells the key the engine reads. It sits besideflowRefusalMessageKey.FLOW_SCREEN_COPY_KEYSis now['title', 'description'], so the extractor and the schema pin pick the key up from the one list.translateFlowoverlays a translateddescriptionon a flow DOCUMENT, only where the screen authors one. It still overlaystitlethere. Both are template-level, the same pick the engine makes, and neither touches a served screen (see Zone 2 item 5 below).resolveFlowScreenTitleandFlowScreenLikeare documented as not for a servedScreenSpec. The function stays exported for now, because the console at the.objectui-shapin still imports it (Post-Task Checklist step 4).ScreenSpec.title/.descriptionandAutomationContext.localeare documented: a client draws the served copy as served and never overlays it, and the locale's readers now include thescreenexecutor.flows-translation-face.test.ts):screen.descriptionmoves fromowedtokeyedatscreens.NODE.description, andOWEDis nowapproval.decisionOutputs[].labelalone.flows.screensrow cites the engine as the reader of the heading and the body text, and carries a dated note for the transition window. Therefusalsrows are repointed from the renamed private method.dropped-refinements.baseline.jsondeclares the 8 published sites where the new text-slot refinement cannot be stated in JSON Schema. These are the same 8 schemas where therefusalsmessage's refinement sits (688 to 696 sites, 226 schemas). The build gate requires the declaration.api-surface/system.json,export-origins/system.jsonandreferences/system/translation.mdx.@objectstack/service-automationAutomationEngine.renderFlowTextSlot(slot, variables, context)is the one translated-template pick, generalized from the refusal's privatetranslatedRefusalTemplate, which is renamedtranslatedFlowTemplate.renderRefusalMessagenow calls it withflowRefusalMessageKey. Its argument typeFlowTextSlotTranslationis exported besideRefusalI18nService.screenexecutor (both branches, the flat field list and the object form) asks it forflowScreenCopyKey(context.flowName, node.id, 'title' | 'description'). The run's locale isAutomationContext.locale, negotiated byresolveBundleLocale. Only then does it render throughrenderTextSlot, so values fill a translated template.warnnames the key. A failure of the authored template still throws, as before.config.title, because the onetitlekey covers the node label the heading falls back to. The body text is picked only where the screen authors one, because a bundle never adds body text the author did not write. That is the toasts' rule.@objectstack/lint: a translateddescriptionover a screen that declares noconfig.descriptionistranslation-target-unknown(error).@objectstack/cli:os i18n extractscaffoldsscreens.NODE_ID.descriptionfor each screen that authors one, seeded with the authored template, holes and all. The coverage gate demands it in theflowbucket. An unauthored description is not even a seed-less entry, so a bundle that externalizes one is not demanded in every locale for a string nothing shows.Examples and docs:
app-todozh-CN and ja-JP translatesuccess_screen.description, andapp-crmzh-CN translates its three screen descriptions. Without these,check:i18n-coveragegrows past its baseline.content/docs/ui/translations.mdxstops saying a description renders as authored. The unreleased sibling changeset from #22555 had a bullet saying a translateddescriptionis still refused by the schema; that bullet now points at this entry.Zone 2: the PM's mechanism assumptions, measured at
d748ae80afscreen-nodes.ts:173definedtext, which callsrenderTextSlot(v, variables)and nothing else, used at:201-:202(object form) and:285-:286(flat screen), and no i18n reader was asked.setI18nServiceSource(engine.ts:4881). The refusal's pick wastranslatedRefusalTemplate(:11114), called byrenderRefusalMessage(:11087). It was private, so the executor could not reach it. It is now the shared publicrenderFlowTextSlot, which reads through the samei18nServiceSourcefield. The run's locale at a screen node is the executor'scontext.locale, the run contextresolveRunContextbuilds (:6160). That context is persisted with a suspended run, so a resumed leg reads the starter's locale. The flow name iscontext.flowName, stamped at the same construction point.d748ae80af, theflowsface keyedscreens.NODE_ID.title, andguidance.descriptionrefuseddescription(translation.zod.ts:1352). The family pin listedscreen.descriptionasowed(flows-translation-face.test.ts:124,OWEDat:179).walkScreenFlowsiteratesFLOW_SCREEN_COPY_KEYS(i18n-extract.ts:1884), so adding the key to the list adds the skeleton and coverage rows. Coverage needs one extra rule here: no seed-less entry for an unauthored description.translateFlowapplies a screentitletoday, but on the flow DOCUMENT'sconfig.title, which is the template, not the served slot..objectui-shapin20c6d351a, objectui's only call istranslateFlow({ name, label })(FlowRunner.tsx:274), which passes no nodes.translateFlow. It gainsdescriptionat the same template level, so the document translator stays complete over the declared face.resolveFlowScreenTitlein objectui'slocalizeScreen(FlowRunner.tsx:252). That is the objectui half; see below.Verification
Readings are at head
a42541293c(merge oforigin/main18d999031b) unless named.@objectstack/speclocal tier: 640 files, 19105 passed and 1 todo.@objectstack/specrepo tier: 54 files, 915 passed.@objectstack/service-automation: 182 files, 2298 passed, including the newscreen-copy-translation.test.ts(13 tests).@objectstack/lint: 134 files, 6122 passed.@objectstack/cliunit tier: 277 files, 4109 passed. The integration tier is declared to CI.pnpm --filter PKG run typecheckexits 0 for all four packages, test layers included. The spec test layer holds 52 files / 246 errors / 135 pinned signatures, unchanged.5a2c8649ee.scripts/ablation-replace.mjs: inscreen-nodes.ts,screenText('description', cfg.description, 'the body text')becamerenderTextSlot(cfg.description, variables). The anchor went x1 to x0, and the blob went5ac2066ctof4aa30f5.5 failed / 8 passed(the zh-CN render, thezhnegotiation, the resumed leg, the object-form screen, the broken-translation warn).5ac2066cmatches HEAD, andgit diff HEADis empty../builtin/index.js, which is source.pnpm --filter @objectstack/spec check:generatedreports "All 15 generated artifacts are up to date", after the merge and the rebuild.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 118 commands ata42541293c. 117 were run with their exit codes recorded.--ranreports 117 run, 0 NOT-MEASURED and 1 UNRUN:check:dual-build-cjs-loads, left unrun by the dispatch because it needs a whole-workspace build.check:skill-examplesafter building@objectstack/client-react, andcheck:i18n-coverageafter building the example closure. The latter reads "OK (13 configs, 621 baselined, none new)".check-empty-changesetexits 1 by design. See "A pending release note, corrected" below.check:platform-checklistexits 1, and that is main's state, not this diff. Its ABSENT SYMBOL lines namepackages/metadata-protocol/src/protocol.tsandpackages/services/service-storage/src/attachment-access-hooks.ts, and the checklist tree plus both files diff empty againstorigin/maine22315238f. ThecanEditline is [finding]check:platform-checklistis red onmain:attachments-storage.jsonanchorsattachment-access-hooks.ts#canEdit, which #22513 (ce3d0ad41) moved tocheckEdit#22557.eslint --no-inline-config --format jsonover the 18 changed.tsfiles reports 18 results, 0 errors and 0 warnings. An ignored file would surface as a warning.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot move a verdict on an untouched file.A pending release note, corrected (
check-empty-changesetstays red).changeset/22507-flow-options-toasts-translation.mdis #22555's unreleased note. Its last bullet said a screen'sdescription"is still refused by the schema". This PR makes that sentence false, and both notes would ship in the same release. So the bullet now points at this PR's entry. Restoring it from the base would publish the false sentence. The gate's own text names this case, a deliberate correction, and asks for confirmation on the PR rather than a restore. The other bullets and the frontmatter of that note are unchanged.The objectui consumer half
This is filed into objectui#12075, as the claim records. At the pin
20c6d351a,FlowRunner.tsxhas to change in these places::252: dropconst title = resolveFlowScreenTitle(...)and return{ ...screen, fields }.:162: drop the import.:83-:96,:106-:108,:119-:122,:240-:242and the JSX comment at:543-:544.descriptionoverlay.__tests__/FlowRunner.flowsTranslation-5920.test.tsxpins the heading overlay (:117,:168) and has to pin the served heading drawn as served.Until that lands, the runner replaces a served, already-translated heading with the translated template. That draws a hole literally exactly where it did before this PR, so the window adds no regression.
Acceptance notes
titletranslation is not judged by the text-slot judge. Judging it would narrow what the face accepts today, which conflicts with the dispatchedClause-②: yes (widening). A single-brace token in a translated heading still renders literally, as it did under the client overlay. The report's open question asks the seat whether to follow up.resolveFlowScreenTitleretires once the.objectui-shapin carries the objectui change. The function has no other caller.approval.decisionOutputs[].labelstaysowed, as pinned.packages/spec/src/contracts/automation-service.ts(docblocks),packages/spec/dropped-refinements.baseline.json,content/docs/ui/translations.mdx, theapp-crmzh-CN bundle and the feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 changeset. Each is a mechanical follow-through of the same change.Generated by Claude Code