Skip to content

fix(collaboration,i18n): PresenceAvatars 的三处硬编码英文接入 i18n (#3440) - #3445

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3440-presence-avatars-i18n
Aug 6, 2026
Merged

fix(collaboration,i18n): PresenceAvatars 的三处硬编码英文接入 i18n (#3440)#3445
yinlianghui merged 1 commit into
mainfrom
claude/issue-3440-presence-avatars-i18n

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3440
Refs #3424

#3424@object-ui/collaboration 整包接上了 @object-ui/i18n,但只改完 CommentThread。同包 PresenceAvatars 还剩三处英文字面量 —— 而它不是没人用的导出:控制台在两个地方渲染它(packages/app-shell/src/layout/AppHeader.tsx:894 的租户在线头像、packages/app-shell/src/views/RecordDetailView.tsx:2066 的「谁也在看这条记录」),zh 会话下这三处就是英文。

改了什么

三处分别是:

  • 头像组的 aria-label(原 `${n} user${n !== 1 ? 's' : ''} present`);
  • 溢出徽标的 title(原 `${n} more user${n !== 1 ? 's' : ''}`);
  • 每个头像的 title(原 `${name} (${status})`)。

第一处尤其要紧:这个控件只有图片和首字母,对读屏软件来说 aria-label 就是它的全部可访问名,没有别的可回落。

复数规则也一并搬出组件

#3424 的评论条数一样,前两处除了没翻译还多带一层缺陷:被编译进组件的不只是词,还有复数规则。两处产出的英文本身都是对的(各自都有真正的单数分支,这不是 #3423 在页签徽标上修的 "1 items"),但 n !== 1 ? 's' : '' 是写死在渲染路径里的英文语法,任何语言都套不上自己的规则。

两处改用本仓的双 key 约定(collaboration.presentUserCount/presentUserCountOnecollaboration.moreUserCount/moreUserCountOne),不用 i18next 的 _one/_other 后缀 —— zh/ja/ko 没有独立单数,那几个包会合理地缺 _one 半边,all-locales-key-parity 会把它读成丢键。count 以字符串插值,i18next 因此跳过自己的复数解析。

见证这次「规则搬家」的是德语:1 anwesender Benutzer vs 2 anwesende Benutzer 要变形容词词尾,原来的三元表达式无论语言包怎么写都产不出来。ru 钉住需要三种形式的语言,ja 钉住一种都不需要的语言。

status:显示层翻译(按 PM 裁定)

头像 tooltip 收成一个 collaboration.userStatusTitle 键({{name}} ({{status}})),括号连同空格都归翻译所有 —— CJK 语言包因此可以去掉英文在 ( 前的那个空格,与各包既有的 edited: '(已编辑)' 一致。

其中的 status显示层翻译(collaboration.statusActive/statusIdle/statusAway):PresenceUser['status'] 的枚举值仍是数据,存储、比较、传递(包括 statusColors 查色)一律不动,只在这一个渲染出口翻译。落在联合类型之外的 status 原样渲染成它自己 —— presence 用户来自宿主提供的 PresenceSource 传输层(WebSocket/SSE,本包不拥有),类型再严运行时也可能收到未映射的值;兜底只回落到原始字符串,不臆造标签、也不留下一对空括号。

顺带一提:单据正文猜的枚举值是 online/away,实际是 active | idle | away(usePresence.ts),键按实际枚举建。

新增 8 个键,十个语言包全部补齐真实翻译。

反向验证(方向先预测、后运行)

PresenceAvatars.tsx 单独 checkout 回 origin/main,两个测试文件不动:

  • 预测:provider 文件里 10 条 zh/de/ru/ja 用例 RED、6 条 en + defaults-map 用例 GREEN;no-provider 文件 8 条全绿(英文文案前后逐字节相同,origin/main 的三元本来就产出正确单数,这里若翻转反而说明文案被改动了)。
  • 实测:Tests 10 failed | 14 passed (24) —— 与预测逐条一致。

唯一一处新增的英文行为是 off-union status 的兜底:origin/main 直接插值原始值,现在仍然如此,所以那条也是两侧皆绿。

测试

仓根跑(packages/collaboration + packages/i18n + 四个 mock 掉 PresenceAvatars 的 RecordDetailView 消费者测试):

pnpm exec vitest run packages/collaboration/ packages/i18n/ \
  packages/app-shell/src/views/RecordDetailView.*.test.tsx
Test Files  31 passed (31)
     Tests  336 passed (336)

新增两个测试文件,按 #3438 立下的规矩拆开:

  • presence-avatars-i18n.test.tsx —— 挂 I18nProvider 的会话语言用例;
  • presence-avatars-no-provider-fallback.test.tsx —— 无 provider 的英文兜底,整个文件不 import I18nProvider(createI18n 会把实例注册成 react-i18next 的模块级默认实例,同文件内挂过一次就再也回不到「无 provider」)。

另外补了一条 #3438 声明过、但没人守的不变量:COLLAB_DEFAULT_TRANSLATIONScollaboration.* 子集与 en.collaboration 逐键逐值相等。本次正是往这两处各加同样 8 个键的形状 —— 只加一边在运行时完全隐形(有 provider 时语言包赢,无 provider 时 map 赢,两条路各看各的都正常)。

type-check(先 build 依赖)与 lint 均通过,scripts/check-control-bytes.mjs 已跑。


Generated by Claude Code

#3424 把 `@object-ui/collaboration` 整包接上了 `@object-ui/i18n`,但只改完
`CommentThread`。同包 `PresenceAvatars` 还剩三处英文字面量 —— 而它不是没人用的
导出:控制台在两个地方渲染它(`app-shell/src/layout/AppHeader.tsx` 的租户在线头像、
`app-shell/src/views/RecordDetailView.tsx` 的"谁也在看这条记录"),zh 会话下这三处
就是英文。

三处分别是:

- 头像组的 `aria-label`:`` `${n} user${n !== 1 ? 's' : ''} present` ``;
- 溢出徽标的 `title`:`` `${n} more user${n !== 1 ? 's' : ''}` ``;
- 每个头像的 `title`:`` `${name} (${status})` ``。

第一处尤其要紧:这个控件只有图片和首字母,对读屏软件来说 aria-label 就是它的
全部可访问名,没有别的可回落。

和 #3424 的评论条数一样,前两处除了没翻译还多带一层缺陷:被编译进组件的不只是词,
还有**复数规则**。两处产出的英文都是对的(各自都有真正的单数分支,这不是 #3423
在页签徽标上修的 "1 items"),但 `n !== 1 ? 's' : ''` 是写死在渲染路径里的英文语法,
任何语言都无法套用自己的规则。两处改用本仓的**双 key** 约定
(`collaboration.presentUserCount`/`presentUserCountOne`、
`collaboration.moreUserCount`/`moreUserCountOne`),不用 i18next 的 `_one`/`_other`
后缀(zh/ja/ko 没有独立单数,parity gate 会把合理缺失读成丢键);count 以字符串插值,
i18next 因此跳过自己的复数解析。见证这次"规则搬家"的是德语:
"1 anwesender Benutzer" vs "2 anwesende Benutzer" 要变形容词词尾,原来的三元表达式
无论语言包怎么写都产不出来。

头像 tooltip 收成一个 `collaboration.userStatusTitle` 键(`{{name}} ({{status}})`),
括号连同空格都归翻译所有 —— CJK 语言包因此可以去掉英文在 `(` 前的那个空格,
与各包既有的 `edited: '(已编辑)'` 一致。

其中的 `status` 走**显示层翻译**(`collaboration.statusActive`/`statusIdle`/
`statusAway`):`PresenceUser['status']` 的枚举值仍是数据,存储、比较、传递
(包括 `statusColors` 查色)一律不动,只在这一个渲染出口翻译。落在联合类型之外的
status 原样渲染成它自己——presence 用户来自宿主提供的 `PresenceSource` 传输层,
类型再严运行时也可能收到未映射的值,兜底只回落到原始字符串,不臆造标签、
也不留下一对空括号。

新增 8 个键,十个语言包全部补齐真实翻译。

Fixes #3440
Refs #3424

Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

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

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-_4_JwT8p.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) 478.80KB 105.18KB
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.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.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 05:16
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit ca0fa8f Aug 6, 2026
17 of 18 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3440-presence-avatars-i18n branch August 6, 2026 05:16
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.

[i18n] PresenceAvatars 三处硬编码英文(控制台头部与记录详情实际在渲染)

2 participants