自建国际化方案-语言包拆分/按需加载/缓存

背景

国际化多语言(i18n)支持是一个古老的需求,当下企业出海潮的大背景下是一项重要的基础功能。本文将结合自己产品的特点,探讨传统国际化方案的优缺点,并给出适合自己的方案。

原理

国际化的基本原理是通过映射,将同一个文案,映射成不同国家语言的过程,大部分是直接映射,当然少数情况会伴随着格式或顺序的变化

我们可以从最简单的解决方案讨论起,即我们设计一个 json 存储这个映射信息,然后在html里加载这个json,根据浏览器语言去动态映射

讨论

需要响应式吗?

响应式就是指,在页面上切换语言,不刷新页面就可以看到效果
大部分spa应用(react/vue等等)如果要做到响应式,在组件内部的文本,比较好实现,因为我们可以控制组件/应用的更新,但是还有少部分不在组件内的文本,例如一些枚举值, 一些函数内的文本,则比较麻烦,需要把这些文本改成可重复执行的响应式的

import { useTranslation } from 'react-i18next'

function TestComponent() {
  // 利用现成的库可以比较容易的实现响应式
  const { t } = useTranslation(['home'])
  return (
     <h2>{t('home')}</h2>
  )
}

// 某些函数内部的文本,需要特殊改造
function transform() {
 // something
 return 'error msg'
}

// 这种静态的映射也需要特殊改造
const customEnum = {
    code1: '编码1',
    code2: '编码2'
}

问题是,我们真的需要响应式吗?
切换语言这个行为本身还是比较低频,一般应用初始化的时候,根据浏览器设置或者应用设置,直接渲染对应语言即可,即使切换了语言,页面reload一下也无大碍,平衡工作量和体验来看,不做响应式也许对我们是更好的选择

谁来翻译?

大型的应用或者企业,都有专门的翻译团队来做文本的翻译。不过我们是初创公司,而且需要支持的语言非常有限,考虑到成本,还是选择让开发同学自己来做翻译,而且现在AI大行其道,针对一些场景化的文案也能翻译的很好。

谁来翻译文本其实决定了翻译的架构。

试想,如果是专业的翻译团队,肯定搭建一个中心化的翻译平台会更好,将各个业务,移动端,h5,pc都统一到一个平台来翻译会更舒服。

那么如果是开发同学自己来翻译呢?肯定更希望直接在代码仓库中填写翻译文件。
普通的json文件用作翻译存在几个体验问题:

  1. 没有自动提示
  2. 不能从组件内直接ctrl跳转到翻译
  3. 不能从翻译ctrl看到有哪些地方引用了该翻译

当然可以通用插件提升翻译体验,i18n-ally是我看到的非常好的vscode插件。

前端存储or服务端存储映射?

将 “语言包” 放在服务端有几个好处

  1. 修改方便,数据库里面修改完直接可以生效

坏处是

  1. 文件名带hash值的语言包是直接可以强缓存的,或者当文件名固定的时候使用协商缓存,而在服务端(数据库里面)存储,缓存做起来不太容易
  2. 一般静态资源(json映射文件)放到nginx上是有gzip压缩的,服务端返回翻译信息则不太好压缩
语言包拆分/按需加载

一个语言包可能需要几百k甚至上M,一次性加载可能会影响首屏性能。如果要拆分,可能有几种办法

  1. 手动拆分,约定好每个页面创建一个语言包文件,然后通过路由与语言包组件的绑定,动态加载
  2. 通过打包工具的某些插件,分析每个页面里用到哪些语言包的哪些块,将其打成不同的子语言包,然后动态加载
打包时需要把语言包独立出来吗?

两种方案

  1. 将语言包打成单独的文件,而且如果做了拆分/按需加载,那么每种语言可能会出现多个(子)语言包
  2. 直接将不同的语言打进js文件中,即有多少语言,就有多少js的不同版本

我觉得两种差别不大吧,我还是倾向于将语言包打成单独的文件,多了一些请求,但是看起来清楚一些

持续维护问题

语言包中的内容可以预见的会逐步增长,持续维护面临两个问题:

  1. 修改,例如产品经理指出某个组件内的文本有误,开发同学顺利找到组件,并找到了这个文本在语言包中的key,修改的时候一定要小心,因为这个key可能在其他地方也用到了,需要全局搜索一下
  2. 移除,组件可能会删除或者废弃,但遗留在语言包中的翻译一般不会跟着删除,除非我们定期清理,每个key都全局搜索一下,有没有引用,当然我们也可能上一些技术手段,例如treeshakeing

国际化方案

权衡之下,我们

  1. 放弃了响应式
  2. 选择了将 “语言包” 放到前端
  3. 希望实现语言包自动拆分/按需加载
  4. 通过语言包文件带hash值实现语言包http强缓存
  5. 语言包自动treeshaking,即使写到语言包里,如果没有地方引用,也不会打包进最终的产物中
  6. 希望有良好的开发体验,按着ctrl可以来回跳转
具体实现

先来看一下用法:

import { t } from '../../utils/getI18nText' // t 是工具函数,自动根据当前语言替换
import {
    text1,
    text2
} from './i18n' 
// i18n.ts 文件是约定的语言包文件,只要是以 i18n结尾的ts文件都会被视作语言包

// 场景1:固定值,直接使用,无需往里传
export const dictMap = {
    '1': t(text1),
    '2': t(text2)
}
export const text = t(text1)

// 场景2:函数内,直接使用,无需hook
export const pureFunction = () => {
    const flag = true
    if(flag) {
        return t(text1)
    } else {
        return t(text2)
    }
}

// 场景3:组件内,直接使用,无需hook
export default function Page1 () {
    return <div>page1 {t(text1)}</div>
}

///// i18n.ts 文件内容,可以看出就是普通的对象导出
export const text2 = {
    zh: '测试文本2',
    en: 'testText2'
}

export const text3 = {
    zh: '测试文本3',
    en: 'testText3'
}

export const text1 = {
    zh: '测试文本1',
    en: 'testText1'
}


这里 t 的内容:

```javascript
const userLanguage = navigator.language || navigator.userLanguage;

const supportLan = ['zh', 'en']
// 初始化的时候确定当前浏览器语言,反正切换语言的时候会重新reload一下,不怕
let lan = supportLan.find(e => userLanguage.startsWith(e)) ?? 'zh'

// 运行时直接简单映射一下就行
export const t = (x: { [key: string]: string }) => {
    return x[lan]
}

可以想象,如果不加处理,那么语言都将打进产物包中,无法根据当前浏览器语言去加载不同的语言包
我们使用的是vite,项目是react,因此我们通过vite插件实现了这个功能

相关vite插件实现

import {
   
    defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import {
   
    nanoid } from 'nanoid'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值