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();
});