Skip to content

Commit d25b187

Browse files
feat(i18n): the engine translates a screen's title and description in the run's locale (#22626)
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 `title` and `description` templates in the run's locale, then renders them. The ruling states the rule once: > a user-read flow string the server renders per run is translated where it is rendered, in the run's locale, before its holes are filled; a client overlay never touches a server-rendered slot. It follows the refusing `end` node'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's `decisionOutputs[].label` row stays `owed`, as the ruling says. ## What changes **`@objectstack/spec`** - The `flows` face gains `flows.FLOW.screens.NODE_ID.description`, beside `screens.NODE_ID.title`. The address of `title` does not change. - A translated `description` is judged by the one text-slot judge (`textSlotTemplateRefusal`), the same rule `ScreenConfigSchema` applies to the source text and the `refusals` message 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 slot `os i18n extract` writes. - The screen face's `guidance.description` entry is removed. A guidance entry filed under a declared key can never fire (the alias-integrity audit in `strict-object.ts` holds that). Its message, "the server picks both", now lives in the `title` and `description` describes and in the `flows` docblock, which replaces the "Not keyed" section with "picked by the engine". - `flowScreenCopyKey(flowName, nodeId, key)` spells the key the engine reads. It sits beside `flowRefusalMessageKey`. `FLOW_SCREEN_COPY_KEYS` is now `['title', 'description']`, so the extractor and the schema pin pick the key up from the one list. - `translateFlow` overlays a translated `description` on a flow DOCUMENT, only where the screen authors one. It still overlays `title` there. Both are template-level, the same pick the engine makes, and neither touches a served screen (see Zone 2 item 5 below). - `resolveFlowScreenTitle` and `FlowScreenLike` are documented as not for a served `ScreenSpec`. The function stays exported for now, because the console at the `.objectui-sha` pin still imports it (Post-Task Checklist step 4). - `ScreenSpec.title` / `.description` and `AutomationContext.locale` are documented: a client draws the served copy as served and never overlays it, and the locale's readers now include the `screen` executor. - The family pin (`flows-translation-face.test.ts`): `screen.description` moves from `owed` to `keyed` at `screens.NODE.description`, and `OWED` is now `approval.decisionOutputs[].label` alone. - The liveness ledger: the `flows.screens` row cites the engine as the reader of the heading and the body text, and carries a dated note for the transition window. The `refusals` rows are repointed from the renamed private method. - `dropped-refinements.baseline.json` declares the 8 published sites where the new text-slot refinement cannot be stated in JSON Schema. These are the same 8 schemas where the `refusals` message's refinement sits (688 to 696 sites, 226 schemas). The build gate requires the declaration. - Regenerated: `api-surface/system.json`, `export-origins/system.json` and `references/system/translation.mdx`. **`@objectstack/service-automation`** - `AutomationEngine.renderFlowTextSlot(slot, variables, context)` is the one translated-template pick, generalized from the refusal's private `translatedRefusalTemplate`, which is renamed `translatedFlowTemplate`. `renderRefusalMessage` now calls it with `flowRefusalMessageKey`. Its argument type `FlowTextSlotTranslation` is exported beside `RefusalI18nService`. - The `screen` executor (both branches, the flat field list and the object form) asks it for `flowScreenCopyKey(context.flowName, node.id, 'title' | 'description')`. The run's locale is `AutomationContext.locale`, negotiated by `resolveBundleLocale`. Only then does it render through `renderTextSlot`, so values fill a translated template. - **Falls back to the authored template** when there is no locale, no service, no entry or an empty one, or a translation that does not compile. In the last case a `warn` names the key. A failure of the authored template still throws, as before. - **Two presence rules.** The heading is picked even with no `config.title`, because the one `title` key 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 translated `description` over a screen that declares no `config.description` is `translation-target-unknown` (error). **`@objectstack/cli`**: `os i18n extract` scaffolds `screens.NODE_ID.description` for each screen that authors one, seeded with the authored template, holes and all. The coverage gate demands it in the `flow` bucket. 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-todo` zh-CN and ja-JP translate `success_screen.description`, and `app-crm` zh-CN translates its three screen descriptions. Without these, `check:i18n-coverage` grows past its baseline. `content/docs/ui/translations.mdx` stops saying a description renders as authored. The unreleased sibling changeset from #22555 had a bullet saying a translated `description` is still refused by the schema; that bullet now points at this entry. ## Zone 2: the PM's mechanism assumptions, measured at `d748ae80af` 1. **Confirmed.** `screen-nodes.ts:173` defined `text`, which calls `renderTextSlot(v, variables)` and nothing else, used at `:201`-`:202` (object form) and `:285`-`:286` (flat screen), and no i18n reader was asked. 2. **Confirmed.** The channel is `setI18nServiceSource` (`engine.ts:4881`). The refusal's pick was `translatedRefusalTemplate` (`:11114`), called by `renderRefusalMessage` (`:11087`). It was private, so the executor could not reach it. It is now the shared public `renderFlowTextSlot`, which reads through the same `i18nServiceSource` field. The run's locale at a screen node is the executor's `context.locale`, the run context `resolveRunContext` builds (`:6160`). That context is persisted with a suspended run, so a resumed leg reads the starter's locale. The flow name is `context.flowName`, stamped at the same construction point. 3. **Confirmed.** At `d748ae80af`, the `flows` face keyed `screens.NODE_ID.title`, and `guidance.description` refused `description` (`translation.zod.ts:1352`). The family pin listed `screen.description` as `owed` (`flows-translation-face.test.ts:124`, `OWED` at `:179`). 4. **Confirmed.** `walkScreenFlows` iterates `FLOW_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. 5. **`translateFlow` applies a screen `title` today**, but on the flow DOCUMENT's `config.title`, which is the template, not the served slot. - At the `.objectui-sha` pin `20c6d351a`, objectui's only call is `translateFlow({ name, label })` (`FlowRunner.tsx:274`), which passes no nodes. - So the ruling's "a client overlay never touches a server-rendered slot" requires no change to `translateFlow`. It gains `description` at the same template level, so the document translator stays complete over the declared face. - The served-slot overlay the ruling retires is `resolveFlowScreenTitle` in objectui's `localizeScreen` (`FlowRunner.tsx:252`). That is the objectui half; see below. ## Verification Readings are at head `a42541293c` (merge of `origin/main` `18d999031b`) unless named. - **Tests**, each under the verify lock: - `@objectstack/spec` local tier: 640 files, 19105 passed and 1 todo. - `@objectstack/spec` repo tier: 54 files, 915 passed. - `@objectstack/service-automation`: 182 files, 2298 passed, including the new `screen-copy-translation.test.ts` (13 tests). - `@objectstack/lint`: 134 files, 6122 passed. - `@objectstack/cli` unit tier: 277 files, 4109 passed. The integration tier is declared to CI. - **Typecheck**: `pnpm --filter PKG run typecheck` exits 0 for all four packages, test layers included. The spec test layer holds 52 files / 246 errors / 135 pinned signatures, unchanged. - **Ablation** of the executor's description pick, at `5a2c8649ee`. - Mutation, through `scripts/ablation-replace.mjs`: in `screen-nodes.ts`, `screenText('description', cfg.description, 'the body text')` became `renderTextSlot(cfg.description, variables)`. The anchor went x1 to x0, and the blob went `5ac2066c` to `f4aa30f5`. - Predicted: red on the 5 translated-description pins, green on the 8 fallback pins. Observed: `5 failed / 8 passed` (the zh-CN render, the `zh` negotiation, the resumed leg, the object-form screen, the broken-translation warn). - Restored: blob `5ac2066c` matches HEAD, and `git diff HEAD` is empty. - No dist step was needed: the test imports the executor through `./builtin/index.js`, which is source. - **Generated**: `pnpm --filter @objectstack/spec check:generated` reports "All 15 generated artifacts are up to date", after the merge and the rebuild. - **Gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 118 commands at `a42541293c`. 117 were run with their exit codes recorded. `--ran` reports 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. - 115 exited 0. Two of them first refused (exit 3, PREREQUISITE NOT MET) and exited 0 after the named build: `check:skill-examples` after building `@objectstack/client-react`, and `check:i18n-coverage` after building the example closure. The latter reads "OK (13 configs, 621 baselined, none new)". - `check-empty-changeset` exits 1 by design. See "A pending release note, corrected" below. - `check:platform-checklist` exits 1, and that is main's state, not this diff. Its ABSENT SYMBOL lines name `packages/metadata-protocol/src/protocol.ts` and `packages/services/service-storage/src/attachment-access-hooks.ts`, and the checklist tree plus both files diff empty against `origin/main` `e22315238f`. The `canEdit` line is #22557. - **ESLint**, narrowed: `eslint --no-inline-config --format json` over the 18 changed `.ts` files reports 18 results, 0 errors and 0 warnings. An ignored file would surface as a warning. - `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict on an untouched file. - **Size**: 1,051 changed lines, generated files included. ## A pending release note, corrected (`check-empty-changeset` stays red) `.changeset/22507-flow-options-toasts-translation.md` is #22555's unreleased note. Its last bullet said a screen's `description` "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.tsx` has to change in these places: - `:252`: drop `const title = resolveFlowScreenTitle(...)` and return `{ ...screen, fields }`. - `:162`: drop the import. - The docs at `:83`-`:96`, `:106`-`:108`, `:119`-`:122`, `:240`-`:242` and the JSX comment at `:543`-`:544`. - Add no `description` overlay. - `__tests__/FlowRunner.flowsTranslation-5920.test.tsx` pins 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 - **The `title` translation is not judged by the text-slot judge.** Judging it would narrow what the face accepts today, which conflicts with the dispatched `Clause-②: 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. - `resolveFlowScreenTitle` retires once the `.objectui-sha` pin carries the objectui change. The function has no other caller. - `approval.decisionOutputs[].label` stays `owed`, as pinned. - The claim's file surface did not name these files: `packages/spec/src/contracts/automation-service.ts` (docblocks), `packages/spec/dropped-refinements.baseline.json`, `content/docs/ui/translations.mdx`, the `app-crm` zh-CN bundle and the #22555 changeset. Each is a mechanical follow-through of the same change. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ad14957 commit d25b187

29 files changed

Lines changed: 882 additions & 169 deletions

‎.changeset/22507-flow-options-toasts-translation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ Clause-②: yes (narrowing)
1919
- **Where it is read.** `translateFlow` (`@objectstack/spec/system`) overlays both, and `resolveFlowScreenFieldOptions(bundle, flowName, nodeId, field)` resolves a served screen field's options. The console's flow runner reads them in a following release; until then a translation is stored and the authored text is drawn.
2020
- **`objectstack validate`** warns on an option key that names no declared option of the field, or names it by its label (`translation-option-key-unknown`).
2121
- **`os i18n extract` and the coverage gate** now scaffold and demand each screen field's option labels, the failure toast of every flow that declares one, and the completion toast of a flow with a screen, in the `flow` bucket (`i18n/missing-flow`). A project that declares `supportedLocales` gets one missing-key issue per locale for each until it translates them.
22-
- **Still not keyed: a screen's `description`.** It is a `{{ }}` template the server renders per run, so its translation has to be chosen before that render; a translated `description` is still refused by the schema with that reason.
22+
- **A screen's `description`** is keyed too, in the same release, and translated by the engine rather than the console: see the entry for `flows.<flow>.screens.<node_id>.description`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/service-automation": minor
3+
---
4+
5+
feat(automation): a screen's heading and body text render in the run's locale
6+
7+
Clause-②: yes (widening)
8+
9+
- **What is new.** The `screen` executor serves a screen's `title` and `description` in the language of the person who started the run. For each, it asks the engine for the translated template at `flows.<flow>.screens.<node_id>.title` / `.description` through the `i18n` service, in `AutomationContext.locale`, and fills the `{{ }}` holes of that template. This is the same pick a refusing `end` node's message already makes, through the same channel (`setI18nServiceSource`).
10+
- **When the authored text renders.** With no locale on the run (a record-change, schedule or webhook trigger), no `i18n` service, no entry or an empty one, or a translation that does not compile, the authored template renders as before. A translation that does not compile also logs a `warn` naming the key. A resumed leg renders in the locale the run was started in.
11+
- **The heading key covers the node label.** A screen with no `config.title` shows its node label, and a translated `title` replaces whichever heading the screen shows. The body text is translated only where the screen authors one.
12+
- **`AutomationEngine.renderFlowTextSlot(slot, variables, context)`** is the one translated-template pick, now shared by the refusing `end` node and the `screen` executor. Its argument type, `FlowTextSlotTranslation`, is exported.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
feat(cli): `os i18n extract` and the coverage gate scaffold and demand a screen's `description`
6+
7+
Clause-②: yes (widening)
8+
9+
- `os i18n extract` now writes `flows.<flow>.screens.<node_id>.description` into the skeleton for every screen that authors body text, seeded with the authored `{{ }}` template so a translator keeps its holes.
10+
- The coverage gate demands it in the `flow` bucket (`i18n/missing-flow`). A project that declares `supportedLocales` gets one missing-key issue per locale for each screen that authors a `description`, until it translates it. A screen with no `description` is never asked for one.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `objectstack validate` judges a translated screen `description`
6+
7+
Clause-②: yes (widening)
8+
9+
- A translation of `flows.<flow>.screens.<node_id>.description` over a screen that declares no `config.description` is reported as `translation-target-unknown` (error). The engine translates body text only where the screen authors one, so nothing would read the key. Declare the screen's `description`, or drop the key.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(i18n): a screen's body text has a translation key, and a screen's heading and body text are translated by the engine in the run's locale (`flows.<flow>.screens.<node_id>.description`)
6+
7+
Clause-②: yes (widening)
8+
9+
- **What is new.** A fully translated screen flow no longer has to keep a screen's body text in the source language. Translate it under `flows.<flow_name>.screens.<node_id>.description`, beside the heading's `flows.<flow_name>.screens.<node_id>.title`, whose address does not change.
10+
- **A translation is a template.** A screen's `title` and `description` are `{{ }}` templates rendered for each run, so a translation keeps the holes of the text it translates: `description: '任务“{{ subject }}”创建成功!'` for `'Task "{{ subject }}" created successfully!'`. The schema judges a translated `description` with the same text-slot rule the source text gets, so a single-brace `{subject}` is refused with the `{{ subject }}` spelling. An empty string is accepted: it is the untranslated slot `os i18n extract` writes.
11+
- **The body text translates only where the screen authors one.** A bundle cannot add body text to a screen that declares no `config.description`.
12+
- **Who translates it.** The automation engine, not the console. It picks the translated template in the locale of the person who started the run and then fills the holes, so the screen a paused run serves is already translated. `ScreenSpec.title` and `ScreenSpec.description` now document that a client draws them as served and never overlays a translation on them.
13+
- **`flowScreenCopyKey(flowName, nodeId, key)`** (`@objectstack/spec/system`) spells the key the engine reads, beside `flowRefusalMessageKey`. `FLOW_SCREEN_COPY_KEYS` is now `['title', 'description']`, and `translateFlow` overlays a translated `description` on a flow document where the screen authors one.
14+
- **`resolveFlowScreenTitle` is not for a served screen.** A served `ScreenSpec.title` is already translated and filled, so overlaying the resolver's answer on it would draw the template's holes as literal text. The console stops calling it in a following release, and the function retires after that.
15+
- A translated screen `title` is not judged by the text-slot rule yet, so a single-brace token in it is drawn as literal text, as it was before.

‎content/docs/references/system/translation.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ Translation data for a single object
260260
| **label** | `string` | optional | Translated flow label |
261261
| **successMessage** | `string` | optional | Translated completion toast — overlays the flow's own `successMessage`, a plain string the engine carries verbatim on the terminal result; only a flow that authors one is translated |
262262
| **errorMessage** | `string` | optional | Translated failure toast — overlays the flow's own `errorMessage`, a plain string the engine carries verbatim on the terminal result; only a flow that authors one is translated |
263-
| **screens** | `Record<string, { title?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
263+
| **screens** | `Record<string, { title?: string; description?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
264264
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
265265

266266
### Nested Shape: `PlatformTranslationData.metadataForms[string]`
@@ -461,7 +461,7 @@ Translation data for a single object
461461
| **label** | `string` | optional | Translated flow label |
462462
| **successMessage** | `string` | optional | Translated completion toast — overlays the flow's own `successMessage`, a plain string the engine carries verbatim on the terminal result; only a flow that authors one is translated |
463463
| **errorMessage** | `string` | optional | Translated failure toast — overlays the flow's own `errorMessage`, a plain string the engine carries verbatim on the terminal result; only a flow that authors one is translated |
464-
| **screens** | `Record<string, { title?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
464+
| **screens** | `Record<string, { title?: string; description?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
465465
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
466466

467467
### Nested Shape: `TranslationData.metadataForms[string]`
@@ -623,7 +623,7 @@ Translation data for a single object
623623
| **label** | `string` | optional | Translated flow label |
624624
| **successMessage** | `string` | optional | Translated completion toast — overlays the flow's own `successMessage`, a plain string the engine carries verbatim on the terminal result; only a flow that authors one is translated |
625625
| **errorMessage** | `string` | optional | Translated failure toast — overlays the flow's own `errorMessage`, a plain string the engine carries verbatim on the terminal result; only a flow that authors one is translated |
626-
| **screens** | `Record<string, { title?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
626+
| **screens** | `Record<string, { title?: string; description?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
627627
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
628628

629629
### Nested Shape: `TranslationItem.metadataForms[string]`

‎content/docs/ui/translations.mdx‎

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export default defineStack({
8080
| Dataset dimension and measure labels | `datasets.<name>.dimensions.<dimension>.label` / `datasets.<name>.measures.<measure>.label` |
8181
| Page labels and `page:header` copy | `pages.<name>.label` / `description` / `title` / `subtitle` — on a `kind: 'slotted'` page the header under `slots.header` is the page's header |
8282
| Page component copy, by component id | `pages.<name>.components.<id>.title` / `description` / `label` / `placeholder` / `emptyText` — reached under `regions[].components[]` and `slots.<slot>`, through `properties.children` and a `page:tabs` / `page:accordion` panel's `items[].children` |
83-
| Screen-flow wizards (flow label, screen headings, screen field copy) | `flows.<flow>.label` / `flows.<flow>.screens.<node_id>.title` / `.fields.<field>.label` / `.placeholder` / `.inlineHelpText` — a screen field's help text is `inlineHelpText`, the key the field itself declares, not `help`; see the boundary note below |
83+
| Screen-flow wizards (flow label, screen headings and body text, screen field copy) | `flows.<flow>.label` / `flows.<flow>.screens.<node_id>.title` / `.description` / `.fields.<field>.label` / `.placeholder` / `.inlineHelpText` — a screen field's help text is `inlineHelpText`, the key the field itself declares, not `help`; see the boundary note below |
8484
| Global actions, messages | `globalActions`, `messages` |
8585
| Settings UI shell copy (the source badge on a settings row) | `settingsCommon.sourceLabels.<layer>` — the per-namespace settings copy under `settings` is **platform-only**: an app bundle carrying it is refused by name, and the platform's own strings are translated in `@objectstack/service-settings`'s bundle |
8686
| A label written as an inline locale map (`label: { en: 'Members', 'zh-CN': '成员' }`) | Nowhere — it is written on the metadata and resolved at render time; see **Current boundaries** below |
@@ -390,23 +390,28 @@ Honest limits worth knowing before you plan around them:
390390
by rule name alone and so could not tell two objects' rules apart; the route
391391
above is object-scoped and shipped with its reader. ADR-0049's 2026-09-04
392392
amendment carries that record.
393-
- **The `flows` group is applied by the console's screen-flow runner, and its
394-
face is deliberately small.** The keys are addressed the way the runner
395-
resolves them — flow name, screen node id, screen field name — and both
396-
halves render in the active locale:
397-
- `screens` is applied to the screen copy: each screen's `title`, and each
398-
field's `label`, `placeholder` and `inlineHelpText`;
393+
- **The `flows` group is applied where each string is rendered, and its face
394+
is deliberately small.** The keys are addressed the way the runner resolves
395+
them — flow name, screen node id, screen field name — and every part renders
396+
in the active locale:
397+
- a screen's `title` and `description` are `{{ }}` templates the server
398+
renders for each run, so the server translates them: it picks the
399+
translated template in the locale of the person who started the run, then
400+
fills its holes, and the console draws the screen as served. A translation
401+
keeps the holes of the text it translates (`'任务“{{ subject }}”创建成功!'`),
402+
and `description` is translated only where the screen authors one;
403+
- each field's `label`, `placeholder` and `inlineHelpText` is applied by the
404+
console's screen-flow runner;
399405
- the flow's own `label` names the flow in the runner's header, above the
400406
step's heading, and in the completion toast. A locale the bundle does not
401407
cover shows the label authored on the flow, and a backend that serves no
402408
label shows the flow's API name.
403409

404-
Three strings stay outside the group. A screen's `description` renders as
405-
authored. A `successMessage` authored on the flow replaces the completion
406-
sentence and renders as authored too, so the translated label appears in the
407-
toast only when the flow declares none. The runner's own chrome — the Cancel
408-
and Submit buttons — belongs to the console's message catalog rather than your
409-
app's bundle.
410+
Two strings stay outside the group. A `successMessage` authored on the flow
411+
replaces the completion sentence and renders as authored, so the translated
412+
label appears in the toast only when the flow declares none. The runner's own
413+
chrome — the Cancel and Submit buttons — belongs to the console's message
414+
catalog rather than your app's bundle.
410415

411416
**The tooling asks for these keys like any other group's.** `os lint` reports
412417
a missing `flows.*` key against `supportedLocales`, and `os i18n extract`

‎examples/app-crm/src/translations/crm.translation.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,15 @@ export const CrmTranslationBundle = defineTranslationBundle({
218218
successMessage: '🎉 线索已转化 — 已创建客户和商机。',
219219
errorMessage: '线索转化未完成 — 请检查线索后重试。',
220220
screens: {
221-
screen_already_converted: { title: '已转化' },
222-
screen_account: { title: '第 1 步,共 2 步 · 客户' },
223-
screen_opportunity: { title: '第 2 步,共 2 步 · 商机' },
221+
screen_already_converted: { title: '已转化', description: '该线索已转化为商机。' },
222+
screen_account: {
223+
title: '第 1 步,共 2 步 · 客户',
224+
description: '核对并补全从线索带过来的客户记录。',
225+
},
226+
screen_opportunity: {
227+
title: '第 2 步,共 2 步 · 商机',
228+
description: '添加商机及其产品明细,二者在同一个事务中一起保存。',
229+
},
224230
},
225231
},
226232
},

‎examples/app-todo/src/translations/ja-JP.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export const jaJP: TranslationData = {
112112
},
113113
},
114114
},
115-
success_screen: { title: 'タスクを作成しました' },
115+
success_screen: { title: 'タスクを作成しました', description: 'タスク「{{ subject }}」を作成しました!' },
116116
},
117117
},
118118
},

‎examples/app-todo/src/translations/zh-CN.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export const zhCN: TranslationData = {
120120
},
121121
},
122122
},
123-
success_screen: { title: '任务已创建' },
123+
success_screen: { title: '任务已创建', description: '任务“{{ subject }}”创建成功!' },
124124
},
125125
},
126126
},

0 commit comments

Comments
 (0)