真正能落地的做法是在运行时劫持i18n函数调用记录key,如重写i18next的t函数或监听react-intl的formatMessage,在开发环境采集真实执行路径的key列表,导出为含key、sourceText、filePath、lineNumber的标准JSON,并适配Crowdin/Lokalise格式要求。

提取 React 组件中待翻译文案的常见错误
直接用正则扫 jsx 文件提取字符串,大概率漏掉动态拼接、条件渲染、JSX 元素嵌套里的文案,比如 {isLogin ? '登录' : '退出'} 或 <span>{t('welcome')}</span> 里的 t 调用。更麻烦的是,有些文案藏在 props、hooks 返回值或第三方 UI 库组件属性里(如 Antd Button 的 children),静态分析根本抓不到。
真正能落地的做法是:在运行时劫持 i18n 函数调用,把每次传入的 key 记录下来。比如用 i18next 时,重写 t 函数,在开发环境把所有调用过的 key 推进一个全局数组;用 react-intl 就监听 FormattedMessage 渲染或 useIntl().formatMessage 调用。这样拿到的是真实执行路径上的 key 列表,不依赖 AST 解析精度。
- 别碰
babel-plugin-i18n-extract这类插件——它们对自定义 hook、高阶组件、TS 类型泛型支持极差,且无法捕获运行时生成的 key - 确保只在
process.env.NODE_ENV === 'development'下启用采集逻辑,避免影响生产构建体积和性能 - 采集后导出为标准
json格式,字段必须含key、sourceText(原文)、filePath、lineNumber,方便后续定位和审核
生成可导入 Crowdin / Lokalise 的标准格式
众包平台不认你本地随便写的 zh.json 或 en.yml,它们要求明确的 source language + target language + context 字段。Crowdin 接收的最小可用单位是 .xliff 或带 _meta 的 json,Lokalise 支持 json 但强制要求顶层为对象,且每个 key 的 value 必须是字符串(不能是 null 或 {})。
最稳妥的转换方式是:用采集到的原始数据,生成符合 Crowdin API v2 文件上传规范 的结构,即每个条目包含 data(原文)、identifier(key)、context(来自 filePath 和 lineNumber 拼接)、comments(可选,比如开发者备注的复数规则说明)。
- 避免用
JSON.stringify直接输出——要手动处理换行符、引号转义,否则 Crowdin 导入会报Invalid JSON structure - 如果文案含变量占位符(如
"Hello {name}"),必须在context或comments中注明,否则译员可能误翻成"你好 {姓名}" - 同一
key在多个文件出现?保留第一个位置,其余在comments中追加also used in: xxx.tsx:42
让译员不改代码也能提交翻译结果
译员不会装 Node、跑 npm run extract,他们只打开网页填表单。所以必须把「提取 → 上传 → 下载 → 合并」流程封装成一键脚本,并提供清晰的 README.md 链接。关键不是自动化程度多高,而是失败时有明确提示——比如下载的 zh.json 缺少某个 key,脚本应报错 Missing translation for "button.submit": found in source but not in Crowdin response,而不是静默跳过。
- 合并时禁止覆盖已有翻译:检查目标语言文件中已存在的 key,只插入新 key,已存在 key 保留原值(防止译员退回旧版本)
- 不要直接写入
src/locales/zh.json——先写到tmp/zh.json,diff 确认无误后再mv,避免中断开发 - 给译员提供最小可用预览页:起个本地 server,用
i18next加载刚下载的zh.json,访问http://localhost:3001/preview?lang=zh即可看效果,不用启动整站
React 组件内文案变更后如何同步更新词条
改了组件里一句文案,比如把 t('welcome') 改成 t('welcome_back'),旧词条 welcome 不会自动下线,新词条也不会自动进平台——这是众包流程里最常被忽略的断点。
解决方案是:每次提取时生成两个 diff 文件:added.json(新增 key)、removed.json(不再使用的 key)。前者自动上传 Crowdin,后者发邮件提醒负责人人工确认是否归档(有些 key 可能只是暂时注释掉,不是真废弃)。
- 不要依赖 Git diff 判断增删——组件可能从 A 文件移到 B 文件,key 没变但路径变了,Git 会误判为删除+新增
-
removed.json必须包含最后一次出现的filePath和lineNumber,方便快速定位是否真该删 - 上线前跑一次
npx i18n-check --strict,扫描所有组件调用的 key 是否都在当前语言包里,缺失就报错,堵住漏翻漏洞
众包翻译真正的难点不在技术实现,而在 key 的生命周期管理——谁创建、谁修改、谁废弃、谁审核,这些信息必须沉淀在结构化数据里,而不是靠人肉记忆或口头同步。


















