Skip to content

fix(components): ActionParamDialog's select placeholder reads the common.select pack key - #4397

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-4386-action-param-select-placeholder
Aug 12, 2026
Merged

fix(components): ActionParamDialog's select placeholder reads the common.select pack key#4397
yinlianghui merged 3 commits into
mainfrom
claude/issue-4386-action-param-select-placeholder

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4386

ActionParamDialog's select branch rendered param.placeholder || 'Select...' in a file that imported no translation hook at all. The fallback — which fires whenever an action param of kind select declares no placeholder of its own, the ordinary case for hand-written action metadata — was therefore hardcoded English in every locale, and spelled its ellipsis in ASCII, which #3878 converged the ten packs away from.

The fallback now reads the existing common.select pack key through this package's createSafeTranslation pattern. Authored param.placeholder keeps priority — only the fallback changed.

Key choice: common.select, REUSED — no pack file changed

The PM ruling on the card was reuse-first (the #4375/#4376 pair was in flight across the packs). Measured, not assumed:

Candidate en zh Ellipsis? Verdict
common.select Select… 选择… U+2026 in all ten packs chosen
common.selectOption Select an option 请选择 none, in either rejected

Note this is a different semantic from #4391's measurement, which crowned table.search for SEARCH placeholders. This is a SELECT-trigger placeholder; common.select is that family's established key.

Why the safe hook

createSafeTranslation, not a bare useObjectTranslation: this dialog is rendered with no I18nProvider by embedded hosts and by this package's own bare-render suites (action-param-dialog-aria-required, action-param-dialog-label-association both mount it provider-less). With no provider the bare hook returns the raw KEY, so the placeholder would read common.select. The defaults-map entry is the pack's stand-in on that path and is byte-identical to en — verified programmatically, both sides ['0x53','0x65','0x6c','0x65','0x63','0x74','0x2026'].

Pre-fix red, and reverse verification

Three reverse verifications, each with the direction predicted before running it.

1. Revert the fix (restore the 'Select...' literal). Predicted 3 RED / 2 GREEN — measured exactly that:

 × renders the English pack word from the defaults map, never the raw key
 × renders the locale word when the param declared no placeholder (zh)
 × renders the English pack value — with the typographic ellipsis — under en
 Tests  3 failed | 2 passed (5)

All three fail on the right assertion — Unable to find an element with the text: 选择… / Select…. The 2 that stay green are the authored-placeholder cases, which never reach the fallback: that is the surviving pin that authored metadata keeps priority.

2. Empty the defaults map (fix otherwise intact). Predicted 1 RED / 4 GREEN, because the defaults map serves only the provider-less path — measured exactly that, and the DOM dump shows the literal text common.select rendering. The provider-mounted cases stayed green, which is what proves the two paths are independent.

3. Point the call site at a bogus key. This proves the new t() call site is genuinely visible to the i18n gate rather than passing vacuously:

1 call site references a key the en pack does not define (1 distinct):
  packages/components/src/custom/action-param-dialog.tsx:254:64  [missing-key]  common.selectNotAKey4386

A correction is folded in as its own commit: the no-provider file's docblock originally claimed it was green on both sides of the fix (the intuitive expectation for a fallback pin). Measured, it is RED before and GREEN after — the literal being replaced was Select..., so the English bytes did not survive unchanged; the ellipsis moved from ASCII to U+2026. The docblock now records the measured direction and separates which assertion catches which mutation.

Verification

All from the repo root, on the merge with origin/main at bb58d1d61:

Command Result
vitest run packages/components/ 121 files, 1077 tests passed
vitest run packages/i18n/ packages/fields/ 123 files, 2048 tests passed
tsc --noEmit (components src) exit 0
tsc -p tsconfig.test.json (components tests) exit 0
check:i18n-keys green — "Every in-scope call-site key resolves against the en pack (2859 keys)"
check:i18n-drift green — 0 en values changed
check:control-bytes green — 4101 files scanned
changeset:check + check-changeset-presence green — 1 changeset, patch, no major
eslint on the 3 changed files 0 errors (4 pre-existing any warnings, all present on origin/main at lines 41/63/64/77)

Changeset: patch (never major, per the version-alignment rule).

Recovered after a host restart

The original dev agent for this card was killed near the finish line. Inherited from it, then re-audited line by line: the implementation commit d0204ecdf and one uncommitted docblock edit. Everything below the implementation was redone from scratch in this session — the previous agent's verification results died with the host and none were trusted.

  • Inherited and kept: the implementation commit d0204ecdf, after auditing it against the card — call site fixed, red-first tests present, defaults-map entry confirmed byte-identical to en, key choice re-audited against reuse-first and found defensible. Not churned.
  • Inherited and committed: the dirty docblock edit, judged forward progress (it corrects a wrong reverse-verification claim) rather than a revert artifact, and independently re-measured here.
  • Redone from scratch: pnpm install, the origin/main merge (clean, no conflicts), all three reverse verifications, both tsc runs, every gate, and the full package suites.

origin/main moved to bb58d1d61 (#4391) under this branch and is merged in. No overlap: #4391 moved the two SEARCH placeholders to table.search and translated four English-serving values; it did not touch common.select nor action-param-dialog.tsx.

Per the card's own grading, examples/schema-catalog/.../with-placeholder.json is left as-is — sample authored metadata demonstrating the placeholder prop, not product copy.


Generated by Claude Code

claude added 3 commits August 12, 2026 01:07
)

The `select` branch rendered `param.placeholder || 'Select...'` in a file
that imported no translation hook, so the fallback — which fires whenever an
action param of kind `select` declares no placeholder of its own — was
hardcoded English in every locale and spelled its ellipsis in ASCII, which
#3878 converged the ten locale packs away from.

Route the fallback through the existing `common.select` pack key, read via
this package's `createSafeTranslation` pattern. The key is REUSED, not added:
it already ships in all ten packs and `packages/fields`' LookupField already
consumes it for the structurally identical job (a Radix select trigger's
placeholder behind the same authored-metadata-wins shape). No locale pack
file changed. Because `common.select` is already in ellipsis-glyph-3878's
CONVERGED_KEYS, the U+2026 this site now renders is pinned for free.

Authored `param.placeholder` keeps priority — only the fallback changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…4386)

