Jsonz bug-log

后续更新会放在 github.com/jsonz1993/blog

0%

多语言需求及后续方案思考

最近接到一个需求,是做tim系统某个模块的多语言,只是多语言还没到本地化的程度。因为这块主要是想解决海外的同事看不懂中文的问题,所以要求没有特别高。

因为目前团队开发和维护多个项目,所以想着多语言的框架可以尽可能的统一,最后选定了社区比较流行的 i18n。
这样虽然项目有些用react,老项目用angular,有一些项目混着angular和react,但是都可以基于i18n去开发,减少开发使用和配置的成本。对于工具这块不再过多赘述。

在上家公司也做过多语言,当时是有个管理后台维护,然后前端负责把词条写上去并且新建一个key去替换本地的文件。这种方案适合一些文案很多,维护灵活的项目,比如电商项目等就比较合适。

目前项目里的多语言都是放在前端维护,前端讲产品和供应商给出的翻译写成两个js文件,然后判断当前的语言环境选择对应的语言包来展示,适合词条比较少的,或者业务文案相对稳定的。比如投资系统就很适合,专业名词改动少,也很少出现大片大片的文案。

这次做的是某个模块的替换,给的时间3-4天,相对比较宽松。所以决定花点时间写个小小的工具来节约一点时间划水摸鱼…

需求分析

这里我负责是tim项目的某个模块,这个模块都是angular1.x实现的。文案绝大部分是写在模板 html中,少部分写在 js中,还有一些是参杂了变量的。

首先我们明确要做的事情:

  • 第一步是将产品给出的在线excel转换为我们的 xxx-en.js 和 xxx-zh-cn.js
  • 第二步将项目中html的词条替换成我们多语言中对应的key
  • 第三步将项目中js的词条也做对应的替换

这里第二步举例来讲就是<span>截止录入时间</span> 替换为 <span>{{ 'portfolioMonitoring:entry_deadline' | i18next }}</span>
这里的 {{ key | i18next }} 是angularJs中filter的写法,其他框架思路不变,key则是由 namespace和wordKey组成。

第三步是js替换,所以和其他框架没有什么区别,比如将

1
2
3
4
5
6
7
8
9
vm.crumbs = [{
name: '截止录入时间',
link: '...'
}];

vm.crumbs = [{
name: $i18next.t('portfolioMonitoring:entry_deadline'),
link: '...'
}];

实现

这次的目标是先写个小工具来简化一些工作。

第一步 excel转语言包:

这一块比较简单,先将产品给的在线文档,导出一份csv格式。然后直接用Node去把csv转成对应的两份语言包文件。
具体的做法是利用 csvtojson 将csv转成json,然后取里面英文翻译做一个 snakeCase 转换,比如 Entry Deadline转为 entry_deadline,作为中英文的key,最后生成两份文件:

1
2
3
4
5
6
7
8
9
// xxx-en.js
{
'entry_deadline': 'Entry Deadline',
}

// xxx-zh-cn.js
{
'entry_deadline': '截止录入时间',
}

第二步 html模板的替换:

读取目标模板,解析模板并对里面单块的文字进行提取(这里没有对值替换模板做处理,省事),然后根据这个文字在我们的语言包里面查找,如果找到的话,就做对应的key值替换。

具体做法是先读取语言包,生成一份中文与key匹配表, map = { '截止录入时间': 'entry_deadline' }。然后对模板文件进行语法分析,这里我用的 posthtml来解析html ast。然后对html中的 innerText 或 attr 属性进行取值匹配,如果匹配到了,就替换为对应的key值,最后将替换后的文件重新写入到对应的模板文件中。

第三步 js模板的替换:

js的替换其实和html的思路一样,只不过选择的解析工具是 @babel/parser,然后配合 @babel/traverse@babel/template 来实现替换。

具体实现是先解析js文件将其解析为 ast,然后用 traverse 来处理这段ast,对里面的 StringLiteral 类型进行处理,如果匹配到对应的中文语言包,则替换为对应的ast。最后将ast重新用 @babel/generator 生成js文件替换源文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
const i18nCallExpression = template(`
$i18n.t('I18N_KEY')
`)
traverse(ast, {
StringLiteral(path) {
... 匹配 ...
path.replaceWith(
i18nCallExpression({
I18N_KEY: `${namespace}:${key}`
})
)
}
})

next

这个小组件花了半天时间写,跑完估摸着比手动替换节约了至少60%的工作量,投入产出比还是挺高的。

不过项目中必不可少的还是要人工过一遍,这和项目的性质有关,如果是一些比如文档类型的,这种很容易做词条替换。但是对于一个投资系统来说,里面有着大量的模板值替换,单复数形式目前还是要手动去替换,比如a company3 companies这种场景。所以下一步想把这块做深入,也不会考虑做构建后的替换,而是采取预构建替换,然后人工再审核一遍。

还有一个问题就是旧项目没有严格的 eslint和 prettier,所以用js转换之后,会出现大量的格式问题,虽然不会报错,但是在cr的时候会造成非常大的影响。
对于这次迭代我的解决方法是直接先用js跑一遍这样格式可以保持统一,然后提交cr,再跑一遍去替换。

这只是一个最基础的beta原型,我们接下来可以往几个方面去深入优化。

  1. 目前文案的收集是产品在页面上人工收集的,可以改为用脚本去扫项目,将识别到的中文提取出来转换为csv文件,减少人力成本。
  2. csv生成js的模式,目前是全量替换,后面应该是做到增量替换,比如我原本已经有一个语言包文件,转换完后我又去改了一个地方,但是后面有一份新的csv,这时候跑完应该是增量的添加到文件中而不是全量替换了。
  3. 对于 多语言方案,应该是有一个工具提供基础的服务,比如csvToJs, 文件提取中文,中文替换为i18nKey。然后对应的各个模块用插件去实现,比如 提取中文,可以是 html,也可以是 js或jsx,这些提取的能力都是插件去提供。比如我在tim项目中用的是angular的模板,那我的解析规则可能就和tpp项目中普通的html模板或者jsx不一样。再比如替换的规则,可能tpp和tip用的都是react,但是他们内部基于 i18next封装的组件有一点差异,这些都可以通过自定义插件去磨平这些差异。
  4. 定位难的问题。 做过多语言项目的人应该都知道,如果替换为词条有一个很蛋疼的问题就是,比如我在页面上发现一个问题,我要去改,最直接的方式就是搜一下这段文案。但是你都替换成词条了,这就很尴尬。所以如果是文档型的页面,你可以使用构建替换的方式去解决这个问题,写的时候常规的写,构建的时候再去替换。但是对于我们项目来说这不太行。解决的方法目前我能想到的有两个:
    • 将中文做为多语言的key,这样对于开发人员相当友好。但是会有一个问题,如果后面这个key改了的话,可能会很诡异,比如最后变成 ‘取消’: ‘确定’,让人摸不着头脑。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // zh-cn.js
      {
      '截止录入时间': '截止录入时间',
      }

      // en.js
      {
      '截止录入时间': 'Entry Deadline'
      }

      // js
      const name = i18next.t('截止录入时间');

      // html
      <span>{{ '截止录入时间' | i18next }}</span>
    • 将中文作为默认的fallback,比如 `i18n.t(‘entry_deadline’, ‘截止录入时间’),然后中文的维护可以不单独写一个语言包,这也是一个折中方案。