前几年做海外业务的时候,最怕听到产品说一句:“这期需求要做多语言适配,一共八个语种。”
经历过的人都懂,传统的国际化(i18n)流程往往充斥着各种机械劳动:开发在代码里写 t('home.header.title'),然后把中文文案导成 Excel 发给运营或翻译团队;两周后收到翻译稿,再吭哧吭哧复制粘贴回各个语种的 JSON 文件里。一旦漏了个 key,或者产品临时改了句文案,上线后页面直接裸奔展示 home.header.title,血压瞬间拉满。
写前端写久了,总有些“偷懒”的执念。既然这套流程本质上是文本提取、差异比对、格式转换和类型约束,那完全可以通过自动化工具解决。今天聊聊我如何用 Go 写一个轻量级 CLI 工具,把这套流程自动化。
痛点复盘:我们在 i18n 上浪费了多少时间
梳理一下日常开发中遇到的几个核心问题:
- 提取低效:新做完一个模块,满屏都是硬编码的中文,手动抽成 i18n key 既费时又容易漏。
- Key 管理混乱:多语言 JSON 文件动辄几千行,随着版本迭代,存在大量无用的“幽灵 key”,或者某些语种缺 key。
- 缺乏类型保障:在 TypeScript 项目里,拼错一个 key 没有任何静态检查提示,非得运行时甚至上线后才发现。
虽然 Node.js 社区有 i18next-parser 这类工具,但在跨项目分发、CI 流水线运行速度以及与公司内部平台对接时,打包体积大、依赖 node_modules 的问题还是让人有点膈应。
为什么用 Go 写这个小工具
写组件库和前端构建流我首选 Node/TS,但写独立的小工具 CLI,我现在基本都用 Go。原因很直接:
- 单二进制分发:编译出一个可执行文件直接扔进
/usr/local/bin或者 CI 镜像里,不需要让运维配一堆 Node 环境和安装依赖。 - 并发与性能:遍历扫描上万个源码文件、做文本匹配和多语种 JSON 递归 Diff,Go 的并发处理和启动速度几乎是毫秒级的。
- 跨平台兼容:团队里有 Mac、Linux 也有 Windows,一次
cross-compile搞定全部。
核心设计与代码实战
这个 CLI 工具主要承担三个职责:
- 递归扫描
locales目录,找出各语种缺失的 key。 - 根据主语言 JSON(通常是
zh-CN.json)自动生成 TypeScript 的类型声明文件。 - 检查代码里是否存在未被引用的“废弃 key”。
这里重点看一下扁平化 Key 比对与 TS 声明生成的逻辑。
1. 扁平化与 Diff 逻辑
嵌套 JSON 在对比时比较麻烦,先把嵌套的 map[string]interface{} 展平为类似 home.user.title 的点分路径:
package main
import (
"encoding/json"
"fmt"
"os"
)
// FlattenMap 递归将嵌套 JSON 展平为点分路径
func FlattenMap(prefix string, nested map[string]interface{}, flat map[string]string) {
for k, v := range nested {
fullKey := k
if prefix != "" {
fullKey = prefix + "." + k
}
switch child := v.(type) {
case map[string]interface{}:
FlattenMap(fullKey, child, flat)
case string:
flat[fullKey] = child
default:
flat[fullKey] = fmt.Sprintf("%v", child)
}
}
}
// FindMissingKeys 找出 target 相比 base 缺失的 key
func FindMissingKeys(base, target map[string]string) []string {
var missing []string
for k := range base {
if _, exists := target[k]; !exists {
missing = append(missing, k)
}
}
return missing
}
2. 生成 TypeScript 类型守卫
前端写代码最爽的是有智能补全。我们可以根据展平后的 key 集合,自动生成一个 i18n.d.ts:
package main
import (
"fmt"
"os"
"sort"
"strings"
)
func GenerateTypeDef(flatKeys map[string]string, outputPath string) error {
var keys []string
for k := range flatKeys {
keys = append(keys, fmt.Sprintf(" | '%s'", k))
}
sort.Strings(keys)
content := fmt.Sprintf(`// Auto-generated by i18n-cli. DO NOT EDIT.
export type I18nKey =
%s;
declare module '@/utils/i18n' {
export function t(key: I18nKey, params?: Record<string, any>): string;
}
`, strings.Join(keys, "\n"))
return os.WriteFile(outputPath, []byte(content), 0644)
}
在前端项目里,封装的 t() 函数直接引入这个类型声明,只要输入 t(',VS Code 就会精准提示所有可用的 key。写错了直接报红,编译阶段就能掐死问题。
落地到前端工程流
工具写好了,关键看怎么接入日常工作流。我们目前的做法是轻量级双向结合:
-
本地开发时(Git Hooks): 在
lint-staged或 pre-commit 钩子里加入检查。当开发人员提交代码且修改了locales/zh-CN.json时,自动触发 Go CLI 重新生成i18n.d.ts并把更新后的定义一起提交。 -
流水线拦截(CI Gate): 在 GitLab CI / GitHub Actions 里增加一步 i18n 校验阶段:
yamli18n-check: stage: test script: - i18n-cli check --base=src/locales/zh-CN.json --target=src/locales/en-US.json如果发现有缺漏的 key,直接终止流水线并高亮打印出漏掉的具体字段,避免带病上线。
总结
其实工程提效不一定非要搞动辄几个月的大重构。很多时候,观察团队里那些反复发生、消耗精力又容易出错的琐事,抽半天时间写个几十上百行的小工具把它闭环掉,带来的收益是非常立竿见影的。
前端视野做久了,偶尔跳出来用 Go 这种偏系统级的语言来写写工具、做做底层支持,思路会开阔很多。把繁琐的交给机器,人才能把精力留给更重要的业务架构与交互体验优化。
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


