目的
研发的应用要服务于不同的地区的用户,所以应用不能单一语言,要能让不同地区的人无障碍使用就需要实现国际化。
背景
vue3 项目
前端方案
借用工具:vue-i18n 实现
i18n → internationalization 的简写
版本:@8 → vue2;@9 → vue3
安装 vue-i18n
pnpm add vue-i18n@9
创建语言包
1、创建文件夹:src/business/lang
mkdir src/business && mkdir src/business/lang
2、创建 lang.json
touch src/business/lang/lang.json
3、在 lang.json 内填入一些假数据
lang.json:唯一的、包含所有的语言包文件
{
"ar": {
"aa": {
"bb": "مرحبا",
"dd": "مع السلامة"
}
},
"en": {
"aa": {
"bb": "welcome",
"dd": "goodbye"
}
},
"zh-CN": {
"aa": {
"bb": "欢迎",
"dd": "再见"
}
}
}
初始化 vue-i18n
4、创建 index.ts
touch src/business/lang/index.ts
5、完善 index.ts 代码
import { createI18n } from 'vue-i18n'
import langs from './lang.json'
const locale = 'en'
const i18n = createI18n({
locale, // 当前显示的语言
fallbackLocale: 'en', // 兜底语言环境
globalInjection: true, // 是否为每个组件注入全局属性和函数。如果设置为true,则以$为前缀的属性和方法将注入Vue Component,eg: $t|$i18n。default:true
legacy: false, // set `false`, 切换到 Composition API 模式
messages: langs // 语言数据
})
export default i18n
app.use 语言包
6、进入 main.ts,新增如下代码
import i18n from './business/lang'
app.use(i18n)
.vue 内使用语言包
7、进入 App.vue,新增如下代码:
使用语言包提供的全局方法 $t
<div> {{ $t("aa.bb") }} </div>
8、页面效果
从本地|链接上获取当前语言
本地存储:localStorage.getItem('SYSTEM_LANGUAGE')
链接存储:xxxxx.xx.xx?languageCode=xx
9、更改 src/business/lang/index.ts
import { createI18n } from 'vue-i18n'
import langs from './lang.json'
const query = () => {
const q = {} as Record<string, any>
const { search } = location
if (search) {
return search
.slice(1)
.split('&')
.reduce((_q, item) => {
const [key, val] = item.split('=')
_q[key] = val
return _q
}, q)
} else return q
}
const { languageCode } = query()
// 优先使用链接上的、localStorage、默认的
const locale = languageCode || localStorage.getItem('SYSTEM_LANGUAGE') || 'en'
const i18n = createI18n({
locale, // 当前显示的语言
fallbackLocale: 'en', // 兜底语言环境
globalInjection: true, // 是否为每个组件注入全局属性和函数。如果设置为true,则以$为前缀的属性和方法将注入Vue Component,eg: $t|$i18n。default:true
legacy: false, // set `false`, 切换到 Composition API 模式
messages: langs // 语言数据
})
export default i18n
代码变更
页面效果
语言包内有变量的处理
10、更改语言包 src/business/lang/lang.json
{
"ar": {
"aa": {
"bb": "مرحبا",
"cc": "العد التنازلي {day} أيام .",
"dd": "مع السلامة"
}
},
"en": {
"aa": {
"bb": "welcome",
"cc": "countdown {day} day(s)",
"dd": "goodbye"
}
},
"zh-CN": {
"aa": {
"bb": "欢迎",
"cc": "倒计时 {day} 天",
"dd": "再见"
}
}
}
新增了:{变量名} 的形式(官方标准)
.vue 里使用有变量的语言包
11、在 App.vue 里面使用
<div>{{ $t('aa.cc', { day: 3 }) }}</div>
页面效果:
自己提供语言转换方法
当不使用 app.use 时,则无法使用全局的 $t,即,则需要自定义一个转换方法
12、src/business/lang/index.ts 末尾,新增如下代码:
// 之前的代码,不动...
type LocaleTree = typeof langs.en
type StringAppend<T, S> = T extends string ? (S extends string ? `${T}.${S}` : never) : never
type KeyTree<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends string ? K : StringAppend<K, keyof T[K]>
}
export type LocaleKeyTree = KeyTree<LocaleTree>
/**
* 语言转化方法
* @param key 要转换的key,eg: aa.bb
* @param variables 提供的变量,eg: { day: 4}
* @returns string
*/
export const localeTranslate = (
key: LocaleKeyTree[keyof LocaleKeyTree],
variables: { [key: string]: any } = {}
) => i18n.global.t(key, variables)
13、App.vue 里面引入 & 使用
自定义变量格式 & 转换函数 & .vue 使用
若因为种种原因,未使用官方标准的 {变量名},自己定义了一套变量的表达,eg:#变量名#
14、更新语言包,使用自定义变量格式:src/business/lang/lang.json
{
"ar": {
"aa": {
"bb": "مرحبا",
"cc1": "العد التنازلي {day} أيام .",
"cc2": "العد التنازلي #day# أيام .",
"dd": "مع السلامة"
}
},
"en": {
"aa": {
"bb": "welcome",
"cc1": "countdown {day} day(s)",
"cc2": "countdown #day# day(s)",
"dd": "goodbye"
}
},
"zh-CN": {
"aa": {
"bb": "欢迎",
"cc1": "倒计时 {day} 天",
"cc2": "倒计时 #day# 天",
"dd": "再见"
}
}
}
15、定义转换函数:src/business/lang/index.ts 末尾,新增如下代码:
// 之前的代码,不动...
/**
* 语言转化方法 - 自定义格式的变量转换
* @param key 要转换的key,eg: aa.bb
* @param variables 提供的变量,eg: { day: 4 }
* @returns string
*/
export const localeTranslateWithVariables = (
key: LocaleKeyTree[keyof LocaleKeyTree],
variables: { [key: string]: any }
): string => {
let langStr = localeTranslate(key)
for (const key in variables) langStr = langStr.replaceAll(`#${key}#`, variables[key])
return langStr
}
16、App.vue 使用
到此,纯前端的技术方案已完成,但完整的国际化链路还缺少了很多,比如:
- lang.json 谁来维护呢?
- 每种语言的数据谁来增删改?
- 那么多种语言数据又要如何有序的维护呢?
- lang.json 是否需要区分环境呢?测试环境一版?正式环境一版?
其他链路
多语言源数据管理
统一采用 Excel 进行管理,每新增一个需求,就新增一个 sheet,每个 sheet 里面存当前需求的所有语言数据
并且需要保持该 Excel 的唯一与可多人编辑,比如用:钉钉、企业微信、飞书等平台的 Excel 文档
模板
源数据管理模板参考:
端:用来区分该语言包用在哪个项目/平台,比如:服务端、FE、APP 内等等,产品统一定义
分类:表明该语言包属于什么种类/模块等等,比如:userinfo、login、setting 等,产品可自定义
分类名:根据 端+分类自动生成的,采用 _ 连接
文案识别码:该语言包具体作用,比如:错误提示(errTip)、弹窗 title(dialogTitle) 等,产品自定义
根据前面的三个:端+分类+文案识别码,最终生成一个唯一的语言包 key,比如:app_userinfo.errTip、appH5_setting.phone
zh-CN、zh-HK、en、ara 等就是该内容对应的每个语种具体内容,一般是先弄好“中文|英文”,然后剩下的自己去一个个手动翻译,然后复制粘贴过来,直至填满
其他语言的生成
一般是先弄好“中文|英文”,然后剩下的就基于这两个进行生成:
1、自己去一个个手动翻译,比如找 AI,写好 prompt,一次性生成好剩余语种,然后复制粘贴过来,直至填满
2、自动生成:
2.1、在公司内部系统(若有),弄一套通用的语言生成的流程,通过提供只有“中文|英文”的 Excel,自动帮你生成好剩余的
2.2、自己写个脚本(sh/python/npm)等,自己实现其他语言的生成, 翻译部分可以借用百度翻译 API
语言包生成
当本次需要的语言包在上面的表格里面弄好后,就需要生成对应的平台所需的语言包了。
上边已讲了技术方案,我们前端所需的 JSON 数据如下:
{
"ar": {
"aa": {
"bb": "مرحبا",
"cc1": "العد التنازلي {day} أيام .",
"cc2": "العد التنازلي #day# أيام .",
"dd": "مع السلامة"
}
},
"en": {
"aa": {
"bb": "welcome",
"cc1": "countdown {day} day(s)",
"cc2": "countdown #day# day(s)",
"dd": "goodbye"
}
},
"zh-CN": {
"aa": {
"bb": "欢迎",
"cc1": "倒计时 {day} 天",
"cc2": "倒计时 #day# 天",
"dd": "再见"
}
}
}
所以从前端角度出发就是要将【表格转为 JSON】文件
这可以采用很多种方式来实现:
1、前端层面:那前端写个 npm 包,拿到本次需求新增的语言包 Excel,然后本地跑下命令,最终增量生成 lang.json
可采用 exceljs 库来实现 Excel 的读取、解析,最终生成一个 lang.json 文件就行
优点:没啥优点
缺点:流程上很狭窄,其他端的也需要自己去实现
2、产品层面:在公司内部系统(若有),弄一套通用的语言包的流程
大概流程:1、支持上传 Excel;2、生成一条语言包转化记录;3、点击可下载语言包
更细节的:1、先将源 Excel 模板内,本次需求新增的语言包,基于模板,分端本地创建 n 个 Excel 文件;2、然后在系统上根据各端上传不同的 Excel;3、再下载各端的语言包,提供给研发。
上述操作全部是产品来弄,最后群提供给研发就行,各端研发拿到后,自行进行替换
优点:流程统一,语言包的输入、输出都由产品来负责,把控质量与进度
缺点:先要找人把生成语言包的功能做出来,涉及到产品设计、前后端开发等等,还有更细节的上传、生成、格式、模板、增量生成等等
语言包环境
无论有没有系统,建议都需要区分语言包的环境,一般分为:测试、线上环境即可。
当项目未上线时,都只能用测试环境的语言包
当项目要上线时,就只能用线上环境的语言包
当有了对应系统时,更要进行环境的区分。
那为什么需要进行环境区分?
主要为了避免语言包互串、缺少、不应该的增多等情况出现。
首先源 Excel 是无环境的概念,所有的版本(无论是否上线)都会在里面新增 sheet,写上本次所需的语言包。
其次各需求版本研发拿到的语言包应该是:上个版本 + 本次新增的
若无环境,则各版本、各研发阶段生成的语言包就不可控了,等需要上线时,你能确定上线的语言包是【线上上个版本+本次新增】的内容吗?
所以建议区分测试、线上两个环境即可
语言包使用
当研发拿到本次需求产品生产好的语言包后,就需要替换当前项目里面的 lang.json 文件
可以通过手动:删除旧的、拷贝新的
也可以自己写个 npm 命令:删除旧的、拷贝新的
结尾
至此国际化实践的分享就结束了,估计有很多的细节在落地时会成阻碍,但本次分享的大方向是正确的,阻碍就当成闯关吧,经历磨难才更享受成功的喜悦~