一个“中用”的前端国际化方案,请注意查收

1,557 阅读4分钟

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

前端国际化是什么

简单说就是翻译一下,切换中英文,但绝不是把整个语言包放进去,那可没必要,只是按需处理即可,那么如何制定一个优雅的国际化方案,才是需要重点研究的。


装配react-intl

react-intl是一个 Yahoo 公司出品的,有兴趣可以自行了解一下。

安装:

    yarn add react-intl

使用:

    ...
    import { IntlProvider } from "react-intl";
    ...
    class Root extends Component {
    render() {
        const {
        global: { locale },//可枚举的值为"zh"和"en"
        } = this.props;
        return (
        <IntlProvider locale={locale} messages={language.getData()[locale]}>
            <App />
        </IntlProvider>
        );
    }
}

分析:

  1. 首先使用的是IntlProvider包裹一下。
  2. 然后传入两个参数:
    • locale: 当前语言环境
    • messages:按需配置的语言包(下面重点分析)。

至此基本装配够用了,其他“高玩”的配置有兴趣的可以继续探究。


重点说一下 配置语言包 的方式

“传统”的模式:

配置语言包:

咱们以 login 为例:

en-US:

const login = {
  "login.title": "User Center",
  "login.username": "please enter username",
  "login.usernameEmpty": "username cannot be empty!",
  "login.maxLength": "username is no more than 100 characters",
};

export default login;

zh-CN:

const login = {
  "login.title": "用户中心",
  "login.username": "请输入用户名",
  "login.usernameEmpty": "用户名不能为空!",
  "login.maxLength": "用户名不得多于100个字符",
};

export default login;

开发使用

...
/* 引入 */
import { injectIntl } from "react-intl";
...
/*  注入专属国际化数据 */
@injectIntl
class Login extends React.Component {
    constructor(props) {
        super(props);
        const {
            intl: { formatMessage },
        } = this.props;
        /*  使用 */
        message.warning(formatMessage({ id: "login.maxLength" }))
    }
    ...
}

分析:

  1. 首先在配置语言包上,需要分别在en-USzh-CN目录下配置两个结构相同,值不同的文件。
  2. 基于之前的装配,我们就可以通过react-intl提供的injectIntl高阶组件对所在组件进行包装,从而可以直接通过 props 获得国际化数据intl
  3. intl中获得formatMessage,传入片段id,就能获得翻译的值。

但有弊端:

  1. 配置上要配置两组大体一样就是值不同的数据,一是重复了,二是还得人工对仗着写,这就很恶心了。

  2. 所有页面都能“用”一个整体🤔️???这不好维护啊,权限没控制好啊,页面对翻译片段的依赖会越来越混乱,最好还是借鉴 mobx 这种仓库的思想,你依赖啥我给你啥,不依赖就不给你。

  3. 每次使用我都需要formatMessage翻译,文件不多片段不多还行,要是都多呢?那岂不是要写很多行,每次 render 都去执行翻译函数,栗 🌰:

    ...
    render() {
        const {
        intl: { formatMessage },
        } = this.props;
        const { codeImage } = this.state;
        const usernamePlaceholder = formatMessage({ id: "login.username" });
        const usernameEmpty = formatMessage({ id: "login.usernameEmpty" });
        const passwordPlaceholder = formatMessage({ id: "login.password" });
        const passwordEmpty = formatMessage({ id: "login.passwordEmpty" });
        const codePlaceholder = formatMessage({ id: "login.code" });
        const maxLength = formatMessage({ id: "login.maxLength" });
        const pwdMaxLength = formatMessage({ id: "header_pwdMaxLength" });
        const codeEmpty = formatMessage({ id: "login.codeEmpty" });
        return (
        <div className="loginpagewrap">
            ...
        </div>)
        }
    ...

这明显不合理啊,得想个辙啊。

优化的模式:

首先针对传统模式各个环节进行优化。

首先从配置方式入手

import BaseIntl from "./baseIntl";

let config = {
  light_searchSelect: {
    en: "searchSelect",
    zh: "联想select",
  },
  light_baseSelect: {
    en: "baseSelect",
    zh: "基本select",
  },
  light_computeNum: {
    en: "computeNum",
    zh: "计算值",
  },
};

export default new BaseIntl({ config });

一个文件解决,这多好,并且通过baseIntl进行扩展,主要为其补充了共用的翻译片段,这样大大的解决了重复翻译片段的问题。

然后基于react-intl定制一个属于我们的高阶组件intlHoc

做这个高阶组件前,得先明确我们不是破坏react-intl,而是扩展 ta。

直接上代码:

import React from "react";
import { inject, observer } from "mobx-react";
import { injectIntl } from "react-intl";
import language from "SRC/language";

function hoc(id) {
  return function (WrappedComponent) {
    @injectIntl
    @inject("global")
    class IntlHoc extends React.Component {
      constructor(props) {
        super(props);
        const {
          global: { locale },
        } = this.props;
        this.state = {
          formatedMessage: this.formatMessage(),
          localeFlag: locale,
        };
      }

      formatMessage() {
        const { intl } = this.props;
        const { formatMessage } = intl;
        let targetArr = language.getIntlById(id);
        let trmpArr = {};
        for (let key in targetArr) {
          trmpArr[key] = formatMessage({ id: key });
        }
        return trmpArr;
      }
      shouldComponentUpdate() {
        const {
          global: { locale },
        } = this.props;
        if (this.state.localeFlag !== locale) {
          this.setState({
            localeFlag: locale,
            formatedMessage: this.formatMessage(),
          });
        }
        return true;
      }
      render() {
        const { formatedMessage } = this.state;
        const props = Object.assign({}, this.props, {
          intlData: formatedMessage,
        });
        return <WrappedComponent {...props} />;
      }
    }
    return IntlHoc;
  };
}

export default hoc;

然后代替injectIntl进行包装,栗 🌰:

...
import injectInternational from "COMMON/hocs/intlHoc";
...
@injectInternational("light")
class TempEdit extends Component {
     const {
            intlData
        } = this.props;
    render(){
        return <div>{intlData.light_editting}</div>
    }
}

分析:

  1. 首先在高阶组件包装的方式上别无二致。
  2. 替代了之前使用formatMessage翻译的方式,并将其在高阶组件内部统一做好之后,再将数据注入到组件props中,通过intlData获取,直接通过“.”的方式就能获得翻译,既简化了流程又避免了函数的多余执行。
  3. 最关键的是可以为页面传入指定的国际化模块,这太舒服了,分开维护,真的很棒。

效果展示


结语

国际化是一件如果你不在意ta,ta会让你很头疼,很痛苦的事情,像是一种习惯,还是早养成为好,等最后再去弄,你会感叹怎么这么多东西要翻译,所以一套整合好的国际化解决方案就很有用。

实行该国际化方案的🌰