Skip to content

fix(components,i18n): 把 form.tsx 剩余的用户可见硬编码英文接入 i18n (#3272) - #3392

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-3272-form-hardcoded-english
Aug 5, 2026
Merged

fix(components,i18n): 把 form.tsx 剩余的用户可见硬编码英文接入 i18n (#3272)#3392
yinlianghui merged 2 commits into
mainfrom
claude/issue-3272-form-hardcoded-english

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3272

背景

packages/components/src/renderers/form/form.tsx 顶部早有 useSafeFormTranslation,校验消息、门控提示、选项空态都已接 i18n,但同一文件里还剩几处用户可见的英文字面量从来没接过 —— zh/ja/ar 会话下,同一张表单里一半句子是中文、一半是英文。

逐项复核

issue 的行号基于 08-03 快照,今日 main 已被 #5407 / #3290 等改过,四条先各自对着 origin/main 复核:

issue 条目 今日 origin/main 的实际情况 本 PR
1. 全屏文本编辑对话框 仍在 已修
2. submitLabel / cancelLabel 英文默认值 仍在 已修(按 PM 裁定)
3. aria-label="required" 已消失 —— objectui#3290 已把必填星号整体移出无障碍树(aria-hidden="true" + data-required-marker),aria-required 改由控件自身承载。原位置现在是一段解释这件事的注释 无需改动
4. select 的 placeholder || 'Select an option' 仍在,但 issue 说它「够不到」并不准确 —— 见下 已删

第 3 条的前提已经失效,如实报告,不强凑改动。

第 1 项 —— 全屏对话框

新增 form.fullscreen.*,十个语言包齐:title(标题回落)、description(sr-only 描述)、done(底部确认按钮)。

底部的 Cancel 复用已有的 common.cancel,不另起 form.fullscreen.cancel —— 「Cancel」这个词全仓十包早就有了,再抄一份正是 #3231 / #3263 花力气收掉的那种漂移。

顺带把同一组件里展开按钮的 aria-label(原文是模板串 Edit ${label ?? 'text'} fullscreen)也接上了。issue 的条目里没逐字点到它,但它就在同一个组件里、是同一类缺陷(屏幕阅读器在任何语言下都念英文),留一处不修会很怪。

这里有一个刻意的取舍:它做成一个带插值的句子 toggle: 'Edit {{label}} fullscreen' 加一个被翻译的通用名词 textFallback: 'text',而不是「有 label 一句 / 没 label 另一句」两个 key。原因是 label 目前恒为 undefined(见下方「顺带发现」#3393),两 key 写法会让「有 label」那个 key 在十个包里全是死翻译;插值写法则两个 key 都在活路径上,而且将来 label 补通了句子直接就对。

第 2 项 —— Submit / Cancel 默认值(按 PM 裁定)

组件侧 prop 默认值从字面量改为 undefined 加渲染时 t() 回落:

  • 解构处不再写 submitLabel = 'Submit',渲染处写 submitLabel ?? t('common.submit');
  • 作者显式写的值逐字优先,并用测试钉住了两个方向:zh 会话下作者写的英文 Create account 照旧是 Create account;en 会话下作者写的中文「立即提交」照旧是「立即提交」;
  • ?? 而不是 ||:submitLabel: '' 是「作者要一个空按钮」的显式声明,不是「没写」。|| 会把它悄悄覆盖掉 —— 那正是英文默认值当初干的事,不该换个方式再干一遍;
  • 复用已有的 common.submit / common.cancel(十包早已齐备),不新造 form.submit / form.cancel 孪生 key;
  • 不动契约面:FormSchema.submitLabel / cancelLabel@object-ui/types 里本来就是 z.string().optional(),本 PR 没改 types / zod / spec。

行为变化面很窄:只有「从来没写过 label、且在非英文会话下看」的表单渲染文本会变 —— 那正是 bug 本身。已写进 changeset。

第 4 项 —— select 的第二层 placeholder 兜底

issue 说它是「够不到的死兜底」,复核下来不完全准确,这里如实修正。

唯一调用点是 placeholder: fieldProps.placeholder ?? (resolvedType === 'select' ? t('common.selectOption') : undefined)?? 只在 null/undefined 时回落,所以作者写 placeholder: '' 时空串会原样传下去;而分支里的 placeholder || 'Select an option' 用的是 ||,空串是 falsy —— 于是「作者刻意要一个空 placeholder」这条路径真的能走到那个英文字面量,拿到一个没被翻译的英文词覆盖掉自己的显式声明。

所以它不是无害的死代码,删掉是有可观察行为的,也是本 PR 里唯一能做「装回去看它变红」的一处。

反向验证(先定方向,再跑)

把三处改动整体还原成字面量,事先的预测是:凡是「英文与译文字节相同」的用例应当保持绿(它们本来就不区分),凡是断言译文出现、或断言英文缺席的用例应当变红。实测 6 红 8 绿,与预测逐条吻合。

变红(6,如预测):全屏对话框 zh / ja / 「Done 按钮仍然接着 handler」,动作栏 zh / ja,select 的 placeholder: ''

保持绿(8,如实说明为何不是漏网):

  • 全屏 en、动作栏 en —— 本 PR 对 en 是字节级 no-op,这两条钉的是「en 没被改坏」,天然不区分;
  • 「作者显式 label 逐字优先」两条 —— 旧代码的 = 'Submit' 解构默认值本来也不会覆盖显式值。这两条钉的是新回落不许越界,不是旧行为的回归;
  • submitLabel: '' 一条 —— 解构默认值只对 undefined 生效,空串在新旧代码下都渲染成空。它钉的是渲染处必须用 ?? 而不是 ||,还原实验不涉及这个选择,所以它绿是对的;
  • select 的另外三条 —— 它们走的是「调用点已给了 t()」那条路,本来就够不到被删的字面量。它们留下来是删除之后的续钉:哪天有人把调用点的 t() 去掉,不再有第二层兜底替它遮丑,zh 那条会直接红。

边界

顺带发现(已另开 issue,不在本 PR 修)

测试

仓根跑(包内跑 vitest 假绿 / 路径过滤被吞,见 #3288 / #3378),全程持 flock /tmp/os-heavy-verify.lockNODE_OPTIONS=--max-old-space-size=4096--maxWorkers=2

三个新测试文件:

Test Files  3 passed (3)
     Tests  14 passed (14)

form 渲染器全部测试加 i18n 全部测试(含 all-locales-key-parity.test.ts,它机械校验新 key 十包齐、插值占位符与 en 一致):

Test Files  47 passed (47)
     Tests  324 passed (324)

规则消费半径(而不是被改的包)扫的下游 fixture —— plugin-form / plugin-detail / fields / app-shell FlowRunner / components 里所有按 /submit/iCancel 取按钮的测试:

Test Files  89 passed (89)
     Tests  1190 passed (1190)

反向验证(三处改动整体还原):

Test Files  3 failed (3)
     Tests  6 failed | 8 passed (14)

类型检查(先 pnpm --filter '@object-ui/components^...' build 建好依赖,新 worktree 不建会报一片假的 TS2307):

packages/i18n type-check: Done
packages/components type-check: Done

其余闸门:check-control-bytes OK(另做了一次 grep -naP 自扫,changed files 无控制字节);check-changeset-fixed / check-changeset-no-major 均 OK;改动文件 eslint 0 error。

远端 CI 已全绿:Lint / Type Check / Test shard 1-4 / Build & E2E / Build Docs / Control Byte Scan / Changeset Fixed Group / Changeset Bump Policy / Bundle Analysis 全部 success(Test (coverage)dependabot 按 path filter skipped)。

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

claude added 2 commits August 5, 2026 15:17
… through i18n (#3272)

The fullscreen long-text dialog (title, sr-only description, footer buttons,
trigger accessible name) now reads `form.fullscreen.*`; the action bar's
`submitLabel`/`cancelLabel` lose their English literal defaults and fall back
at render through `common.submit`/`common.cancel`, so an authored label still
wins verbatim while an unset one follows the session language. The built-in
select branch's second `|| 'Select an option'` fallback is dropped — the call
site already supplies `t('common.selectOption')`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
…edence (#3272)

Three suites: the fullscreen dialog in en/zh/ja (positive per locale plus the
English literals asserted absent, and a commit-the-draft case so the translated
Done button is still the one wired to the handler); the action bar's
submit/cancel fallback with the authored-label-wins-verbatim nail in both
directions and the `submitLabel: ''` case that pins `??` over `||`; and the
built-in select placeholder, whose authored-empty case is the one stack the
deleted `|| 'Select an option'` literal actually reached.

Also adds the changeset — the item-2 default-value semantics change is spelled
out there for release notes.

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

vercel Bot commented Aug 5, 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 5, 2026 3:32pm

Request Review

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CA2UEbPY.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) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 477.89KB 104.89KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 227.18KB 55.72KB
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.01KB 28.86KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 231.54KB 57.09KB
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.55KB 10.59KB
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

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.

form.tsx 仍有一批用户可见的硬编码英文(全屏文本编辑对话框、Submit/Cancel 默认值、required aria-label)

2 participants