记一次大型React项目的国际化方案探索

4,935 阅读4分钟

背景

  • 项目:大型数据管理系统,涉及硬件设备数据监控、日常业务信息管理等
  • 技术:前后端分离,前端主要基于React-Redux
  • 需求:前端一键无缝切换多国语言

使用 react-intl

提起React项目国际化,首先想到著名的 react-intl 库,这个库提供了针对组件、日期、数字、字符串等多种国际化方法。使用方法也很简单:

  1. 将不同语言的翻译文件放在各自的js文件中,同一处文本的多种语言翻译使用相同的key

    // en_US.js
    const en_US = {
        "intl_hello": "Hello!",
    }
    export default en_US;
    
    // zh_CN.js
    const zh_CN = {
        "intl_hello": "你好!",
    }
    export default zh_CN;
    
  2. 在入口文件中配置 react-intl 库

    // index.js
    import { addLocaleData, IntlProvider } from 'react-intl';
    // 引入多语言环境
    import en from 'react-intl/locale-data/en';
    import zh from 'react-intl/locale-data/zh';
    addLocaleData([...en, ...zh]); 
    // 引入翻译文本
    import en_US from '.../intl/en_US.js';
    import zh_CN from '.../intl/zh_CN.js';
    const messagesMap = {
        en: en_US,
        zh: zh_CN
    }
    const locale = 'zh'; // 此处做了简化,下文将从redux中获取语言环境
    render((
    	// 使用<IntlProvicer>包装项目组件,配置语言环境和翻译文本
        <IntlProvider locale={local} messages={messages[local]}>
            //···
        </IntlProvider>
    ), document.getElementById("root"));
    
  3. 使用 react-intl 中内置的组件或方法替换需要做多语言的字符串、时间等,具体可参考 API文档


react-intl与redux

由于项目使用redux来管理状态,将语言环境与翻译文本都放入reducer中,使用相关action来触发语言切换:

// actions.js
export const switchLocal = local => ({
    type: 'SWITCH_INTL_LOCAL',
    payload: { local },
});

// reducers.js
import en_US from '.../intl/en_US.js';
import zh_CN from '.../intl/zh_CN.js';
const messagesMap = {
    en: en_US,
    zh: zh_CN
}
const defaultLocal = { //默认语言环境,也可从浏览器或用户配置数据中获取
    local: 'zh',
    messages: messagesMap.zh
};
export const intlLocal = (state=defaultLocal, action) => {
    switch(action.type) {
        case 'SWITCH_INTL_LOCAL':
            return {
                local: action.payload.local,
                messages: messagesMap[action.payload.local],
            }
        default:
            return state;
    }
}

// index.js
// 略去了文件中的redux配置等代码
const { local, messages } = store.getState().intlLocal; // 从store中获取语言配置
render((
	// react-redux中的Provider需要包在IntlProvider之外,IntlProvider才能访问到store
    <Provider store={store}>
        <IntlProvider locale={local} messages={messages}>
            //···
        </IntlProvider>
	</Provider>
), document.getElementById("root"));

完成之后发现初始化的时候可以访问到store,使用指定的默认语言环境,但切换语言无效,排查后发现触发action后reducer确实更改了,但没有触发组件更新。查阅相关文档后,使用react内部的key属性来强制触发更新:

// index.js
render((
    <Provider store={store}>
       // 加入key属性来强制触发更新
        <IntlProvider key={local} locale={local} messages={messages}>
            //···
        </IntlProvider>
	</Provider>
), document.getElementById("root"));

存在问题与方案探讨

在上一步中使用key来强制触发更新,对于一般简单的网站或前端系统来说,到这一步就可以了。

万恶的但是,由于接手的系统过于复杂,使用key强制触发组件更新时,会引起此<IntlProvider>包裹下的所有组件全部被更新,导致类似于页面整体被刷新的效果,从而出现websocket重连、数据丢失等一系列问题,由于不便于动用其他模块,思考过后剩下两种方案:

  1. 语言切换时给予相应提示,然后跳转到欢迎界面,这样重新进入各子系统时会重新发起各种连接。由于并不会经常切换语言,而且语言切换一般也就是发生在刚进入系统的时候,所以这个方案是最实用也最省力的。又是万恶的但是,由于项目背景比较复杂,上面领导的意思是像那些大型网站一样“无缝”切换中英文,一跳转就“有缝”了。。根本不考虑一个网站和一个大型B/S系统的差异,于是在需求降级可能性微乎其微的条件下,这个最合适的方案也只能作为紧急备用方案了。
  2. 修改 react-intl 库,需要包装库中用到的每个方法,将数据源由Context改为redux的store。做的时候发现基本只是在处理字符串,就干脆去掉了 react-intl 库的依赖,手写了个类似于intl库中的 <FormattedMessage> 组件,使用的时候又发现只能用于组件的局限性,又参考阿里的 react-intl-universal ,写了个直接由key生成翻译文本的方法。而这个方案目前还在完善和测试中。