Skip to content

fix(i18n): resolve _views translation keys by the bare view name only (#3502) - #3768

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3502-view-suffixes-bare-key
Aug 8, 2026
Merged

fix(i18n): resolve _views translation keys by the bare view name only (#3502)#3768
yinlianghui merged 1 commit into
mainfrom
claude/issue-3502-view-suffixes-bare-key

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3502

objectstack#5164 裁决 A(维护者 2026-08-06 批复)的第 3 棒 / objectui 段:_views 翻译键 canonical = 运行时视图身份的裸键,四面收敛。前两棒已落地 —— 提取器改为向组装器查询运行时身份键(objectstack#6124,b3c1f3cd5)、packages/lint collectViewRecord 收窄到单拼写(objectstack#6038,7618ee814)。本 PR 收掉最后一面:客户端二次解析。

改了什么

packages/i18n/src/useObjectLabel.tsviewSuffixes 原本剥掉对象前缀得到裸键后,若裸键落空还会再试带前缀的全名作为第二候选:

  • 第 1 候选 objects.crm_opportunity._views.pipeline_kanban.label(裸键,保留)
  • 第 2 候选 objects.crm_opportunity._views.crm_opportunity.pipeline_kanban.label(带前缀全名,已删除)

删掉第 2 候选后,按带前缀键编写的译文块完全不再被读取,标签回落到 metadata 默认文案 —— 与「根本没写译文」表现一致。按裸键编写的(提取器唯一产出、os lint 唯一接受的拼写)不受影响。

对象名那一轴不动:objects.{ns__obj} 落空后仍退到 objects.{obj},所以按短对象名写的译文包在运行时给出带命名空间对象名时照样命中。这是另一条正交的兜底,和视图名无关,已单独钉测试。

为什么是删而不是留

服务端解析只认那一个裸键(objectstack#5165,刻意为之)。客户端多接受一个拼写 ⇒ 两端宽容度不对称:按带前缀键编写的包在 Console 里显示中文,而 REST 边界 / 移动端 / 纯 HTTP / SDUI 这些不做二次解析的消费方拿到的仍是英文。半成功比干净的落空更难被发现,还把一套第二方言事实上固化成了并行契约(违反 AGENTS #0.1 contract-first:宽容读取的正确修法在生产方)。

关于验收词「响亮回退」—— 需要如实说明它在本文件里落成什么:

  • 响亮的部分:回退是全消费面一致可见的(屏幕上就是未翻译的默认文案),不再是「只有 Console 对」。
  • 不新增 warn 通道:本文件的 convention 探测刻意I18N_PROBE_FLAG,被 i18n.ts 的 missing-key warner 跳过(见该文件 36-45 行的设计说明)。那个 hook 是在键落空时触发的,而「没写任何视图译文」恰恰是绝大多数 app 的健康状态 —— 让本路径去命中它,会在健康路径上刷警告,而不是在出错路径上。它也无法表达「键写了但拼写已被拒绝」这件事(那需要枚举 bundle)。
  • 真正的响亮在生产方:带前缀键在 os lint 里被 translation-target-unknown 点名(第 2 棒 objectstack#6038 收窄后,该对象没有任何视图声明这个名字)。这符合裁 A 的「声明即强制」本义 —— 消费端不做第二份弱化副本。

如维护者希望消费端另加一条一次性 dev 警告(仅当带前缀键确实存在于 bundle 时才响,零噪音),可以再开一单;本 PR 不擅自新增警告通道。

测试

packages/i18n/src/__tests__/useObjectLabel-view.test.tsx 新增 4 例(原有 2 例保留、未改):

  1. 带前缀键不解析 —— label / description / emptyState 三个走 viewSuffixes 的出口全部回落到默认文案(反向验证的锚点)。
  2. 裸键与带前缀键同时存在时命中裸键(钉候选顺序未被反向改动)。
  3. 对象名那一轴仍在:短对象名译文包 + 带命名空间的运行时对象名 → 命中;运行时 id 用短对象名限定也照样剥掉。
  4. 回退在 dev 控制台不产生噪音(warnMissingKeys: trueconsole.warn 零调用),把上面那条 probe 约定钉住,防止后人用「取消 probe 标记」来实现响亮。

反向验证(先声明预期方向再跑):把删掉的第二候选加回去 → 预期 2 红 4 绿,红的是两条 prefixed-only 钉子(裸键在候选链首位,所以另外 4 例不该动)。实跑一致:

× does not resolve a view translation authored under the prefixed full name
    AssertionError: expected 'Prefixed pipeline' to be 'Sales Pipeline'
× falls back visibly on screen, without adding dev-console noise
 Tests  2 failed | 4 passed (6)

恢复改动后:

pnpm exec vitest run packages/i18n --maxWorkers=2
 Test Files  26 passed (26)      Tests  361 passed (361)      # 改前基线 357
pnpm exec vitest run packages/layout --maxWorkers=2
 Test Files  6 passed (6)        Tests  92 passed (92)        # 消费方 NavigationRenderer
pnpm exec tsc --noEmit -p packages/i18n/tsconfig.json         # exit 0
pnpm --filter @object-ui/i18n lint                            # 0 errors(32 warnings 全部为存量)
node scripts/check-i18n-call-site-keys.mjs                    # 门禁绿,3486 call sites 全解析
node scripts/check-i18n-en-drift.mjs                          # 0 en value changed
node scripts/check-control-bytes.mjs                          # OK(3699 files)
node scripts/check-changeset-fixed.mjs / -no-major.mjs         # 绿

all-locales-key-parity 在上面 26 个文件内一并跑绿。另外手工扫了改动文件的控制字节(grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]',零匹配)。

存量键排查(派发说明第 3 条)

全仓 git grep _views(排除 CHANGELOG):没有任何仓内译文包/fixture 使用带前缀拼写 —— packages/i18n 两个测试与 spec-translations 都是裸键,app-shell / layout 的命中全是 list_views 元数据或注释。第 1 棒已把 showcase / app-todo / app-crm 的存量迁完(在 objectstack 仓),因此本 PR 无 fixture 改名,消费方测试(viewLabel 全为 mock)也无一依赖被删的那条候选。

影响面

用户可见行为变更,已带 .changeset(@object-ui/i18n patch,含 FROM→TO 迁移指引:去掉键里的对象前缀)。按仓库约定不标 major


Generated by Claude Code

…ly (objectui#3502)

`viewSuffixes` stripped the object prefix off the runtime view id to build the
canonical `_views.<view>.<tail>` key, then — if that missed — tried the prefixed
full name `_views.<object>.<view>.<tail>` as a second candidate. That made this
client strictly more lenient than the server-side resolver, which reads the one
bare key (objectstack#5165): a bundle authored against the prefixed spelling
showed a translated label in the Console and English at the REST boundary, on
mobile, over plain HTTP and in SDUI — every consumer that does not run a second
resolution pass. A half-success is harder to spot than a clean miss.

Third and last leg of the objectstack#5164 convergence (maintainer ruling A,
2026-08-06: canonical `_views` key = the runtime view identity's bare name).
Leg 1 made the extractor ask the view composer for the key (objectstack#6124);
leg 2 narrowed `packages/lint` to that single spelling (objectstack#6038); this
resolver now accepts exactly what those produce. A prefixed key falls back to the
metadata default on every surface alike, and is named at authoring time by
`os lint`'s `translation-target-unknown` — loudness at the producer, per
contract-first, rather than a lenient read here.

The object-name axis is untouched: a bundle keyed by the short object name still
resolves when the runtime presents the namespaced one.

Tests pin both directions — bare key resolves (including when a prefixed sibling
is present), prefixed-only falls back on label / description / emptyState — plus
that the fallback adds no dev-console noise, since convention probes are
deliberately excluded from the missing-key warner and would otherwise warn on
every app that authored no view translations at all.

No in-repo bundle used the prefixed spelling, so no fixture migration was needed.

Co-Authored-By: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 8, 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 8, 2026 1:06pm

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
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) 26.07KB 7.56KB
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) 6.05KB 2.52KB
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) 481.28KB 105.96KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.85KB 3.18KB
data-objectstack (index.js) 138.22KB 35.46KB
fields (index.js) 230.90KB 56.84KB
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) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
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.32KB 1.64KB
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) 117.06KB 30.24KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.71KB 49.68KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
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.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 8, 2026 13:14
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 32413ec Aug 8, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3502-view-suffixes-bare-key branch August 8, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useObjectLabel viewSuffixes:去掉带前缀全名第二候选 —— objectstack#5164 裁 A 的 objectui 段

2 participants