Skip to content

fix(components,fields,plugin-detail,i18n): console chrome i18n gaps beyond #5084 (objectstack#5407) - #3379

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5407-console-chrome-i18n
Aug 5, 2026
Merged

fix(components,fields,plugin-detail,i18n): console chrome i18n gaps beyond #5084 (objectstack#5407)#3379
yinlianghui merged 2 commits into
mainfrom
claude/issue-5407-console-chrome-i18n

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5407

issue 列了四项。逐项复核 origin/main 后:第 1、2(部分)、4 项成立并已修;第 2 项的 Clear / Close 复现不出来;第 3 项的归因不成立(是 app 元数据,不是 console chrome)。下面逐项给测量依据。


1. 依赖门控的 lookup 占位符插的是 API 名 — 成立,已修

issue 猜"插的是 object 的 API 名,应改成 object label"。实测是受控字段(sibling field)的 API 名:

packages/fields/src/widgets/LookupField.tsx 三处(trigger 正文、trigger title、browse-all 按钮 title)都写着

t('lookup.selectFirst', { fields: dependsOn.map(d => d.field).join(', ') })

depends_on 存的就是 API 名,所以句子被翻译、被插进去的名字没有 —— 十种语言一律如此,en 也一样,英文用户看到的是内部标识符。

对照组说明这不是设计:packages/components/src/renderers/form/form.tsx固定选项字段(select / radio / multiselect / checkboxes)走的是同一个 key,但它把名字解析成了 label(fieldLabelByName[fn] || fn),而且源码注释里就写着 widget 那边插的是 raw metadata names。也就是说仓库早就知道两边不一致,只是没有把 label 递过去。

修法(contract-first,不在消费端兜底): 只有 form 知道 label,所以由 form 递 —— 新增一个 widget prop dependsOnLabels(field name 到 label 的映射),就是 emptyHint 在 lookup 侧的对应物。widget 读 dependsOnLabels?.[d.field] || d.field,映射里没有的名字回落到自身 —— 没有宿主的独立 widget 渲染结果与改前逐字节相同。

顺带修掉一个自己引入的问题:builtin 分支(input/textarea/…)不经过 stripRegisteredFieldProps,会把剩余 props 直接摊到控件上,所以新 prop 必须同时进 stripRendererOnlyProps,否则每个普通文本框都会打印 "React does not recognize the dependsOnLabels prop on a DOM element"。这条已由测试钉住。

