Skip to content

Commit f89b5f5

Browse files
feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face (#22555)
Part of #22507 Clause-②: yes (narrowing) This PR keys two of the three user-read flow slots that had no key in the `flows` translation face: a screen field's option labels and the flow's terminal toasts. It also adds the family pin. The third slot, a screen's `description`, is server-rendered, so this PR stops there and reports it for a decision (see "Where this PR stops"). That is why the first line is `Part of` rather than the closing keyword the dispatch named: merging this must leave the card open for the `description` half. ## What changes **`@objectstack/spec`** - `TranslationDataSchema.flows` gains three keys. - `flows.FLOW.successMessage` and `flows.FLOW.errorMessage`: the terminal toasts. Aliases `success` / `successText` / `error` / `errorText` rename onto them. - `flows.FLOW.screens.NODE_ID.fields.FIELD.options.VALUE`: a screen field's option labels. `choices` / `values` rename onto `options`. - Each option is keyed by its value coerced to a string, `String(value)`, through one function: `flowScreenFieldOptionKey` (`@objectstack/spec/automation`). This is the seat's choice in the claim. It is also the console select's own key for an option (`SelectItem value={String(o.value)}` in objectui's `ScreenView`), and the coercion `translateAction` already applies to an action param's inline options. - **The one narrowing.** `ScreenFieldConfigSchema` refuses two options of one field whose values collide as text, such as `1` and `"1"`, `true` and `"true"`, or a repeated value. - The refusal lands at the second option's `value`. At the flow door it reads `nodes[1].config.fields[0].options[1].value`. - It leads with `SCREEN_FIELD_OPTION_VALUE_COLLISION`, names both options by position and value, and gives the remedy. - Measured reach: 0 collisions. The examples hold 35 flows, 6 screens, 2 option fields and 10 options, all with string values (measured at `37c711449` by loading the `app-crm`, `app-todo` and `app-showcase` flows). hotcrm at `f0afcbda0` and hotclm at `14c899fc4` were read by inspection: their option values are string literals or come from an object field's string options. - `translateFlow` applies the toasts, only where the flow authors one, so a bundle cannot add a toast. It also applies the option labels, option by option, falling back to the authored label. - `resolveFlowScreenFieldOptions(bundle, flowName, nodeId, field, opts)` is new, for a runner that holds a served `ScreenFieldSpec`. - `FLOW_TERMINAL_MESSAGE_KEYS` is new, the key list the extractor and the schema pin share. - The exclusion texts that no longer hold were rewritten: `FLOW_SCREEN_FIELD_NO_OPTIONS` is removed, the toast guidance is removed, and the `description` guidance now says why it is unkeyed and what the screen face carries. - The liveness ledger gains `translation/flows.successMessage` and `flows.errorMessage`, both `planned` (contract-first split, as `screens` was), and dated notes on `flows` and `flows.screens` for the option map. **`@objectstack/lint`**: `validateTranslationReferences` judges both new key families. - A translated toast over a flow that declares none is `translation-target-unknown` (error). - An option key that names no declared option, or names one by its label, is `translation-option-key-unknown` (warning). This is the same rule and severity as the object-field and action-param legs. **`@objectstack/cli`**: `os i18n extract` and the coverage gate scaffold and demand the new keys in the `flow` bucket. - The option keys go through the same key function. - The completion toast is demanded for a flow with a screen, and the failure toast for every flow. Each predicate mirrors the reader that will draw it (see below). **Examples**: `app-crm` (zh-CN toasts) and `app-todo` (zh-CN and ja-JP option labels) are translated, so `check:i18n-coverage` holds at its baseline. Without this it grew 101 → 103 and 106 → 126. These files are outside the claim's file surface; the report declares it. ## The enumeration, and the family pin `packages/spec/src/system/flows-translation-face.test.ts` walks the flow schemas mechanically and lists every text-bearing leaf. Each leaf is held against one recorded disposition: `keyed` (a face key that must parse and survive), `noReader`, `elsewhere` (another declared route, which must exist) or `owed` (pinned exactly, shrink-only). - **Tier 1** covers every text leaf of `FlowSchema` (minus `nodes`), `FlowNodeSchema` (minus `config`), `ScreenConfigSchema` and `EndConfigSchema`. - **Tier 2** covers the copy-named text leaves of every other declared node contract (the builtin executor contracts and `approval`), plus every `FLOW_NODE_TEXT_SLOTS` entry. It also fails on a stale row, and on a face key that no slot fills. | Slot | Disposition | |---|---| | `Flow.label` | keyed `label` | | `Flow.successMessage` / `errorMessage` | keyed (new) | | screen `title`, and a screen node's `label` as its fallback heading | keyed `screens.NODE_ID.title` | | screen field `label` / `placeholder` / `inlineHelpText` | keyed | | screen field `options[].label` | keyed `…options.VALUE` (new) | | end `message` (refused) | keyed `refusals.NODE_ID.message` | | **screen `description`** | **owed: server-rendered, decision pending** | | notify `title` / `message` | elsewhere: the notify `template` path (sys_email_template, per recipient locale) | | **approval `decisionOutputs[].label`** | **owed: read in the approval decision dialog, no face at all** | | `Flow.description`, edge `label`, variables, ids, predicates, `defaultValue`, `defaults`, `objectName`, framework/lock fields, … | noReader, each with its reason | `noReader` means that no person running the flow reads the text. `Flow.description` is drawn by Studio's metadata viewer only, so it is recorded that way, with that reason. **It bites** (ablation, each leg through `scripts/ablation-replace.mjs` with an anchor hit of 1 → 0, the pin run under the verify lock, then restored with blob == HEAD and an empty `git diff HEAD`). Predicted: red. Observed: red. - Planting `subtitle: z.string().optional()` on `ScreenConfigSchema` (blob `5b4cf3fe` → `ac16dd5a` → `5b4cf3fe`) gives 1 failed / 6 passed: `expected [ 'screen.subtitle' ] to deeply equal []`. - Renaming the face's `successMessage` key (blob `8d5dd093` → `05480fb1` → `8d5dd093`) gives 2 failed / 5 passed: the keyed-row check and the face-fill check. ## Where this PR stops: a screen's `description` is server-rendered The dispatch orders a stop before touching `service-automation` if a slot proves server-rendered, and `description` does. `service-automation`'s screen executor renders `config.description` per run through `renderTextSlot` (`{{ }}` holes over the flow's variables) and serves the result as `ScreenSpec.description`. An overlay on the served string would draw a translated hole literally. The face's existing `title` key already has that boundary, as FlowRunner's own header records. Templated descriptions are real: app-todo's `success_screen` (`Task "{{ subject }}" created successfully!`) and hotclm's `contract_intake` "First version" screen (`description: '{templateNote}'`). So the key is not added. The options for the seat: - **A.** The engine picks the translated template in the run's locale before `renderTextSlot`, the `refusals` route. This touches `service-automation`, and the same change should move `title` too. - **B.** A client overlay, with the schema refusing a `{{ }}` hole in a translated `description`. A templated description then stays untranslatable. - **C.** A client overlay with `title`'s silent boundary. The pin records `description` as owed, so whichever lands must delete that row. ## What the objectui runner must read (for the seat's objectui card) - `flows.FLOW.screens.NODE_ID.fields.FIELD.options.VALUE`: in `FlowRunner.tsx` `localizeScreen`, set each field's `options` to `resolveFlowScreenFieldOptions(bundle, flowName, screen.nodeId, field, { locale: language })` before `ScreenView` draws its `SelectItem`s. - Not through `FLOW_SCREEN_FIELD_COPY_KEYS`: the option map is not a string key and is deliberately not in that list. - `flows.FLOW.successMessage`: in `FlowRunner.tsx`, the terminal-success toast (`outcome.successMessage || t('flowRunner.completed', …)`), via `translateFlow({ name: flowName, successMessage: outcome.successMessage }, bundle, { locale: language }).successMessage`. - `flows.FLOW.errorMessage`: in `utils/flowResponse.ts` `flowFailureMessage`, which reads `data.errorMessage`, from `error.details.errorMessage` on the 400 `FLOW_FAILED` path. It is read by FlowRunner's resume and by both launch hosts through `judgeFlowLaunch` (`useConsoleActionRuntime`, `RecordDetailView`). - The launch path has no bundle or flow name there today, so the card threads them. - The runner's header notes that say `description` / `inlineHelpText` / `FlowSchema.successMessage` are off the surface are stale. - ⛔ No runner-side guessing from an object field's options: a screen field declares no such binding. ## Verification (final head `c17df03f0`, which merges `origin/main` `4638625e0`) - **Typecheck**: `pnpm --filter PKG run typecheck` exited 0 for `@objectstack/spec` (source, scripts, and the test layer: 52 files / 246 errors / 135 pinned signatures held), `@objectstack/lint` and `@objectstack/cli`. - **Spec tiers in full**, under the verify lock: - `local`: 637 files, 18977 passed and 1 todo. - `repo`: 54 files, 915 passed. - **Lint**: the full suite, 133 files / 6096 passed. `validate-translation-references.test.ts` alone: 162 passed. - **CLI**: the unit-tier `test/i18n-*` files, 24 files / 285 passed. The new `i18n-flow-toast-option-coverage.test.ts` is among them. The integration tier is declared to CI. - **Generated**: `pnpm --filter @objectstack/spec check:generated` reports "All 15 generated artifacts are up to date", measured against a dist built by that run after the merge. The dropped-refinements count is unchanged at 688 / 226: the collision check lives in the existing `ScreenFieldConfigSchema` refinement. - **Gates**: `node scripts/pm/dispatch-gates.mjs --commands` derived 115 commands at this head. All 115 were run with their exit codes recorded, and `--ran` reports "115 run, 0 NOT-MEASURED (a DERIVED zero)". - 114 exited 0. - `check:skill-examples` and `check:dual-build-cjs-loads` first refused (exit 3, PREREQUISITE NOT MET). They exited 0 after building the named packages. - `pnpm check:platform-checklist` exits 1, and it is not this diff. It reads `ABSENT SYMBOL … attachment-access-hooks.ts#canEdit` and `SYMBOL ANCHORS LOST — 27 … floor is 28` in `areas/attachments-storage.json`. A clean worktree of `origin/main` `4638625e0` exits 1 with the same two lines. This diff touches neither file. - **ESLint**, narrowed to the 17 changed `.ts` files: `eslint --no-inline-config --format json` reports 17 files, 0 errors and 0 warnings. - None of the files is ignored; an ignored file would be a warning. - `eslint.config.mjs` enables no type-aware linting, so this diff cannot move a verdict on an untouched file. - **Size**: about 1,690 changed lines, generated files included. ## Acceptance notes - The two new key families are `planned`. The objectui runner reads neither yet, so until its card lands a translated option or toast is stored and the authored text is drawn. Meanwhile the extractor demands them, the same contract-first window `screens` had. - The screen `title` key has the same server-render boundary as `description` (a translated `{{ }}` hole draws literally). Option A above would fix both. - Bulk-action params still exclude option labels with the old "value is unconstrained" reasoning (`BULK_PARAM_NO_OPTIONS`). An action param's inline options are keyed by `String(value)` with no collision refusal. Neither is in this card. --- _Generated by [Claude Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fbb065f commit f89b5f5

27 files changed

Lines changed: 1592 additions & 97 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/lint": minor
4+
"@objectstack/cli": minor
5+
---
6+
7+
feat(i18n): a screen field's option labels and a flow's terminal toasts have translation keys (`flows.<flow>.screens.<node_id>.fields.<field>.options.<value>`, `flows.<flow>.successMessage` / `.errorMessage`)
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) A value-domain narrowing on one metadata shape, ScreenFieldConfigSchema's options array. No spec key, export, option, config field or stored shape is removed, renamed or re-shaped, so there is no tombstone and no rename for `objectstack migrate meta` to apply. What narrows is which option-value combinations one screen field accepts: two options whose values read the same as text are now refused at parse. There is no lossless rewrite for such a pair (nothing in the metadata says which of the two values the author meant, or what the other should become), and the refusal itself names the pair and the fix at every door that parses a flow. Measured reach is 0: no flow in this repository's examples carries such a pair. The other categories are closed on facts: the packages publish (not unpublished); no ADR-0087 id covers this rule and this diff adds none (not registered / already-registered); and no published interface or type is narrowed, the new members being additive (not runtime-interface-only / type-surface-only). -->
12+
13+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. Two options of one screen field whose values read the same as text — `1` and `"1"`, `true` and `"true"`, or a repeated value — are now refused when the flow is parsed (`FlowSchema`, `objectstack validate`, the metadata save door, `registerFlow`), at the second option's `value`. Neither the option's label translation (keyed by the value as text) nor the console's select can tell such a pair apart. To fix a refused flow, give each option of the field a value that stays distinct as text, or drop the duplicate. Measured reach in this repository: 0 — no flow in its examples carries such a pair.
14+
15+
- **What is new.** A fully translated screen flow no longer has to keep its select options and its completion or failure toast in the source language. Translate an option's label under `flows.<flow_name>.screens.<node_id>.fields.<field_name>.options.<value>`, and the flow's own toasts under `flows.<flow_name>.successMessage` and `flows.<flow_name>.errorMessage`.
16+
- **How an option is addressed.** By its value read as text: `String(value)`, so the option `{ value: 1, label: 'Tier 1' }` is translated under `options.1` and `{ value: true, … }` under `options.true`. `flowScreenFieldOptionKey(value)` (`@objectstack/spec/automation`) spells the key. Not by position (reordering options would re-point every translation), and not by an object field's options (a screen field declares no such binding).
17+
- **One new refusal**, the narrowing the BREAKING note above describes: option values of one screen field must stay distinct as text, because the label translation is keyed by them.
18+
- **The toasts translate only where the flow authors one.** A bundle cannot add a toast to a flow that declares none; `objectstack validate` refuses such a key (`translation-target-unknown`, error).
19+
- **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.
20+
- **`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`).
21+
- **`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.

‎content/docs/references/api/protocol.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1623,7 +1623,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16231623
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
16241624
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
16251625
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
1626-
| **flows** | `Record<string, { label?: string; screens?: Record<string, object>; refusals?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
1626+
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
16271627
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
16281628
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
16291629
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |

‎content/docs/references/automation/builtin-node-config.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
241241
| **label** | `string` | optional | Display label |
242242
| **type** | `string` | optional | Input type |
243243
| **required** | `boolean` | optional | Whether a value is required to submit |
244-
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field |
244+
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field; option values must stay distinct when read as text |
245245
| **defaultValue** | `any` | optional | Prefilled value (interpolates `{token}` templates) |
246246
| **placeholder** | `string` | optional | Input placeholder text |
247247
| **visibleWhen** | `string` | optional | CEL predicate controlling visibility, evaluated client-side |
@@ -263,7 +263,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
263263
| **label** | `string` | optional | Display label |
264264
| **type** | `string` | optional | Input type |
265265
| **required** | `boolean` | optional | Whether a value is required to submit |
266-
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field |
266+
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field; option values must stay distinct when read as text |
267267
| **defaultValue** | `any` | optional | Prefilled value (interpolates `{token}` templates) |
268268
| **placeholder** | `string` | optional | Input placeholder text |
269269
| **visibleWhen** | `string` | optional | CEL predicate controlling visibility, evaluated client-side |
@@ -276,7 +276,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
276276

277277
| Property | Type | Required | Description |
278278
| :--- | :--- | :--- | :--- |
279-
| **value** | `any` | ✅ | Stored value |
279+
| **value** | `any` | ✅ | Stored value; it also addresses the option as text (its label translation key), so it must stay distinct from its siblings as text |
280280
| **label** | `string` | ✅ | Display label |
281281

282282

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ Platform translation data — the per-app groups plus the platform-only `setting
176176
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
177177
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
178178
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
179-
| **flows** | `Record<string, { label?: string; screens?: Record<string, object>; refusals?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
179+
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
180180
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
181181
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
182182
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
@@ -258,6 +258,8 @@ Translation data for a single object
258258
| Property | Type | Required | Description |
259259
| :--- | :--- | :--- | :--- |
260260
| **label** | `string` | optional | Translated flow label |
261+
| **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 |
262+
| **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 |
261263
| **screens** | `Record<string, { title?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
262264
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
263265

@@ -376,7 +378,7 @@ Per-app translation data for objects, apps, and UI messages
376378
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
377379
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
378380
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
379-
| **flows** | `Record<string, { label?: string; screens?: Record<string, object>; refusals?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
381+
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
380382
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
381383
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
382384

@@ -457,6 +459,8 @@ Translation data for a single object
457459
| Property | Type | Required | Description |
458460
| :--- | :--- | :--- | :--- |
459461
| **label** | `string` | optional | Translated flow label |
462+
| **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 |
463+
| **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 |
460464
| **screens** | `Record<string, { title?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
461465
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
462466

@@ -526,7 +530,7 @@ One locale of translations — the `translation` metadata type
526530
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
527531
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
528532
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
529-
| **flows** | `Record<string, { label?: string; screens?: Record<string, object>; refusals?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
533+
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
530534
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
531535
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
532536
| **locale** | `string` | ✅ | BCP-47 locale this item translates (e.g. "zh-CN") |
@@ -617,6 +621,8 @@ Translation data for a single object
617621
| Property | Type | Required | Description |
618622
| :--- | :--- | :--- | :--- |
619623
| **label** | `string` | optional | Translated flow label |
624+
| **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 |
625+
| **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 |
620626
| **screens** | `Record<string, { title?: string; fields?: Record<string, object> }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) |
621627
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
622628

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ export const CrmTranslationBundle = defineTranslationBundle({
215215
flows: {
216216
crm_convert_lead_wizard: {
217217
label: '将线索转化为客户和商机',
218+
successMessage: '🎉 线索已转化 — 已创建客户和商机。',
219+
errorMessage: '线索转化未完成 — 请检查线索后重试。',
218220
screens: {
219221
screen_already_converted: { title: '已转化' },
220222
screen_account: { title: '第 1 步,共 2 步 · 客户' },

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,15 @@ export const jaJP: TranslationData = {
101101
title: 'タスクの詳細',
102102
fields: {
103103
subject: { label: 'タスクの件名' },
104-
priority: { label: '優先度' },
104+
priority: {
105+
label: '優先度',
106+
options: { low: '低', normal: '通常', high: '高', urgent: '緊急' },
107+
},
105108
dueDate: { label: '期日' },
106-
category: { label: 'カテゴリ' },
109+
category: {
110+
label: 'カテゴリ',
111+
options: { personal: '個人', work: '仕事', shopping: '買い物', health: '健康', finance: '財務', other: 'その他' },
112+
},
107113
},
108114
},
109115
success_screen: { title: 'タスクを作成しました' },

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,15 @@ export const zhCN: TranslationData = {
109109
title: '任务详情',
110110
fields: {
111111
subject: { label: '任务主题' },
112-
priority: { label: '优先级' },
112+
priority: {
113+
label: '优先级',
114+
options: { low: '低', normal: '普通', high: '高', urgent: '紧急' },
115+
},
113116
dueDate: { label: '截止日期' },
114-
category: { label: '分类' },
117+
category: {
118+
label: '分类',
119+
options: { personal: '个人', work: '工作', shopping: '购物', health: '健康', finance: '财务', other: '其他' },
120+
},
115121
},
116122
},
117123
success_screen: { title: '任务已创建' },

‎packages/cli/src/utils/i18n-coverage.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,10 @@ const COVERAGE_SOURCE: Record<ExpectedEntry['source'], CoverageIssue['source']>
356356
picklist: 'picklist',
357357
page: 'page',
358358
// Screen-flow copy (`flows.<f>.label`, `flows.<f>.screens.<n>.title`, and
359-
// the per-field `label` / `placeholder`) and a refused `end` node's message
360-
// (`flows.<f>.refusals.<n>.message`, #22450) — the author's own text, so
359+
// the per-field `label` / `placeholder` / option labels), the terminal
360+
// toasts (`flows.<f>.successMessage` / `.errorMessage`, #22507) and a refused
361+
// `end` node's message (`flows.<f>.refusals.<n>.message`, #22450) — the
362+
// author's own text, so
361363
// it keeps its own bucket and reports as `i18n/missing-flow` rather than
362364
// folding away with `--include-platform`. Until this bucket existed the
363365
// family could not report a screen-flow gap at all: HotCRM measured
@@ -426,7 +428,7 @@ const SOURCE_SURFACE: Record<CoverageIssue['source'], string> = {
426428
dataset: 'datasets',
427429
picklist: 'picklists',
428430
page: 'pages',
429-
flow: 'flow screens and refusals',
431+
flow: 'flow screens, toasts and refusals',
430432
metadataForm: 'metadata forms',
431433
};
432434

0 commit comments

Comments
 (0)