i18n在rn中的实现范例
初始化
- 略
允许用户手动设置App语言
如果允许用户设置App语言,那么我们需要持久化一个全局状态来保存用户选择的语言。这里我们使用redux和redux-persist插件来完成。
- 第一步,定义一个action type,在src/redux/action.js文件中:
/*
* 本文件用于定义action常量,所有常量声明及对应值必须大写
*/
const USER_SET_LANGUAGE = 'USER_SET_LANGUAGE'; // 用户手动设置语言
export {
USER_SET_LANGUAGE
};
- 第二步,定义一个actionCreator函数,构造action对象,在src/redux/actionCreators.js文件中:
/*
* 本文件用于定义创建action对象的函数
* 在创建action对象时,必须包含action类型,以及action改变全局状态的最小数据集(payload)
*/
import * as actionType from './actions';
// languageCode一定要严格按照RNLocalize.getLocales()中返回各语言字段编码来定义
const setLanguage = (languageCode) => {
return {
type: actionType.USER_SET_LANGUAGE,
payload: {
languageCode
}
}
}
export {
setLanguage
};
- 第三步,在已经定义好的reducers函数中添加对这一action的处理,在src/redux/reducers.js文件中:
/*
* 本文件用于定义初始全局状态对象以及用于处理action的reducer函数
*/
import * as actionType from './actions';
let initialState = {
userLanguageSetting: null // 用户手动设置的语言
};
const publicReducer = (store = initialState, action) => {
const { type, payload } = action;
switch(type) {
case actionType.USER_SET_LANGUAGE:
const { languageCode } = payload;
return {
...store,
userLanguageSetting: languageCode
}
default:
return store;
}
}
export default publicReducer;
上面的代码中,我是通过 userLanguageSetting 这一全局状态来保存用户设置的语言(languageCode的形式)。当用户没有手动设置语言时,userLanguageSetting字段为null,App自动加载系统语言。反之如果userLanguageSetting不为null,优先采用用户设置的语言。
如果我们只是用redux保存应用状态,那么这些全局状态会在App重新启动时全部丢失。所以我们需要对redux状态对象树进行持久化,这里可以借助redux-persist插件完成,具体如何配置,去查阅官方文档。
- 然后需要修改语言配置文件,在src/languages/index.js文件中,修改如下:
/**
* 多语言配置文件
*/
import I18n from "i18n-js";
import * as RNLocalize from "react-native-localize";
import cn from './cn';
import en from './en';
import rus from './rus';
import { store } from '@redux';
const locales = RNLocalize.getLocales();
const systemLanguage = locales[0]?.languageCode; // 用户系统偏好语言
const { userLanguageSetting } = store.getState(); // 用户手动设置语言
if (userLanguageSetting) {
I18n.locale = userLanguageSetting;
} else if (systemLanguage) {
I18n.locale = systemLanguage;
} else {
I18n.locale = 'en'; // 用户既没有设置,也没有获取到系统语言时,默认加载英语语言资源
}
// 监听应用运行过程中语言的变化
store.subscribe(() => {
const { userLanguageSetting: newUserLanguageSetting } = store.getState();
if (newUserLanguageSetting && newUserLanguageSetting !== userLanguageSetting) {
I18n.locale = newUserLanguageSetting;
}
});
I18n.fallbacks = true;
I18n.translations = {
zh: cn,
en,
ru: rus
};
export default I18n;
export { systemLanguage };
在新的配置文件中,我们需要从redux中获取全局状态判断用户是否设置了系统语言。由于这里并不是在一个React组件中,无法使用react-redux提供的connect方法获取该状态。所以直接导入store对象,通过getState()方法获取。
上面代码中,我们还使用store.subscribe来订阅redux状态的变化,这样在App运行过程中,如果用户选择/切换了语言,我们就能够立刻监听到最新的userLanguageSetting值,并立刻将它设置到I18n.locale中(注:store.subscribe订阅的是整个对象状态树的变化,所以为了避免不必要的更新locale值,需要判断一下变化的状态是否是userLanguageSetting字段)。
那么在用户切换语言的界面中:
/**
* 设置语言界面
*/
import React from 'react';
import { View, StyleSheet } from 'react-native';
import I18n, { systemLanguage } from '@languages';
import { Header, MenuItem } from '@components';
import { connect } from 'react-redux';
import { actionCreator } from '@redux';
const SetLanguage = (props) => {
const { userLanguageSetting, setLanguage } = props;
const formatLanguageCodeToKey = (languageCode) => {
switch(languageCode) {
case 'zh':
return 0;
case 'en':
return 1;
case 'ru':
return 2;
default:
return 1;
}
}
// 根据当前语言状态,获取对应语言选项key,用绿色区分显示
let currentLanguageKey = 1; // 默认英文
if (userLanguageSetting) {
currentLanguageKey = formatLanguageCodeToKey(userLanguageSetting);
} else if (systemLanguage) {
currentLanguageKey = formatLanguageCodeToKey(systemLanguage);
}
const updateLanguage = (key) => {
switch(key) {
case 0:
setLanguage(actionCreator.setLanguage('zh'));
break;
case 1:
setLanguage(actionCreator.setLanguage('en'));
break;
case 2:
setLanguage(actionCreator.setLanguage('ru'));
break;
}
}
const languageGroups = [
{ key: 0, centerText: '简体中文', pressFunc: updateLanguage },
{ key: 1, centerText: 'English', pressFunc: updateLanguage },
{ key: 2, centerText: 'русский', pressFunc: updateLanguage },
];
return (
<View style={styles.container}>
<Header title={I18n.t('Setting.setting')} />
<View style={styles.container}>
{
languageGroups.map(languageObj => {
const { key, centerText, pressFunc } = languageObj;
const isCurrentLanguage = currentLanguageKey === key;
return (
<MenuItem
key={key.toString()}
pressFunc={() => {
if (pressFunc) {
pressFunc(key);
}
}}
centerText={centerText}
customCenterTextColor={isCurrentLanguage ? '#26C283' : '#333333'}
/>
);
})
}
</View>
</View>
);
};
const mapStateToProps = (state) => {
const { userLanguageSetting } = state;
return {
userLanguageSetting
};
};
const mapDispatchToProps = (dispatch) => {
return {
setLanguage(setLanguageAction) {
dispatch(setLanguageAction);
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SetLanguage);
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F3F4F8'
}
});
当用户点击语言选项,会立刻派发一个action,修改userLanguageSetting字段,并在index.js中监听到userLanguageSetting字段值,并更新I18n.locale。这样似乎就大功告成了。
然而,当我们点击切换语言时,会发现一个问题,只有未渲染的页面语言切换了过去,而已渲染的页面则由于没有更新依旧显示的是切换之前的语言(特别是Tab路由加载的页面)。这个问题处理起来是有一点棘手的。我之前没有想到好的办法,去stackoverflow查了一下,发现有部分人建议通过使用react-native-restart这个插件重新加载js资源来解决,但是这样用户体验可能不太好。
后来我想了一下,只要保证用户切换语言后,对应的已渲染页面重绘一次就能够解决这个问题。按照这个思路,写一个自定义hook,来监听语言的变化,并返回最新的语言。
- src/hooks/useLanguageUpdate.js文件中:
import React, { useState, useEffect } from 'react';
import { store } from '@redux';
import I18n from '@languages';
const useLanguageUpdate = (funcWhenUpdate, listenParamArr = []) => {
const [currentLanguageCode, setCurrentLanguageCode] = useState( I18n.locale);
useEffect(() => {
return store.subscribe(() => {
const { userLanguageSetting: newLanguageCode } = store.getState();
if (newLanguageCode && newLanguageCode != currentLanguageCode) {
setCurrentLanguageCode(newLanguageCode);
if (funcWhenUpdate) funcWhenUpdate();
}
});
}, [currentLanguageCode, ...listenParamArr]);
return currentLanguageCode;
};
export default useLanguageUpdate;
- 然后在渲染后会长时间存在的页面组件中,使用该hook。
import { useLanguageUpdate } from '@hooks';
const Home = () => {
useLanguageUpdate();
return (
...
);
};
export default Home;
- 如果组件中有状态依赖更新后语言,可以使用useLanguageUpdate hook的返回值,例如:
let currentLanguage = useLanguageUpdate();
switch(currentLanguage) {
case 'zh':
...
break;
case 'en':
...
break;
...
}
- 如果组件中需要在语言更新时执行某些特定的行为,就可以用上funcWhenUpdate参数,如:
// 自定义Hook,根据切换语言更新当前页面
useLanguageUpdate(() => {
getAllDevsAndStragetiesRequest();
getAllRoomsRequest();
});