React App Framework - 5. i18n

前言

已有的react-i18n方案和框架也有一些了, 基本都是使用react-i18nexti18next来实现多语言支持的, 唯一稍有不足的几点是:

  1. 语言状态保存在i18n库中, 需要手动和全局绑定
  2. 通过t('str')的方式来调用, 如果手误写错了key不容易发现, 而且后期在维护key的使用的情况的时候不够友好, 需要修改为ts更友好的方案
  3. 切换语言的时候还需要同时切换momentantd的语言
  4. 子应用语言需要隔离使用, 不能互相干扰

开发

全局绑定

首先是需要将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
// globalConfigDucks.ts
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组件中, 第一次加载的时候初始化momentantd的语言, 然后通过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
// translation.ts
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
// use-translation.ts
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
// use-app-translation.ts
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;
作者

Mosby

发布于

2019-11-14

许可协议

评论