何为i18n?
系统支持多语言的功能称之为国际化,英文为 internationalization
一共18个字母,简称i18n
。随着近些年国内市场饱和,各厂商纷纷出海,i18n
成了必要的能力。
如何做?
常见维度
语言文字、货币类型、时间格式、数字格式。。。
基本原理
可以通过用户IP、浏览器语言等用户当前信息初始化用户的语言、货币类型,同时支持用户在系统内通过内置功能个性化修改,然后保存至cookie,前端使用时从cookie取即可,之所以保存到cookie是为了方便携带给server端。
语言文字
其实本质上就是事先生成一份针对不同语种的静态数据配置。
比如:
{
"zh-CN": {
"key1": "你好",
"key2": "苹果"
},
"en-US": {
"key1": "hello",
"key2": "apple"
}
}
然后通过 json[language][key]
在项目中使用即可。
至于配置存放的地方,npm包、cdn、数据库都可以,视业务场景选择。
保存位置 | 适用场景 |
---|---|
npm包 | 实时更新频率低、SSR页面 |
cdn | 实时更新频率高、CSR页面 |
数据库 | server端使用 |
数字、货币、时间
toLocaleString
toLocaleString()
是 JavaScript 中的一个非常有用的方法,它主要用于日期 (Date) 和数字 (Number) 对象,以本地化的方式显示值。这个方法可以根据用户的语言环境和地区设置返回一个字符串表示形式,且浏览器兼容性较好,非常适合用于国际化应用中。
- Number.prototype.toLocaleString() 处理货币和数字格式
- Date.prototype.toLocaleString()处理时间格式
Intl
Intl
对象是 ECMAScript 国际化 API 的一个命名空间,它提供了精确的字符串对比、数字格式化,和日期时间格式化等api,但是部分对象属性可能存在浏览器兼容的问题。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl
上代码
先定义模型对象
export default class I18n {
constructor(props = {}) {
const DEFAULT = {
locale: '',
defaultLocale: '',
langPacks: {},
cookieKey: 'x-locale',
};
const config = Object.assign({}, DEFAULT, props);
Object.keys(config).forEach((key) => {
if (['locale'].includes(key)) {
this[`_${key}`] = config[key];
} else {
this[key] = config[key];
}
});
}
/**
* 语言地区
*/
private _locale: string;
/**
* 默认语言地区
*/
public defaultLocale: string;
/**
* 语言翻译配置
*/
public langPacks: {
[key: string]: any;
};
/**
* cookie标识
*/
public cookieKey: string;
/**
* 找不到key对应文案时的兜底默认值
*/
public defaultValue: string;
}
模型对象定义好了以后要考虑的是如何来操作对象来满足业务基本诉求。
1、支持初始化配置和修改配置
通过构造函数初始化
export default class I18n {
constructor(props = {}) {
const DEFAULT = {
locale: '',
defaultLocale: '',
langPacks: {},
cookieKey: 'x-locale',
};
const config = Object.assign({}, DEFAULT, props);
Object.keys(config).forEach((key) => {
if (['locale'].includes(key)) {
this[`_${key}`] = config[key];
} else {
this[key] = config[key];
}
});
}
setConfig(config) {
if (typeof config !== 'object') {
config = {};
}
Object.keys(config).forEach((key) => {
if (this[key] != undefined || this[`_${key}`] != undefined) {
if (['locale'].includes(key)) {
this[`_${key}`] = config[key];
// 伪代码
cookies.setItem(this.cookieKey, config[key]);
} else {
this[key] = config[key];
}
}
});
}
}
2、支持业务读取配置
export default class I18n {
public get locale() {
const locale = this._locale || this.getCookieLocal() || this.defaultLocale || this.getBrowserLanguage() || 'en-US';
return locale
}
private getCookieLocal() {
// 伪代码
return cookies.getItem(this.cookieKey);
}
private getBrowserLanguage(): string {
return (navigator.language || navigator?.browserLanguage || '');
}
}
3、通过key获取对应文本
export default class I18n {
private getLangPack(locale) {
locale = locale && typeof locale !== 'undefined' ? locale : this.locale;
return this.langPacks[locale];
}
private getValue(key, locale) {
if (typeof key !== 'string') {
console && console.warn && console.warn('文案索引 [%s] 不合符要求。', key);
return false;
}
const langPack = this.getLangPack(locale);
return langPack ? langPack[key] : undefined;
}
private formateByData(content: string, data: { [x: string]: any }) {
return content.replace(/{
{\s*([\w_]+)\s*}}/g, function (match, key) {
return data[key] || match;
});
}
has(key: string, locale?: string) {
return !!this.getValue(key, locale || this.locale);
}
get(key: string, localeOrData?: string | { [x: string]: any }, data?: { [x: string]: any }) {
let locale = localeOrData;
if (typeof localeOrData === 'object') {
data = localeOrData;
locale = this.locale;
}
// 直接获取
let content = this.getValue(key, locale || this.locale);
// 使用默认文案
if (!content) {
console &&
console.warn &&
console.warn(
'指定的文案索引 %s 在语言包 [%s] 中不存在。',
this.namespace ? `[${this.namespace}].[${key}]` : `[${key}]`,
locale || this.locale,
);
content = this.defaultValue;
}
// 其他语言包查找,兜底'en-us'查找,
if (!content) {
for (let l of [this.locale, this.getCookieLocal(), this.defaultLocale, this.getBrowserLanguage(), 'en-US']) {
if (content) {
break;
} else {
if (l) {
const langPack = this.langPacks[l];
const resKey = this.getKey(key);
content = langPack ? langPack[resKey] || langPack[key] : undefined;
}
}
}
}
// 还没有直接返回key
if (!content) {
content = key;
}
if (data) {
content = this.formateByData(content, data);
}
return content;
}
}
4、支持拓展自定义方法
registerUtils(utils: { [x: string]: Function }) {
Object.keys(utils).forEach((key) => {
// 仅允许注册工具函数,想要拓展属性可以通过继承类的方式实现
if (typeof utils[key] === 'function') {
this[key] = utils[key];
} else {
console?.warn('注册的工具函数必须是函数');
}
});
}