React Native爬坑之多语言切换

React Native中多语言切换一般都是采用react-native-i18n。

具体的使用方法很简单,在导入该库之后,针对不同的语言创建对应的js文件,以键值对的形式表达匹配同一文本的不同语言版本。然后将其导入到一个i18n的配置文件中。例如:

import I18n from "react-native-i18n";
import en from "./en";
import zh from "./ch";
import RCTDeviceEventEmitter from "RCTDeviceEventEmitter";
import { AsyncStorage } from "react-native";

// 多语言退化,例如en-US,en-GB退化为en
I18n.fallbacks = true;
// I18n.defaultLocale = "zh"
// 多语言支持,导入不同语言对象
I18n.translations = {
	en,
	zh
};

// 所有的页面,渲染时读取本地存储的用户所选择的语言
AsyncStorage.getItem("appLanguage").then((lang) => {
	if (lang === "简体中文") {
		I18n.locale = "zh";
	} else if (lang === "English") {
		I18n.locale = "en";
	}
}).catch((err) => {
	console.log(err.message);
	I18n.locale = "zh";
});

// 监听LANGUAGE_CHANGE事件,当用户改变语言设置时,修改locale属性为对应的语言值
RCTDeviceEventEmitter.addListener("LANGUAGE_CHANGE", (selectedLanguage) => {
	if (selectedLanguage === "简体中文") {
		I18n.locale = "zh";
	} else if (selectedLanguage === "English") {
		I18n.locale = "en";
	}
});

export default I18n;

如上的代码中,存储字段appLanguage表示应用中所使用语言的字段。以上代码,通过每次读取存储的该字段,来设置选用不同的语言配置文件(en.js,ch.js)。而该字段可以暴露出来,让用户手动设置。这样就达到了,应用初始化时,展示的语言用户选择的语言(如果是初始状态可以设置一个默认的语言)。

还有一段代码是用于监听语言切换事件的,比如当用户选择切换应用语言时,会全局emit一个LANGUAGE_CHANGE事件,这样所有已卸载组件,再重新挂载的时候,都会重新加载对应的语言文件。

en.js节选:

export default {
    loginInvalid: "Login Invalid",
    readProtocol: "I have read and agreed carefully",
    serviceProtocol: "《Service and privacy clause》",
    backupPopUpDialogText: {
        title: "Please Backup your wallet immediately",
		......
	}
	......
}

ch.js节选:

export default {
    loginInvalid: "登录失效",
    readProtocol: "我已经仔细阅读并同意",
    serviceProtocol: "《服务及隐私条款》",
    backupPopUpDialogText: {
        title: "请立刻备份钱包",
        text_1: "区块链钱包不同于传统网站账户,它是基于密码学的去中心化账户系统。",
		......
	}
	......
}

在具体的组件中,可以通过如下方式使用:

import i18n from "@i18n";

// ......
// 所有需要切换的文本,都采用以下形式
i18n.t("backupPopUpDialogText.title");
// ......

以上就是采用react-native-i18n切换语言的大致过程。接下来就具体谈谈一些问题:

(1)切换语言时,未卸载的组件及其子组件不会自动更新。
熟悉React的都知道,如果没有改变组件的state或者是父组件传入的props,那么该组件是不会自动触发重新渲染的。这个问题可以通过对那些未卸载的组件进行手动监听事件,并触发setState方法改变状态来完成。

(2)使用TabNavigation时,Tab标签的tabBarLabel不会自动切换的问题。
这个问题实质上是如何动态改变react-navigation中navigationOptions选项的问题。具体的方法将navigationOptions写成箭头函数的形式,然后传入navigation参数,动态的设置tabBarLabel,例如:

//......

static navigationOptions = ({navigation}) => {
	return {
		tabBarLabel: navigation.getParam("userTabText", i18n.t("WalletUser.tabText")),
        tabBarIcon: ({ focused }) => {
            return focused ? <Image source={require("@images/walletUserFocused.png")} /> : <Image source={require("@images/walletUser.png")} />
        }
    }
}

然后在compnentDidMount中动态的监听LANGUAGE_CHANGE事件,例如:

RCTDeviceEventEmitter.addListener("LANGUAGE_CHANGE", (selectedLanguage) => {
	this.props.navigation.setParams({
		userTabText: i18n.t("WalletUser.tabText")
	});
});

但是这样做在多Tab的情况下就会出现问题,由于Tab子组件是懒加载的(lazy load)。所以在切换语言之前,没有点击该Tab,那么该Tab压根就不会渲染,那么componentDidMount方法也不会执行,监听器也就无效了。这个问题该如何解决呢?如果不想改变懒加载的模式,只希望Tab的tabBarLabel能够动态的切换语言,实际上可以这样做:

之前的navigationOptions可以独立的写在对应的Tab组件下,但是这个方法要求我们把所有Tab的navigationOptions都写在导出的createBottomTabNavigator方法执行的地方,例如:

import { createBottomTabNavigator } from "react-navigation";
import WalletAsset from "./WalletAsset/WalletAsset";
import WalletMarketPrice from "./WalletMarketPrice/WalletMarketPrice";
import WalletInformation from "./WalletInformation/WalletInformation";
import WalletUser from "./WalletUser/WalletUser";
import i18n from "@i18n";
import RCTDeviceEventEmitter from "RCTDeviceEventEmitter";
import React from "react";
import { Image } from "react-native";

// 配置主界面标签页路由
export default MainInterface = createBottomTabNavigator({
    WalletAsset: {
        screen: WalletAsset,
        navigationOptions: ({ navigation }) => {
            RCTDeviceEventEmitter.addListener("LANGUAGE_CHANGE", (selectedLanguage) => {
                navigation.setParams({
                    WalletAssetTabText: i18n.t("WalletAsset.tabText")
                });
            })
            return {
                tabBarLabel: navigation.getParam("WalletAssetTabText", i18n.t("WalletAsset.tabText")),
                tabBarIcon: ({ focused }) => {
                    return focused ? <Image source={require("@images/assetFocused.png")} /> : <Image source={require("@images/asset.png")} />
                }
            }
        }
    },
    WalletMarketPrice: {
        screen: WalletMarketPrice,
        navigationOptions: ({navigation}) => {
            RCTDeviceEventEmitter.addListener("LANGUAGE_CHANGE", (selectedLanguage) => {
                navigation.setParams({
                    MarketPriceTabText: i18n.t("WalletMarketPrice.tabText")
                });
            })
            return {
                tabBarLabel: navigation.getParam("MarketPriceTabText", i18n.t("WalletMarketPrice.tabText")),
                tabBarIcon: ({ focused }) => {
                    return focused ? <Image source={require("@images/marketPriceFocused.png")} /> : <Image source={require("@images/marketPrice.png")} />
                }
            }
        }
    },
    WalletInformation: {
        screen: WalletInformation,
        navigationOptions: ({navigation}) => {
            RCTDeviceEventEmitter.addListener("LANGUAGE_CHANGE", (selectedLanguage) => {
                navigation.setParams({
                    InformationTabText: i18n.t("WalletInformation.tabText")
                });
            });
            return {
                tabBarLabel: navigation.getParam("InformationTabText", i18n.t("WalletInformation.tabText")),
                tabBarIcon: ({ focused }) => {
                    return focused ? <Image source={require("@images/walletInformationFocused.png")} /> : <Image source={require("@images/walletInformation.png")} />
                }
            }
        }
    },
    WalletUser: {
        screen: WalletUser,
        navigationOptions: ({navigation}) => {
            RCTDeviceEventEmitter.addListener("LANGUAGE_CHANGE", (selectedLanguage) => {
                navigation.setParams({
                    userTabText: i18n.t("WalletUser.tabText")
                });
            });
            return {
                tabBarLabel: navigation.getParam("userTabText", i18n.t("WalletUser.tabText")),
                tabBarIcon: ({ focused }) => {
                    return focused ? <Image source={require("@images/walletUserFocused.png")} /> : <Image source={require("@images/walletUser.png")} />
                }
            }
        }
    }
}, {
        tabBarOptions: {
            activeTintColor: "#5EB849",     // 标签激活时的颜色
            inactiveTintColor: "#585A67",      // 标签未激活时的颜色
            showIcon: true,
            showLabel: true,
            upperCaseLabel: false,
            pressOpacity: 0.8,
            style: {
                backgroundColor: '#fff',
                paddingVertical: 3,
                borderTopWidth: 1,
                borderTopColor: '#eaeaea',
                height: 56
            },
            labelStyle: {
                fontSize: 13
            },
        },
        tabBarPosition: "bottom",
        swipeEnabled: true,    // 是否允许手势切换
        animationEnabled: false,    // 切换过程中是否有动画效果
        lazy: true,     // 是否只渲染需要显示的标签
        backBehavior: "none",   // 返回键返回的路由页
    });

以上代码的重点在于RCTDeviceEventEmitter.addListener必须写在navigationOptions的方法中。这样才能正确获取到this.props.navigation的值,从而设置其路由参数。

实际编码中碰到的问题基本就以上这些,不过个人觉得他们已经足够覆盖大多数的语言切换情景了。

值得一提的是,由于我开发的应用需求是要求提供让用户手动切换语言的功能,实际上react-native-i18n结合读取用户手机的语言环境的相关api能够达到自动切换语言的效果,这样其实反而更简单:)。

发表评论

电子邮件地址不会被公开。 必填项已用*标注