React Native多语言切换应该是一个比较常见的需求,具体可以分为两种方式:
1. 识别手机系统语言,app自动加载相应的语言文件;
2. 允许用户在App内手动切换语言,这种情况并不需要保证App语言与手机系统语言一致;
下面就来介绍一下这两种方式如何完成,在介绍之前先声明一下本人目前开发所使用的react native版本是0.61.5,且使用的是react hook语法进行组件的编写。
一. 识别系统语言完成自动切换
实现语言的切换,我们需要借助github上的插件,在之前的RN开发中我一般都是使用react-native-i18n这个插件,但是从github上的说明来看,这个库已经废弃而且作者也不再维护了。因此,在新的项目中,我决定使用官方推荐的插件,也就是react-native-localize和i18n-js 。
具体的使用方法也很简单,简而言之就是通过react-native-localize插件获取手机系统语言,然后将其设置给I18n.locale变量即可。
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';
const locales = RNLocalize.getLocales();
const systemLanguage = locales[0]?.languageCode; // 用户系统偏好语言
if (systemLanguage) {
I18n.locale = systemLanguage;
} else {
I18n.locale = 'en'; // 默认语言为英文
}
I18n.fallbacks = true;
I18n.translations = {
zh: cn,
en,
ru: rus
};
export default I18n;
上面的代码中一定要确保I18n.translations对象的键值一定是你设置I18n.locale属性的值,否则翻译文件会对不上。
接下来就是将应用中需要多语言国际化的文本统一配置到若干个语言文件中(上面代码中的cn.js、en.js以及rus.js)。例如:
src/language/cn.js
export default {
filter: '筛选',
delayTime: '延时',
Home: {
family: '家庭',
device: '%{counts} 个设备',
...
},
...
}
src/language/en.js
export default {
filter: 'filters',
delayTime: 'delay time',
Home: {
family: 'family',
device: '%{counts} devices',
...
},
...
}
最后在用到相应文本的视图组件中,通过如下形式即可引用当前语言对应的文本内容。
I18n.t('filter');
I18n.t('Home.family');
I18n.t('Home.device', { counts: 2 })
这样第一种语言切换的需求就完成了,是不是很简单。但是假如我们碰到产品要求App不仅能够实现识别系统语言进行自动切换,还要允许用户在App内手动切换语言。这就有点麻烦了。接下来我们看看应该怎么做。
二. 允许用户手动设置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();
});
在我目前的项目中,基本上需要添加的页面组件也就7、8个,主要是Tab路由栈的页面,tab标签等。这样基本上就完美的解决了已渲染页面显示切换前语言的问题。