在开发插件的时候,最让我头疼甚至恶心的工作就是 i18n。每次需要添加新的文本或者修改现有文本时,我都要经历以下繁琐的步骤:
- 在代码中定位需要国际化的文本
- 为每个文本创建一个唯一的 key
- 将这个 key 添加到各种语言的翻译文件中
- 手动翻译或找人翻译这些文本
- 在代码中用 i18n 函数调用替换原来的文本
这个过程不仅耗时,而且容易出错。可以说很多时候 i18n 都大大减弱了想要写代码的欲望。
我想要的 i18n 解决方案
现成的自动化 i18n 工具不是没有,但是说实话我用来不是很满意。
首先很多工具操作上并不好用。比如我之前走了一点弯路,尝试使用甚至开发某相关的 vscode 插件,试了两下就觉得插件的操作体验还是有些繁琐,我实在不喜欢鼠标在菜单里面繁琐地点来点去。
最后敲定,一个良好的 i18n 工具应该是基于 cli 程序为好,能直接在命令行里快速运行。(至于什么需要运行额外的 GUI 程序甚至使用 XXX 网站来帮忙 i18n 的还是有多远滚多远吧)。
但是 cli 的自动 i18n 我看了几个,感觉也不是很喜欢。
有些工具很好很强大,但是有点过于强大了:他们会自行维护一个 i18n manager,直接跳过思源插件的机制。这个想了想还是算了吧,没必要为了一个 i18n 引入一些乱七八糟的东西(特别是引入到打包当中,100% 拒绝);keep simple, enough。
还有些工具对源代码文本进行预处理,把指定的文本替换为 i18n 变量——嗯,这个很简单粗暴,我喜欢,但是看了一下他们是把:
<input
type="二"
placeholder="一"
value="s 四 f"
/>
变成:
<input
type={this.$t('0')}
placeholder={this.$t('1')}
value={`s ${this.$t('2')} f`}
/>
可以看到完全丢失了所有的语义信息,看着有点倒胃口了。
考虑了一下,一个(至少对我而言)理想的 i18n 解决方案应该:
- 批量处理:可以一次性处理整个项目的 i18n,操作尽可能简单。
- 无侵入:不引入任何额外的打包,只作为一个单独的 i18n 工具使用。
- 自动抽取:能够自动从代码中提取需要翻译的文本,并转换为对 i18n 文本变量的引用
- 保留 key 结构:我不太喜欢那些把所有文本变成扁平列表的方案。我希望能保留 key 的层次结构,最好能映射到一个
interface
对象中,这样可以在代码中得到更好的类型提示。 - 自动翻译:能自动翻译,而且最好是能自己指定一些翻译的词典。比如之前用翻译软件有的都不知道要怎么翻译「SiYuan」这个词。
- 可配置:有些工具,每次运行的时候都要附带一大堆参数,光看着就头疼,我的想法是能写入配置文件保存起来的就不要缝合一堆命令参数进去
auto-i18n
趁着国庆闲着也是闲着,做了一个小工具: auto-i18n。一个基于 Python(\geqslant 3.9) 的命令行工具。使用原理很简单:正则匹配 + GPT。
- 自动抽取:在编写源代码的时候,基于一定的格式,例如 ((`xxx`)) 来编写需要转换为 i18n 的文本。运行命令的时候会自动抽取这些文本
- key 生成:使用 GPT 为每个提取的文本生成一个合适的 key;并将源代码中的文本替换为对 i18n 变量的引用(直接简单粗暴地文本替换)。
- 翻译:再次使用 GPT 将主语言文件中的文本翻译成其他语言。
基本上使用就是下面几个步骤:
-
安装
pip install auto-i18n
-
在项目根目录初始化
i18n init
-
配置 GPT
-
在你的代码中使用特定模式标记需要翻译的文本
console.log(((`这是需要翻译的文本`)));
-
提取文本并生成 i18n 文件
i18n extract
-
翻译到其他语言
i18n translate
具体的详情可以阅读:https://github.com/frostime/py-auto-i18n
使用样例
用法细节请参考:auto i18n 的 README。这里只简单展示一下使用的流程。
现在假定我们在开发某插件,并安装了 auto-i18n
。首先我们运行 i18n init
在项目当中创建必要的配置文件。
确保 i18n_dir
下面已经创建好了我们需要的 i18n 文件。文件在就行,内容为空的也无所谓。
在正式的代码当中,使用 i18n_pattern
配置的正则语法编写常量文本。比如下面给了三个样例:
这里用
((
xxx))
的写法的好处在于不影响编译,从语法上讲外面的两个 () 并不会影响内部表达式的取值。
现在运行:
i18n extract
CLI 命令会扫描所有的代码文件,提取 i18n 文本,并自动转换为一个 i18n 变量引用。
回到我们的主 i18n 文件 zh_CN.yaml 中,发现变量也已经写进去了。
不过 en_US.yaml 还没有写入,所以我们运行 i18n translate
这里需要注意,由于我们在配置中指定的策略为 diff
,在该模式下程序只翻译增量部分,而不会全部翻译(以节省 token 和时间消耗)。
如果有必要,你还可以运行 i18n export
,他会自动导出一个 i18n 文件对应的 interface 到 src/types
下:
⚠️ 实际引用 i18n
可以看出上面的用法存在的一些问题,他会非常僵化地把问题替换为 i18n.xxx
。问题是 i18n 要从哪里来呢?所以在实际使用的时候,需要有一个模块,可以自动导入 i18n 对象。
<!-- src/sample.svelte -->
<script>
import { i18n } from 'somewhere-in-your-project';
</script>
<div>
{ i18n.samplesvelte.welcometoautoi18n }
</div>
最简单的办法是在 onload 里面赋值。
export let i18n: I18n = null;
export default class PluginSample extends Plugin {
async onload() {
i18n = this.i18n;
}
}
然后在别的地方引入:
<script>
import { i18n } from '.';
</script>
讨论
-
python or node
- auto-i18n 是用 python 写的而非 node,这可能最麻烦的一个地方:明明是开发前端项目却需要用 py 环境,这点值得诟病。
- 我本来想要写完之后翻译成 node,不过我懒,写完之后不想搞了。有志愿者可以自行 node 化。
-
GPT 的能力问题
- 目前这个工具本体最大的一个缺点是,对 gpt 模型的能力有一定要求。
- 工具要求 gpt 必须输出 json 格式(实际上可以做到不用 json,但是我懒,不想改了),实测有些比较弱鸡的模型(比如 doubao-4k-lite)会经常违反 prompt 的约束,输出非纯 json 的内容。
-
CLI vs 编译插件
- 目前我是选择了 CLI 方案,在不侵入 JS 项目的编译、运行的前提下,直接替换了源代码。直接替换源码这点可能有些风险(所以你最好和 git 搭配使用)。
- 还有另一种方案,是写一个 vite 的插件,将 i18n 变量的提取转换什么的放到编译器完成,不去破坏源代码。
- 这两种的优劣见仁见智,但我最后还是倾向于前者。一个是我们这种弱鸡小项目破坏源代码不是啥大问题,git discard 一下就解决了;二个是 CLI 灵活性更好,做成 vite 或 webpack 的插件那绑定的也太死了。
- 另外,虽然我们的样例是前端开发,但可以看到 auto-i18n 是完全语言无关的一个工具;这个项目本身的 i18n 都是用它自己来完成的。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于