fix(components,i18n): 把记录浮层与页签计数徽标的剩余硬编码英文接入 i18n (objectstack#5506) - #3423
Merged
Conversation
#5430 之后仍有四处控制台 chrome 是硬编码英文字面量。和 #5430 不同的是,
这批里有一处是**可见文案**,还有一处是组件**默认值** —— 只是恰好被控制台
自己覆盖掉了,别的宿主拿到的就是那句英文。
- page:tabs 计数徽标的 aria-label 原本是模板字符串 `${formatTabCount(count)} items`。
徽标只渲染数字,这个 label 对屏幕阅读器而言就是徽标本身;而且英文复数是
拼接出来的,根本没有单数分支 —— 只有一条关联记录时会念成 "1 items"。
现在走 common.itemCount / common.itemCountOne。
- NavigationOverlay 的拖拽改宽手柄(role="separator",完全没有可见标签)
→ common.resizeDrawer。
- NavigationOverlay 的 expandLabel **默认值**。宿主可以覆盖(控制台的
ObjectView 就传了自己的),但默认值才是其它宿主真正发出去的东西,而且它同时
喂给一个纯图标按钮的 aria-label 和 title → detail.openAsFullPage,prop 覆盖行为不变。
- NavigationOverlay 的 resolvedTitle 兜底 'Record Detail' —— 这是**可见**的浮层标题,
不只是无障碍名 → detail.recordDetail。
- sr-only 的 SheetDescription/DialogDescription 文案 `Record detail overlay for {title}.`
原本抄了三份(drawer/modal/popover),现在合成一条带 {{title}} 占位符的
detail.recordDetailOverlay。
复数项走本仓既有的**双 key** 约定(detail.reactionCount/reactionCountOne、
detail.relatedRecords/relatedRecordOne),不用 i18next 的 _one/_other 后缀:
zh/ja/ko 没有独立单数形式,那几个包会合理地缺 _one 半边,
all-locales-key-parity 会把它读成丢键。
插值传的是**格式化后**的计数(1.2k,不是 1200),让无障碍名和可见数字始终一致;
同时 i18next 在 count 为字符串时会跳过自己的复数解析(needsPluralHandling =
count !== undefined && !isString(count)),所以选单复数的仍然是双 key 方案。
两个组件从 useSafeTranslate 换成 createSafeTranslation:新增的两条 key 需要插值,
而 useSafeTranslate 的签名里没有 options bag。defaults map 同时是无 provider 时
English 兜底的来源 —— 包外消费者依赖这一点:plugin-view 的 ObjectView.test.tsx
用 getByLabelText('Close panel')、e2e/live/inline-edit-polish-2572.spec.ts 用英文
无障碍名定位表头工具条,两者都没挂 I18nProvider。
六条新词条十个语言包全补。
测试方向(先预测后验证):把两个源文件回滚到 origin/main、保留新测试后,
en 用例仍全绿(这正是要保住的英文兜底),zh/ja/ko/de 用例全红;
页签徽标的 en 用例也红,因为旧代码根本没有单数形式("1 items")。
无 provider 那个文件里,NavigationOverlay 的断言前后都绿 —— 这是意料之中的方向,
不是漏测:它钉的就是"英文兜底不许被 t() 吃掉";该文件唯一由红转绿的是单数徽标。
无 provider 的断言必须单独成文件:createI18n 调 instance.use(initReactI18next),
而 initReactI18next 会把该实例注册成 react-i18next 的模块级默认实例,
卸载和 cleanup() 都不会撤销。同一文件里只要先挂过一次 I18nProvider,
之后所有"无 provider"渲染都会静默落到那个语言上(第一版就在 de 上撞了)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 6, 2026 02:46
This was referenced Aug 6, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 6, 2026
…read 不再硬编码英文 (objectstack#5506) (objectstack-ai#3438) `@object-ui/collaboration` 此前只依赖 `@object-ui/types`,所有用户可见文案都是 英文字面量 —— zh 会话下是一个中文控制台里套着一个英文评论区:"3 comments"、 "Reply"、"Resolve"、"just now"、"Add a comment... (use @ to mention)"。 按 PM 裁定走方案 A(整包一次接入),不做单串补丁、不做 formatter prop 兜底: 包内新增唯一翻译入口 `useCollaborationTranslation` / `COLLAB_DEFAULT_TRANSLATIONS`, 底层是 `createSafeTranslation`(data-table / form / filter-builder 既有模式)。 有 provider 时跟随会话语言,无 provider 时落到 defaults map 的英文 —— 后者不是锦上添花:CommentThread 是可独立使用的导出组件,宿主可能整棵树都没挂 I18nProvider,把字面量塞进没有兜底的 t() 正是把标签变成 `collaboration.reply` 这种裸 key 的方式。 单据列了 13 处,逐行复核实际是 **20 处**。原 sweep 漏掉的 7 处: `{n}h ago`、`{n}d ago`、`(edited)`、评论条数、`Oldest`/`Newest` 排序项、 `Replying to {name}...`、发送按钮 `Send`。20 处全部入键 —— 漏掉任何一处 都会发出一个"翻译了九成"的评论区。 其中两处除了没翻译,还多带一层缺陷:被编译进组件的不只是词,还有**复数规则**。 - 头部是 `` `${n} comment${n !== 1 ? 's' : ''}` ``; - 表情回应 tooltip 是 `` n === 1 ? '1 reaction' : `${n} reactions` ``。 两处产出的**英文都是对的** —— 这不是 objectstack-ai#3423 在页签徽标上修的那个 "1 items" 缺陷 —— 但选哪一种形式是硬写进渲染路径的英文语法。任何语言都无法套用自己的规则: ru 需要三种形式、ja 一种都不需要,无论语言包里写什么都表达不出来。 两处都改用本仓既有的**双 key** 约定(`collaboration.commentCount`/`commentCountOne`、 `collaboration.reactionCount`/`reactionCountOne`),不用 i18next 的 `_one`/`_other` 后缀:zh/ja/ko 没有独立单数形式,那几个包会合理地缺 `_one` 半边, all-locales-key-parity 会把它读成丢键。count 以**字符串**插值, i18next 因此跳过自己的复数解析(needsPluralHandling 对字符串 count 为 false), 选单复数的始终是双 key 方案。 reaction tooltip 用**专属**键对,没有复用 `detail.reactionCount`:后者插值 `{{emoji}}`,而这个调用点的 emoji 就是按钮本身的可见内容,没有值可以传给占位符, 复用会在每种语言下把字面量 `{{emoji}}` 留在无障碍名里。 相对时间按词条级处理:分/时/天分档原样不动,没有引入任何日期库。 `>= 7d` 分支仍走运行时自己的 `toLocaleDateString()` —— 它本来就不是硬编码英文, 而把它钉到会话语言另有失败模式(不认识的 tag 抛 RangeError 落进外层 catch, 反而会渲染出原始 ISO 串),另行跟踪。 `Save` / `Cancel` / `Edit` / `Delete` 读共享的 `common` 命名空间,没有在 `collaboration` 下再拼一遍:它们是通用动作词,十个包里早就有译文, 再写一份只是多一处要同步的地方。真正新增的 21 条 key 十个语言包全补真实译文。 测试方向(先预测,再按实跑结果订正 —— 有一条预测错了,如实记下而不是抹掉): - 有 provider 的文件:把 CommentThread.tsx 回滚到 origin/main、保留新测试后, zh/de/ja 共 13 个用例全红,4 个 en 用例全绿。en 两侧都绿正是目的,不是漏测: 英文文案必须原样存活,翻转反而说明文案被改动了。 - **预测错的那一条**:原以为 en 单数用例("1 comment")改前就红,即假定头部像 objectstack-ai#3423 里的页签徽标那样只拼了个 `s`、根本没有单数分支。实跑证明不是 —— origin/main 两处都写了单数分支,英文输出本来就正确。所以它和其余 en 用例 一样两侧都绿,真正钉住"复数规则不能编译进组件"的是 de 用例。 - 无 provider 的文件:**所有**断言两侧都绿。这就是它要钉的不变量(英文兜底不许被 t() 吃掉);它能抓的是本次改动自己引入的破坏 —— 组件里接了某个 key 而 COLLAB_DEFAULT_TRANSLATIONS 里漏了或拼错了,只有这个文件会红。 无 provider 的断言必须单独成文件:`createI18n` 调 `instance.use(initReactI18next)`, 该实例会被注册成 react-i18next 的模块级默认实例,卸载和 cleanup() 都不撤销; 同一文件里只要先挂过一次 I18nProvider,之后所有"无 provider"渲染都会静默落到 那个语言上(踩坑记录见 PR objectstack-ai#3423)。 Fixes objectstack-ai#3424 Refs objectstack-ai/objectstack#5506 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 6, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 6, 2026
…-ai#3440) (objectstack-ai#3445) objectstack-ai#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 就是它的 全部可访问名,没有别的可回落。 和 objectstack-ai#3424 的评论条数一样,前两处除了没翻译还多带一层缺陷:被编译进组件的不只是词, 还有**复数规则**。两处产出的英文都是对的(各自都有真正的单数分支,这不是 objectstack-ai#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 objectstack-ai#3440 Refs objectstack-ai#3424 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 6, 2026
…bjectstack-ai#3426) (objectstack-ai#3457) PR objectstack-ai#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 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes objectstack-ai/objectstack#5506
#5430 扫完之后仍留了一批同类字面量。和 #5430 不同的是,这批里有一处是可见文案,
还有一处是组件默认值 —— 只是恰好被控制台自己覆盖掉了,别的宿主拿到的就是那句英文。
每处的处理
`${formatTabCount(count)} items`renderers/layout/containers.tsx的page:tabs计数徽标common.itemCount/common.itemCountOneResize drawercustom/navigation-overlay.tsx拖拽改宽手柄的aria-labelcommon.resizeDrawerOpen as full pageexpandLabelprop 默认值detail.openAsFullPage(prop 覆盖行为不变)Record DetailRecord detail overlay for {resolvedTitle}.×3{{title}}占位符的detail.recordDetailOverlay计数徽标只渲染数字,它的
aria-label对屏幕阅读器而言就是徽标本身;而且英文复数是拼接出来的,根本没有单数分支 —— 只有一条关联记录时会念成 “1 items”。
复数:双 key,不用
_one/_other沿用本仓既有约定(
detail.reactionCount/reactionCountOne、detail.relatedRecords/relatedRecordOne)。i18next 的_one/_other后缀会被all-locales-key-parity判死:zh/ja/ko 没有独立单数形式,那几个包会合理地缺_one半边,而 gate 只会读成丢键。
插值传的是格式化后的计数(
1.2k,不是1200),让无障碍名和可见数字始终一致。这同时是安全的:i18next 只在
count不是字符串时才跑自己的复数解析(
needsPluralHandling = opt.count !== undefined && !isString(opt.count),i18next@26.3.6的Translator),formatTabCount恒返回字符串,所以选单复数的仍然是双 key 方案,不会被 i18next 抢走。
useSafeTranslate→createSafeTranslation两个组件都换了。原因是新增的两条 key 需要插值,而
useSafeTranslate的签名(
(keyOrKeys, fallback) => string)里没有 options bag。换过来还带来一份显式的English defaults map —— 那份 map 就是无 provider 时英文兜底的来源,包外消费者
依赖这一点:
packages/plugin-view/src/__tests__/ObjectView.test.tsx用getByLabelText('Close panel')e2e/live/inline-edit-polish-2572.spec.ts:61用英文无障碍名定位表头工具条两者都没挂
I18nProvider。前者本 PR 实跑过,32 tests 全绿。六条新词条(
common.resizeDrawer/common.itemCount/common.itemCountOne/detail.recordDetail/detail.openAsFullPage/detail.recordDetailOverlay)十个语言包全补,zh 及其余九包都写了真实译文。
测试与方向(先预测,后跑)
新增三个测试文件,共 30 个用例。反向验证:把两个源文件回滚到
origin/main、保留新测试和词条,预测 = en 全绿、非英文全红;实测 15 红 15 绿,与预测一致:
navigation-overlay-chrome-i18n.test.tsx:4 个en用例 + 2 个“宿主传值仍然赢”的用例保持绿,7 个 zh/ja/ko/de 用例全红。
page-tabs-count-badge-i18n.test.tsx:en用例也红 —— 旧代码根本没有单数形式,1 item在每种语言下都拿不到。只有纯复数的格式化用例(1.2k items)保持绿。chrome-i18n-no-provider-fallback.test.tsx:NavigationOverlay 的两条断言前后都绿。这是预期方向,不是漏测 —— 该文件钉的正是“英文兜底不许被
t()吃掉”,它本来就该在改动两侧都绿;该文件里唯一由红转绿的是单数徽标那条。
为什么无 provider 的断言必须单独成一个文件
createI18n调instance.use(initReactI18next),而initReactI18next会把该实例注册成 react-i18next 的模块级默认实例,卸载和
cleanup()都不撤销。所以同一文件里只要先挂过一次
I18nProvider,之后所有“无 provider”渲染都会静默落到那个语言上。第一版就撞上了:一个没挂任何 provider 的用例渲染出了
Als ganze Seite öffnen,因为同文件上一个 describe 挂过
de。domproject 是isolate: true,所以拆成一个从不 import
I18nProvider的文件才是干净的(该文件的 stderr 里能看到
NO_I18NEXT_INSTANCE,正是这份干净的证据)。验证记录
越界发现(未在本 PR 修)
packages/plugin-detail/src/RecordDetailDrawer.tsx:277有一份一模一样的aria-label="Resize drawer"(同样挂在role="separator"的拖拽手柄上)。该文件已经接好了
useDetailTranslation()(:136),所以本 PR 落地common.resizeDrawer之后那是一行的事 —— 但plugin-detail在本任务的文件围栏之外,单独归档。
packages/collaboration/src/CommentThread.tsx:520的'1 reaction'/`${n} reactions`未在本 PR 处理,原因见 issue 上的说明:
@object-ui/collaboration当前只依赖@object-ui/types,接入 i18n 需要改它的package.json(围栏之外),而且同文件还有另外 11 处硬编码英文 —— 只翻这一处会让文件更不自洽。已上报等决策。
Generated by Claude Code