前端国际化,从0开始。

代码溯月人
• 阅读 852

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

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

手动替换

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

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

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

碰到的问题

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

  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文件中。
    做翻译工作时,就会丢失上下文,不知道这个吗?是要干嘛,也不知道你确定要删除是要干什么。
    这样不好。

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

总结

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

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Easter79 Easter79
3年前
typeScript数据类型
//布尔类型letisDone:booleanfalse;//数字类型所有数字都是浮点数numberletdecLiteral:number6;lethexLiteral:number0xf00d;letbinaryLiteral:number0b101
Easter79 Easter79
3年前
springboot2之优雅处理返回值
前言最近项目组有个老项目要进行前后端分离改造,应前端同学的要求,其后端提供的返回值格式需形如{"status":0,"message":"success","data":{}}方便前端数据处理。要实现前端同学这个需求,其实也挺简单的,
Wesley13 Wesley13
3年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
ELK学习笔记之配置logstash消费kafka多个topic并分别生成索引
0x00 filebeat配置多个topicfilebeat.prospectors:input_type:logencoding:GB2312fields_under_root:truefields:添加字段
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
Linux应急响应(二):捕捉短连接
0x00前言​短连接(shortconnnection)是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。在系统维护中,一般很难去察觉,需要借助网络安全设备或者抓包分析,才能够去发现。0x01应急场景​