Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

createSafeTranslation 的无 provider 回退插值只认 {{name}} 一种写法,i18next 还认带空格 / 带格式化 / 嵌套三种 #3512

Closed

Description

@yinlianghui

做 #3418(回退插值只替换第一次出现)时,把回退路径和 i18next 逐条对过一遍,顺带发现这条分叉比 #3418 记的那一条更宽。观察类 —— 今天没有任何一条文案命中,所以打 finding、不排 pm:queue。

前置:#3418 的修法(PR #3510)只把「替换第一次」改成「替换全部」。下面这三条在那个 PR 之后依然存在,不是它引入的,也不在它的裁定范围里。

事实

packages/i18n/src/useSafeTranslation.ts 的 fallbackT 用的是精确的字面量 needle:

value = value.split(`{{${k}}}`).join(String(v));   // PR #3510 之后的样子

也就是说它只认 {{name}} 这一种拼写。i18next(挂了 provider 时真正干活的那个)认的更多。在一个按 createI18n 同样配置(interpolation: { escapeValue: false })的真实 i18next 实例上跑出来的对照:

默认文案 options i18next 回退(现状)
Hello {{ name }} {name:'Ada'} Hello Ada Hello {{ name }} ❌
Total {{count, number}} {count:12345} Total 12,345 Total {{count, number}} ❌
A $t(otherKey) — 展开 otherKey 原样吐出 ❌

(第四种 {{- name}} 反转义前缀同理,不过 escapeValue: false 下它和普通写法等价,没有实际差别。)

失败形态和 #3418 一模一样:不抛错、不打日志,把 {{ name }} 这种字面量直接吐到屏幕上,而且专挑没有挂 I18nProvider 的宿主(独立 / 嵌入式渲染)—— 我们最看不到的那类运行环境。

为什么今天踩不到

扫过 packages/ + apps/ 全部源码字符串:

  • 花括号内带空格的 i18n 占位符:0 条(grep 到的 {{ opacity、{{ transform 之类全是 JSX 的 style 对象字面量,不是文案);
  • 带格式化参数 {{count, number}}:0 条;
  • i18next 嵌套 $t(:0 条。

所以和 #3418 当初一样,是一条休眠的分叉:语义两边不一致,但目前没有输入能把差异暴露出来。

为什么仍然值得记一笔

  1. 触发它只需要有人在默认表里顺手写成 {{ count }}(多打两个空格),或者想用 i18next 的数字格式化。在语言包里这么写是完全合法的,i18next 会正确渲染,于是作者不会有任何理由怀疑自己写错了 —— 只有在没有 provider 的宿主上才炸。
  2. 回退表和语言包之间已有 parity 测试互钉字面量,useSafeTranslation.test.tsx(PR fix(i18n): fallback interpolation replaces all placeholder occurrences #3510)现在钉了替换范围,但没有任何测试钉占位符拼写的接受集合。
  3. 这类「AI 写的元数据 / 文案在宽容消费端悄悄出错」正是仓库口味最想避免的形状。

可能的方向(未裁定,留给 triage)

  1. 让回退更接近 i18next:把字面量 needle 换成 {{\s*name\s*}} 的正则(名字要转义),格式化和嵌套仍不支持 —— 只补最便宜的那条,分叉变窄但没消失。
  2. 收紧契约:加一条守卫/测试,要求默认表里的占位符只能用回退能认的那种精确拼写,写错就在 CI 上红。方向上是「declared = enforced」,但 createSafeTranslation 的无 provider 回退只替换每个占位符的第一次出现 #3418 的裁定里明确否决过「为绕开实现 bug 去限制作者能写的句式」—— 这里性质不同(不是绕 bug,是声明一个可支持子集),值得单独判一次。
  3. 回退直接复用 i18next 的 interpolator(不 init 完整实例,只借插值),分叉一次性消掉,代价是回退路径多一份依赖/体积。

倾向 3 > 2 > 1(3 最彻底、也最难写错),但这是契约形状的选择,不自作主张。


⚠️ 去重说明:开 issue 前的「搜一遍现存 open issue」这一步没做成 —— 共享身份的 GitHub 列表/搜索 API 当前处于 rate limit(API rate limit already exceeded),list_issues 连续多次失败,无法枚举。内容本身已按 #3418 的线索逐条核验过,但是否与某条已存在的 issue 撞车没能确认,triage 时请顺手查一下。


Generated by Claude Code

Activity

  1. yinlianghui commented on Aug 7, 2026

    @yinlianghui
    ContributorAuthor

    发现分诊轮(持有):前提复核 live(fallback 仍只认 {{name}} 精确拼形),但三种方言形态在仓内实测零命中、全部休眠;修形要回答"回退该重实现 i18next 方言到什么程度",宜等首个真实出现或并入未来 i18n 加固批,不单独派。(objectui 车道 PM,会话 session_01GTRjn8xBqp75dk7kFupVRt)


    Generated by Claude Code

  2. claude commented on Aug 9, 2026

    @claude
    Contributor

    Findings round — held (finding kept).

    Premise re-verified on origin/main @ 53b7b88: the fallback needle is still the exact literal value.split({{${k}}}) (packages/i18n/src/useSafeTranslation.ts:40), so the spaced/formatted/nested divergence vs i18next stands. Still dormant — the finder's sweep found zero in-repo strings using any of the wider spellings, and no new i18n work has changed that. Held because the two candidate directions (widen the fallback vs gate the accepted spellings in CI) were explicitly left un-adjudicated and #3418's ruling constrains which are legal; nothing hits it today. Restart condition: first in-repo default string using a spaced/formatted/nested placeholder, or a ruling on the accepted-spelling contract.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. claude commented on Aug 10, 2026

    @claude
    Contributor

    Triage (findings pass): held, premise re-verified on origin/main @ c29ceff — grade unchanged, no ownership taken.

    • The fallback needle is still the exact literal (useSafeTranslation.ts:40, split/join on {{k}}).
    • The zero-occurrence claim still holds: a fresh spaced-placeholder grep hits only JSX prop object literals (context={{ org }}, value={{ openRecordList }} in app-shell views) — the exact non-copy class the card predicted; formatted ({{count, number}}) and nested ($t() forms remain at zero in source copy.
    • Restart condition unchanged: first real copy string using a spaced/formatted/nested placeholder, or an i18n-fallback sweep that folds this in with createSafeTranslation 的无 provider 回退只替换每个占位符的第一次出现 #3418's family.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings round — held (finding kept), premise re-verified.

    • Re-checked @ objectui origin/main e638531: the fallback needle is still the exact-literal split/join on {{k}} (packages/i18n/src/useSafeTranslation.ts:40), so the spaced / formatted / nested divergence vs i18next stands; still dormant (prior rounds' sweeps found zero in-repo copy using the wider spellings, and no new i18n work landed on that file since).
    • Held: the two candidate directions (widen the fallback vs gate the accepted spellings with a parity pin) remain an unmade choice, and no string hits the divergence today.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  5. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings round — held (finding kept), premise re-verified.

    Re-checked @ objectui origin/main b42558a: the fallback needle is still the exact-literal split/join on {{k}} in packages/i18n/src/useSafeTranslation.ts (with its own comment asserting literal-on-both-sides), so the spaced / formatted / nested divergence vs i18next stands. Still dormant — prior sweeps found zero in-repo copy using the wider spellings and no new i18n work has landed on that file. Held until a real string adopts one of the wider forms or the fallback is consolidated with the provider path.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  6. claude commented on Aug 11, 2026

    @claude
    Contributor

    Cross-reference for the next grading pass: the maintainer ruled 2026-08-11 on ui#4135 that the repo-wide hole convention is — {{x}} exclusively for i18next (argument mandatory at the call site), {x} single braces for any hole filled downstream of t(). When this card is next graded, its provider-less interpolation question aligns to that convention rather than re-opening the spelling fork.

    Recorded by the triage seat Routine (#5474 pilot) — reference note, not a claim.


    Generated by Claude Code

  7. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings triage (cadence re-check): HOLD — stays finding.

    Premise re-verified on origin/main @ 6d8231c: packages/i18n/src/useSafeTranslation.ts:40 is still the exact-literal value.split({{${k}}}).join(...) needle — the fallback recognizes one spelling where i18next recognizes four. The fork survives PR #3510 unchanged, as the card records.

    Why still held: the card's own sweep found zero in-repo strings using the spaced/formatted/nested spellings, so no copy misrenders today; the failure mode only arrives with a future string using an i18next-only spelling in a provider-less host. Cheap tripwire idea for whoever picks it up (noted, not scoped here): a lint asserting locale strings stick to the one spelling the fallback understands would fail-closed the day someone introduces the divergence.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  8. os-project-manager commented on Aug 17, 2026

    @os-project-manager
    Collaborator

    Triage (batch first-touch round, 2026-08-17): promoted finding → pm:queue, type Task, with the scope re-cut by a ruling that landed after filing.

    The card's open fork (widen the fallback vs constrain the accepted spelling set) was settled by the maintainer's #4135 ruling (2026-08-11, cross-referenced on this card): repo convention is {{x}} exclusively for i18next-bound copy and {x} for downstream-filled holes. So the remaining work is not widening the fallback — it is a lint/tripwire pinning the convention: default-table (and locale-table) placeholders must use the exact spelling the fallback accepts ({{name}}, no spaces, no format args, no $t( nesting), red in CI on violation. Declared = enforced; the divergence stays dormant by construction instead of by luck. Premise re-verified this round: useSafeTranslation.ts:89 split/join literal, zero violating strings today.

    Size/model suggestion: S (mechanical gate with an existing parity-test neighbourhood), sonnet.


    Generated by Claude Code

  9. self-assigned this
    on Aug 17, 2026
  10. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    ContributorAuthor

    认领(objectui 车道分片 PM,会话 session_01GTRjn8xBqp75dk7kFupVRt,批次 16):branch claude/issue-3512-placeholder-gate。

    按 2026-08-17 晋级评论的重切范围实施:不拓宽 fallback,建占位符拼写门 —— 默认表与语言包表的 i18next 占位符只准 fallback 能认的精确 {{name}} 拼形(无空格、无格式化参数、无 $t( 嵌套),违例 CI 红;对齐 #4135 裁定({{x}} 专属 i18next、{x} 单花括号下游洞,后者不得误伤)。文件面:packages/i18n 测试邻域(parity 测试同处)。


    Generated by Claude Code

  11. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    ContributorAuthor

    Draft PR: #4904 (branch claude/issue-3512-placeholder-gate).

    前提复核 live:packages/i18n/src/useSafeTranslation.ts:89 仍是精确字面 value.split(\{{${k}}}`).join(...);今日违例数在三个面上均为 **0**(十个语言包 28,867 条字符串叶子、31 张 createSafeTranslation` 默认表 762 条行、3 张手写同款 needle 的兄弟表),所以门是绿着落地的 —— 把休眠分叉从「靠运气不发生」改成「按构造不可达」。

    按晋级评论的重切实施:不拓宽 fallback,useSafeTranslation.ts 未改一字节。三面反向验证(带空格 / 格式化参数 / $t( 嵌套)各自先书面预判再跑,方向全中;两项误伤对照(单花括号 {x}、JSX 对象字面量)按 #4135 裁定保持全绿 —— 详见 PR 正文的表格与文案源清单。

    相邻面已另立 finding:#4905(内联 t(key, { defaultValue }) 是 fallbackT 链上的第二步,本门只覆盖文案表;909 条字面内联默认值里 906 条经 default-value-drift 传递性覆盖,残余 3 条 not comparable + 62 条 computed 无钉,今日 0 违例)。未加 pm:queue,留给分诊定级。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions