做 #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 当初一样,是一条休眠 的分叉:语义两边不一致,但目前没有输入能把差异暴露出来。
为什么仍然值得记一笔
触发它只需要有人在默认表里顺手写成 {{ count }}(多打两个空格),或者想用 i18next 的数字格式化。在语言包里这么写是完全合法的 ,i18next 会正确渲染,于是作者不会有任何理由怀疑自己写错了 —— 只有在没有 provider 的宿主上才炸。
回退表和语言包之间已有 parity 测试互钉字面量 ,useSafeTranslation.test.tsx(PR fix(i18n): fallback interpolation replaces all placeholder occurrences #3510 )现在钉了替换范围 ,但没有任何测试钉占位符拼写的接受集合 。
这类「AI 写的元数据 / 文案在宽容消费端悄悄出错」正是仓库口味最想避免的形状。
可能的方向(未裁定,留给 triage)
让回退更接近 i18next :把字面量 needle 换成 {{\s*name\s*}} 的正则(名字要转义),格式化和嵌套仍不支持 —— 只补最便宜的那条,分叉变窄但没消失。
收紧契约 :加一条守卫/测试,要求默认表里的占位符只能用回退能认的那种精确拼写,写错就在 CI 上红。方向上是「declared = enforced」,但 createSafeTranslation 的无 provider 回退只替换每个占位符的第一次出现 #3418 的裁定里明确否决过「为绕开实现 bug 去限制作者能写的句式」—— 这里性质不同(不是绕 bug,是声明一个可支持子集),值得单独判一次。
回退直接复用 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
做 #3418(回退插值只替换第一次出现)时,把回退路径和 i18next 逐条对过一遍,顺带发现这条分叉比 #3418 记的那一条更宽。观察类 —— 今天没有任何一条文案命中,所以打
finding、不排pm:queue。事实
packages/i18n/src/useSafeTranslation.ts的fallbackT用的是精确的字面量 needle:也就是说它只认
{{name}}这一种拼写。i18next(挂了 provider 时真正干活的那个)认的更多。在一个按createI18n同样配置(interpolation: { escapeValue: false })的真实 i18next 实例上跑出来的对照:Hello {{ name }}{name:'Ada'}Hello AdaHello {{ name }}❌Total {{count, number}}{count:12345}Total 12,345Total {{count, number}}❌A $t(otherKey)otherKey(第四种
{{- name}}反转义前缀同理,不过escapeValue: false下它和普通写法等价,没有实际差别。)失败形态和 #3418 一模一样:不抛错、不打日志,把
{{ name }}这种字面量直接吐到屏幕上,而且专挑没有挂I18nProvider的宿主(独立 / 嵌入式渲染)—— 我们最看不到的那类运行环境。为什么今天踩不到
扫过
packages/+apps/全部源码字符串:grep到的{{ opacity、{{ transform之类全是 JSX 的 style 对象字面量,不是文案);{{count, number}}:0 条;$t(:0 条。所以和 #3418 当初一样,是一条休眠的分叉:语义两边不一致,但目前没有输入能把差异暴露出来。
为什么仍然值得记一笔
{{ count }}(多打两个空格),或者想用 i18next 的数字格式化。在语言包里这么写是完全合法的,i18next 会正确渲染,于是作者不会有任何理由怀疑自己写错了 —— 只有在没有 provider 的宿主上才炸。useSafeTranslation.test.tsx(PR fix(i18n): fallback interpolation replaces all placeholder occurrences #3510)现在钉了替换范围,但没有任何测试钉占位符拼写的接受集合。可能的方向(未裁定,留给 triage)
{{\s*name\s*}}的正则(名字要转义),格式化和嵌套仍不支持 —— 只补最便宜的那条,分叉变窄但没消失。倾向 3 > 2 > 1(3 最彻底、也最难写错),但这是契约形状的选择,不自作主张。
API rate limit already exceeded),list_issues连续多次失败,无法枚举。内容本身已按 #3418 的线索逐条核验过,但是否与某条已存在的 issue 撞车没能确认,triage 时请顺手查一下。Generated by Claude Code