Skip to content

fix(i18n): ListView / ObjectGrid 记录详情浮层标题改为入键,不再自拼英文 (#3426) - #3457

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3426-overlay-caller-titles
Aug 6, 2026
Merged

fix(i18n): ListView / ObjectGrid 记录详情浮层标题改为入键,不再自拼英文 (#3426)#3457
yinlianghui merged 1 commit into
mainfrom
claude/issue-3426-overlay-caller-titles

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3426

PR #3423NavigationOverlayresolvedTitle 接上了 i18n 默认值(detail.recordDetail),但两个调用方在传入侧自拼英文标题、直接塞进 title prop —— 默认值根本轮不到生效。结果是:一个 zh/ja/de 会话拿到一个通体本地化的抽屉,顶上却挂着唯一一句英文标题。

  • packages/plugin-list/src/ListView.tsx —— `${schema.label} Detail`
  • packages/plugin-grid/src/ObjectGrid.tsx —— 同样的模板串,外加无 label 时的裸字面量 'Record Detail'

前提核验:这两条路径确实用户可达

Issue 明确把可达性留给实现者判定,这是核验结论(证据均在 origin/main):

  • list-view / object-grid公开页面块(packages/core/src/registry/public-blocks.ts),navigation 是它们 schema 上的可编写键(ViewNavigationConfig,对齐 @objectstack/spec ListView.navigation)。
  • useNavigationOverlaymode: 'drawer' | 'modal' | 'split' | 'popover' 翻成 isOverlay;两个插件都把 navigation.handleClick 接到行点击上,并各自渲染 NavigationOverlay
  • 所以任何页面元数据只要写下 navigation: { mode: 'drawer' },点行就会开出这个浮层 —— 不经过 console/app-shell。

唯一会压制它的宿主是 app-shell 的 ObjectView:它传自己的 onRowClick(在 useNavigationOverlay 里具有最高优先级,直接 return),并渲染自己的 NavigationOverlay。那是一个宿主对公开块的覆盖,不能反推分支是死的。

新增的四个测试文件就是这条可达性的可执行证据:它们完全按用户路径打开浮层(点行 → 断言标题),没有任何对内部状态的直接操作。

改法(调用方入键,不给消费方加宽容)

两处都改成从 key 取标题,而不是拼字符串:

  • 新键 detail.recordDetailWithLabel(en: '{{label}} Detail')用插值携带对象标签,这样限定词后置的语种(de 用连字符复合词)或需要领属助词的语种(ja/zh)可以自己排语序,而不是被英文语序焊死;
  • 无 label 的分支复用 detail.recordDetail —— 正是浮层自身的默认键 —— 而不是另铸一个孪生键,避免同一个控件上的同一句话分裂成两份会各自漂移的译文。

新键补进全部十个语言包,并且同时补进两个插件各自的英文兜底表(LIST_DEFAULT_TRANSLATIONS / GRID_DEFAULT_TRANSLATIONS)—— 那才是没有挂 I18nProvidercreateSafeTranslation 真正读的东西。

英文输出逐字节不变

三个分支都不变:Contacts Detail / Contacts Detail / Record Detail,挂 provider 与不挂 provider 皆然(兜底路径里 createSafeTranslation 自己会替换 {{label}})。既有 e2e 用英文名寻址这块 chrome 的写法照旧可用。

验证方向(先预判,再运行)

用例 改前 改后
zh / ja / de(挂 provider) RED GREEN
en(挂 provider) GREEN GREEN(钉住字节不变)
无 provider 兜底 GREEN GREEN(钉的是兜底,不是修复)

反向核验实测(用 git checkout origin/main -- 取回原始源文件后重跑,再还原):

ObjectGrid.overlayTitleI18n.test.tsx     Tests  5 failed | 1 passed (6)
ListView.overlayTitleI18n.test.tsx
  + ListView.overlayTitleNoProviderFallback.test.tsx
                                         Tests  5 failed | 4 passed (9)

两次唯一活下来的绿色用例,都正是 en 那条 —— 与预判完全一致。

正向全量(仓根,flock + --maxWorkers=2):

pnpm exec vitest run packages/i18n packages/plugin-grid packages/plugin-list
 Test Files  96 passed (96)
      Tests  987 passed (987)

all-locales-key-parity(含 placeholder 形状比对)覆盖了新键,已在上面这 987 条里绿。

type-check / lint:

pnpm --workspace-concurrency=2 --filter @object-ui/plugin-grid --filter @object-ui/plugin-list --filter @object-ui/i18n type-check   → Done ×3
pnpm --workspace-concurrency=2 --filter … lint   → 0 errors(仅存量 warning)
node scripts/check-control-bytes.mjs → OK (3648 tracked text files)

远端 CI 已全绿(17 项 success / skipped,含 Test 四个分片、Type Check、Lint、Control Byte Scan、Changeset Bump Policy 与 Fixed Group Check)。

关于「为什么兜底测试要单独一个文件」

createI18ninstance.use(initReactI18next),而 initReactI18next 把该实例注册成 react-i18next 的模块级全局默认,这个注册在 unmount 和 cleanup() 之后依然存活。所以只要同一文件里有任何一个用例挂过 I18nProvider(哪怕 defaultLanguage 是 zh 且早已卸载),后面所有「不挂 provider」的渲染都会静悄悄地落到那个中文实例上 —— 看起来是绿的,其实什么都没断言。沿用 plugin-detail 在 objectstack#5733 立下的同一处理(两个文件、文件头写清原因)。

越界未改(已记录为 out-of-scope)

同一类缺陷在本 PR 文件围栏之外还有三处,均未在此 PR 触碰,已单独立为 #3459(未指派、未打标,待 PM triage):plugin-kanban/src/ObjectKanban.tsx(${objectName} Detail'Card Details')、plugin-tree/src/ObjectTree.tsx(title 直接给字面量 "Record Details")、plugin-view/src/ObjectView.tsx(${objectLabel} Detail)。

…3426)

PR #3423 gave NavigationOverlay's `resolvedTitle` an i18n default
(`detail.recordDetail`), but two hosts never let it run: they string-built
an English heading in TypeScript and passed it as the `title` prop, so a
zh/ja/de session got a fully localized drawer with one English heading.

Both paths are user-reachable: `list-view` / `object-grid` are public page
blocks and `navigation` is an authorable key on their schema, so authoring
`navigation: { mode: 'drawer' }` opens exactly this overlay on row click.
app-shell's ObjectView suppresses it (own `onRowClick` + own overlay), but
that is one host overriding a public block, not a dead branch.

- new `detail.recordDetailWithLabel` (`{{label}} Detail`) in all ten packs,
  so packs whose qualifier trails the noun (de) or that need a possessive
  particle (ja/zh) write their own arrangement instead of English word order
- the no-label branch reuses `detail.recordDetail`, the key the overlay
  itself defaults to, rather than minting a twin
- both keys added to LIST_DEFAULT_TRANSLATIONS / GRID_DEFAULT_TRANSLATIONS
  so a provider-less host still reads English, byte-identical to before

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

Request Review

@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-BRI5fLFw.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.51KB 105.39KB
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.25KB 49.08KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 105.19KB 25.39KB
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 6195841 Aug 6, 2026
18 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3426-overlay-caller-titles branch August 6, 2026 06:46
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 6, 2026
…tstack-ai#3459) (objectstack-ai#3464)

* fix(i18n): key the kanban / tree / view record-detail headings (objectstack-ai#3459)

objectstack-ai#3426 (PR objectstack-ai#3457) keyed ListView and ObjectGrid; the same grep found three
more hosts string-building an English heading in TypeScript, so the
surrounding drawer/panel was fully localized with one English phrase on it.

All three were re-verified against origin/main before implementing, each
with a test that drives the real interaction rather than by inspection:

- ObjectKanban — reachable, and needs no authoring at all: navConfig
  DEFAULTS to `{ mode: 'drawer' }` and every card's onCardClick is wired to
  navigation.handleClick. Two premise corrections: the consumer is
  RecordDetailDrawer, not NavigationOverlay, and its title renders as an
  sr-only SheetTitle — the drawer's accessible name, not a visible heading.
- ObjectTree — reachable, but only with `navigation` authored explicitly
  (no default) and only standalone; ListView passes its own onRowClick and
  neither host forwards `navigation`.
- ObjectView (mode 'split') — reachable; `navigation` is a declared
  authorable input and `layout` cannot reach the branch
  (ObjectViewSchema.layout excludes 'split', deriveRecordSurface never
  returns it). app-shell pins layout 'page', a host override.

Zero new locale keys: `detail.recordDetail` and
`detail.recordDetailWithLabel` already ship in all ten packs from objectstack-ai#3457,
and reusing them keeps one heading on one control. Each plugin gains its
own English defaults map for the provider-less path; plugin-tree gains a
dependency on @object-ui/i18n for it.

Visible English change, one, deliberate: the tree overlay goes from the
plural `Record Details` to the singular `Record Detail` (maintainer ruling
on objectstack-ai#3459 — normalize the strays, do not mint a plural key). A repo-wide
grep found no e2e spec and no unit test addressing the old string. Every
other branch is byte-identical with and without a provider.

The kanban's other plural (`'Card Details'`) is NOT a visible change: that
branch fires only when schema.objectName is falsy, while the drawer
consuming it returns null on the same condition. Keyed anyway so the
literal cannot leak if the guard relaxes; deliberately untested, with the
deadness itself pinned instead.

Reverse-verified by restoring all three files to origin/main: 11 of 20
assertions go red (kanban zh/ja/de, view zh/ja/de, tree en+zh/ja/de, tree
no-provider en) and 9 stay green (every byte-identical English branch).

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

* test(kanban): preload the lazy KanbanImpl chunk at module scope (objectstack-ai#3459)

`KanbanRenderer` renders `React.lazy(() => import('./KanbanImpl'))` behind a
Suspense boundary, and every assertion in the two new overlay-heading files
sits AFTER that boundary — a card must be on screen before it can be clicked.
That is exactly the flake class AGENTS.md §测试纪律 describes: an unbounded
module load charged to a bounded `findBy` window (a first `import()` measured
at ~976ms against RTL's 1000ms default under full CI parallelism).

Moving the import to module scope pays the cost in the import phase, which no
test/hook timeout constrains. The specifier is byte-identical to `./index`'s
so ESM resolves both to one cache entry and the component's own lazy factory
settles immediately — not `beforeAll`, which is bound by the NARROWER 10s
hookTimeout.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

[i18n][finding] ListView 与 ObjectGrid 自拼英文标题传给 NavigationOverlay 的 title prop,绕过 detail.recordDetail 默认值

2 participants