Skip to content

ui: 8 个组件内联 toLocale* 渲染日期/时间,输出跟随宿主 OS 区域而非应用语言 #292

Description

@wxrbyte

问题与复现

@finagent/i18n 的 format.ts 头部把日期/时间的渲染职责写死在这一层(packages/i18n/src/format.ts:4-7):

Every money/percent/date/relative-time value must render through this layer so the UI locale controls presentation.

金额侧的同一契约写得更直白(packages/ui/src/lib/money.ts:5-7):

…so the active UI locale controls presentation (issue #86): the OS locale no longer participates, and one view can never mix US$1,234.56 and $1,234.56.

i18nCurrentLocale()(format.ts:30-32)存在的唯一目的就是让渲染方不去读宿主区域,仓库里已有按此写法的先例(components/automation/RuleCard.tsx:30,44、components/workspace/PortfolioSection.tsx:43)。

但 UI 包里有 8 处直接把区域参数留给运行时(toLocaleString() / toLocaleDateString() / toLocaleTimeString([], …)),于是输出由宿主 OS 的区域与日期模式决定,与应用语言无关。

以 WhatChangedSection 为例,取 previousReport.generatedAt = 1732180800000(2024-11-21T09:20:00Z):

date: new Date(previousReport.generatedAt).toLocaleDateString(),

应用语言为 zh-CN、宿主也是中文 Windows 时,Date.prototype.toLocaleDateString() 走宿主 yyyy/M/d 模式 ⇒ 渲染成 2024/11/21,而不是本应用统一层对该语言的形状 2024年11月21日。即宿主语言与应用语言一致时照样错,因为内联调用用的是宿主日期模式,不是本应用的格式化策略;宿主语言不同时则连语言都会串(zh-CN 应用 + en-US 宿主 ⇒ 11/21/2024)。

8 个调用点(行号基于 3a17eca6):

# 文件:行 内联写法
1 components/primitives/DataFreshness.tsx:15 new Date(updatedAtMs).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit', second:'2-digit' })
2 components/research/EvidenceList.tsx:26 new Date(ref.fetchedAt).toLocaleTimeString()
3 components/research/WhatChangedSection.tsx:53 new Date(previousReport.generatedAt).toLocaleDateString()
4 components/research/ResearchReportView.tsx:86 new Date(report.generatedAt).toLocaleString()
5 components/settings/ConnectionCard.tsx:323 new Date(lastCheck).toLocaleString()
6 components/settings/EvaluationSettingsTab.tsx:39 new Date(updatedAt).toLocaleDateString(undefined, { year:'numeric', month:'short', day:'numeric' })
7 components/thesis/ThesisImpactList.tsx:40 new Date(impact.evaluatedAt).toLocaleDateString()
8 components/discover/DiscoverView.tsx:471 new Date(run.createdAt).toLocaleString()

预期与修复方向

统一改走 @finagent/i18n 的日期/时间 formatter:每个调用点一行 + 一行 import。

选 formatDate / formatDateTime / formatMarketTime 而不是在组件里补一个 i18nCurrentLocale(),是因为 8 处的 options 都能在统一层找到逐项等价的实现,不需要新增任何 API:

  • 1、2 的「时钟」形状 ⇒ formatMarketTime(format.ts:136-144,其 docstring 就是 "Clock time only (market-data freshness lines)")
  • 3、6、7 的「只有日期」形状 ⇒ formatDate(:114-122;其中 6 传入的 options 与该方法内部实现逐项相同)
  • 4、5、8 的「日期 + 时间」形状 ⇒ formatDateTime(:124-134)

单位不需要处理:DataFreshness 的入参注释是 "Epoch ms."(DataFreshness.tsx:9 附近 docstring),ProviderHealth.lastCheck 也是 epoch ms,与 @finagent/i18n 各 formatter 的入参单位一致 ⇒ 本 PR 不涉及任何秒/毫秒改动。

复现证据

  • 环境:Windows(10.0.26200.9457,x64)、Bun 1.4.2、main 3a17eca6(2026-10-03T16:16:35Z)。
  • 新增回归守卫 packages/ui/src/i18n/localeRendering.test.tsx(6 个用例,逐站点断言「应用语言决定输出」);packages/ui/src/components/discover/DiscoverView.test.tsx 追加 1 个同型用例。
  • 修复前:bun test packages/ui/src/i18n/localeRendering.test.tsx = 0 pass / 6 fail,断言对如下(左=应用语言期望,右=宿主区域实际):
DataFreshness        Expected: "09:20:00 AM"        Received: "Longbridge · Updated 09:20:00"
EvidenceList         Expected: "09:20:00 AM"        Received: "claimQuote · 09:20:00"
WhatChangedSection   Expected: "2024年11月21日"      Received: "What ChangedSince 2024/11/21No material changes."
ResearchReportView   Expected: "2024年11月21日 09:20" Received: "… · 2024/11/21 09:20:00Evidence…"
ThesisImpactList     Expected: "2024年11月21日"      Received: "Weakened2024/11/21weakened by a downgrade"
ConnectionCard       Expected: "2024年11月21日 09:20" Received: "…Portfolio ✓2024/11/21 09:20:00"

bun test packages/ui/src/components/discover/DiscoverView.test.tsx = 12 pass / 1 fail,唯一失败即新用例:

Expected to contain: "2023年11月14日 22:13"
Received: "…Strong Momentum2023/11/14 22:13:20 · 1 candidateReopen"
  • 修复后:localeRendering 6 pass / 0 fail;DiscoverView 13 pass / 0 fail。
  • 受影响测试回归(bun test --isolate):DataFreshness.test.tsx 3/0、WhatChangedSection.test.tsx 6/0、evaluation.test.tsx 5/0、localeRendering.test.tsx 6/0、DiscoverView.test.tsx 13/0 ⇒ 合计 33 pass / 0 fail。前三个既有套件在修复前同样为 3/0、6/0、5/0(已用「还原为 main 原始字节后重跑」做过 A/B 对照),因此无行为回归。
  • bun run i18n:check ⇒ key parity: en-US 1487 keys ≡ zh-CN 1487 keys (0 issues)(零新增 key)。
  • tsc --noEmit(packages/ui)exit 0。
  • 上游 PR checks(.github/workflows/pr.yml)当前 state=disabled_manually,新建 PR 不会产生 check run,故以上均为本地等价命令的结果。

范围

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

claimedClaimed by a contributor and currently in progress

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions