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

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions