前端国际化(Web Internationalization)是指在进行Web开发时,使网站或Web应用程序能够支持多种语言和地区文化,以满足全球用户的需求
概述
由于目前前端形态和框架技术各不相同,如果国际化方案需要根据不同框架进行切换,开发成本可能大大增加。有没有一种可以无缝适配多种主流框架的国际化库呢?
i18-fe,通用react、vue、原生HTML等多种Web环境,写法统一且简单,无需额外工作量,便于修改、调试、拓展
特性
⛰️ 适配 React
、 Vue
、Angular
、vanilla js
等多种 Web 框架
🛶 多框架写法统一,无切换负担
🏗️ 支持 ts
提示,填词效率飞快
🚝 API 简洁,无学习成本
适用端
web(HTML),PC端、移动端(浏览器环境)、webview。暂不包括原生安卓、iOS和小程序
安装
npm install i18-fe
or yarn add i18-fe
API
初始化
使用前需初始化&配置I18,后续都通过i18
实例来操作
// src/i18/index.ts
// 引入i18-fe
import I18 from 'i18-fe';
// 初始化I18
const i18 = new I18();
// 导出i18实例
export default i18;
配置语言包
需要翻译的内容需要在初始化时配置,传入语言包pack
。翻译内容是国际化的重点,也是工作量较大的部分
const pack = {
// 翻译标识(key)
你好: {
// 语言标识(locale)
zh: '你好',
en: 'hello',
},
};
new I18({ pack });
上述代码中,你好
是翻译标识(key),zh
(中文)和en
(英文) 是 语言标识(locale)
key能不能用中文?
作为中文开发者,中文更方便开发维护,可以直观的明白页面内文本含义,当然,你也可以不用
语言标识是作为一级key还是二级key好?
-
翻译标识作为一级key,1是代码量通常会更少,2是突出了翻译重点,整体集中在一起,便于对照和修改
-
语言标识作为一级key,可以将不同的语言类型拆分开来
默认是第一种格式,如果需要使用第二种格式,可以使用I18.packFmt(pack)
进行格式化后再传入
const pack = {
zh: {
hello: '你好',
},
en: {
hello: 'hello',
},
};
new I18({ pack: I18.packFmt(pack) });
pack是不是必填项?
不是。如果不需要使用翻译函数(t)可以不配置pack
pack模块化
语言包配置拆分成各个模块,最后统一导入I18
默认语言
默认:zh
new I18({ defaultLocale: 'en' });
如果翻译时,未从语言包找到当前语言的翻译文本,则会返回默认语言的翻译文本
替换模式
默认:false
用于原生JS DOM替换,用法见下文最佳实践-原生JS
new I18({ replace: true });
翻译(t)
使用i18.t(key)
进行翻译
import i18 from '@/i18'
// 会根据语言标识返回 zh: 你好 en: hello
i18.t('你好')
拼接文本翻译(插值)
有时候文本中夹杂需要动态填入的变量,例如:本页于3分钟前更新
,其中3和分钟需要动态传入
const pack = {
本页于N时间前更新: {
zh: '本页于{time}{unit}前更新',
en: 'This page was updated {time} {unit} ago',
},
分钟: {
zh: '分钟',
en: 'minutes',
}
};
{time}
和{unit}
是占位符,翻译时通过t(key, params)
第二个参数以对象形式传入
import i18 from '@/i18'
const { t } = i18;
// zh: 本页于3分钟前更新
// en: This page was updated 3 minutes ago
t('本页于N时间前更新', { time: 3, unit: t('分钟') })
英文的单数和复数
t('本页于N时间前更新', { time: 1, unit: t('分钟') })
得到This page was updated 1 minutes ago
,minutes
需要修正为单数minute
这里介绍一种方法
const pack = {
本页于N时间前更新: {
zh: '本页于{time}{unit}前更新',
en: 'This page was updated {time} {unit} ago',
},
分钟(单): {
zh: '分钟',
en: 'minutes',
},
分钟(复): {
zh: '分钟',
en: 'minute',
}
};
t('本页于N时间前更新', { time: 1, unit: t(`分钟(单)`) })
t('本页于N时间前更新', { time: 9, unit: t(`分钟(复)`) })
t('本页于N时间前更新', { time: 0, unit: t(`分钟(复)`) })
获取语言(getLocale)
i18.getLocale(): string
可以在页面任何地方获取当前的语言标识(locale),在一些复杂的场景会需要用到它
I18
初始化时,会从依次从url query
、localStorage
、navigator.language
获取locale值,再将其赋值到i18
对象上
切换浏览器语言,getLocale()
得到的locale也会随之改变
日期格式
比如中国的日期格式是YYYY-MM-DD
,美国的日期格式是MM/DD/YYYY
可以使用i18.getLocale()
获取当前语言进行判断
const dateFormat = i18.getLocale() === 'zh' ? 'YYYY-MM-DD': 'MM/DD/YYYY'
moment().format(dateFormat)
图片
很多图片都带有文字,也需要进行国际化处理
<img src={`/static/img/logo-${i18.getLocale()}.png`} />
动态数据请求
很多时候文案是由服务端返回,可以和后端开发者约定,比如在请求头里加上locale
参数,由后端进行翻译。前端一般有统一的请求入口,可使用请求拦截器进行统一处理
接口文案的处理工作量也是比较大,很多数据的文字是不好改的
从华为云管理端可以看到,有些数据也是没办法翻译成英文的
这里我列举几个常见的,被翻译优先级比较高的:
状态码,比较重要且翻译工作量不大,一般会返回statusCode
和statusName
,前端可以根据statusCode
进行匹配翻译
切换语言(setLocale)
i18.setLocale('en')
一般在页面固定位置可以进行语言切换,具体看设计
切换语言时会将url query
和localStorage
中的locale
修改为指定语言,并自动刷新页面
你也可以不调用语言切换,因为i18-fe
会自动获取浏览器默认的语言
最佳实践
包含在多种web框架下的使用方法
react(+ts)
首先在src
下建一个i18
目录
配置语言包
// src/i18/pack.ts
export default {
线索单据漏斗图: {
zh: '线索单据漏斗图',
en: 'Lead document funnel diagram',
},
订单金额: {
zh: '订单金额',
en: 'Order amount',
},
SKU成交转化率: {
zh: 'SKU成交转化率',
en: 'SKU conversion rate',
},
};
初始化
具体初始化和语言包的配置,请查阅API-初始化
// src/i18/index.ts
import I18 from 'i18-fe';
import pack from './pack'
const i18 = new I18({ pack });
export default i18;
翻译
import i18 from '@/i18'
const { t } = i18;
//...
t('线索单据漏斗图')
//...
<div>{t('SKU成交转化率')}</div>
支持ts联想,引导填入key值,方便开发
具体用法见API-翻译
react组件
组件一般与当前开发的工程分离,如何将组件国际化呢?
这里有几个方案:
- 从外部传递语言标识,组件内部自行翻译
- 组件内部自行获取语言标识,自行翻译
- 从外部传递翻译内容,组件接收进行填写
3种方式灵活性 3>1>2,操作难度也是3>1>2
通用组件文案基本固定且较少,如果组件只对内部使用,且都使用的是i18-fe
,可以使用方案2,分工明确,便于操作。如果像ant design
、Material-UI
那样开放给全球用户使用,因为语言标识获取的不统一, 可以使用方案1
业务组件文案其实也变化不大,可选择方案2或3
翻译内容需不需要传入?
查询组件SearchButton
,中文叫查询
,那英文可以叫Search
,应该没有其他叫法了,所以内部维护比较好
无数据组件Nodata
,英文可以叫no data
,中文叫暂无数据
,还可以叫未查找到数据
、真的没有数据了
,这种情况已经脱离了翻译的范畴,应该把文案当做props
传入组件
内部获取语言标识,内部翻译
操作方式和上面react类似,确保组件内和使用组件的工程统一使用i18-fe
import React from 'react';
import I18 from 'i18-fe';
const i18 = new I18({
pack: {
查询: {
zh: '查询',
en: 'Search',
},
重置: {
zh: '重置',
en: 'Reset',
},
},
});
const SearchButton = () => {
return (
<div>
<div>{i18.t('查询')}</div>
<div>{i18.t('重置')}</div>
</div>
);
};
export default SearchButton;
外部传入语言标识,内部翻译
外部传入最好通过Provider
形式注入,避免每次使用组件都传入
// 组件
import React from 'react';
export const Context = React.createContext({} as any);
// 提供全局Provider
export const ConfigProvider = ({
/** 语言标识 */
locale,
children,
}: {
locale: string;
children: React.ReactNode;
}) => <Context.Provider value={{ locale }}>{children}</Context.Provider>;
const pack = {
查询: {
zh: '查询',
en: 'Search',
},
重置: {
zh: '重置',
en: 'Reset',
},
};
type GetKey<T> = T extends Record<string, infer R>
? R extends Record<infer L, string>
? L
: never
: never;
const TestCom = () => {
const { locale }: { locale: GetKey<typeof pack> } = React.useContext(Context);
return (
<div>
<div>{pack['查询'][locale]}</div>
<div>{pack['重置'][locale]}</div>
</div>
);
};
export default TestCom;
外部传入翻译内容,组件接收
同样以Provider
形式将翻译的内容传入组件,组件获取后负责展示
// 组件
import React from 'react';
export const Context = React.createContext({} as any);
// 提供全局Provider
export const ConfigProvider = ({
locale,
children,
}: {
locale: Record<string, string>;
children: React.ReactNode;
}) => <Context.Provider value={{ locale }}>{children}</Context.Provider>;
const TestCom = () => {
const { locale } = React.useContext(Context);
return (
<div>
<div>{pack['查询']}</div>
<div>{pack['重置']}</div>
</div>
);
};
export default TestCom;
// 使用组件
const pack = {
查询: {
zh: '查询',
en: 'Search',
},
重置: {
zh: '重置',
en: 'Reset',
},
};
const i18 = new I18({ pack });
<ConfigProvider locale={{
查询: i18.t('查询'),
查询: i18.t('查询'),
}}>
<TestCom />
<TestCom />
<TestCom />
</ConfigProvider>
react(iframe/微前端/不同工程)
原生JS (freemarker、jQuery等)
如果未使用React、vue等框架,写的是原生DOM,如何国际化处理呢?
<body>
<!-- 测试 → test -->
<div>测试</div>
<!-- 输入框的 请输入 → please input -->
<input type="text" placeholder="请输入" />
<div>
<!-- 中文的百度logo换成英文logo -->
<img src="./img/zh-baidu.png" alt="" />
</div>
</body>
我们常规修改DOM元素一般是给元素绑定id
或class
,再通过document.getElementById
等方法获取到DOM元素,再进行修改
设置replace
模式
开启replace模式后会对页面DOM进行筛选,并修改匹配i18
前缀的DOM
<script src="/node_modules/i18-fe/dist/index.js"></script>
<script>
const i18 = new I18({
replace: true,
pack: {
测试: {
zh: "测试",
en: "test",
},
请输入: {
zh: "请输入",
en: "please input",
},
},
});
</script>
加上i18-
属性
i18-children
匹配修改元素的html
内容(类似于react中的children)
i18-placeholder
匹配修改元素的placeholder
属性
i18-src
匹配修改元素的src
属性
上述匹配机制,如果内容中包含{locale}
字样,会将{locale}
替换为语言标识,如zh
、en
<div i18-children="测试"></div>
<input type="text" i18-placeholder="请输入" />
<div>
<img i18-src="./img/{locale}-baidu.png" alt="" />
</div>
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div i18-children="测试"></div>
<input type="text" i18-placeholder="请输入" />
<div>
<img i18-src="./img/{locale}-baidu.png" alt="" />
</div>
<div>
<button onclick="changeLocale('zh')">中文</button>
<button onclick="changeLocale('en')">English</button>
</div>
<script src="/node_modules/i18-fe/dist/index.js"></script>
<script>
const i18 = new I18({
replace: true,
pack: {
测试: {
zh: '测试',
en: 'test',
},
请输入: {
zh: '请输入',
en: 'please input',
},
},
});
function changeLocale(locale) {
i18.setLocale(locale);
}
</script>
</body>
</html>