前言
已有的react-i18n
方案和框架也有一些了, 基本都是使用react-i18next
和i18next
来实现多语言支持的, 唯一稍有不足的几点是:
- 语言状态保存在
i18n
库中, 需要手动和全局绑定
- 通过
t('str')
的方式来调用, 如果手误写错了key
不容易发现, 而且后期在维护key
的使用的情况的时候不够友好, 需要修改为ts
更友好的方案
- 切换语言的时候还需要同时切换
moment
和antd
的语言
- 子应用语言需要隔离使用, 不能互相干扰
开发
全局绑定
首先是需要将language
状态保存到全局store
中, 初始值根据本地存储->浏览器语言的优先级来确认
这里注意在获取默认语言和切换语言之后, 记得将html
标签的lang
属性设置为当前语言, 否则浏览器会提示语言翻译提示弹窗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| let storeLanguage: StoreTranslationLanguage = store.get(CONSTANT.STORE_KEYS.STORE_TRANSLATION_LANGUAGE, '') as StoreTranslationLanguage; if (!CONSTANT.LANGUAGE_OPTIONS.map((x) => x.value).includes(storeLanguage)) { if (_.startsWith(navigator.language, 'zh')) { storeLanguage = 'zh'; } else { storeLanguage = 'en'; } } window.document.querySelector('html').setAttribute('lang', CONSTANT.LANGUAGE_OPTIONS.find((item) => item.value === storeLanguage).htmlLang);
const initGlobalConfig: GlobalConfig = { language: storeLanguage, app: null, project: null, permission: null, permissionLoading: false, };
const reducer = (state: GlobalConfig = initGlobalConfig, action: Action): GlobalConfig => { switch (action.type) { case 'GLOBAL_CHANGE_LANGUAGE': window.document.querySelector('html').setAttribute('lang', CONSTANT.LANGUAGE_OPTIONS.find((item) => item.value === action.payload).htmlLang); store.set(CONSTANT.STORE_KEYS.STORE_TRANSLATION_LANGUAGE, action.payload); return { ...state, language: action.payload }; default: return state; } };
|
应用多语言
还是在Root
组件中, 第一次加载的时候初始化moment
和antd
的语言, 然后通过useEffect
监听language
的变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const Root = () => { const { i18n } = useTranslation(); const language = useSelector((state: AppState) => state.globalConfig.language);
const [languageState, setLanguageState] = useState < typeof CONSTANT.LANGUAGE_OPTIONS[number] > (() => { const selectLanguage = CONSTANT.LANGUAGE_OPTIONS.find((x) => x.value === language); moment.locale(selectLanguage.moment); i18n.changeLanguage(selectLanguage.i18n); return selectLanguage; });
useUpdate(() => { const selectLanguage = CONSTANT.LANGUAGE_OPTIONS.find((x) => x.value === language); moment.locale(selectLanguage.moment); i18n.changeLanguage(selectLanguage.i18n); setLanguageState(selectLanguage); }, [language]);
return ( <ErrorBoundary> <ConfigProvider locale={languageState.antd}></ConfigProvider> </ErrorBoundary> ); };
|
ts
友好
为了使应用多语言的时候出现ts
的代码检测和提示, 需要修改下语言文件的声明结构和t
的调用方式, 保证如果t('str')
里面的str
如果填写错误, ts
编译时就能检查出来, 而且在使用的时候也会出现智能提示
使用下面方式声明的好处是能保证每个key
一定存在所有语言的翻译, 不会出现人工疏忽漏掉某个语言的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import CONSTANT from './constant';
export type Translation = { [x: string]: { [x in typeof CONSTANT.LANGUAGE_OPTIONS[number]['value']]: string }; };
const TRANSLATION = { ERROR: { zh: '错误', en: 'Error', }, LOGIN_IN: { zh: '登录', en: 'Login', }, LOGIN_OUT: { zh: '登出', en: 'Login Out', }, LOADING: { zh: 'Loading...', en: 'Loading...', }, } as const;
export default TRANSLATION;
|
1 2 3 4 5 6 7 8 9 10
| import { useTranslation } from 'react-i18next'; import TRANSLATION from '../config/translation';
const _useTranslation = (): ((key: keyof typeof TRANSLATION) => string) => { const { t } = useTranslation(); return (key: keyof typeof TRANSLATION) => t(key); };
export default _useTranslation;
|
i18n
加载
这里全局的语言比较简单, 直接初始化的时候将translation
文件里的数据结构变为i18n
需要的数据传入即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import CONSTANT from './constant'; import TRANSLATION from './translation';
export type Resources = { [x in typeof CONSTANT.LANGUAGE_OPTIONS[number]['value']]: { [x: string]: { [key: string]: string; }; }; };
const resources: Resources = _.extend({}, ..._.map(CONSTANT.LANGUAGE_OPTIONS, (x) => ({ [x.i18n]: { translation: {} } })));
_.map(TRANSLATION, (translation, key) => { _.map(CONSTANT.LANGUAGE_OPTIONS, (x) => { resources[x.i18n]['translation'][key] = translation[x.i18n]; }); });
i18n.use(initReactI18next).init({ fallbackLng: CONSTANT.LANGUAGE_OPTIONS[0].value, debug: false, interpolation: { escapeValue: false, }, resources: resources, });
i18n.changeLanguage(CONSTANT.LANGUAGE_OPTIONS[0].value);
export default i18n;
|
子应用语言加载
这里就会比较麻烦一点, 类似子应用store
的方案, 需要在切换子应用的时候自动注入子应用的翻译到全局
函数式组件没有construct
方法, 想要在第一时间注入翻译的话, 这里使用了useMemo
模拟初次加载, useEffect
不合适是因为调用时组件已经渲染过一次了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| const SubApp: React.FC<{ app: AllAppOptions, menu: SubAppMenu, showMenu: boolean, translation: Translation, reducers?: ReducersMapObject<any, any>, preloadedState?: any, store?: Store<{}, Action>, }> = ({ app, menu, showMenu, children, reducers, preloadedState, store, translation }) => { const { i18n } = useTranslation();
useMemo(() => { const resources: Resources = _.extend({}, ..._.map(CONSTANT.LANGUAGE_OPTIONS, (x) => ({ [x.value]: { [app]: {} } }))); _.map(translation, (translation, key) => { _.map(CONSTANT.LANGUAGE_OPTIONS, (x) => { resources[x.i18n][app][key] = translation[x.i18n]; }); }); _.map( CONSTANT.LANGUAGE_OPTIONS.map((x) => x.i18n), (x) => { i18n.addResourceBundle(x, app, resources[x][app], true, true); }, ); }, [i18n]); useEffect( () => () => { _.map( CONSTANT.LANGUAGE_OPTIONS.map((x) => x.i18n), (x) => { i18n.removeResourceBundle(x, app); }, ); }, [i18n], ); };
|
子应用的i18n
子应用的useTranslation
会稍有区别, 因为我们是将不同的子应用注入到了不同的namespace
下, 所以需要特殊指定一下即可, 并且它们各自的翻译key
都是独立的, 所以需要从自己的translation
中获取
1 2 3 4 5 6 7 8 9 10
| import { useTranslation } from 'react-i18next'; import TRANSLATION from '../config/translation';
const useAppTranslation = (): ((key: keyof typeof TRANSLATION) => string) => { const { t } = useTranslation('app'); return (key: keyof typeof TRANSLATION) => t(key); };
export default useAppTranslation;
|