背景
在国际化业务中,多语言是开发流程中必须要处理的一环,如何更方便的处理多语言,则是开发中的一个痛点。
痛点
可以看到在业务中,我们通常会使用 i18n 相关库,比如 vue-i18n,使用方法来控制程序多语言显示,而这里会经历以下步骤
- 开发时,需要注意将本地化语言转为
$t(’key’) - 维护多个语言映射表,每个映射表里,将
key: ‘lang text‘映射起来
OK,以上步骤执行下来,大部分两种方式,一种是边开发,遇到多语言就先整个 key 插入,同时改动一个常用的映射文件,如 zh.js;另一种是先全部按中文做完,然后再统一收集到 zh.js 中,同时做 key 命名和替换。前者应该很少人这么做,以为会不断的打断开发的思路,而后者的缺点是可能造成遗漏,导致线上出现了未翻译的语言。
以上流程中出现了两个比较明显的问题,第一是 key 的命名与替换,第二是 key 的维护。先看第二个问题,key 的维护,本质上是多语言文件的维护,在只有一个语言时,每有一个新的 key 出现,都需要在文件中新增一行映射,而当出现多个语言时,每次新增如果还是手动去维护,那就会是一件非常恶心的事情,这里就出现了操作空间。
多语言维护
针对多语言文件维护,比较合理的方案是维护在 Excel 表格中,结构上天然契合,只需要写一个脚本打通 Excel ⇒ lang/*.js 即可,这一步不复杂,相信做多语言的朋友都知道。这里的另一个注意点是,维护多语言的表格尽量放在线上,比如 google excel 等在线文档,或者自己动手做一个线上表格维护,好处是多人协同,其中包括产品、翻译、开发都可以在一份表格上做增删改查,不会各自维护造成冲突,等翻译完了,前端只需要执行转换下载命令,生成多语言文件即可。
如何优化多语言key的开发流程
至于 key 的命名和替换,这里的痛点在于,你需要检索所有文件,将未翻译的字符串拉出来,检查现有的多语言表格中是否存在相同的字段,有则替换,若没有则需要新增一行,并重新设置一个不重复的 key,然后文件中替换,最后,执行更新多语言文件的命令。这么一连串操作下来,多费时间不用说了吧?就拿命名 key 来说都够头疼,当然如果不需要有一定的意义当我没说。
那么以上流程有没有可以优化的点?假如有脚本能把代码中的未翻译字符给一键收集替换了,岂不美哉🤓。那如果我们要自己动手写,遇到的第一个问题是什么——如何识别未翻译的字符串。如何不能识别到未翻译的字符串,那么一切都无从开始,这里有两个策略可以帮助我们去做识别操作
- 使用特殊符号标记未翻译字符串,比如
## hello world ## - 使用中文作为开发中写入的基础语言
使用特殊符号的好处是显式的标记了未翻译的字符串,缺点是必须在项目的一开始就这么做,侵入性很大,对于已有的项目几乎不用考虑。而使用中文,其实本质和特殊符号一样,就是能被正则识别到,而且国内开发者通常在开发的时候写在代码中的,一般也是中文,对于转国际化来讲也是几乎不用改动到旧代码。
自动化程序的思路
OK,这个时候的整体思路就变成了识别中文→收集中文→转换成key→替换,识别中文可以采用正则:/\p{Unified_Ideograph}/u ,那能不能直接使用文件扫描成字符串,然后正则匹配提取后做替换呢?我们先考虑以下几个点
- 注释如何区分
- 字符串模板问题,比如
你获得了${x}张优惠券 - vue template 中带有未翻译字符串的 attrs or props,比如
<input placeholder=”请输入”></input>
可能还有些没有考虑到的边缘情况,但就上面三点而言,不能使用纯粹的正则查找替换,那么现在能解题的就变成了 AST。解析成 AST 也需要区分不同的文件类型,拿 vue 项目来说,一般我们的业务代码会集中在两种类型的文件上,.ts | .vue,这里我们先讲如何解析 ts 文件。
import parser from "@babel/parser";
import generator from "@babel/generator";
// code -> ast
const ast = parser.parse(file.toString(), {
sourceType: "module",
plugins: ["typescript"],
});
// ast -> code
const code = generator(ast).code;
babel 系列的工具已经提供了从 code → ast → code 的处理方式,我们只需要遍历 ast ,识别到未翻译的字符串,做处理转换操作,更新 ast 即可。
import traverse from "@babel/traverse"
import t from "@babel/types";
const zhREG = /\p{Unified_Ideograph}/u
traverse.default(ast, {
// 扫描字面量
Literal(path) {
if (t.isStringLiteral(path)) {
const slNode = path as NodePath<StringLiteral>;
// 字符串中含有中文,且非注释
if (
zhREG.test(slNode.node.value)
) {
/* 操作节点 */
}
}
// 模板字符串
if (t.isTemplateLiteral(path)) {
const tlNode = path as NodePath<TemplateLiteral>;
// 模版中存在中文,提取
const hasZh = tlNode.node.quasis.some((item) =>
zhREG.test(item.value.raw)
);
if (hasZh) {
/* 操作节点 */
}
}
},
});
ts 文件的处理因为有 babel 可以解析,所以很方便,但是 vue 文件的处理就没那么简单了。现在项目大部分都升级到了 vue3,官方配套的解析器是 @vue/compiler-sfc ,但是,这个包仅包含了解析,并没有包含 ast → code 的过程,需要自己或找到工具去做这部分的逻辑。先看下 @vue/compiler-sfc 解析出来是什么
看下 ts 定义
export declare interface SFCDescriptor {
...
template: SFCTemplateBlock | null;
script: SFCScriptBlock | null;
scriptSetup: SFCScriptBlock | null;
...
}
// SFCTemplateBlock & SFCScriptBlock extends SFCBlock
export declare interface SFCBlock {
type: string;
content: string;
attrs: Record<string, string | true>;
loc: SourceLocation;
map?: RawSourceMap;
lang?: string;
src?: string;
}
通过 compiler-sfc 可以拿到 template & script 的代码,script 部分可以使用 ts 的处理逻辑,而只要解决 template 部分,再组装起来,即可完成整个 vue 文件的处理。 template 部分可以当做 xml 来处理,我们使用 fast-xml-parser 来处理这部分
import { XMLParser, XMLBuilder } from "fast-xml-parser";
const parser = new XMLParser(option)
const builder = new XMLBuilder(options)
// 解析
const dom = parser.parse(template)
// 生成 html
const code = builder.build(dom)
经过实践,xml parser 基本能满足我们的需求,缺点是会自动闭合标签,这个需要通过配置来避免。
key 语义显示
OK,目前为止自动转换多语言key我们已经实现了大概的框架流程,这篇不会讲太多详细的实践。目前为止已经可以解决多语言维护和代码中的替换问题,算是大大优化了这部分的开发流程,但是,开发的时候是爽了,维护的时候又变得痛苦了起来,当开发者检查代码的时候,会发现通过key没法看完全当前表达的意思是什么,这个时候就需要配合编辑器插件来做优化显示了,例如 i18n-ally,在编辑器中显示指定的语言,这样维护的时候就不会一脸懵逼了。
总结
以上部分,都是自己在做国际化业务中的一些槽点与难点,从一开始接触的懵逼,到后面开发过程中的烦躁,都是促使我去做整个流程思考与优化的动力。如果有想了解更多,欢迎沟通。