fix(collaboration,i18n): 给 CommentThread 三颗纯 emoji 按钮加可访问名,并让 7 天以上的日期跟随会话语言 (#3441) - #3481
Merged
Merged
Conversation
…s and localize its >= 7d date (#3441) Two leftovers from objectstack#5506 / objectui#3424 in the same component. `CommentThread` is exported and published with no in-repo consumer, so both only ever bite an external host. One — each comment's two quick-reaction buttons ('👍' / '❤️') and the reply banner's dismiss button ('✕') had no aria-label and no title. The '+' picker beside them has had one since #3424 (collaboration.addThumbsUp), which is what makes these three an omission rather than a design choice. aria-label, not the title the '+' uses: a button's accessible name is computed from CONTENT (accname 2F) before the title tooltip (2I), so a title on a glyph button decorates the mouse and leaves the name alone. Screen readers read the codepoint — "thumbs up" / "red heart" in English whatever the session language, and for U+2715 MULTIPLICATION X often nothing at all. Three new keys in all ten packs: collaboration.reactThumbsUp / reactHeart / cancelReply. reactThumbsUp is deliberately NOT a reuse of addThumbsUp even though both dispatch onReaction(id, thumbs-up) today: addThumbsUp names the reaction bar's picker entry point, and on a comment that already has reactions the two controls are on screen together, so one shared key would put two visibly different buttons under one name. cancelReply rather than the generic common.cancel because an accessible name has to say what is cancelled (only the reply target is dropped; composer text survives). Two — formatTimestamp ended in a bare toLocaleDateString(), the RUNTIME's locale, so a zh session read "6 天前" at six days and 8/1/2026 at eight. The session language is now passed, but not straight through: toLocaleDateString canonicalizes its tag and throws RangeError on anything not well-formed per BCP 47, and the session language reaches the component verbatim — a host configuring defaultLanguage: 'en_US' hands Intl a tag it refuses. That RangeError would land in formatTimestamp's outer catch, whose fallback is `return iso`, replacing the date with a raw ISO string: worse than the un-localized date. The absolute-date branch therefore gets its own local try/catch falling back to the no-argument call, so a malformed tag degrades to exactly the previous behaviour. A well-formed but unknown tag (xx-YY) does not throw at all. No date library, no month/weekday copy in the packs. Tests assert the computed accessible name via getByRole('button', { name }), not the presence of an attribute — that distinction is what the fix turns on — and pin that no button answers to a bare emoji. Directions: the name cases and the zh/de absolute-date cases are red-before/green-after; the malformed-tag case is green on BOTH sides and is recorded as such, because origin/main never passed a tag anywhere. Its counterfactual is the naive fix — dropping the inner catch turns it red with the raw ISO in the DOM. Fixes #3441 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
|
This was referenced Aug 6, 2026
yinlianghui
marked this pull request as ready for review
August 6, 2026 13:51
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 #3441
objectstack#5506 / #3424 在同一个组件里留下的两处尾巴。
CommentThread是 published exported component,仓内无消费方 —— 两处都只有外部宿主会踩到。前提已在
origin/main(2a9513d)上逐条核对:三颗按钮仍无名字,toLocaleDateString()仍是裸调用,而旁边的+仍有名字 —— 最后这一点正是"遗漏而非设计"的证据。一、三颗纯 emoji 按钮的可访问名
每条评论的两颗快捷回应按钮(
'👍'/'❤️')和回复提示条的关闭按钮('✕')既无aria-label也无title。用
aria-label,不是旁边+用的title。 对button来说 accname 先算内容(§2F name from content),title是最后一档兜底(§2I) —— 所以在一颗内容是字形的按钮上挂title只装点了鼠标,名字纹丝不动。屏幕阅读器读出来的是码位本身:"thumbs up"、"red heart",不管会话是什么语言都是英文;而 U+2715 MULTIPLICATION X 则往往什么都不读。十个包各加三个 key:
collaboration.reactThumbsUp/reactHeart/cancelReply。reactThumbsUp刻意不复用addThumbsUp,尽管两者今天都派发同一个onReaction(id, '👍'):addThumbsUp命名的是回应条上那颗选择器入口(样式名reactionPicker,是个桩),它的文案将来要跟着选择器走;而在任何已有回应的评论上,两颗控件是同屏并存的,共用一个 key 会让两颗外观不同的按钮顶着同一个名字。同理cancelReply而非通用的common.cancel—— 可访问名必须说清取消的是什么(只丢弃回复目标,输入框里已经打的字不受影响)。二、7 天以上的时间戳不跟随会话语言
formatTimestamp最后一档是裸的date.toLocaleDateString(),取的是运行时的 locale,于是 zh 会话里 6 天前显示"6 天前",8 天前却显示8/1/2026。改成传入会话
language,但不是直接传 —— 那正是 #3424 点明并绕开的坑。toLocaleDateString(tag)会先 canonicalize 参数,凡不符合 BCP 47 well-formed 的一律抛RangeError;而会话语言是原样到达组件的:宿主若配了defaultLanguage: 'en_US'(POSIX 写法,看着像模像样,实际被拒),Intl就会拒收。这个RangeError会一路落进formatTimestamp的外层 catch,而它的兜底是return iso—— 日期被换成裸的2026-08-01T09:30:00.000Z,比原来那个没本地化的日期更糟。所以绝对日期这一档单独包了一层
try/catch,失败时回退到无参调用:畸形 tag 退化成恰好是原来的行为(运行时自己的 locale),跟随会话语言的最坏情况是原状,永远不会是回退。well-formed 但未知的 tag(如xx-YY)根本不抛 ——Intl会解析到默认值 —— 所以只有真正畸形的 tag 会走到兜底。不引日期库,locale 包里也不加月份/星期文案:Intl已经拥有各 locale 的顺序与分隔符。验证方向(先预测,后运行)
预测写在跑之前,结果与预测完全一致。
A. 把
CommentThread.tsx退回origin/main、其余全留 —— 预测 9 条红;实测 9 条红:注意可访问名这一族连
en都是红的 —— 这与本文件里既有的 copy pin 不同:这些名字在origin/main上任何语言下都不存在,不存在"英文本来就对"的那一半。模板预设的"en 两侧皆绿"在这里不成立,如实记录。B. 畸形 tag 那条:两侧皆绿,如实上报。 它的反向验证不是退回旧代码 ——
origin/main从没把 tag 传到任何地方,自然无从被畸形 tag 绊倒。它的反事实是天真修法,也确实是拿它验的:删掉formatAbsoluteDate里那层try/catch(把language直接传进去),精确地只有这一条转红:即渲染出来的正是外层 catch 吐出的裸 ISO 串。
C. 探针实测(非推演):
language原样透传,toLocaleDateString('en_US')在本运行时确实抛RangeError,且 ICU 齐全:测试
断言的是
getByRole('button', { name })算出来的可访问名,而不是某个属性在不在 —— 这个区分正是本次修改的立足点(title在,名字不在)。并且反向钉住:没有任何按钮的名字还是一个裸 emoji。无 provider 的英文断言留在它自己的文件里(module-globalinitReactI18next陷阱),并新增一条属性扫描 —— 可访问名住在属性里,既有那条container.textContent的裸 key 扫描根本看不见它。lint的告警全部是既有的(CommentThread.tsx:505那条setState in effect在origin/main上就有,不在本次改动行上)。控制字符另做了越过闸门的自查(grep -naP扫\x00-\x08等)—— 本 PR 正文与代码注释都提到了 U+2715,属于闸门盲区那一类。消费半径
CommentThread与这三个 key 在仓内无其他消费方(grep过全仓:仅ROADMAP.md与两个既有 changeset 提到组件名),所以不存在 PR #5046 那种"改在 A 包、fixture 在 B 包"的漏扫面。顺带发现(已另行归档,未在本 PR 修)
#3478 —— 旁边那颗
+选择器的title其实从未成为它的可访问名(内容'+'优先),#3424 只翻译了 tooltip。证据就是本 PR 里现在就绿的那条断言:getByTitle('Add thumbs up')找得到,而queryAllByRole('button', { name: 'Add thumbs up' })长度为 0。#3441 正文把这颗按钮当作参照物明确排除在范围外,故单开一条,未在此 PR 内修改。Generated by Claude Code