Found while implementing objectstack#5430 (the nav-overlay Close item). Not fixed there — the fix lands in a No-Touch zone and needs a route decision.
What
Both auto-rendered close buttons in the Shadcn primitives carry their accessible name as a hardcoded English sr-only span:
| File |
Line |
packages/components/src/ui/sheet.tsx |
80 |
packages/components/src/ui/dialog.tsx |
57 |
The button is icon-only (a lucide X), so that span is the control's name to a screen reader. Under a zh/ja/es session every drawer and every modal in the console announces "Close" in English.
Why it is live, and how #5430 surfaced it
NavigationOverlay is the one consumer that hides the primitive's own close ([&>button:last-of-type]:hidden) because it renders its own in the header — so there it is display:none and out of the accessibility tree. Everywhere else it renders normally. Roughly 20 SheetContent consumers plus every DialogContent consumer, e.g.:
packages/app-shell/src/layout/ChatDock.tsx, layout/ActivityFeed.tsx
packages/app-shell/src/views/metadata-admin/* (PackagesPage, PermissionMatrixEditor, AccessExplainPanel, ResourceEditPage, MetadataDetailDrawer)
packages/app-shell/src/console/ai/AiChatPage.tsx, console/ai/BuildDebugDrawer.tsx
packages/fields/src/widgets/PeoplePicker.tsx, packages/plugin-dashboard/src/RecordDetailDrawer.tsx, packages/plugin-charts/src/ObjectChart.tsx, packages/plugin-chatbot/src/AiPendingActionsInbox.tsx
It surfaced in #5430 as a test artifact: getByRole('button', { name: 'Close' }) matched two elements under an en session, because jsdom does not apply the Tailwind hidden class. The #5430 test worked around it by addressing its own button via title; the primitive's label was left alone on purpose.
Why it is not a one-line fix
packages/components/src/ui/** is a No-Touch zone (AGENTS.md Commandment #7) — those files are overwritten by pnpm shadcn:update / scripts/shadcn-sync.js. Editing the literal in place would be silently reverted by the next sync. Options worth weighing:
- A. Wrapper in
packages/components/src/custom/ — the documented escape hatch, but SheetContent/DialogContent render the close internally, so a wrapper would have to hide the built-in one and re-render its own (what NavigationOverlay already does by hand). That is the pattern to generalize, at the cost of every consumer migrating to the wrapper.
- B. Teach the sync script a patch/allowlist for this one literal, so the i18n call survives regeneration.
- C. Accept a prop (
closeLabel) threaded from consumers — pushes the burden onto ~20 call sites and leaves the default English.
A relevant precedent: common.close already exists in all ten locale packs (#5430 wired NavigationOverlay's own close to it), so only the delivery mechanism is open, not the key.
Acceptance
- The close affordance of
SheetContent and DialogContent reads common.close under every locale.
- Whatever route is chosen survives
pnpm shadcn:check / a sync run.
- Tests: en positive + at least one non-en positive + a negative assertion on the English literal, for both primitives.
Generated by Claude Code
Found while implementing objectstack#5430 (the nav-overlay
Closeitem). Not fixed there — the fix lands in a No-Touch zone and needs a route decision.What
Both auto-rendered close buttons in the Shadcn primitives carry their accessible name as a hardcoded English
sr-onlyspan:packages/components/src/ui/sheet.tsxpackages/components/src/ui/dialog.tsxThe button is icon-only (a lucide
X), so that span is the control's name to a screen reader. Under a zh/ja/es session every drawer and every modal in the console announces "Close" in English.Why it is live, and how #5430 surfaced it
NavigationOverlayis the one consumer that hides the primitive's own close ([&>button:last-of-type]:hidden) because it renders its own in the header — so there it isdisplay:noneand out of the accessibility tree. Everywhere else it renders normally. Roughly 20SheetContentconsumers plus everyDialogContentconsumer, e.g.:packages/app-shell/src/layout/ChatDock.tsx,layout/ActivityFeed.tsxpackages/app-shell/src/views/metadata-admin/*(PackagesPage, PermissionMatrixEditor, AccessExplainPanel, ResourceEditPage, MetadataDetailDrawer)packages/app-shell/src/console/ai/AiChatPage.tsx,console/ai/BuildDebugDrawer.tsxpackages/fields/src/widgets/PeoplePicker.tsx,packages/plugin-dashboard/src/RecordDetailDrawer.tsx,packages/plugin-charts/src/ObjectChart.tsx,packages/plugin-chatbot/src/AiPendingActionsInbox.tsxIt surfaced in #5430 as a test artifact:
getByRole('button', { name: 'Close' })matched two elements under anensession, because jsdom does not apply the Tailwindhiddenclass. The #5430 test worked around it by addressing its own button viatitle; the primitive's label was left alone on purpose.Why it is not a one-line fix
packages/components/src/ui/**is a No-Touch zone (AGENTS.md Commandment #7) — those files are overwritten bypnpm shadcn:update/scripts/shadcn-sync.js. Editing the literal in place would be silently reverted by the next sync. Options worth weighing:packages/components/src/custom/— the documented escape hatch, butSheetContent/DialogContentrender the close internally, so a wrapper would have to hide the built-in one and re-render its own (whatNavigationOverlayalready does by hand). That is the pattern to generalize, at the cost of every consumer migrating to the wrapper.closeLabel) threaded from consumers — pushes the burden onto ~20 call sites and leaves the default English.A relevant precedent:
common.closealready exists in all ten locale packs (#5430 wiredNavigationOverlay's own close to it), so only the delivery mechanism is open, not the key.Acceptance
SheetContentandDialogContentreadscommon.closeunder every locale.pnpm shadcn:check/ a sync run.Generated by Claude Code