⚠️ 越出了派发时的文件面:PM 预定位第 1 项在 form.tsx,但 form.tsx 的门控提示只覆盖固定选项字段(CASCADE_OPTION_FIELD_TYPES),lookup 的那句在 packages/fields。据此改动了 packages/fields/src/widgets/LookupField.tsxtypes.ts。已核对无并行冲突:同容器另外三个 worktree(#3314 / #5406 / #5408)分别只碰 packages/corepackages/i18n/src/provider.tsx + index.tspackages/plugin-form。本 PR 未碰这三处任何文件。

2. Toolbar / list chrome 无翻译 key — 两条成立已修,两条复现不出

  • More actions — 成立。 containers.tsx 的 page header 溢出触发器写死 aria-label="More actions"。它是纯图标按钮,这个字面量就是它对读屏器/悬浮提示的全部名字。改读 detail.moreActions —— 刻意复用 action:menu 自己那个溢出触发器已经在用的同一个 key,不新开一个:一张记录页可能同时出现两个 ,两个 key 会让它们在某个语言下读法不一致。该 key 十包齐全,零新增词条。
  • Add reaction — 成立。 ReactionPicker.tsx 写死 aria-label="Add reaction"。新增 detail.addReaction,十个语言包全部补齐(all-locales-key-parity 强制全量对齐)。
  • Clear — 复现不出。 console chrome 里每一处 Clear 都已经过 t(),且十包均已翻译:list.clear(ViewSettingsPopover)、grid.bulkClear(BulkActionBar)、lookup.clearapprovalsInbox.clearfilterBuilder.*。唯一没走 t() 的是 plugin-gantt 的 QuickFilterBar —— 而它在所有语言下渲染的是中文"清除筛选",与 issue 描述的"stay English in every locale"正好相反。那是另一个 bug,已另开 objectstack#5427。
  • Close — 定位不到。 issue 说是"visible button"。仓库里带可见文字的 Close 只有 app-shell/.../external/ImportObjectDialog.tsx:192(Setup 的外部对象导入对话框);其余(navigation-overlay.tsxaria-label/titleplugin-designer/DashboardEditor.tsx)都是图标按钮,而 components/src/ui/dialog.tsx 那个是 sr-only —— 属于 #5084 的清单,本单明确排除。没有截图无法判定验收扫到的是哪一个,不硬猜。剩余的图标按钮 label 已归入 objectstack#5430。

3. Dashboard filter chips / KPI 副标题 — 归因不成立,本 PR 不改

issue 说这四项"unreachable from app metadata; every one needs a console bundle key"。第 3 项不是这样:

  • chips 的 Owner: / Lead Source: 前缀:DashboardFilterBar.tsx:226def.label || def.name,def 是 dashboard 元数据里的 filter 定义(DashboardFilterDef.label,packages/core/src/utils/dashboard-filters.ts:37)。冒号后面那个会翻译的值(全部 / すべて)才是 chrome(dashboard.filters.all)—— 这恰好解释了 issue 观察到的"值翻了、前缀没翻"。console 不可能用一个 bundle key 去翻一个它无从得知的 app 字段标签。
  • KPI 副标题 Total Amount / Accounts / Contacts / Leads:DatasetWidget.tsx:404headerLabel(values[0]),即语义层 dataset measure 的 label,而且它已经过 useSafeFieldLabel().fieldLabel(object, name, fallback) 的约定式解析 —— 只是 measure 不是对象字段,约定 key 命不中,于是落到 dataset 里作者写的英文 label。同样是 app 元数据。

测量过程中确实翻出了 plugin-dashboard 自己的渲染缺陷(私有 resolveLabel 接受 I18nLabel 却从不调 t();type: 'metric' 的 self-contained 分支整条绕过 tWidgetTitle/tWidgetDescription),但它牵涉一个需要维护者定夺的设计选择,已单独开 objectstack#5428,不塞进本 PR

4. en 校验 toast 用顿号连接字段名 — 成立,已修

form.tsxannounceFieldErrors 里写死 join('、')(U+3001)。zh/ja 碰巧对,其余全错,英文最扎眼。

取舍(PM 给了两个方向,这里选前者,附实测理由):

  • 选中:每个语言包自带 validation.formInvalidJoiner 拉丁语系 + 韩语 ", ",zh/ja "、",阿拉伯语 U+060C + 空格。新语言缺词条时经 i18next 回落到 en", ";all-locales-key-parity 保证不漏包。
  • 排除:Intl.ListFormat 零词条听起来更优,但实测输出不能用 —— type: 'unit' 在 zh 和 ru 下分隔符是空串(比现状更糟),type: 'conjunction' 会插入 "and"/"和"/"y",而这是一个会被截断成 "A, B, C…" 的列表,插连词读起来是句子不是列表。而且它的输出取决于运行时 ICU 数据,词条则是确定且可 review 的。

同层顺带修了 issue 点名的 es-ES 性数一致:{{field}} es obligatorio 只在字段标签是阳性时成立("Cuenta es obligatorio" 不对)。形容词要与中心词一致,而运行时标签无法声明性别,所以模板自带阳性中心词 —— El campo {{field}} es obligatorio,任何标签都成立。validation.unique 同一处同一毛病,一并改。其余语言包未动。


测试

新增 7 个文件里的 22 条断言,四项各自有钉子:

  • packages/fields/src/widgets/LookupField.gateHintLabel.test.tsx — 三个被门控的界面各断言一次(它们本来是同一个表达式的三份拷贝,只修一份从外面看不出来);另有两条回落断言(宿主没给映射时仍渲染 API 名),故意不具判别性,用来保住独立使用场景。
  • packages/components/src/renderers/form/__tests__/form-depends-on-labels.test.tsx — form 到 widget 的管道;两侧的 strip(注册 widget 与 builtin 分支)都钉住。两个包不能互相 import(依赖方向如此),所以 widget 侧和 form 侧分开断言。
  • packages/components/src/renderers/form/__tests__/form-invalid-toast-joiner.test.tsx — en 与 zh 两个"面"。
  • packages/i18n/src/__tests__/validation-list-joiner-locale-parity.test.ts — 按文字体系钉住 joiner 的(parity 测试只管 key 在不在,而这个 bug 从头到尾是值的问题;把 en 的 ", " 抄进 zh 同样能过 parity 并重新弄坏中文)。
  • packages/components/src/__tests__/page-header-more-actions-i18n.test.tsxpackages/plugin-detail/src/ReactionPicker.i18n.test.tsx — zh/ja/es 正向 + en 正向 + 英文字面量反向。

反向验证(先写下预期方向,再跑)

预期:把 LookupField 那三处还原成 d.field,五条里三红两绿 —— 两条回落断言按设计不具判别性;把 joiner 还原成写死的 ,en 那条红、zh 那条绿(zh 本来就是碰巧对的那一侧,它是回归护栏不是判别项)。

实跑结果与预期逐条吻合:

× form-invalid-toast-joiner > joins with a comma+space under an en session, never the CJK comma
✓ form-invalid-toast-joiner > still joins with the CJK comma under a zh session
× LookupField.gateHintLabel > interpolates the label the host supplied, not the API name
× LookupField.gateHintLabel > labels the gated "browse all" button the same way
× LookupField.gateHintLabel > joins several controlling fields by their labels
✓ LookupField.gateHintLabel > falls back to the API name for a field the host did not map
✓ LookupField.gateHintLabel > falls back to the API name when the host passes no map at all

Test Files  2 failed (2)
     Tests  4 failed | 3 passed (7)

受影响包全量

npx vitest run packages/i18n packages/plugin-detail packages/fields
  Test Files  115 passed (115)
       Tests  1421 passed (1421)

npx vitest run packages/components
  Test Files  75 passed (75)
       Tests  554 passed (554)

npx turbo run type-check --filter=@object-ui/components --filter=@object-ui/fields \
                        --filter=@object-ui/i18n --filter=@object-ui/plugin-detail
  Tasks:  15 successful, 15 total

npx eslint (13 个改动文件)
  0 errors, 254 warnings   # 全部是既有的 no-explicit-any / react-compiler 提示

规则消费半径已扫:lookup.selectFirst 的调用方(LookupField、OptionsEmptyState、form.tsx)与 validation.* 的调用方全部落在上面四个包内,greppackages/+apps/+e2e/,没有第五个包的 fixture 引用这些串。

Changeset:.changeset/console-chrome-i18n-5407.md(minor,按 AGENTS.md 版本策略绝不标 major)。

顺带开出的 issue(均未修进本 PR)

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

claude added 2 commits August 5, 2026 10:34
…objectstack#5407)

- lookup gate hint names the controlling field by LABEL, not API name
- page-header overflow trigger reads detail.moreActions
- reaction button reads detail.addReaction (new key, 10 packs)
- formInvalid joins with a per-locale validation.formInvalidJoiner
- es validation.required/unique carry their own head noun (gender agreement)

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

Also strips `dependsOnLabels` in `stripRendererOnlyProps`: the builtin field
branch spreads leftover props straight onto its control, so every plain input
logged "React does not recognize the `dependsOnLabels` prop on a DOM element"
until the strip covered it. Pinned by the new builtin-branch test.

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 11:10am

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-DPCx4NSq.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.30KB 104.75KB
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) 60.83KB 17.25KB
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.17KB 56.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.25KB 39.55KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 5, 2026 11:15
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 3889ffb Aug 5, 2026
16 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5407-console-chrome-i18n branch August 5, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants