前端国际化业务中的多语言开发流程思考

1,061 阅读3分钟

背景

在国际化业务中,多语言是开发流程中必须要处理的一环,如何更方便的处理多语言,则是开发中的一个痛点。

痛点

可以看到在业务中,我们通常会使用 i18n 相关库,比如 vue-i18n,使用方法来控制程序多语言显示,而这里会经历以下步骤

  1. 开发时,需要注意将本地化语言转为 $t(’key’)
  2. 维护多个语言映射表,每个映射表里,将 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 ,那能不能直接使用文件扫描成字符串,然后正则匹配提取后做替换呢?我们先考虑以下几个点

  1. 注释如何区分
  2. 字符串模板问题,比如 你获得了${x}张优惠券
  3. 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 解析出来是什么

image.png 看下 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,在编辑器中显示指定的语言,这样维护的时候就不会一脸懵逼了。

总结

以上部分,都是自己在做国际化业务中的一些槽点与难点,从一开始接触的懵逼,到后面开发过程中的烦躁,都是促使我去做整个流程思考与优化的动力。如果有想了解更多,欢迎沟通。