Skip to content

fix(fields): 注册路径的全屏长文本对话框接上 i18n(#3404) - #3405

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3404-fullscreen-editor-i18n
Aug 5, 2026
Merged

fix(fields): 注册路径的全屏长文本对话框接上 i18n(#3404)#3405
yinlianghui merged 1 commit into
mainfrom
claude/issue-3404-fullscreen-editor-i18n

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3404

问题

packages/fields/src/widgets/FullscreenFieldEditor.tsx 四处英文字面量:展开按钮的可访问名(Edit ${label ?? 'text'} fullscreen)、标题兜底 Edit textCancelDone

键一个都不缺 —— form.fullscreen.*common.cancel#3272 给内置分支做国际化起就在十个语言包里齐全,只是这条路径从没消费。可达性也不是理论上的:mobile.fullscreenLongText 打开后 ObjectForm 给每个长文本字段盖 mobile_fullscreen,于是同一张表单内,内置分支渲染的字段显示「取消 / 完成」,注册 widget 渲染的显示 Cancel / Done。

改动

四处全部改为消费既有键,零新增键、十个语言包一行未动。另补上内置分支早已有的 sr-only form.fullscreen.description,详见下。

关键实现选择:走 useFieldTranslation() 而不是 issue 正文提到的裸 useObjectTranslation 这一点是实测决定的,不是偏好。本包 widget 会被独立/嵌入宿主以及本包绝大多数测试不带 I18nProvider 裸渲染,而在改动前的树上实测裸 hook 在无 provider 时返回的是原始键:

{"cancel":"common.cancel","done":"form.fullscreen.done",
"title":"form.fullscreen.title","toggle":"form.fullscreen.toggle"}

{{label}} 插值也整个塌掉 —— 比它要替换的英文字面量更差useFieldTranslation(createSafeTranslation)正是内置分支自 #3272 起用的形状,其英文兜底与原字面量逐字节相同,因此无 provider 的渲染结果完全不变。相应地,英文兜底项加在了本包既有的 FIELD_DEFAULTS(useFieldTranslation.ts)里 —— 那是代码内的英文兜底表,不是语言包,仍是零新增 i18n 键;common.cancel 本就在表内,直接复用。

「顺带观察」的 DialogDescription:补了,但理由与 issue 猜测的不同

issue 猜测可能触发 Radix 的 missing-Description 警告。实测该警告不存在:锁定的 @radix-ui/react-dialog@1.1.23 整个文件里一个 console.* 调用都没有,上游已移除该警告,baseline 跑也确实没有任何告警输出。

补它的真实理由是另外两条:Radix 仅在存在 Description 时才设置 aria-describedby(descriptionPresent ? descriptionId : undefined),所以改动前这条路径的对话框根本没有可访问描述,而逐像素相同的内置对话框有;且 form.fullscreen.description 在此路径上是个无消费者的键 —— 与本 issue 主体是同一处漂移。同文件、零新键。

测试

新增两个文件,合计 14 例,textarea 与 richtext 两个 widget 各覆盖:

  • __tests__/FullscreenFieldEditor.i18n.test.tsx —— en 正向(与旧字面量逐字节相同,证明 en 是 no-op)、zh/ja 正向 + 英文字面量反向断言(只做正向断言的话,重新内联的字面量挨着翻译过的兄弟节点照样能过);ja 是因为 toggle 名是插值的、两个包把通用名词放在句子两端,只测 zh 会蒙混过关。另含 label 插值、aria-describedby、以及提交/取消仍然接线正常。
  • __tests__/FullscreenFieldEditor.no-provider.test.tsx —— 无 provider 的那条腿,含 Commandment #-1 的无 CJK 断言。

为什么无 provider 那几例必须单独成文件(这是本 PR 返工过的一处): 它们最初写在 i18n 文件里。反向验证(把 useFieldTranslation 换回裸 useObjectTranslation,预期变红)时它们依然全绿,而同一次实验正确地把 RichTextField.mobileFullscreen.test.tsx 弄红了。原因是 I18nProvider 会调用 initReactI18next,把实例装成 react-i18next 的全局默认;同文件内只要有一例挂过 provider,就再也没有「无 provider」状态可观测 —— 那几例是靠读泄漏的全局实例而绿的,属于「因为什么都没发生而通过」的假绿。拆成独立文件后,同样的替换实验让它们如实变红(aria-label="form.fullscreen.toggle"),这才算真的钉住。同样的拆分理由见 TagsField.placeholder.no-provider.test.tsx

反向验证方向

  • 主改动: 预期红 → 实测红。改动前跑新 i18n 套件:Tests 6 failed | 2 passed,失败的正是 zh/ja 各例(Received: aria-label="Edit text fullscreen");通过的 2 例是两个 en 用例 —— 它们改动前后都绿,因为字面量与 en 包逐字节相同。这一点如实记录:en 用例是 no-op 保护,不是缺陷复现器。
  • 实现选择: 换回裸 hook → 4 例红,报出 aria-label="form.fullscreen.toggle" 原始键,其中一例是既有的RichTextField.mobileFullscreen.test.tsx(它按 'Edit Release notes fullscreen' 查按钮)。

命令与结果

仓根跑(包内跑会假绿 / 路径过滤被吞,见 #3288/#3378),并以文件名核对确实执行:

npx vitest run --maxWorkers=2 packages/fields packages/plugin-form \
packages/components/src/renderers/form packages/i18n
→ Test Files 139 passed (139) / Tests 1552 passed (1552)
pnpm --filter @object-ui/fields type-check → 干净(需先 build 依赖图)
pnpm check:control-bytes → OK(3598 个文件)
eslint(4 个改动文件) → 0 问题

按消费半径而非编辑包扫了 fixture:packages/plugin-form/src/__tests__/ObjectForm.mobileFullscreen.test.tsxpackages/fields 之外也消费这个组件,已纳入上面的运行范围;packages/i18n 的语言包 parity 套件一并跑过,确认没有包漂移。

范围

严格限定在 FullscreenFieldEditor.tsx + 测试 + changeset(patch),外加 useFieldTranslation.ts 里的英文兜底项 —— 那是让本次改动不产生回归所必需的最小外扩(理由见上),且不属于裁定禁止的 form.tsx / 语言包 / TextAreaField / RichTextField 任何一项。未碰 form.tsx,未碰任何 locale 文件,未改两个宿主 widget。


Generated by Claude Code

…log (#3404)
`FullscreenFieldEditor` — the expand button and dialog `TextAreaField` and
`RichTextField` render under `mobile.fullscreenLongText` — carried four English
literals: the toggle's accessible name, the `Edit text` title fallback,
`Cancel` and `Done`.
No translation was missing: `form.fullscreen.*` and `common.cancel` have
shipped in all ten locale packs since #3272 translated the built-in branch;
this path never consumed them. Inside one form a zh session saw the built-in
branch's translated footer next to this path's English one.
All four now consume those EXISTING keys — no new keys, no locale-pack change
— plus the sr-only `form.fullscreen.description` the built-in branch already
carries, so the dialog has an `aria-describedby` instead of none.
Copy resolves through `useFieldTranslation()` (`createSafeTranslation`), as the
built-in branch does, not bare `useObjectTranslation`. Measured with no
provider mounted, the bare hook returns raw keys
(`common.cancel` -> "common.cancel") and drops the `{{label}}` interpolation —
strictly worse than the literals. The safe hook's English defaults are
byte-identical to those literals, so provider-less hosts render what they did
before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 5, 2026 8:15pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-_Ryj8XSd.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.47KB3.09KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.25KB0.53KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)478.45KB105.05KB
core (index.js)2.25KB0.80KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.23KB34.75KB
fields (index.js)227.94KB55.90KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)3.26KB1.44KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.01KB28.86KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)231.54KB57.09KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)111.54KB26.97KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)185.08KB49.04KB
plugin-kanban (index.js)47.89KB13.18KB
plugin-list (index.js)105.02KB25.36KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.55KB10.59KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.34KB2.82KB
plugin-view (index.js)83.67KB20.43KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.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 5, 2026 20:19
@yinlianghui
yinlianghui added this pull request to the merge queueAug 5, 2026
Merged via the queue into main with commit 6fe485bAug 5, 2026
17 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3404-fullscreen-editor-i18n branch August 5, 2026 20:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude