Skip to content

fix(components,i18n): Sheet/Dialog 关闭按钮接入 common.close,并让 shadcn 同步机制携带该补丁 (objectstack#5505) - #3455

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5505-shadcn-close-i18n
Aug 6, 2026
Merged

fix(components,i18n): Sheet/Dialog 关闭按钮接入 common.close,并让 shadcn 同步机制携带该补丁 (objectstack#5505)#3455
yinlianghui merged 2 commits into
mainfrom
claude/issue-5505-shadcn-close-i18n

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5505

问题

Sheet / Dialog 两个 primitive 自动渲染的关闭按钮是纯图标(lucide X),它的 sr-only span 不是装饰 —— 那就是该控件的可访问名称,屏幕阅读器朗读的是它,getByRole('button', { name }) 匹配的也是它。上游 shadcn 把这个名称硬编码成英文字面量,所以在 zh/ja/es 下,控制台里每一个抽屉和模态框都朗读英文 "Close":约 20 个 SheetContent 消费方,以及全部 DialogContent 消费方(ChatDock、ActivityFeed、metadata-admin、AiChatPage、BuildDebugDrawer、PeoplePicker、RecordDetailDrawer、ObjectChart、AiPendingActionsInbox 等)。

唯一的例外是 NavigationOverlay,它用 [&>button:last-of-type]:hidden 把 primitive 自带的关闭按钮藏掉并自渲染一个 —— 这一点在 origin/main 上仍然成立,已复核。

为什么不能直接改那两个文件

packages/components/src/ui/** 是 No-Touch 区(AGENTS.md 第 7 条),由 registry 重新生成。就地手改会被下一次 pnpm shadcn:update 静默还原,而且type-check 抓不到:上游会连同用法一起删掉,重新生成后的文件照样编译通过。这正是 shadcn-sync.js 自己在 command.tsx 注释里记录过的失效模式。

方案(PM 裁定的 Route B)

补丁以数据形式声明在 scripts/shadcn-local-patches.mjs,由同步流程本身负责携带,而不是靠人记得重做。

载荷放在重新生成区之外。 真正的 i18n 实现落在 packages/components/src/lib/close-label.tsx(src/lib/ 不参与重新生成),primitive 里只留一行引用。这样每个文件的补丁只有两处单行锚点 —— 加一行 import、换一个元素 —— 对上游改动的抗性远高于把 hook 内联进去。

翻译走 createSafeTranslation,解析 common.close(自 objectstack#5430 起十个语言包都有),未挂载 I18nProvider 时回退英文。这一条是硬要求:仓内大量既有用例按英文名定位这些按钮且不挂 provider(plugin-form/src/discardGuard.test.tsxplugin-detail/src/__tests__/InlineCreateRelated.closeButtonName.test.tsx 等),渲染出原始 key 会连带打挂它们。

declared = enforced

声明本身不值钱,能否大声失败才是。三处独立强制:

  1. --update 写入时重新施加,包括 --force。补丁在本地改动拒绝逻辑之前施加,因此仅有已声明补丁这一处差异的组件会与上游比较为相等、正常同步,"拒绝覆盖"继续只对未声明的改动报警。
  2. 锚点失效即拒绝写入。 锚点消失、或出现多次变得有歧义(occurrences 固定为 1),--update 报错退出而不是静默漏掉该补丁 —— 猜一个位置比停下来更糟。
  3. pnpm shadcn:check 非零退出,当补丁从磁盘文件里丢失(被 --force、手改或错误合并抹掉),或已无法重新施加到当前上游(上游挪了锚点,下次同步会失败 —— 提前预警)。

另外:registry 返回无法解析的内容(离线、egress 403、schema 变更)时按 fetch 错误处理,不会把网络故障误报成补丁失效。

验证

方向在跑之前先写死,结果与预测一致。

反向验证 —— 把两个 primitive 还原成 origin/main 的英文字面量后重跑:

× reads the zh bundle value under a zh session   (Sheet + Dialog)
× reads the ja bundle value under a ja session   (Sheet + Dialog)
× reads the es bundle value under an es session  (Sheet + Dialog)
× sheet.tsx / dialog.tsx carries its declared patches
× sheet.tsx / dialog.tsx has no hardcoded English close label
Test Files  2 failed | 1 passed (3)
      Tests  10 failed | 13 passed (23)

en 正例与 no-provider 回退两侧都绿(回退语义未变,这正是它们作为 pin 的意义);非 en 正例、英文字面量负例、源码级闸门在改动前全红、改动后全绿。

大声失败路径(补丁被还原时跑 pnpm shadcn:check):

DECLARED LOCAL PATCHES — FAILED
✗ Required local patches are MISSING from the file on disk:
  [dialog-i18n-close-label] declared for dialog.tsx (objectstack#5505)
    why: DialogContent's close button is icon-only (lucide X), so this
         sr-only span IS the control's accessible name. ...
    missing marker: "CloseSrLabel"
  Restore with: node scripts/shadcn-sync.js --update dialog
✗ 2 component(s) with declared local patch failures — see above.
ELIFECYCLE Command failed with exit code 1.

补丁齐全时同一命令退出码 0 —— 且此时 46 个 registry 请求全部失败(沙箱 egress 拦截),闸门没有误报,证明了上面那条防误报保护。

测试命令(均在仓根,加 flock 串行化):

  • pnpm exec vitest run 四个新增/改动测试文件 → 29 passed
  • pnpm exec vitest run packages/components89 files / 648 tests passed
  • pnpm exec vitest run packages/plugin-form packages/plugin-detail packages/plugin-view91 files / 856 passed
  • pnpm exec vitest run packages/app-shell packages/plugin-dashboard305 files / 2632 passed
  • pnpm --filter @object-ui/components type-check → 通过
  • eslint 改动文件 → 0 error 0 warning
  • node scripts/check-control-bytes.mjs → OK;另对本次全部改动文件做了控制字符自查(gate 扫不到的范围)
  • changeset 两个 guard 均通过

需要 reviewer 注意的两点

  1. packages/components/src/lib/close-label.tsx 是新增文件,严格说超出了派发单列的文件清单。这是刻意的、也是 Route B 的必要条件:载荷若留在 src/ui/** 内,它自己就会被重新生成掉,补丁将不得不整段重新插入实现代码。放在 src/lib/ 才能让补丁缩到两行锚点。请确认这个取舍。
  2. registry 联网未能实测。 本沙箱 egress 不允许访问 ui.shadcn.com,因此无法真跑一次线上 --update。补丁引擎改为用忠实还原上游内容的 fixture 做确定性离线验证(施加 / 幂等 / 锚点消失 / 锚点有歧义 / 报错文案),并且仓库里这两个 primitive 的当前内容就是该引擎生成的输出。离线也是 CI 能真正设闸的唯一形态 —— 顺带说明:.github/workflows/shadcn-check.yml 每周跑且带 continue-on-error: true,吞掉退出码,所以 PR 级的实际闸门是那个离线测试(在 ci.yml 里跑),而不是该 workflow。

Generated by Claude Code

…携带该补丁 (objectstack#5505)

Sheet/Dialog 两个 primitive 自动渲染的关闭按钮是纯图标(lucide X),
其 sr-only span 就是该控件的可访问名称。上游 shadcn 把它硬编码成英文
字面量,因此在 zh/ja/es 下控制台里每一个抽屉与模态框都朗读英文 "Close"
(~20 个 SheetContent 消费方,以及全部 DialogContent 消费方)。

改动把该 span 换成 <CloseSrLabel />,实现放在
packages/components/src/lib/close-label.tsx —— 用 createSafeTranslation
解析 common.close,未挂载 I18nProvider 时回退英文,以保证仓内大量按英文
名定位这些按钮的既有用例与 e2e 不受影响。

关键点:src/ui/** 由 registry 重新生成(AGENTS.md 第 7 条),直接手改会被
下一次 pnpm shadcn:update 静默还原,且因为上游会连同用法一起删掉,
type-check 也发现不了。因此补丁以数据形式声明在
scripts/shadcn-local-patches.mjs,由同步流程在每次写入时自动重新施加
(包括 --force),并做双向强制:

- 锚点在当前上游已失效 / 变得有歧义时,--update 拒绝写入而不是静默漏掉;
- pnpm shadcn:check 在补丁从磁盘文件丢失、或已无法重新施加到上游时
  以非零码退出并打印 id/issue/原因;
- 一个纯离线测试在每个 PR 上守住同一不变量(registry 在 CI 不可达,
  依赖网络的检查无法充当闸门)。

registry 返回无法解析的内容(离线、egress 403)时按 fetch 错误处理,
避免把网络故障误报成补丁失效。

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

vercel Bot commented Aug 6, 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 6, 2026 5:58am

Request Review

同一个组件可能同时命中两类失败(磁盘文件丢了补丁 + 上游挪了锚点),
相加会把它报成 2 个组件。改为按组件名去重计数。

已验证:仅还原 dialog.tsx 时输出 "1 component(s)" 并以 1 退出;
补丁齐全时退出 0。

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

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
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) 21.35KB 5.70KB
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) 5.30KB 2.24KB
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) 479.63KB 105.43KB
core (index.js) 2.47KB 0.91KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 229.92KB 56.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.53KB 10.71KB
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.05KB 1.53KB
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) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.03KB 28.88KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.53KB 57.37KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 185.08KB 49.04KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 105.02KB 25.36KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.67KB 20.43KB
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) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
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 (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) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 6, 2026 06:45
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 71be406 Aug 6, 2026
17 of 18 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5505-shadcn-close-i18n branch August 6, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants