You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b9a79ef
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,lint): flows translation face gains approvals.NODE.label for an approval node's step label (#24943)
Closes#24915
Clause-②: yes (widening)
## What changes
`TranslationDataSchema.flows.FLOW` carried `label`, `successMessage`,
`errorMessage`, `screens.NODE` (screen nodes) and `refusals.NODE`
(refusing `end` nodes), and nothing for an `approval` node. A language
pack had nowhere to put an approval step's name. HotCRM measured this:
`flows.opportunity_approval.screens.manager_review.title` is refused
`translation-target-unknown`, because `manager_review` is an `approval`
node and not a screen.
- **Spec** (`packages/spec/src/system/translation.zod.ts`):
`flows.FLOW.approvals.NODE.label` is a declared key. It is addressed by
the `approval` node's `FlowNode.id` and overlays that node's own
`label`. The entry is closed (`strictObject`). `title`, `name`,
`heading` and `stepLabel` are renames onto `label`, and `approval` on
the flow level is a rename onto `approvals`.
- **`objectstack validate`**
(`packages/lint/src/validate-translation-references.ts`): the
translation-reference check collects the flow's `approval` node ids at
any depth, using the same `walkFlowNodes` universe, so regions are
included. An `approvals.NODE` key over an approval node resolves. Over
anything else it is refused `translation-target-unknown`, with the
existing diagnosis style:
- a node of another type: "...declares as a `decision` node, not an
`approval`, so nothing resolves this key."
- a `screen` node: "...declares as a `screen` node; its heading is keyed
`screens.NODE.title`."
- an unknown id: "...declares no node for, so the step keeps its
source-locale label.", followed by "Did you mean" over the approval
nodes.
- **Wrong-group guidance**: a `screens.NODE` or `refusals.NODE` key
written over an approval node now says "...declares as an `approval`
node; its step label is keyed `approvals.NODE.label`." The old message
only said the node is not a screen. It also read "as a `approval` node".
## Why this spelling
- **Group `approvals`, keyed by node id**: this follows the grammar the
face already has. `screens` addresses `screen` nodes and `refusals`
addresses refusing `end` nodes, each by `FlowNode.id`, the identifier
the approvals service already holds (`sys_approval_request` stores it as
`flow_node_id`).
- **Leaf `label`, nothing else.** I read `ApprovalNodeConfigSchema`
(`packages/spec/src/automation/approval.zod.ts`). Its keys are
`approvers`, `behavior`, `minApprovals`, `lockRecord`,
`approvalStatusField`, `onEmptyApprovers`, `fallbackApprovers`,
`decisionOutputs`, `escalation` and `maxRevisions`. None of them is a
heading, instructions text or a comment prompt. The one person-facing
string an approval step carries is `FlowNode.label`. `plugin-approvals`
copies that label onto `current_step_label` when a request opens
(`approval-service.ts`, `labelOrMachineName(input.nodeLabel, ...)`). A
screen spells its leaf `title` because it overlays
`ScreenConfigSchema.title`. This leaf overlays the node label, so it is
spelled `label`, and `title` is an alias onto it.
- **Plain string, not a `{{ }}` text slot**: the node label is plain
text that is copied, not rendered through `renderTextSlot`. This is the
same as the flow's own `label`.
- **Out of scope, on purpose**: a typed `decisionOutputs[]` entry's
`label` (a field label in the decision dialog). It stays an `owed` row
in the family pin, with its reason updated. It is a separate surface
that needs its own reader.
## Tests
Measured at head `606faa06ee` (merged with `origin/main`).
- Repro first: commit `ebb460a714` added the tests before the fix.
- **Reverse verification**: after the fix was committed, I restored
`translation.zod.ts` and `validate-translation-references.ts` to the
base `81e6edd463` (tree only, `git restore --source`). The mutation was
confirmed on disk (`approvals: z.record` count 0, `APPROVAL_NODE_TYPE`
count 0). I rebuilt spec and ran the suites:
- spec: `6 failed | 158 passed`. The four new approvals cases failed,
and so did the two face-pin cases ("every keyed row ... exists in the
face", "every face key is filled").
- lint: `6 failed | 180 passed`. The five new refusal/redirect cases
failed, and so did the one-line-verdict arm coverage.
- Restored with `git checkout HEAD -- PATHS`. `git diff HEAD` was empty
and `git status` was clean. I rebuilt spec, and
`ablation-dist-preflight` found the marker present in 20 built files.
- **Controls**:
- screen and refusal keys of the same flow still resolve beside an
approval key;
- a `screens` key over a `decision` node keeps its own diagnosis;
- an approval key on a non-approval node is refused by name;
- a key the approval entry does not declare (`instructions`) is refused
at parse.
- **Family pin** (`flows-translation-face.test.ts`): `node.label` is now
`keyed` to both `screens.NODE.title` and `approvals.NODE.label` (new
`also` field), so the face-key-filled check accounts for the new key.
- **Package runs**:
- `pnpm --filter @objectstack/spec test`: 703 files, 21365 passed.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
- `pnpm --filter @objectstack/lint test`: 151 files, 7002 passed (after
the merge).
- `pnpm --filter @objectstack/lint typecheck`: OK.
- **Generated artifacts and data checks**:
- `pnpm --filter @objectstack/spec check:generated`: all 14 artifacts
are up to date. These were regenerated with `--fix`:
`content/docs/references/system/translation.mdx`, the strictness-ledger
counts and the liveness state counts.
- `pnpm check:i18n`: OK, and `check:nul-bytes`: OK.
- **Derived gates** (`dispatch-gates --commands`, reconciled with
`--ran`): 113 derived, 113 run, all `exit 0`, 0 NOT-MEASURED (a derived
zero: every family recorded an exit code), re-run on the merged head
`606faa06ee`.
- **eslint, narrowed to the 5 touched source and test files**
(`--no-inline-config --format json`): 5 files, 0 errors, 0 warnings. All
5 are inside the config (`--print-config` resolves each). The config
declares no type-aware linting (no `parserOptions.project`), so this
diff cannot change the verdict on any untouched file.
## Liveness
`packages/spec/liveness/translation.json` gains a `flows.approvals` row
(drilled to `label`) with status `planned`, not `live`. The reader that
renders the step label in the reader's locale is the `plugin-approvals`
half of #24863, which this PR does not touch. It is a contract-first
split, the same way `screens` and the toasts landed. #24863 remains
open.
## Review tier
`packages/spec` is a published schema, so this PR owes a
`CONTRACT_REVIEW_TIER` review of record before it enters the queue
(pm-dispatch `references/contract-review.md`). It touches no governed
surface (`.claude/**`, `docs/adr/**`, `AGENTS.md`, workflows,
`skills/**`), so no Tier S or Tier H landing rule applies.
## Acceptance notes
- The CLI i18n extractor (`packages/cli/src/utils/i18n-extract.ts`) does
not emit `approvals.NODE.label` slots in a skeleton. Asking apps to
translate a key with no reader would be a declared-but-not-honoured
demand. That demand belongs with the reader in #24863.
`check:i18n-walk-parity` is green with this diff.
- The flow-level alias `steps: 'screens'` is left as it is. An author
who writes `steps` for an approval step is told it renames to `screens`,
and validate then points them at `approvals`.
- No `flowApprovalLabelKey` resolver helper is added. With no reader it
would be an export with no consumer. The reader card can add it next to
`flowRefusalMessageKey`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec): the `flows` translation face gains `approvals.<node_id>.label`, the step label of an `approval` node
7
+
8
+
Clause-②: yes (widening)
9
+
10
+
-**Before.**`TranslationDataSchema.flows.<flow>` carried `label`, `successMessage`, `errorMessage`, `screens.<node_id>` (screen nodes) and `refusals.<node_id>` (refusing `end` nodes), and nothing for an `approval` node. A language pack had nowhere to put an approval step's name: `approvals` was refused as an unknown key, and a `screens.<node_id>.title` key over an approval node was refused `translation-target-unknown` by `objectstack validate`, because the node is not a screen.
11
+
-**Now.**`flows.<flow>.approvals.<node_id>.label` is a declared key, addressed by the `approval` node's `id` (at any depth, regions included) and overlaying that node's own `label`, the step name an approval request shows. The entry is closed: `title`, `name`, `heading` and `stepLabel` are named as renames onto `label`, and `approval` as the rename onto the `approvals` group.
12
+
-**`objectstack validate`.**`@objectstack/lint`'s translation-reference check accepts the key over an `approval` node and refuses it, as `translation-target-unknown`, over an unknown node or a node of another type (a `screen` node is pointed at its `screens.<node_id>.title`). A `screens` or `refusals` key written over an approval node is now pointed at `approvals.<node_id>.label` instead of being told only that the node is not a screen.
13
+
-**Not yet rendered.** This is the spec half: the key parses and validates, and the approvals service does not read it yet, so an approval request still shows the authored step label. Its liveness row is `planned` until that reader lands.
14
+
-**Unchanged.** Every existing `flows` key, and every verdict on a key over a node that is not an approval node.
Copy file name to clipboardExpand all lines: content/docs/references/system/translation.mdx
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -262,6 +262,7 @@ Translation data for a single object
262
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 |
263
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`) |
264
264
|**refusals**|`Record<string, { message?: string }>`| optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'`|
265
+
|**approvals**|`Record<string, { label?: string }>`| optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
@@ -464,6 +465,7 @@ Translation data for a single object
464
465
|**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 |
465
466
|**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`) |
466
467
|**refusals**|`Record<string, { message?: string }>`| optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'`|
468
+
|**approvals**|`Record<string, { label?: string }>`| optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
@@ -626,6 +628,7 @@ Translation data for a single object
626
628
|**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 |
627
629
|**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`) |
628
630
|**refusals**|`Record<string, { message?: string }>`| optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'`|
631
+
|**approvals**|`Record<string, { label?: string }>`| optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
0 commit comments