跳转至

前端国际化

1. Ti18n

Ti8n相比于vue-i18n等工具,多了词条管理、部署CDN等功能,还有权限控制及一些部署选项,相当于一个后台管理系统,可以看作是其他工具的超集。

对于一个多人协作、大型项目而言,这种后台词条管理是很方便的。

另外,Ti18n是基于i18next的,一些插值、格式化、复数这些都是支持的。

多考虑一下,如果将来Ti8n废弃掉,也是可以很快换成其他方案,或者完全自己实现的,因为后台管理和前端国际化是分开的。本质上我们只需要一个cdn链接和一个t函数,Ti18n无非是在词条管理上方便了些。

2. 插值

插值可以实现复杂点的需求,比如中文的2022年3月,对应英文的2022/3,可以设置为:

1
2
3
4
5
const config = {
  key: 'monthYear',
  'zh-CN': '{year}年{month}月',
  'en-US': '{year}/{month}'
}

其实,函数也可以实现类似的功能,甚至更强大,能实现更丰富的功能,vant组件库的国际化就包含有函数。但是函数不能json化,也不是i18n的主流方案,这里还是推荐用插值、格式化等统一方案。

3. 组件插值

有时有这种需求:

目前上阶段有<span>{{ notFinishNum }}场比赛尚未结束</span>,开启本阶段比赛将强制结束。

有几种方式可以解决,一是将它们分成几段,二是用v-html。第一种方法的缺点是麻烦,第二种方法的缺点是存在XSS攻击的风险。

vue-i18n提供了组件插值的方式,可以比较简单的解决。

1
2
3
4
5
6
<i18n
  path="emptyScheduleTip"
  tag="label"
>
  <span place="number">{{ notFinishNum }}</span>
</i18n>
1
2
3
4
5
6
7
export default {
  data() {
    return {
      notFinishNum: 12
    }
  }
}

emptyScheduleTip可以配置成:

1
2
3
4
5
{
  key: 'emptyScheduleTip',
  'zh-CN': '目前上阶段有{{number}}场比赛尚未结束,开启本阶段比赛将强制结束。',
  'en-US': 'At present, there is {{number}} games in the last stage that has not ended, and the game will be forced to end when this stage starts.',
}

需要注意的是,i18n是函数式组件,uni-app的小程序是不支持的,所以需要加条件编译。

4. 图片

应该是建一个海外存储桶,或者额外的文件夹(不考虑延迟的话)。前端可以这样处理:

1
2
3
<template>
 <img :src="$imgT('key')">
</template>
1
2
3
4
5
6
function $imgT(key) {
  if (!this.lang || this.lang === 'zh-CN') {
    return `https://x.${key}.png`
  }
  return `https://y.${key}.png`
}

5. 后台接口

后台返回的文案、提示要做国际化,有多种方案:

  1. 后端自闭环,反正部署在海外,应该是能区分当前需要的语言的,后台直接给前端返回相应的文字即可。如果后台获取不到当前语言,也可以由前端传递。这种缺点是前端和后台各自维护词条,可维护性低。优点是前端完全不参与。
  2. 后端传递id给前端,前端匹配cdn的词条。举例如下:

之前内容:

1
2
3
{
  errMsg: '参数错误'
}

改造后:

1
2
3
4
5
{
  errMsg: {
    id: 10001,
  }
}

6. uni-app

uni-app中需要特殊处理的地方只有page.jsonmanifest.json配置,而manifest.json没有需要国际化的地方,所以只需要关注pages.json

uni-app提供了一种%%方案,即占位符,可以用在设置navigationBarTitleText/text等场景。也可以用动态设置title的方式,比如:

{
  "root": "pages/press/button",
  "pages": [
    {
      "path": "button",
      "style": {
        "navigationBarTitleText": ""
      }
    }
  ]
}
1
2
3
4
5
6
onReady() {
  const newTitle = this.t(`titleMap.${name}`);
  uni.setNavigationBarTitle({
    title: newTitle,
  });
}

实践发现,动态设置title,在小程序上会有一段时间title为空的情况,体验不如%%方案好。

7. 时区问题

new Date()方法会自动根据当前浏览器、系统的设置,得出对应的时间,所以不需要额外设置。

Chrome如何切换其他时区,来测试页面呢,方法如下:

Chrome Dev Tools > Hamburger Menu > More Tools > Sensors

另外,后台不适合处理带有时间的文案,因为它并不知道用户所在的时区,除非每次前端传给他,那样就更麻烦了。

更简单的解决方案是前后端约定替换规则,后台返回时间戳,前端去匹配这个规则,拿到时间戳,然后转成对应的当地时间。

比如如果后台文案中包括{{time:timeStamp}},前端就取出来替换,代码如下:

1
2
3
4
export function getTextWithLocalTime(text, format = 'yyyy-MM-dd hh:mm:ss') {
  const reg = /\{\{\s*time\s*:\s*(\d+)\s*\}\}/g;
  return text.replace(reg, (a, time) => timeStampFormat(time, app.$t(format)));
}

参考:https://stackoverflow.com/questions/16448754/how-to-use-a-custom-time-in-browser-to-test-for-client-vs-server-time-difference