背景

随着出海的业务越来越多,web 应用面临越来越多的国际化的工作。如何高效,高质量的完成 Web 前端国际化工作,已经是摆在 web 前端同学的急需解决的问题。

i18n-helper-cli 是什么

i18n-helper-cli 是一个 Web 国际化整体解决方案,包含自动包裹词条提取词条翻译词条词条翻译统计节省人力预估统计网页多语言显示异常检测(Coming soon)等功能。可以大大减低开发,测试,翻译各个角色的人力成本,减少重复劳动,低级错误。

为什么需要 i18n-helper-cli

Web 国际化流程

简单来说可以分为以下 5 个步骤

  1. 【选型】多语言框架选型(这里不深究,不在此篇范围),我们选定 i18next,18n-helper-cli 对多语言框架并不限制
  2. 【开发 - 包裹词条】从上面这步骤,我们知道需要把词条包裹起来 e.g 你好 => t('你好')
  3. 【开发 - 提取词条】把上一步中包裹的词条 copy 到翻译文件中
  4. 【翻译 - 翻译】翻译把词条翻译好,填入翻译文件
  5. 【测试 - 测试页面】开发提交测试后,对多语言页面进行测试

问题

通过上面 5 步,可以完成站点国际化。大多数场景大家就是这么做的,但这里充斥着大量人工劳动,大量人工劳动意味着重复低效出错几率提高。让我们从以下三个阶段分析下这些问题

  • 【开发阶段】

    1. 人工操作包裹和提取词条耗时长,但对个人无任何成长。如果是【全新开发】的站点,大家还可以耐着性子包裹词条提取词条,但如果是【存量修改】及对已有的站点做国际化,而且这里的页面几十上百,甚至更多,这里的包裹词条提取词条的工作量会让人崩溃

    2. 遗漏包裹,提取词条(代码多,词条隐藏在各个文件的各个角落里。。。)

    3. 提取词条后,运行多语言界面无法看到效果,需要等到翻译返回

  • 【翻译阶段】

    1. 翻译耗时长
    2. 遗漏翻译
  • 【测试阶段】

    1. 多语言页面测试每个都要测,耗费大量时间
    2. 遗漏测试某个多语言页面

所以这里最大的问题是上面这些工作都需人工操作,问题清楚了,那接下来我们要做的就是把这些人工操作能够交给机器,实现自动化,提高效率降低出错几率

解决方案

先上结论,i18n-helper-cli 可以很好的解决上述问题。

原理

整体方案

  • 【词条包裹】通过对代码进行编译,得到AST,找到符合条件(中文,或者其他语言,可配置)的 Node,根据配置创建新 Node,替换老的 Node
  • 【词条提取】同上,也是AST, 找到的符合条件的词条以及原代码已经包裹的词条会被一起提取,根据配置写入文件
  • 【词条翻译】
    1. 从源文件翻译:如果有一份翻译词库(这里有常见的翻译),我们提取出来的未翻译词条在这里有,我们就可以直接从这里翻译
    2. 机器翻译:未翻译词条调用云服务实现翻译(这里我们用的是腾讯云的翻译服务)
  • 【网页多语言显示异常检测】提供一份页面 url 列表,用 Cypress 进行截图,调用腾讯云 OCR 服务提取图片文字,进行对比,假设我们有个叫你好的词条翻译成 en 为Hello,如果我们通过 OCR 得到的是Hel,那么我们可以认为这个页面有问题(Coming soon)
  • 【统计】
    1. 翻译词条统计:根据当前语言下未翻译词条数 / 词条总数
    2. 减低人工耗时预估:根据包裹,提取,翻译词条数预估

包裹词条方案详解

接下来我们详细分析下词条包裹的方案。我们要实现的是类似你好 => t('你好'),所以:

  1. 找到你好
  2. 替换成t('你好')

哈哈,刚说的就像网上的经典问题: 如何把大象放到冰箱?

回答:先打冰箱门,然后把大象放进去,在关上冰箱门

听起来没问题,好像很有道理的样子,但没有任何实际价值。言归正传,我们来探讨下实际解决方案:

方案 1 - 正则

针对匹配到中文,这里我们第一个想法应该就是正则表达式了。/[\u4e00-\u9fa5]可以匹配中文。至此,我们完成了第一步找到需要包裹的文字。接下来就是把包裹上了'你好'.replace(/([\u4e00-\u9fa5]+)/gi,'t(\'$1\')'),搞定。慢着,真的这么简单吗,想想我们真实代码的情况,注释,各种复杂的模板字符串,换行等等。这意味着这个正则到后面会巨复杂,到后面会面对一个晦涩难懂,难于维护的正则表达式。

