/**
* @description
* 用于国际化多语言支持
*
* 在需要多语言的文本处,将文本提取使用全局函数 __() 包装即可(为了避免下面注释中的示例被捕获到,所以下面示例使用 __1 代替):
* const text = __1('需要翻译的文案');
*
*
{__1('需要翻译的文案')}
*
* 然后需要运行 npm run i18n-scan 命令,扫描所有源文件中需要翻译的文案,并整理输出为excel文件。
*
* 你可以将输出的excel进行翻译,翻译好后返回原来位置,再次运行 npm run i18n-read 即可将翻译好的文件同步回语言包。
*
* ********************************************************************************************************
*
* 对于有动态变量的字符串,可以使用i18n.printf方法来辅助替换:
* import { printf } from 'utils/i18n';
*
* {printf(__1('我今年%s岁'), age)}
*
* ********************************************************************************************************
* SSR开启时,在Server端,需要注意应当使用withI18n高阶组件来调用i18n对象,不可以直接引用utils/i18n 中的__()方法
*
* 更详细用法,参考项目README.md
*/
import cookick from 'cookick';
import { createContext } from 'react';
import pkg from 'package.json';
import URL from 'utils/URL';
// 可用的语言
// @ts-ignore
const allowedLangs = pkg.locals || ['zh_CN', 'en_US'];
const LOCAL_LANG_FLAG = 'lang';
const isBrowser = typeof window !== 'undefined';
// 根据语言前缀映射支持的语言
const langBaseMap = {
zh: 'zh_CN',
en: 'en_US'
};
/**
* globalTranslation为以语言标识符作为key,对应的语言包json为内容的对象
*/
const globalTranslation = allowedLangs.reduce((translation, lang) => {
try {
// @ts-ignore
if (pkg.locals) {
// eslint-disable-next-line
translation[lang] = require(`locals/${lang}.json`);
}
} catch (err) {}
return translation;
}, {});
const langPatterns = {
zh_CN: /cn/i,
en_US: /en/i,
zh_TW: /tw|hk/i
};
// 从浏览器语言字符串中解析对应的默认语言, 例如:zh-CN,zh;q=0.9,en;q=0.8
function getBrowserLang(browserlang: string, langs: string[] = allowedLangs): string | undefined {
const validLang = browserlang?.split(';')[0].split(',')[0]?.replace('-', '_');
return langs.includes(validLang) ? validLang : langs.find(lang => langPatterns[lang]?.test(validLang));
}
/**
* @description
* 格式化字符串输出
*/
export function printf(text: string, ...args: Array): string {
let i = 0;
return text.replace(/%s/g, () => {
const holder = args[i++];
return holder === undefined ? '' : (holder as string);
});
}
/**
* 使用cookie存储lang标识,主要用于开启SSR时
*/
const getLangByCookie = key => cookick.getCookie(key);
const setLangByCookie = (key, lang) => cookick.setCookie(key, lang);
/**
* 使用localStorage存储lang标识,主要用于未开启SSR时
*/
const getLangByStorage = key => localStorage.getItem(key);
const setlangByStorage = (key, lang) => localStorage.setItem(key, lang);
/**
* 创建默认的i18n对象
* 注意,在server端,所有用户共享默认的同一种语言。server端应当通过withI18n高阶组件调用i18n对象,
* 并且所有的语言文案都应当在组件生命周期内创建,即不可直接声明组件外的变量作为多语言文案
*/
const defaultI18n = isBrowser
? createI18n(window.location.href, window.navigator.language)
: createI18n('/', allowedLangs[0], () => '');
export default defaultI18n;
export const __ = defaultI18n.__;
export const language = defaultI18n.language;
export const context = createContext(defaultI18n);
type I18nParser = typeof __;
declare global {
const __: I18nParser;
}
/**
* 如果开启了SSR,那么就不应该调用全局__()
* 这里故意不创建全局的__函数,那么在node端将会报错,以提醒开发者通过withI18n高阶组件形式调用
* else {
* global.__ = __;
* }
*/
export function createI18n(
url: string,
browserLanguage: string,
getLocalLang: (langKey: string) => string | undefined | null = __SSR__ ? getLangByCookie : getLangByStorage,
setLocalLang: (langkey: string, lang: string) => void = __SSR__ ? setLangByCookie : setlangByStorage
) {
const queryObj = URL.parse(url, true).query;
// 从地址中解析 ?lang=xxx 或者从本地存储中获取存在标识符
const queryLang = Array.isArray(queryObj.lang) ? queryObj.lang[0] : queryObj.lang;
const localLang = getLocalLang?.(LOCAL_LANG_FLAG);
const browserLang = getBrowserLang(browserLanguage);
const language =
[queryLang, queryLang && langBaseMap[queryLang.slice(0, 2)], localLang, browserLang].find(lang =>
allowedLangs.includes(lang || '')
) || allowedLangs[0];
// 如果本地的语言标识符与当前不一致,则更新本地存储
if (localLang !== language) {
setLocalLang?.(LOCAL_LANG_FLAG, language);
}
const translation = globalTranslation[language] || {};
const baseTranslation = globalTranslation[langBaseMap[language.slice(0, 2)]] || {};
/**
* @description
* 语言包匹配
*/
function __(text: string): string {
return translation[text] ?? baseTranslation[text] ?? text;
}
const i18n = {
__,
printf,
language
};
return i18n;
}