Uh oh!
There was an error while loading. Please reload this page.
fix(spec): translateAction overlays an action's description and its param dialog copy - #14707
Conversation
`TranslationItemSchema` declares `_actions.ACTION.description` and
`_actions.ACTION.params.PARAM.{label,helpText,placeholder,options}`, and the
translation linter validates both — `checkActionParams` even reports a param
key the action does not declare with a did-you-mean. `translateAction`
applied neither, so a translated deployment rendered a translated action
button that opened an untranslated parameter dialog.
The resolver now overlays `description` through the existing
object-scoped-then-`globalActions` lookup, and each `params[]` entry matched
by `name` with a `field` fallback — the same collection rule the linter
validates against — for `label` / `helpText` / `placeholder` / `options`.
`options` is matched on the stored `value` because the translation side is a
`value -> label` map while the authored side is an array. The params array
keeps its identity when nothing matched, so an action with no param
translations comes back with the array it was authored with.
No schema and no validator change: every key applied here was already
declared and already validated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE`ActionParamLike` is a new exported interface, so `api-surface/system.json` and `export-origins/system.json` gain one name each. `translateAction`'s own exported signature is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 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 a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 && git checkout a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 22e5236c54c7a02d5fba9325d75055a0ee96ef62 84b89af72fc225c0f1925413adce388b816afd30 && git checkout -B drift-repro 22e5236c54c7a02d5fba9325d75055a0ee96ef62 && git merge --no-ff 84b89af72fc225c0f1925413adce388b816afd30
node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14254
Placeholders below are written
ACTION/PARAMrather than in angle brackets, the way the card writes them: GitHub's body sanitizer eatsa-shaped fragments.What was broken
TranslationItemSchemadeclares, per action,_actions.ACTION.descriptionand_actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both —checkActionParamsreports a param key the action does not declare, with a did-you-mean naming the declared ones.translateActionapplied neither. The keys parsed, they linted, and they resolved to nothing: a translated deployment rendered a translated action button that opened an untranslated form, because an action'sdescriptionis the explanatory line under the dialog title and its params'label/helpText/placeholder/ option labels are the rest of that dialog.Premise re-verified on
origin/mainbefore any edit:translateAction(now atpackages/spec/src/system/i18n-resolver.ts:719, the card's:543after #14381 landed) readlabel,confirmText,successMessageandresultDialogand nothing else.What this changes
packages/spec/src/system/i18n-resolver.tsonly, plus its test and a changeset.lookupActionField's field union gainsdescription, so the overlay reuses the existing object-scoped-then-globalActionswalk. Both branches are the same lookup, so theglobalActionshalf is covered by construction and pinned by a test.translateActionParamsoverlays each declaredparams[]entry forlabel/helpText/placeholder/options, mirroringtranslateBulkActionDefs'paramsChangedshape so the array keeps its identity when nothing matched — an action with no param translations comes back with the very array it was authored with, andtranslateActionthen writes noparamskey at all.ActionLikegainsdescription?: stringandparams?: ActionParamLike[];ActionParamLikeis a new exported interface (theBulkActionParamLikepattern one surface over).Three measurements decided the details rather than guesswork:
helpText, nothelp.translation.zod.ts:135-137declareshelpTexton an action param and aliaseshelp/hint/tooltiponto it; the BULK param declareshelpand aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts nohelpkey appears on a translated action param.namewith afieldfallback —strName(param.name) ?? strName(param.field),packages/lint/src/validate-translation-references.ts:1224-1227insidecheckActionParams(line 1215). Mirroring it exactly is what keeps the two halves from disagreeing about which params are addressable: every key the linter accepts is a key the resolver finds. Note the card attributes this validator to@objectstack/rest; onmainit lives inpackages/lint.optionsis matched on the storedvalue.ActionParamSchema.optionsis an ARRAY of{ label, value }entries whileActionParamTranslation.optionsis avalueto label MAP — the same mismatchtranslateObjectalready resolves for field options, and the overlay follows that existing shape ({ ...opt, label: translated }, untranslated entries returned untouched).A bundle key naming a param the action does not declare is ignored: the walk is over the declared params, never over the bundle's keys. The linter already reports that key, and a resolver that invented a param from a translation file would put a control in the dialog the action cannot receive.
No schema change, no validator change, no new key, no widening of any accepted shape. Every key applied here was already declared and already validated; this is the resolver half catching up.
Public surface
translateAction's exported signature does not move. The additive changes are two optional members onActionLikeand one new exported interface,ActionParamLike;api-surface/system.jsonandexport-origins/system.jsongain exactly one name each (regenerated, not hand-edited) andcheck:api-surfacereportspublic API surface + factory signatures unchanged.Verification
All readings below are on
84b89af72, this branch's final commit, unless the line says otherwise.pnpm --filter @objectstack/spec testTest Files 455 passed (455)·Tests 12243 passed (12243)· exit 0pnpm --filter @objectstack/spec typechecktsc --noEmit+check:scripts-typecheck+check:test-typecheckall clean; debt ledger unchanged at54 file(s) / 262 error(s) / 146 pinned signature(s)· exit 0pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1)·Tests 59 passed (59)· exit 0pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged· exit 0pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date.· exit 0pnpm lint(repo-wideeslint . --no-inline-config, not narrowed)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackThirteen new tests sit beside the existing
translateActioncases inpackages/spec/src/system/i18n-resolver.test.ts, one per newly-overlaid key in both directions, using the card's own zh-CN fixture: overlay applied per key; authored value kept when the bundle is silent (with theparamsarray asserted to be the SAME reference);namematched,fieldmatched whennameis absent,namepreferred when both are present; an undeclared bundle param ignored;globalActionsfor an object-less action; and the whole thing reached throughtranslateMetadataDocument('object', ...)for an inline action.Reverse verification (ablation), on
3a2fdfe73— the implementation commit, soHEADheld the fix before anything was mutated. The test imports./i18n-resolverby relative source path, so nodistis involved and no rebuild leg is required; the mutation was proved on disk before the run and the restore proved by blob hash after it:descriptionandparamslegs deleted fromtranslateAction. First attempt was rejected by the script's own guard — the anchor...(description !== undefined ? { description } : {})matched 8 times in the file, nothing was written, andhash-objectequalled theHEADblob. Recorded because a silent no-op there would have been read as "the pins do not fire".const params = translateActionParams(action, bundle, opts);count 0,const description = lookupActionField(bundle, action, 'description'count 0, injected marker count 1; blob1ad0166ddto5903515f1.Tests 8 failed | 225 passed (233), exit 1. The eight are every overlay assertion; the other five new pins are negative by construction (authored value kept, unknown key ignored, no-params action untouched) and are green in both directions, which is what they are for.git checkout HEAD -- ABSOLUTE_PATH(never the bare form, which reads from the index), thenhash-objectback to1ad0166dd,git diff HEADempty, marker count 0.Not measured, with reasons — four of the 55 derived gates are prerequisite-blocked by the same condition, an unbuilt workspace in this worktree (only
@objectstack/specand the@objectstack/lintdependency closure were built); CI builds first, so all four run there:node scripts/check-dev-prereqs.mjs— exit 1,The workspace is not built — 1 unmet precondition, not a list of problems.pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET — this gate reads built outputpnpm check:type-check-debt— exit 3,check-type-check-coverage: PREREQUISITE NOT METpnpm --filter @objectstack/lint run check:doc-formula-expressions— exit 3,PREREQUISITE NOT MET — the workspace package @objectstack/formula is not builtA fifth,
node scripts/check-test-completeness.mjs, exits 3 withthe local reading for this gate is NOT MEASUREDby its own design: it parses a savedturbo run testlog that only CI produces.Every exit code above was captured before any pipe.
🤖 Generated with Claude Code
https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
Generated by Claude Code