5、国际化实现方案

239 阅读7分钟

目的

研发的应用要服务于不同的地区的用户,所以应用不能单一语言,要能让不同地区的人无障碍使用就需要实现国际化。

背景

vue3 项目

前端方案

借用工具:vue-i18n 实现

i18n → internationalization 的简写

版本:@8 → vue2;@9 → vue3

官网:Installation | Vue I18n

安装 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 使用

到此,纯前端的技术方案已完成,但完整的国际化链路还缺少了很多,比如:

  1. lang.json 谁来维护呢?
  2. 每种语言的数据谁来增删改?
  3. 那么多种语言数据又要如何有序的维护呢?
  4. 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 命令:删除旧的、拷贝新的

结尾

至此国际化实践的分享就结束了,估计有很多的细节在落地时会成阻碍,但本次分享的大方向是正确的,阻碍就当成闯关吧,经历磨难才更享受成功的喜悦~