Skip to content

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

Description

@yinlianghui

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

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

事实

packages/i18n/src/useSafeTranslation.tsfallbackT 用的是精确的字面量 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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions