fix(i18n): fallback interpolation replaces all placeholder occurrences - #3510
Merged
Conversation
`createSafeTranslation`'s no-provider fallback used
`value.replace('{{k}}', String(v))`, which substitutes only the FIRST
occurrence of each placeholder. i18next — which serves the provider path —
substitutes every one, so a default string repeating a placeholder leaked
literal braces to users on hosts with no `I18nProvider` mounted.
Switches to `value.split(needle).join(String(v))`. Not `replaceAll`: both
`replace` and `replaceAll` interpret `$&`, `` $` ``, `$'` and `$$` in the
*replacement* string, which i18next does not — and interpolated values are
runtime data (record labels, search terms), so that divergence was already
reachable. split/join is literal on both sides and needs no regex escaping.
Key resolution (`defaults[key] || key`), the `String(v)` coercion and the
leave-it-literal behaviour for an unmatched placeholder are unchanged.
Fixes objectui#3418.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
AGENTS.md commandment #-1: code comments are English. The repeated-placeholder example carried a CJK sentence to illustrate the case; describe the word-order motivation instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3418
问题
createSafeTranslation的无 provider 回退插值用的是value.replace('{{k}}', String(v))。String.prototype.replace传字符串 needle 时只替换第一次出现;而挂了 provider 的路径走 i18next 自己的插值,替换全部出现。于是任何一条在同一句里重复用同一个占位符的默认文案(
Selected {{count}} of {{count}} items—— 在很多语言里都很自然,RTL / 黏着语为了语序还经常必须这么写),在没有挂I18nProvider的宿主(独立 / 嵌入式渲染)上会把第二次及以后的{{k}}原样吐给用户。不抛错、不打日志,专挑我们最看不到的运行环境 —— 一条静默的语义分叉。改法
value.split(needle).join(String(v)),一行。裁定见 issue:修插值语义,不加守卫。为绕开实现 bug 去限制文案作者能写的句式,是把 bug 固化成契约;语义修齐后,重复占位符在两条路径上同样合法。
为什么是 split/join 而不是
replaceAll:replace和replaceAll都会解释替换串里的$&、$'、$$和反引号形式,i18next 不会。这里插进去的是运行时数据(记录 label、搜索词),所以这第二条分叉今天就够得着 —— 一个名字里带$&的记录在回退路径上会被改坏('Hello {{name}}'+'$& raw'旧实现给出'Hello {{name}} raw')。split/join 两侧都是字面量,顺带也省掉了占位符名的正则转义。defaults[key] || key的取值、String(v)强转、以及「没有对应 option 时占位符保持字面量」的行为,全部原样保留。前提核验
replace在origin/main上确实还活着 —— 前提 live。packages/+apps/全部单行字符串字面量,0 条重复使用同一个占位符。所以 (1) 类分叉今天没有触发点,(2) 类($替换模式)有。测试
新增断言的期望值不是猜的 —— 每一条都先在一个按
createI18n同样配置(interpolation: { escapeValue: false })的真实 i18next 实例上跑过,测里写的就是 i18next 自己的输出:selection+{count: 3}Selected 3 of 3 itemsSelected 3 of {{count}} itemspair+{a:'A', b:'B'}A/B — A again, B againA/B — {{a}} again, {{b}} againgreeting+{name:'$& raw'}Hello $& rawHello {{name}} rawgreeting+{other:'x'}Hello {{name}}greeting(无 options)Hello {{name}}反向验证(先定方向再跑):预测「两条重复占位符用例 +
$模式用例 RED,其余 GREEN」。测试先写、源码未动时跑:与预测逐条吻合。改完后
Tests 8 passed (8)。消费半径清扫(这条规则跑在哪就要扫到哪,不是只扫被改的包):
createSafeTranslation有 ~15 个包在用,root vitest 把@object-ui/i18nalias 到 src,所以下游 no-provider 测试直接吃到这次改动。下游那 100 条断言没有一条方向翻转,与前提核验一致:仓库里既没有重复占位符的文案,也没有把
$替换模式当值插进t()的地方。Changeset:
.changeset/safe-translation-fallback-replace-all.md(@object-ui/i18npatch)。Generated by Claude Code