另外还有个很蛋疼的事情,比方说我们的调试,上报等等代码,如console.log('不需要提取'),怎么办?感觉脑子不够用了。

方案 2 - AST

我们希望只匹配我们想要的词条。比如下如下代码,我们预期匹配你好(注释和 console.log 的里的都不需要),如果有个方式只给到我们你好,然后我再判断它是不是包含中文,再包裹就再好不过了。

// 这是一段注释
const word = '你好';
console.log('世界');

有没有这样的好事?答案是还真有。是时候上这张神图了

Babel 的工作流程主要分为以下 3 个阶段:

  1. Parse阶段:词法分析 & 语法分析
  2. Transform阶段:生成AST,抽象语法树
  3. Generate阶段:生成代码

这里我们着重说下Transform阶段,AST 处理的核心要素:

  • babel-core 通过 transform 将代码字符串转换为 AST 树;
  • babel-types 一个强大的用于处理 AST 节点的工具库,它包含了构造、验证以及变换 AST 节点的方法;
  • visitor 当 Babel 处理 Node 时,以访问者的形式获取节点信息,并进行相关操作。这种方式是通过一个 visitor 对象来完成,在 visitor 对象中定义了对于各种节点类型函数,我们可以通过不同类型节点做出相应处理。

通过上述要素,我们既可以完成对 AST 的修改。下面我们看下这里的核心代码:

return {
visitor: {
StringLiteral(path: NodePath<tt.StringLiteral>) {
let { value } = path.node;
value = replaceLineBreak(value); if (needWrap(wrapCharacter, value)) {
let newNode = t.CallExpression(t.Identifier(T_WRAPPER), [
combine(value),
]); path.replaceWith(newNode);
}
},
CallExpression(path: NodePath<tt.CallExpression>) {
switch (path.node.callee.type) {
case 'MemberExpression': {
const excludeFuncName = i18nConf.parsedExcludeWrapperFuncName;
if (excludeFuncName.length > 0) {
const names: string[] = [];
const me = path.node.callee as tt.MemberExpression;
getName(me, names);
const MEName = names.reverse().join('.');
if (excludeFuncName.includes(MEName)) {
path.skip();
}
}
break;
}
default:
break;
}
},
}

针对上面我们诉求的例子,当我们得到 AST 后

  • // 这是一段注释 - 实际上会被解析成 CommentLine 类型,我们的代码不处理,所以该什么样还是什么样
  • const word = '你好' - 你好被解析为StringLiteral,判断是中文,这时候我们再重新构造一个新的节点,替换老的及完成了包裹
  • console.log('世界') - console.log被解析为CallExpression,我们可以通过在配置文件中配置需要忽略的包裹的方法,如果解析到的方法名在配置中,则忽略掉,这样就不会出来这里的世界

至此,我们即可完成我们的诉求,完美的对符合我们需要的词条就行包裹。

题外话 - 如何编写自己的 babel 插件

通过上面 AST 的方案,我们可以看得出这里的功能很强大,业界eslint,prettierwebpack等等都是通过对源码进行分析,转换,生成实现各种各样的功能。

我们可以开发自己的插件,去做各种有意思的事情,比如说代码埋点,国际化方案等等。看到这里我想大家一定会有个问题:

  1. 上面说的代码转 AST 时的各种类型,我们怎么知道转成什么类型了呢?

答:https://astexplorer.net/

  1. 另外这些类型如何构造新的节点?

答:https://babeljs.io/docs/en/babel-types

如何使用 i18n-helper-cli

实例

请参考 example

安装

注意:请确保 Nodejs 版本大于 14!!!

# npm 安装
npm install i18n-helper-cli -D
# yarn 安装
yarn add i18n-helper-cli —dev

快捷使用

  1. 在项目根目录下生成 i18n.config.json 文件
# 交互式命令行
i18n-helper init
# 生成默认配置文件,具体参见【配置说明】( 推荐大家用这个哈,交互方式的的后面加了不少配置海内来得及补齐)
i18n-helper init -y
  1. 包裹 & 提取 & 翻译 & 统计
# 包裹 & 提取 & 翻译 & 统计 i18n.config.json 中 srcPath 文件中的中文词条
i18n-helper scan -wetc
  1. 切换 Cli 语言
# cli 默认为中文,支持语言切换,目前支持zh & en
i18n-helper switch en

命令详情

# 包裹 & 提取 & 翻译 & 统计 i18n.config.json 中 srcPath 文件中的中文词条
# w:wrap e:extract t:translate tm: translate machine c:count
# l:language
# 这 5 个操作可以随意组合 e.g. i18n-helper scan -we 则只会翻译 & 提取
i18n-helper scan -wetc
i18n-helper scan -we -tm -c
# 包裹 & 提取 & 翻译 & 统计 指定路径,指定语言内符合规则的词条
# e.g i18n-helper scan -wetc -l en ./src/test/index.js
i18n-helper scan -wetc -l [language] [filepath]
i18n-helper scan -we -tm -c -l [language] [filepath] # 包裹 i18n.config.json 中 srcPath 文件中的中文词条
i18n-helper wrap
i18n-helper scan -w
# 包裹指定文件中的中文词条
i18n-helper wrap [filepath]
i18n-helper scan -w [filepath] # 提取 i18n.config.json 中 srcPath 文件中的中文词条到所有配置语言文件
i18n-helper extract
i18n-helper scan -e
# 提取指定文件中文词条到指定语言文件
# e.g i18n-helper extract -l en ./src/test/index.js
i18n-helper extract -l [language] [filepath]
i18n-helper scan -e -l [language] [filepath] # 翻译 i18n.config.json 中配置翻译文件词条, -m 腾讯翻译君机器翻译
# 从翻译源文件文件中翻译
i18n-helper translate
i18n-helper scan -t
# 腾讯翻译君自动翻译
i18n-helper translate -m
i18n-helper scan -tm
# 翻译指定语言
# 从翻译源文件文件中翻译
i18n-helper translate [language]
i18n-helper scan -t -l [language]
# 腾讯翻译君自动翻译指定语言文件
i18n-helper translate -m [language]
i18n-helper scan -tm -l [language] # 统计 i18n.config.json 中翻译文件的翻译情况
i18n-helper count
i18n-helper scan -c
# 统计指定语言翻译文件的翻译情况,多个语言用,分隔
i18n-helper count [language]
i18n-helper scan -c -l [language]

配置详情

module.exports = {
// cli 语言
cliLang: 'zh',
// 项目类型:react | vue | js
projectType: '[react]',
// 默认包裹和提取词条的目录
srcPath: './',
// 扫描文件格式
fileExt: 'js,ts,tsx',
// 包裹的字符集,下面是中文
wrapCharacter: '[\u4e00-\u9fa5]',
// 包裹词条的名字
wrapperFuncName: 't',
// 忽略掉包裹的方法,多个用,分隔
excludeWrapperFuncName: 'console.log,console.error',
// jsx中的文字包裹方式,true用<trans></trans>, false用【wrapperFuncName】的value包裹
jsx2Trans: false,
// 当文件需要翻译时引入的文件
importStr: `import {t} from './i18n;';\n`,
// 排除目录,此目录下的不会不会执行包裹和提取词条操作
exclude: 'node_modules,dist,git',
// 翻译词条目录
localeDir: './locales',
// 翻译语种
languages: 'zh,en',
// 源语言
sourceLanguage: 'zh',
// 翻译词条文件名
transFileName: 'translation',
// 翻译词条文件格式: json, po
transFileExt: 'json',
// 翻译词库目录(自动翻译目录)
targetTransDir: './translations',
// 翻译词库文件名
targetTransFile: 'sourceTranslation.json',
// 腾讯云 secretId
secretId: '',
// 腾讯云 secretKey
secretKey: '',
};

未来规划

  • [ ] 网页多语言显示异常检测
  • [ ] 丰富提取文件(po, csv, excel 等等)
  • [ ] 增加 git 模式,针对当前改动的文件才转 AST 包裹,提取
  • [ ] 词条提取 cleanMode,目前如果代码中没有这个词条了,提取后的文件依然会有

其他

源码

https://github.com/wuqiang1985/i18n-helper

NPM 包

https://www.npmjs.com/package/i18n-helper-cli

目前还在完善中,欢迎大家试用,大家有问题可以提 issue。

一个命令搞定 Web 国际化的更多相关文章

  1. Java 11 快要来了,编译 & 运行一个命令搞定!

    Java 11 马上要来了,原定于 9 月发布,还有不到 3 个月了,敬请期待更多新功能被加入到 11 当中,本文本讲的是 JEP 330 这个新特性. 化繁为简,一个命令编译运行源代码 看下面的代码 ...

  2. Centos安装桌面环境(一个命令搞定)

    # yum groupinstall "X Window System" "GNOME Desktop Environment"

  3. 一行命令搞定node.js 版本升级

    from:http://www.16boke.com/article/detail/26 今天,又发现一个超级简单的升级node.js的方法.一行命令搞定,省去了重新编译安装的过程. node有一个模 ...

  4. 一行命令搞定VS2012无法安装cocos2d-x-2.1.4及创建跨平台项目(二)

    转自:http://blog.csdn.net/yangjingui/article/details/9418843 由于上次发了一个比较二的方法来解决VS2012无法安装cocos2d-x-2.1. ...

  5. 将你的前端应用打包成docker镜像并部署到服务器?仅需一个脚本搞定

    1.前言 前段时间,自己搞了个阿里云的服务器.想自己在上面折腾,但是不想因为自己瞎折腾而污染了现有的环境.毕竟,现在的阿里云已经没有免费的快照服务了.要想还原的话,最简单的办法就是重新装系统.而一旦重 ...

  6. python技巧一行命令搞定局域网共享

    python超强玩法--一行命令搞定局域网共享 ​ 今天刷到python的一个新玩法,利用python自带的http服务,快速创建局域网共享服务,命令如下: python -m thhp.server ...

  7. Jquery一个slideToggle搞定div的隐藏与显示

    Jquery一个slideToggle搞定div的隐藏与显示 <!DOCTYPE html> <html> <head> <script src=" ...

  8. [转] 一句shell命令搞定代码行数统计

    今天面试时,突然被面试官问到怎样用shell命令搞定某个文件夹下java代码行数的统计. 想了一下,基本思路就是找到这个文件夹下面的所有java文件,然后每个文件统计一下代码,外层套个for循环,叠加 ...

  9. 企业sudo权限规划详解 (实测一个堆命令搞定)

    简述问题:         随着公司的服务器越来越多,人员流动性也开始与日俱增,以往管理服务器的陈旧思想应当摒弃,公司需要有 更好更完善的权限体系,经过多轮沟通和协商,公司一致决定重新整理规划权限体系 ...

随机推荐

  1. mac sudo: /etc/sudoers is world writable

    今天误操作修改了/etc/sudoers的权限,将它的权限改成了777,结果就导致执行所有sudo的命令都报错. sudo: /etc/sudoers is world writable sudo: ...

  2. linux 中只显示目录的几种方法

    ls 参数 -a 表示显示所有文件,包含隐藏文件-d 表示显示目录自身的属性,而不是目录中的内容-F 选项会在显示目录条目时,在目录后加一个/ ls -l total 8 drwxrwxr-x 2 r ...

  3. leetcode 1122

    思路分析: 主要思想:计数排序 先遍历arr1,然后计数,再遍历arr2时同时又排完序了,再继续把arr2不存在的数字,再遍历加到数组后面,也同时排完序了.方便快捷

  4. VS2017 常用快捷键

    项目相关的快捷键 Ctrl + Shift + B = 生成项目 Ctrl + Alt + L = 显示 Solution Explorer(解决方案资源管理器) Shift + Alt+ C = 添 ...

  5. Java基础00-数据输入5

    1. 数据输入 1.1 数据输入概述 我们需要的数据(比如账号密码)并不是一开是就有的,而是要输入的. 1.2 Scanner使用的基本步骤 1.3 案例

  6. glibc库和glib库

    http://ftp.acc.umu.se/pub/GNOME/sources/ ubuntu16上glib的安装包的名是libglibXX  XX是版本号 ubuntu上查看glibc的方法 ldd ...

  7. Python urllib翻译笔记一

    22.5.urllib- URL处理模块urllib 是一个收集几个模块以处理URL的包: urllib.request 用于打开和阅读URL urllib.error 包含由urllib.reque ...

  8. P5311 [Ynoi2011] 成都七中

    P5311 [Ynoi2011] 成都七中 题意 给你一棵 \(n\) 个节点的树,每个节点有一种颜色,有 \(m\) 次查询操作. 查询操作给定参数 \(l\ r\ x\),需输出: 将树中编号在 ...

  9. 第二十五篇 -- QTreeWidget右击菜单的实现

    效果图: 自己画一个ui,如图,前面已经讲过怎么用Qt Designer画图了,现在就不赘述了. ui_play.py # -*- coding: utf-8 -*- # Form implement ...

  10. C++第三十五篇 -- 写第一个驱动开发程序

    VS2017+WDK+VMware12+Win10环境配置完毕,接下来写第一个驱动程序. 1.新建一个KMDF的程序. 2.配置项目属性. 3.编译项目.一般这里应该成功,我一台电脑成功了,另一台电脑 ...