fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(spec): translateAction overlays an action's description and its param dialog copy - #14707

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params
Sep 2, 2026
Merged

fix(spec): translateAction overlays an action's description and its param dialog copy#14707
os-sam merged 2 commits into
mainfrom
claude/issue-14254-translate-action-description-params

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14254

Placeholders below are written ACTION / PARAM rather than in angle brackets, the way the card writes them: GitHub's body sanitizer eats a-shaped fragments.

What was broken

TranslationItemSchema declares, per action, _actions.ACTION.description and _actions.ACTION.params.PARAM.{label, helpText, placeholder, options}, and the translation linter validates both — checkActionParams reports a param key the action does not declare, with a did-you-mean naming the declared ones. translateAction applied 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's description is 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/main before any edit: translateAction (now at packages/spec/src/system/i18n-resolver.ts:719, the card's :543 after #14381 landed) read label, confirmText, successMessage and resultDialog and nothing else.

What this changes

packages/spec/src/system/i18n-resolver.ts only, plus its test and a changeset.

  • lookupActionField's field union gains description, so the overlay reuses the existing object-scoped-then-globalActions walk. Both branches are the same lookup, so the globalActions half is covered by construction and pinned by a test.
  • A new translateActionParams overlays each declared params[] entry for label / helpText / placeholder / options, mirroring translateBulkActionDefs' paramsChanged shape 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, and translateAction then writes no params key at all.
  • ActionLike gains description?: string and params?: ActionParamLike[]; ActionParamLike is a new exported interface (the BulkActionParamLike pattern one surface over).

Three measurements decided the details rather than guesswork:

  1. The param key is helpText, not help.translation.zod.ts:135-137 declares helpText on an action param and aliases help / hint / tooltip onto it; the BULK param declares help and aliases the other way. The resolver reads the spelling the schema declares for THIS surface, and a test asserts no help key appears on a translated action param.
  2. Params are collected by name with a field fallbackstrName(param.name) ?? strName(param.field), packages/lint/src/validate-translation-references.ts:1224-1227 inside checkActionParams (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; on main it lives in packages/lint.
  3. options is matched on the stored value.ActionParamSchema.options is an ARRAY of { label, value } entries while ActionParamTranslation.options is a value to label MAP — the same mismatch translateObject already 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 on ActionLike and one new exported interface, ActionParamLike; api-surface/system.json and export-origins/system.json gain exactly one name each (regenerated, not hand-edited) and check:api-surface reports public API surface + factory signatures unchanged.

Verification

All readings below are on 84b89af72, this branch's final commit, unless the line says otherwise.

CommandVerdict
pnpm --filter @objectstack/spec testTest Files 455 passed (455) · Tests 12243 passed (12243) · exit 0
pnpm --filter @objectstack/spec typechecktsc --noEmit + check:scripts-typecheck + check:test-typecheck all clean; debt ledger unchanged at 54 file(s) / 262 error(s) / 146 pinned signature(s) · exit 0
pnpm --filter @objectstack/lint exec vitest run src/validate-translation-references.test.tsTest Files 1 passed (1) · Tests 59 passed (59) · exit 0
pnpm --filter @objectstack/spec check:api-surface@objectstack/spec public API surface + factory signatures unchanged · exit 0
pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. · exit 0
pnpm lint (repo-wide eslint . --no-inline-config, not narrowed)exit 0, no output
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack55 commands derived on this head; 51 green, 4 not measured (below)

Thirteen new tests sit beside the existing translateAction cases in packages/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 the params array asserted to be the SAME reference); name matched, field matched when name is absent, name preferred when both are present; an undeclared bundle param ignored; globalActions for an object-less action; and the whole thing reached through translateMetadataDocument('object', ...) for an inline action.

Reverse verification (ablation), on 3a2fdfe73 — the implementation commit, so HEAD held the fix before anything was mutated. The test imports ./i18n-resolver by relative source path, so no dist is 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:

  • Expected direction, stated before the run: RED.
  • Mutation: the description and params legs deleted from translateAction. First attempt was rejected by the script's own guard — the anchor ...(description !== undefined ? { description } : {}) matched 8 times in the file, nothing was written, and hash-object equalled the HEAD blob. Recorded because a silent no-op there would have been read as "the pins do not fire".
  • Second attempt, mutation confirmed on disk: removed text const params = translateActionParams(action, bundle, opts); count 0, const description = lookupActionField(bundle, action, 'description' count 0, injected marker count 1; blob 1ad0166dd to 5903515f1.
  • Result: 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.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the index), then hash-object back to 1ad0166dd, git diff HEAD empty, 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/spec and the @objectstack/lint dependency 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 output
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built

A fifth, node scripts/check-test-completeness.mjs, exits 3 with the local reading for this gate is NOT MEASURED by its own design: it parses a saved turbo run test log 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/ai/connect-mcp.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/api/error-catalog.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/automation/flows.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx(via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx(via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx(via helpText (symbol, a field of interface ActionParamLike), confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/layout-dsl.mdx(via helpText (symbol, a field of interface ActionParamLike))
  • content/docs/protocol/objectui/record-alert.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/views.mdx(via helpText (symbol, a field of interface ActionParamLike))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22e5236c54c7a02d5fba9325d75055a0ee96ef62packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1078cb0f4441ccae3f3c3bcae1548984cfd6fd3 — the merge of head 84b89af72fc225c0f1925413adce388b816afd30 into base 22e5236c54c7a02d5fba9325d75055a0ee96ef62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 22e5236c54c7a02d5fba9325d75055a0ee96ef62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 2, 2026
@os-sam
os-sam marked this pull request as ready for review September 2, 2026 20:05
@os-sam
os-sam enabled auto-merge September 2, 2026 20:05
@os-sam
os-sam added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 809d417Sep 2, 2026
43 checks passed
@os-sam
os-sam deleted the claude/issue-14254-translate-action-description-params branch September 2, 2026 20:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates

2 participants

@os-sam@claude