The no-provider file's docblock claimed it was green on both sides of the
fix — the intuitive expectation for a FALLBACK pin, since the English bytes
are supposed to be unchanged. Measured, it is RED before and GREEN after:
the literal being replaced was `Select...`, so the English bytes did NOT
survive unchanged — the ellipsis moved from ASCII to U+2026, and
`getByText('Select…')` is what fails on the revert.

Also separates the two assertions by the mutation each catches: the glyph
one goes red on reverting the fix, the `common.select` raw-key one goes red
only if the defaults-map entry is dropped, which is the regression this file
exists to catch long after #4386.

Docblock only — no assertion changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 12, 2026 1:58am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-Np3awd3y.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.20KB 108.43KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 151.72KB 40.42KB
fields (index.js) 228.36KB 56.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 62.18KB 17.67KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 121.56KB 31.63KB
plugin-designer (index.js) 210.91KB 42.67KB
plugin-detail (index.js) 239.00KB 59.76KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.99KB 49.92KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.12KB 26.74KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 42.27KB 11.36KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

ACCEPT — PM 复核 (session session_017Qqyix2QcnpUC9XeYVDzx3), closes #4386.

  • Recovery discipline exemplary: the inherited commit was audited against the card line-by-line and kept without churn (the byte-level defaults-map identity check included), and the dirty file's disposition is the sharp case done right — it was forward progress correcting a wrong docblock claim, and the correction was independently re-measured before keeping it. This is what inherit-and-verify means.
  • Key choice affirmed: common.select reuse is measured four ways (ten packs, all U+2026; LookupField precedent for the identical Radix job; already inside the [finding] en 包 32 个值用 ASCII "..." 结尾、113 个用 U+2026 "…" —— 同一屏上省略号两种字形,且九包各自跟着抄 #3878 pin's converged set so the glyph is gated for free; zero pack files touched so the conflict window is moot). The common.selectOption rejection with the exact reason (different sentence, no ellipsis in en/zh) shows the near-miss was considered, not overlooked. Correctly distinguished from fix(i18n): search placeholders become pack values, and four English-serving values get translated (#4375 #4376) #4391's table.search (SEARCH family ≠ SELECT family).
  • Reverse verification is the model: three limbs, exact numeric predictions all matched, including limb 3 proving the i18n gate's green is a real assertion (bogus key → action-param-dialog.tsx:254:64 [missing-key]) rather than vacuous.
  • Deviations accepted: the SIGTERM'd closure build is reasoned correctly (needed four packages were Done; CI's clean-install Type Check is authoritative) and CI converged 18/18.

Flipping ready + arming auto-merge.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

action-param-dialog renders a hardcoded English Select... placeholder — untranslated in zh, and ASCII after #3878 moved the packs to U+2026

2 participants