两周前接到了国际化前端项目的需求。

为方便行文,假设我们只需要中英两个版本。

手动替换

起初,我觉得很简单:把所有文本全换成变量,根据配置读取就行了嘛。

幸好产品只要求国际化项目的一部分,需要替换的文本不多

一边摸鱼一边复制粘贴,花了三天时间,终于全部替换完成了。

碰到的问题

但是,手动替换,除了不需要动脑筋以外,毫无优点:

  1. 需要给每一条文本取一个变量名。
  2. 需要确定每一条文本所属的域。
    对一个大项目而言,文本肯定会有大量重复。当不需要国际化时,文本就hard code在各自的地方。但是,当需要将他们提取出来时,就需要纠结了,两个相同的文本,在整个系统中,是同一个文本,还是只是两个不同文本碰巧相同了。确定这一点也是一个非常纠结头大的事情。
    不解决这个问题,日后修改文本时,可能导致两个不同的文本被错误的提取为同一个变量,导致日后无法维护。
  3. 需要不停地复制粘贴。
  4. 开发时就需要将文本改成变量,日后开发起来很麻烦。

解决方案

但是我已经替换完成了,只能祈祷产品不会提进一步的要求了。

但是这不可能,终于过了一周,我又接到了国际化整个项目的需求。

打开vscode,用正则全局搜索了一下,我惊讶的发现,如果继续手动替换,有400个文件等着我修改。

于是我只能想办法去弄自动国际化了。

自动国际化

自动国际化前置要求

想要进行自动国际化,必须有这两个能力:

  1. 能够编译Vue源码成AST,并把修改后的AST转换成Vue源码.
  2. 能够编译TS源码,并把修改后的AST转换成TS源码.
  • Vue转AST

可以用vue-template-compiler把vue的template部分转换成AST

vue-template-compiler的compile方法的whitespace需要设置成condense
生成的AST中会删除多余的空白文本节点,否则会因为这些多余的节点,导致布局错乱。

const compiler = require("vue-template-compiler");

const { ast } = compiler.compile(source, {
    whitespace: "condense",
});
  • 修改Vue template AST树。

修改node.textnode.attrsMap 两个属性即可。

  • AST转Vue

我没有找到成熟的AST转VUE的包,
只找到了这个:vue-template-ast-to-template
这个包有许多bug,比如

  1. 会生成重复的属性
  2. 会丢失命名slot

好在相比生成AST设计编译的知识,由AST得到Vue源码的逻辑还是很简单的,稍微改了改这个包,把这些问题都解决了。

import TemplateTransform from "./src/vue-template-transformer.mjs";

const transformer = new TemplateTransform();
// 传入vue-template-compiler创建的ast
const { code } = transformer.generate(ast);
  • 搜索Vue template AST树。
    首先,我们需要递归搜索Vue template的AST树。
    树结构,一般子节点都会保存在名为children的属性中,
    但是vue template的AST有些特殊。
    以下三个节点都保存了子节点
  • children: 绝大部分子节点保存在这里
  • ifConditions :相邻的v-if, v-else-if, v-else的节点保存在这个属性中
  • scopedSlots:slot节点保存在这个属性中
    所以,搜索整棵树时,需要把这三个属性中保存的子节点都找出来,否者会漏掉一些节点

以下是我写的搜索Vue template AST树的方法,供参考。

其中,因为某个节点的ifConditions中会包含自身,需要使用变量记录节点是否被处理过,防止出现回路死循环了。

function vueAstLooper(node, cb) {
  if (node.__scanned__) return;
  node.__scanned__ = true;
  cb(node);
  const children = [
    ...(node.children || []),
    ...Object.values(node.scopedSlots || {}),
    ...(node.ifConditions || [])
      .map((k) => k.block)
      .filter((k) => !k.__scanned__),
  ];
  if (children.length > 0) {
    children.forEach((item) => vueAstLooper(item, cb));
  }
};

然后,需要找到树中的包含中文的节点和属性
对于文本节点,那么文本保存在node.text
对于属性,文本保存在node.attrsMap

{
  ref: "cp-table",
  class: "adjust-tab-margin",
  "@change": "queryRuleList",
  ":loading": "ruleListLoading",
}
  • 遍历和修改TS的AST

使用Babel全家桶:

const parser = require("@babel/parser");
const _traverse = require("@babel/traverse");
const _generate = require("@babel/generator");
const _template = require("@babel/template");
const template = _template.default;
const traverse = _traverse.default;
const generate = _generate.default;
  const ast = parser.parse(source, {
    sourceType: "unambiguous",
    plugins: ["typescript", "decorators-legacy"],
  });
  traverse(ast, {
    StringLiteral(path) {
        //在这里处理普通字符串
    const originString = path.toString();
     path.replaceWith(template.ast(`"new String"`));
     path.skip()
    },
    TemplateLiteral(path) {
        //在这里处理模板字符串
      const originString = path.toString();
     path.replaceWith(template.ast(`"new String"`));
     path.skip()
    },
  });
  • TS的AST还原成TS

因为我使用了装饰器,所以需要把decoratorsBeforeExport设为true,否则生成的代码里 export default 的位置不对。

  const { code } = generate(ast, {
    decoratorsBeforeExport: true,
  });

自动国际化步骤

前置技能要求准备就绪,可以开始了。

第一步

使用nodejs写一个脚本,用于提取文本。

把每个文件中的代码转换成AST,从AST中把中文文本全部提取并保存。

将扫描出来的所有中文文本,保存在zh.json文件里。key是文本的hash,值是文本的本身

注意,必须是文本原文的hash。
{
    "e9e8054f8b9b30a5bc0eab3aa4645f9c": "邮件",
}

得到zh.json文件之后,复制一份,然后将翻译,得到en.json文件。

{
    "e9e8054f8b9b30a5bc0eab3aa4645f9c": "Email",
}

在项目源码中,我们写下的是邮件二字,他的hash值是e9e8054f8b9b30a5bc0eab3aa4645f9c,这个hash作为key,能从zh.jsonen.json文件中找到其对应的中英双语。

第二步

写一个webpack的loader。

  1. 在loader中获取Vue和TS文件中的文本,对文本取hash
  2. 拿着hash,取前保存好的zh.jsonen.json中找到所需的翻译
  3. 拿着得到的翻译,去替换掉代码中的文本节点。

这样,基本上,核心功能,就大功告成了。

但是不要得意,接下来还需要解决这些问题:

  1. 如何解决Vue的模板变量
    大部分时候,模板变量都会很简单,用正则可以轻松提取someAppName变量

    <div>你确定要删除{{someAppName}}吗?</div>

    但是防不住自己手贱写得过于复杂
    这时用正则就不行了

<div>你确定要删除{{booleanA?firstAppName:booleanB?secondAppName:thirdAppName}}吗?</div>

因为vue的模板变量中可以写任意合法的js的表达式,所以需要将{{}}之间的代码提取出来,交给babel去解析,在这里可以复用解析ts文件的代码。

  1. 如何解决TS的模板字符串
`你确定要删除{{booleanA?firstAppName:booleanB?secondAppName:thirdAppName}}吗?`

表面上看,TS的模板字符串和Vue的模板变量是同样的问题。但其实不是。

  1. 因为Vue的模板变量尚且可以用当成TS代码去解析,TS本身已经是代码了,babel默认就解析好了。
  2. 对一段话中的变量解析得太细,会丢失上下文。
    如果没有将这一整段话:你确定要删除{{booleanA?firstAppName:booleanB?secondAppName:thirdAppName}}吗?
    保存下来,而是进一步解析,保存成了你确定要删除吗?到json文件中。
    做翻译工作时,就会丢失上下文,不知道这个吗?是要干嘛,也不知道你确定要删除是要干什么。
    这样不好。

所以国际化时,也需要规范一下代码,不要在模板变量和模板字符串中写过于复杂的表达式。

总结

因为两周前开始做国际化时,除了能想到准备好翻译,在运行时替换,我对国际化一无所知,可谓被赶鸭子上架。如果我的方法中有错误或漏洞,那是很正常的。请大家帮我指出来。


OLong
450 声望318 粉丝