Skip to content

objectui: the shadcn Sheet/Dialog primitives ship a hardcoded English Close sr-only label — every drawer and modal in the console announces it untranslated #5505

Closed
objectstack-ai/objectui
#3455

Description

@yinlianghui

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

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    repo:objectuiSeam card: cross-repo ordering with objectui is the substance (pure objectui fixes live in objectui)
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 5, 2026
  2. self-assigned this
    on Aug 6, 2026
  3. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    认领:objectui 分片 PM 并行批第 37 单
    会话:session_01GTRjn8xBqp75dk7kFupVRt
    分支:claude/issue-5505-shadcn-close-i18n(落地仓 objectui)
    Worktree:objectui-issue-5505
    文件面:packages/components/src/ui/sheet.tsx + dialog.tsx(经 sync 补丁机制)、scripts/shadcn-sync.js(或其补丁清单)、packages/components 测试、changeset。⛔ 不动语言包(common.close 已在十包)。

    PM 路线裁定(issue 三选项):采 B——教 sync 脚本携带这一处 i18n 补丁,使 t('common.close') 在再生成后存活。理由(两轴):A 要 ~20 个消费方迁移 wrapper 且复制 NavigationOverlay 的手工模式,爆炸半径大;C 把负担推给每个调用点、默认值仍是英文(消费端宽容形状);B 在唯一源头修、全体消费方自动受益。硬性要求:补丁必须可验证——pnpm shadcn:check / sync 在补丁失配时响亮失败而非静默还原(声明即强制);若 sync 脚本结构不支持补丁机制,STOP 报 needs_decision,不得改走 A/C。如有异议此处否决。


    Generated by Claude Code

  4. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    验收:ACCEPT → objectui PR #3455(并行批第 37 单,会话 session_01GTRjn8xBqp75dk7kFupVRt),已跑检查 9 绿 0 红(shards/Type Check/Lint 排队中,本地等价全绿);ready + auto-merge 因 GraphQL 配额排队,恢复即执行。

    复核要点:

    1. B 路线的落法优于 issue 草图:dev 先读 sync 脚本,发现既有保护是「拒绝覆盖」型且被 --force 旁路、无 check 可见——于是把补丁做成声明式数据,每次写入(含 --force)重放,--check 双向验证(本地标记 + 上游锚点适用性)失配即响亮 exit 1。副产物:仅含已声明补丁的组件与上游比对相等,「拒绝覆盖」信号回归只指未声明漂移的本义。
    2. 围栏外新增 src/lib/close-label.tsx 判合理:载荷放 ui/** 会被再生成吞掉,B 路线的结构前提;补丁因此缩到每文件两行锚点。
    3. 两处诚实偏差均接受:egress 封锁 ui.shadcn.com → 离线确定性证明(恰是 CI 唯一可门禁的形态);no-provider 断言独立成文件(已知模块级实例陷阱)。自己引入的 jsdom 双 Close 命中按该测试文件自带的处方修正。
    4. 反向验证 10/13 按预测;补丁引擎自测含幂等/锚丢失/锚歧义/计数修正。
    5. 两条顺带发现已分诊入队(bug+pm:queue+repo:objectui):objectstack#5803(fetch 缓存 403 错误体污染后续 --check,已复现)、objectstack#5805(shadcn-check.yml 的 continue-on-error 吞掉本 PR 刚赋予意义的退出码——门禁家族的下一环)。

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch looprepo:objectuiSeam card: cross-repo ordering with objectui is the substance (pure objectui fixes live in objectui)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions