Skip to main content

国际化

i18n 全称 Internationalization,也就是国际化的意思,因为单词太长,所以中间的 18 个字母被缩写为 18,再加上开头和结尾的字母,就组成了 i18n

文案展示

文案的国际化实质上是 编译时或运行时的 key - value 替换 在业务文案渲染时用方法包裹,动态替换为字典中的对应文案,不再采用原先硬写的方式。任何需要被展示到页面上的前端文案,都被方法包裹。

render() {
return (<span>下一步</span>)
}
// env zh_CN
// dict { 'common.btn.net': '下一步' }
render() {
return (<span>TEXT('common.btn.next')</span>)
}

由于各个国家与地区的语境差异,像「20000」这样的数字会被格式化成 「20,000」,摄氏度会转华氏度等等。 因此静态内容的替换不仅仅只是 key - value 的直接映射,在替换的过程中还需要有额外的格式化能力。

动态内容

动态内容指代需要后端配合的内容,比如某种枚举类型的中文叫「商务合同」,英文叫「Bussiness Contract」,而这部分的数据都存放在业务数据库中,因此需要与后端约定一种格式来声明前端需要哪种语言的信息

方式一 url query

get("/backend/info?lang=en_US");
get("/backend/info?lang=zh_CN");
// cookie value = en_US
get("/backend/info");
// cookie value = zh_CN
get("/backend/info");

方式三 header

第三方库国际化部分

这部分翻译由 Ant.Design 的 Local Provider 提供,如 PM 不需要特殊定制的话,业务代码则无需太关注这部分的文案,出默认的就好。

后端接口国际化部分

这部分通常是后端定义的枚举值,我们与后端约定,所有枚举值 xxx_status 都会赠送一个 xxx_status_name。 status 用于逻辑判断,status_name 用于页面渲染。 因此表格中的「状态」一栏,我们直接拿 xxx_status_name 显示即可。

暂无国际化的部分

这部分内容更多是 DB 中存的无法翻译的内容。 比如外部平台通过接口交互,告诉我们的后端服务有个客户【北京 XXXX 有限公司】给我们打了【200000】元,后端将信息入库,接口推送格式如下:

这个 「customer」 现阶段我们是无法做国际化处理的。

时间

时间信息强依赖于当前用户的系统时区,与用户的语言无关 传 Unix 时间戳'1550132531' 而不是'2019/02/14 12:55:02'

由于跨国使用的业务系统需要在某些字段上区分时区,因此后端需要将存储的 UTC 时间直接吐给前端,由前端将 ++UTC 时间结合当前浏览器所处时区++做格式化展示。

同时也需要区分某些不需要做时区转化的字段,需要与后端做些约定 当返回 Unix 时间戳 (如: 1550132531) 时,前端会将该时间戳结合时区做一次转换; 当返回 时间字符串 (如: '2018/09/03 15:21:01') 时,前端则不做处理;

API

https://developer.mozilla.org/zh-CN/docs/Web/API/NavigatorLanguage/language 语言: navigator.language 只读属性返回一个表示用户偏好语言的字符串,通常指浏览器 UI 的语言 提到 navigator.language 就需要提一下 navigator.languages languages 返回一个数组,该数组将返回 当前浏览器/系统 的语言优先级列表,而 language 则是 languages 的第 0 项,指代首选语言

时区/时间

其实我们并不需要获取当前的时区是多少,我们的最终目的是将 unix 时间戳渲染为当前系统环境的时间。 因此 Date 的构造方法就能直接支持我们的需求

数据持久化

我们虽然通过 BOM API 获取到了当前用户的语言环境,但可能有些用户用着泰文操作系统,但我们的业务系统针对泰文还没有做整体的翻译工作,因此需要先将业务系统切换为英文。

这个切换后的设置将被保存至 cookie 中,优先读取 因此获取当前用户的语言优先级如下: cookie > 默认

由于我们的字典包是由后端注入至 index.html,我们需要通过 cookie 告诉后端,用户需要什么语言。 如放在 localStorage 中,当我们的 JS 开始运行时,index.html 在 JS 运行前就存在了,意味着错误的字典也存在了。如需纠正则需要通过 JS reload 刷新一次,用于获取正确的 index.html。这个 reload 显然是我们不想要的。 同时,我们的业务系统是以 iframe 的形式嵌在统一门户里,我们与后端约定,当 url query 中带有语言信息时,会帮我们设置一波 cookie。 因此在 index.html 中注入什么语言的字典包在后端视角里优先级如下:url query > cookie > 默认

无法被扫描到的 Runtime Key

目前我们的扫描文案的脚本是基于源代码的,因此我们无法获取运行时里的 key。 如:

const prefix = 'contract'
render () {<span{ TEXT('合同名称', ${prefix}.name) }</span
}

像这种,按照现在的扫描方案我们扫不到 '合同名称' 与 'contract.name' 之间的对应关系。 我们目前避免这种动态 code 的定义。

接口数据持久化带来的问题

缓存没考虑切换环境 我们的公共组件库 AntX 针对接口数据做了缓存优化,但当时并没有考虑到国际化场景。 举个例子,中文环境下有个接口返回枚举值 在页面中渲染如下 通用组件会在 session 中缓存住这部分数据,当我们切换到这英文环境时,如再从缓存中读取接口数据,得到的并不是我们想要的数据。 因此我们在做缓存时需要将当前环境的语言信息加到缓存键中。

文案防漏模式

前后端在实现业务需求的过程中,难免会漏掉一些本该被国际化的文案,因此前端在测试环境加入了「文案防漏」开关,测试同学可在 test 环境修改自身的 localStorge,可打开这个防漏模式。打开后,前端国际化文案会被双叹号所包裹,方便查漏补缺。

线上

我们将原先存储于前端部分的字典迁移到了 Starling,后端服务在渲染模版时会从文案平台拉取对应项目的字典,打到前端的 index 模版上;js runtime 在需要翻译时,从 window 里获取字典对象

样式布局

将样式布局问题简单分为两类,一类是阅读顺序不一致,一类是文案长度不一致 由于目前我们的业务方主要使用中、英、日三种语言,因此当前主要问题还是各国文案长度不一致导致的样式异常。 产生异常的根本原因 各种语言采用了同一套样式( CSS Style )、同一套 (JS Config)

CSS 快速方案

通过 js 往 body 中动态注入 lang 信息,在 css 中可以通过 lang() 选择器来获取对应的个性化样式

// index.js
// 中文document.body.lang = 'zh'
// 英文document.body.lang = 'en'
.less.container {
&:lang(zh) {
flex-direction: row;
}
&:lang(en) {
flex-direction: column;
}
}

JS 快速方案

以 Ant.Design 的 Table 举例

常规的 columns 写法

action: {
title: TEXT('common.operation'),
width: 180,
}
改造后的 columns 写法
action: {title: TEXT('common.operation'),width: getI18nOpt({'zh': 180,'en': 240,
})
}

资源部署

原先我们这边脚手架里的模版只针对国内 CDN 做了资源推送,为了进一步提升海外用户的访问速度,还需要将静态资源推送到海外节点上。 相应的,后端服务也需要在多地机房部署。 对于前端工程而言,我们需要把打包机的编译产物推送到各个 CDN 节点上。 后端服务将前端工程里的 index.html 作为 jinjia 的 templates,因此可以在 render templates 那一步将 CDN 的域注入至模版。以便获得指定 CDN 上的静态资源。 由此我们实现了根据访问节点的不同,自动的切换 CDN hostname

index.html:
<!doctype html>
<% if (htmlWebpackPlugin.options.env === 'production') { %>
{% from './cdn_map.macro' import cdn_domain %}
<% } %>
<html<head<meta charset="utf-8"<meta name="format-detection" content="telephone=no" /></head<body</body</html>

简单版

在项目的初期,只需支持中英双语,文案数量也相对较少,我们把更多的精力放在业务需求的实现、技术的可行性实践上,把整个工程化流程跑通。 各文案由 PM 将各个词条的英文翻译整理至 doc,再由前后端认领各自的文案,分别维护到各自的代码仓库里。 至此,前端需要将这些 key 用合理的方式管理起来,以便于能达到快速在本地维护的目的。

这时候我们需要能够快速找到对应页面的对应文案,作出修改,同时确保没有影响到其他文案。 当时我们的目录管理方式如下: 业务代码分模块放在 routes 目录下

| ---- routes
| ---- Bar (Module)
| | ---- A1(Sub Module)
| | ---- A2
|
| ---- Foo
| ---- Home

对应的字典文件按语言分模块放在 i18n 目录下

| ---- i18n
| ---- en_US
| | ---- common
| | ---- Bar
| | ---- Foo
| | ---- Home
|
| ---- zh_CN
| ---- common
| ---- Bar
| ---- Foo
| ---- Home

我们将项目里通用的文案(如按钮里的「确定」、「取消」等等) 放在 common 下,业务相关的文案放置在对应的模块中,类似于命名空间的概念