Repository navigation
fix(plugin-approvals): serve the approval flow, step and object labels in the reader's locale - #24977
Conversation
…reader's locale The approvals door served the request's open-time snapshot of the flow and step labels to every reader, and the object's schema label on the inbox row. Pick flows.FLOW.label, flows.FLOW.approvals.NODE.label and objects.OBJECT.label through the i18n service in ExecutionContext.locale at read time, falling back to the stored snapshot. The stored columns are not touched. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK
…al labels Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK
…label; the door overlays the reader's translation Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK
…proval-labels-reader-locale
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 6 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 eceb464d8622837a6be49b6e4b0fd9392ee4f1c5 && git checkout eceb464d8622837a6be49b6e4b0fd9392ee4f1c5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6440cf16334e1c8106dfdb31d87b6ca728d176c2 2335508393a37cc3463b8b6721c9434d71264995 && git checkout -B drift-repro 6440cf16334e1c8106dfdb31d87b6ca728d176c2 && git merge --no-ff 2335508393a37cc3463b8b6721c9434d71264995
node scripts/docs-audit/affected-docs.mjs --json 6440cf16334e1c8106dfdb31d87b6ca728d176c2
|
席位核对 · ACCEPT ·
|
Fixes #24863
Clause-②: no
This is the services half of #24863 (triage split R539): the request labels and the inbox row's object label. The
approval.requestednotification half is NOT in this PR. It needs a decision, set out in Open questions below, so the card stays open (Part of, notFixes).What changes
The approvals door now serves a request's flow, step and object labels in the reader's locale, picked when the row is read.
GET /api/v1/approvals/requests(the inbox and the record's approval panel),GET /api/v1/approvals/requests/:id, and the request a decision answers with.process_label,step_label/current_step_label(two names of one value, which move together) andobject_label. The get's step progress (flow_steps) names each step the same way, so the strip andcurrent_step_labelagree.flows.FLOW.label,flows.FLOW.approvals.NODE.label(the key PR feat(spec,lint): flows translation face gains approvals.NODE.label for an approval node's step label #24943 added) andobjects.OBJECT.label(objectLabelKey).i18nservice, or a service that throws.process_label/current_step_labelcolumns. No column is added, rewritten or migrated.Files
packages/plugins/plugin-approvals/src/reader-locale-labels.ts(new): the keys, the pick and the overlay.approval-service.ts: the new optionApprovalServiceOptions.i18nServiceSource, andreaderLabels(context).listRequestsandloadRequestapply it afterenrichRows, andattachFlowStepstakes the same labels.approvals-plugin.ts: one lazyi18nreader feeds bothmessageTranslator(unchanged behaviour) andi18nServiceSource.display-labels.tsand the open-time snapshot comment: the docblocks now say the chain answers the authored label and the door overlays the translation..changeset/24863-approval-labels-reader-locale.md:@objectstack/plugin-approvalspatch.Mechanism (measured; reused, not a second one)
Ruling 6093472708 on #22507 says a server-rendered user string is translated where it is rendered. I measured how this repo's server already renders a user-read string in the caller's language. The pattern is the same in three places:
objectDisplayLabel(objectLabelKey);packages/rest/src/not-null-refusal-label.ts;renderFlowTextSlot.All three take
ExecutionContext.locale, negotiate it against the i18n service'sgetLocales()withresolveBundleLocale, callII18nService.t(key, locale)(a miss echoes the key), and fall back to the authored text. This PR uses the same sequence. The service is read lazily from the kernel (ctx.getService('i18n')), the same read the plugin's existingmessageTranslatoralready makes.The mechanism assumptions, measured on
origin/main693352e4c2rowFromRequest(approval-service.ts:1203, labels at:1233–:1239) builds the labels fromrequestDisplayLabels(the stored column, then the config snapshot, then the machine name).listRequests(:8109, rows at:8160) andloadRequest(:8274). Those feedgetRequest(:8247) andreadBackRequest(:8232, a decision's echo).object_labelwasobjectLabelOf(:7374), the schema label, set at:7513.attachFlowSteps(:8391/:8403) used the live node's authored label.packages/rest/src/approvals-routes.ts:214(list) and:310(get).sys-approval-request.object.ts:130–:163) and the token action page (action-link-pages.ts:65–:68).ExecutionContext.locale. The shared assembler sets it torequestLocale ?? localization?.locale(packages/core/src/security/assemble-execution-context.ts:388), and the REST approvals routes pass that context (resolveExecCtx). Thei18nservice is reachable fromplugin-approvalsthroughctx.getService('i18n'), already used atapprovals-plugin.ts:252for refusal copy.process_nameisflow:plusFlow.name(the engine's$flowName,approval-node.ts:180,service-automationengine.ts:13331), andcurrent_stepis the node id (approval-service.ts:3750). No flow definition is read. A process name without theflow:prefix names no flow, so its labels stay as stored.Tests
All runs are on head
2335508393(the merge oforigin/main6440cf1633), underos-verify-lock.sh.reader-locale-labels.test.ts(unit, 15 cases).TranslationDataSchema, with a control that a step leaf the face does not declare is refused.zhis negotiated tozh-CN, also against an exact-match service.t()per key per read.reader-locale-display-labels.integration.test.ts(10 cases). The rig is a realObjectQLover better-sqlite3, the realAutomationEnginerunning a two-step flow (so the real approval node opens the request), the realApprovalsServicePlugin.startover a kernel whosei18niscreateMemoryI18nloaded with a zh-CN pack, and the pack parsed byTranslationDataSchemafirst.flow_steps, and a decision's echo.zhreader reaches the zh-CN pack.Account Approval/Manager Reviewafter the reads.pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2:Test Files 92 passed (92),Tests 1203 passed (1203).pnpm --filter @objectstack/plugin-approvals typecheck: exit 0. It includescheck:test-typecheck: OK, so the new test files compile with no new debt.pnpm --filter @objectstack/plugin-approvals build: exit 0.turbo run build --filter=@objectstack/plugin-approvals^... --concurrency=2, 32 of 32 successful.scripts/ablation-replace.mjsin WRAP mode with a trap restore. The new tests import the subject by relative path, so they readsrc/and no build is needed for either leg.readerLabelsreturn nothing (on disk: anchor 1→0, marker 0→1). Result:Tests 6 failed | 19 passed (25). Exactly the six zh-CN pins went red; every as-stored pin and the unit file stayed green.attachFlowStepsname steps by the authored label only. Result:1 failed | 9 passed (10), and the one red was the step-progress pin.git checkout HEAD -- PATH. The blob after restore equals HEAD (06899aa721b5),git diff HEADis empty, andgit status --porcelainis clean.Gates
Local subset, at head
2335508393, under the lock. Each exit code was captured before any pipe.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 70 families (vs merge base6440cf163, 7 paths). All 70 were run.pnpm check:dual-build-cjs-loadsansweredPREREQUISITE NOT MET. It reads every package'sdist/, and 38 packages are unbuilt in this tree. NOT MEASURED locally; CI runs it inLint & Repo Gates.pnpm check:i18n. It first exited 3 (the CLI was not built). After the prerequisite closure the gate names (turbo run build, 61 of 61 tasks), it exited 0:check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys).--ranreconciliation:70 derived famil(ies) accounted for — 69 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).test:repo: the diff touches none of its files, so it runs in CI'sTest Core.pnpm lintand the whole-repo farm.Open questions (decision frame: real business need, long-term soundness, preventing AI mistakes, no scope creep at the startup stage)
1. The
approval.requestednotification body (and its siblings)Measured.
approval-service.ts:3890–:3901): titleApproval requested, bodyA decision on ${subject} is waiting on you., wheresubjectisnameSubjectRecord(the schema label plus the record id).service-messaging/src/recipient-locale.ts(the stated ONE read point): the recipient'ssys_user.locale, then the deployment default, resolved after fan-out.@objectstack/spec/system) holds no notification sentences, and the plugin's packs hold object translations only.packages/spec(or a new plugin catalog). This dispatch forbids both.approval.reminder,approval.sla_breached,approval.approved/approval.rejectedand the OOO notices.Options.
recipient-locale.tsand the shared catalog, which gains notification entries in en / zh-CN / ja-JP / es-ES.Recommendation: B. It fixes the measured defect at the smallest cost that keeps one catalog and one locale read point. A stays the target if a second non-approval producer asks for localized notifications.
2. The generic data door's list views
Setup → Approvals → Requestsand its siblings are list views oversys_approval_request. They serve the stored columns by the #23889 contract, so a zh-CN reader still sees the authored labels there.?q=search matches the stored text.Recommendation: A, until a measured user need appears. HotCRM measured the inbox and the record panel, which this PR fixes.
Acceptance notes
action-link-pages.ts, bilingual en/zh copy) has no reader locale: the bearer has a token, not a session, and its context is the system's.?q=search matches the stored labels.packages/specfollow-ups this reader makes due. They are spec-lane edits, outside this dispatch.packages/spec/liveness/translation.json→flows.approvals(and.label) says "Flip toliveon the commit that cites that reader". This PR is that reader.ApprovalRequestRow.process_label/current_step_label/object_label(packages/spec/src/contracts/approval-service.ts:220–:243) describe the authored/schema label. They now hold only for a reader with no translation.flowApprovalLabelKeyhelper next toflowRefusalMessageKey. This PR spells the two flow keys locally inreader-locale-labels.ts, held toTranslationDataSchemaby a test. A spec helper would let the reader import them.flows.FLOW.labelonly on flows with a screen node, soi18n/missing-flownever asks an app to translate an approval flow or its steps (split from #24863) #24916). The extractor asking apps to translate approval flows and steps is cli(i18n): the extractor and coverage ask forflows.FLOW.labelonly on flows with a screen node, soi18n/missing-flownever asks an app to translate an approval flow or its steps (split from #24863) #24916's. With this reader landed, that ask is no longer a declared-but-not-honoured demand.step_name, not the authoredstep_labelthat everysys_approval_actionrow stores objectui#13248 (the history row drawsstep_name) will readsys_approval_action.step_label, a stored snapshot served through the data door. It will show the authored step label, not the reader's. That is a carrier note for objectui#13248, not filed.getLocales()now have four consumer copies (objectql, rest, automation, here) around the one rule. This is an observation, not filed.Generated by Claude Code