文案外置:从硬编码到语言包

把字符串搬进 JSON 不算文案外置。真正的改造有三条硬规则:整句外置不拼片段、key 按语义命名、复数与性别交给 ICU 规则。拼接句、语序、复数形式是本地化返工的主要来源,先按这三条改,再谈翻译,能省掉一半的后期修补。

本地化最难的部分不在翻译,在于让句子从代码里独立出来。

一、一个拼接出来的句子,翻不了

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

许棠做一个团队日程工具。英文版里有一句提示:

const text = `You have ${n} new message${n === 1 ? '' : 's'}`;

翻成日文时,译者要求把语序改成「新着メッセージが 3 件あります」。但代码里只有两段碎片:You have 与 new message,中间夹着变量。译者只能在语言包里写两段,运行起来就是语序错乱的句子。

俄文更麻烦:1 файл、2 файла、5 файлов 是三种形式,代码里的 n === 1 ? '' : 's' 只能处理两种。

这不是翻译质量问题,是代码结构问题。只要句子在代码里被拆开,任何语言都拼不回正确的句子。

二、三条硬规则

规则一:整句外置,不拼片段。

语言包的最小单位是「一句完整的话」,不是「一个词」。以下三类都必须作为整句:

  • 带变量的句子:已邀请 {name} 加入 {workspace}。
  • 带条件的句子:{count} 个文件,其中 {failed} 个失败。
  • 带单位或格式的句子:剩余 {days} 天(不同语言的数量词与单位位置不同)。

规则二:key 按语义命名,不按文案内容命名。

common.save、billing.invoice.due、onboarding.step2.title 这类 key,改文案时不用改代码;保存、SaveButton 这类 key,文案一变就失配。

推荐的格式是「域.组件.语义」三段:

billing.invoice.due
billing.invoice.paid
onboarding.step2.title
errors.upload.too_large

规则三:复数、性别、序数交给规则,不写 if。

不同语言的复数类别数量不同。按 Unicode CLDR 的规则:中文只有 other 一类,英文有 one、other,俄文有 one、few、many、other,阿拉伯文有六类。用 n === 1 判断,最多只能覆盖一部分语言。

ICU MessageFormat 把这些规则写进文案本身:

{
  "files.count": "{count, plural, one {# file} other {# files}}",
  "invite.sent": "{name} 已加入 {workspace}"
}
{
  "files.count": "{count, plural, one {# файл} few {# файла} many {# файлов} other {# файла}}",
  "invite.sent": "{name} присоединился к {workspace}"
}

运行时由 ICU 库按当前语言选择形式,代码里不再出现任何语言判断。

三、语言包的结构与回退链

一个可维护的语言包至少要有四件事:

组成作用常见错误
源语言文件唯一真源,其他语言从它派生多份源文件互相覆盖
目标语言文件按 key 对齐的翻译key 被手工改名后失配
回退链缺翻译时用哪一级回退到空字符串,界面出现空白
元数据语言标记、版本、译者、日期无法判断哪一版对应哪个发布

回退链建议写成显式配置,例如 pt-BR → pt → en,并规定「回退时记录一次缺失事件」。没有记录,就永远不知道哪种语言漏了多少条。

文件组织有两种常见做法:按页面拆分(billing.json、onboarding.json)与单文件(en.json)。前者适合大产品与多人协作,后者适合一个人的小产品。选择标准是「一次改动会碰到几个文件」,不是「哪种更规范」。

还有一个容易忽略的细节:不要把富文本标签写进文案值里再拼接 HTML。 用占位符配合组件,例如 terms.link 只放文案,链接作为参数传入;否则译者可能改坏标签,也可能引入注入风险。

四、迁移顺序与工时估算

改造不要一次做完,按「用户看得到 → 用户收得到 → 只有你看得到」的顺序:

  1. 界面文案:按钮、标题、空状态、错误提示。优先,因为直接影响转化。
  2. 邮件与通知:注册验证、付款收据、失败提醒。它们常常在另一套模板里,容易漏。
  3. 后台与日志:可以先保持源语言,不阻塞上线。

工时可以用一个粗略公式估:

工时 ≈ 字符串条数 × 1.5 分钟
     + 拼接句条数 × 5 分钟
     + 关键页面数 × 6 分钟(回归测试)

按 620 条字符串、74 条拼接句、28 个关键页面算:

620 × 1.5 = 930 分钟 = 15.5 小时
 74 × 5   = 370 分钟 = 6.2 小时
 28 × 6   = 168 分钟 = 2.8 小时
合计 ≈ 24.5 小时

这是「代码写得还算规整」的情况。如果模板里到处是内联字符串与条件拼接,实际工时通常是估算的两到三倍。先做一次扫描再报价,比事后加班便宜。

扫描可以用最笨的办法:搜索引号包住的文本、模板字符串与拼接操作符,人工过一遍。不要指望正则一次找全,它会把日志、CSS 类名与测试数据一起抓出来。

五、行动清单与复算验收

  • 扫描全部字符串,统计条数与其中拼接句的数量
  • 定 key 命名规则,写进 README,避免后人改回内容命名
  • 把带变量的句子改成 ICU 格式,删掉代码里的单复数判断
  • 建立源语言文件与回退链,缺失时记录事件而不是显示空白
  • 按「界面 → 邮件 → 后台」的顺序迁移,每步跑一次回归
  • 用公式估一次工时,与实际耗时对比,校准下一种语言

验收标准:语言包里不存在只有半句的条目,代码里不存在按语言分支的字符串判断,缺翻译时界面显示的是回退语言而不是空白。 三条同时成立,才算外置完成。

使用边界与实操验收

本文只讲代码侧的外置与结构,不涉及翻译质量与工作流,那属于第 04 篇;长度膨胀、断词与字体属于第 05 篇;漏译检测与回归测试属于第 06 篇。

站内已有面向游戏的本地化管线案例,处理的是贴图文字与字体图集,工程约束不同,可参考个人游戏技术选型案例:本地化管线为什么要先处理字体和文本长度 。本文面向有界面与表单的软件产品。

验证以实际渲染为准:切换语言后逐页检查,重点看带变量的句子、错误提示与邮件模板。自动化只能证明 key 存在,不能证明句子通顺。

关键参考与核验范围

本文的字符串条数、工时与比例为教学演示。核验日期:2026-10-23。

  • ICU MessageFormat :复数、选择与插值的写法规范,是「把语法写进文案」的标准做法。
  • Unicode CLDR :各语言的复数类别、数字与日期格式数据,决定了一门语言需要几种形式。

上述两条只用于说明技术规范,不构成对任何库或框架的推荐。各语言的具体复数规则以 CLDR 当日数据为准,本次核验未逐条比对。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元