Fusion组件库是如何支持多语言能力的

2,743 阅读6分钟

随着国际化发展,多语言的需求越来越常见,单一的语言已经远不能满足需求了。作为一个组件库,支持多语言也是基本能力。 多语言功能的本质其实是文本的替换,一个词汇“OK”,在英文语境下是“OK”,日语语境下是“確認”,中文语境下可能是“确定”也可能是“确认”“好的”等等。

本文将以简单组件为切入点,向大家展示Fusion组件库是如何支持多语言能力的。

单组件的多语言

我们以一个常见的组件Search举例,用户输入内容后可通过点击“搜索”、“清除”按钮触发相应的事件,简化代码后如下:

class Search extends React.Component {
  render() {
    return (
        <div>
	    <input />
	    <button>搜索</button>
	    <button>清除</button>
	</div>
    );
  }
}

export default Search;

多语言处理最简单直接的办法是直接替换文本,不同语言环境下可能要将“搜索”替换为“search”、“サーチ”,将“清除”替换为“clear”、"クリア"等。同时作为一个组件库,涉及到的大多是简单词汇而不是句子,因此我们首选配置化的方式:

// 抽取语言包
// search-en-us.js
{
    search: 'search',
    clear: 'clear'
}
// search-zh-cn.js
{
    search: '搜索',
    clear: '清除'
}
import searchZhCN from 'search-zh-cn';

class Search extends React.Component {
    static propTypes = {
        locale: PropTypes.object
    };
    static defaultProps = {
        locale: searchZhCN
    };
    render() {
        return (
            <div>
                <input />
                <button>{locale.search}</button>
                <button>{locale.clear}</button>
            </div>
        );
    }
}

export default Search;

这样就实现了单个组件Search的多语言支持。

但是,为每个组件对应一个语言包文件的做法显然很不方便。Fusion Next作为一个PC端的React组件库有50+组件,内置词汇70多条,目前有13个组件需要国际化语言能力。

以语种为单位,将同一种语言下的映射关系放到一个文件里进行处理的方式更为高效。

多组件的多语言

为便于维护管理,增强可拓展性,我们以语种为单位抽取语言包。将同一语种下所有组件的语言对象{key: '文案'}放到一起,以displayName作为key,语言对象作为value,调整语言包如下:

// 抽取语言包
// zh-cn.js
{
  Search: {
    search: '搜索',
    clear: '清除'
  },
  Dialog: {},
  ...
}

这也是Fusion现在语言包的结构 unpkg.com/@alifd/next… 由于语言包结构的调整,需要同时修改Search组件语言对象的默认值:

import zhCN from 'zh-cn';

class Search extends React.Component {
  ...
  static defaultProps = {
    locale: zhCN.Search
  }
 ...
}
 
export default Search;

在使用时,用户将语言包对象以props参数的形式传给组件即可直接改变文案:

import jaJP from 'xxxx/ja-jp.js';

<Search locale={jaJP.Search}>
<Dialog locale={jaJP.Dialog}>

然而,在web应用越来越复杂的现在,很多项目里里可能会用到几十甚至上百个组件,这样给每个组件手动传locale参数的方式一方面比较蠢,另一方面开发者需要关心locale参数,在语言切换时改变值的内容。

并且语言的设置大都是以项目(或者页面)为单位的,有没有更聪明、对开发者更友好的使用方式呢?

一键设置语言

undefined
如果你使用过Fusion Next或者体验过多语言demo,就可以发现使用方式是这样的:

import zhCN from 'zh-cn';

<ConfigProvider locale={zhCN}>
  <Search />
  <Dialog />
</ConfigProvider>

使用者在使用时基础组件时不用关心locale的变化,子组件们共享了<ConfigProvider>组件上传入的语言配置,更改这一配置可以一键设置子组件的语言包。如何实现的这一功能呢?

React中,如果不想通过逐层传递props或者state的方式来传递数据,不如考虑考虑Context

1. React Context共享上下文数据

借助Context可以实现跨层级的组件数据传递。

它的使用场景是生产者消费者模式,在上面的例子中,<ConfigProvider>就是生产者,<Search> <Dialog>组件就是消费者。 他们分别通过一系列属性方法(childContextTypes属性 getChildContext方法/contextTypes属性),建立起一条通信线。

// 生产者
class ConfigProvider extends React.Component {

  // 声明Context对象
  static childContextTypes = {
    nextLocale: PropTypes.object
  }
  
  // 返回Context对象
  getChildContext () {
    return {
      nextLocale: {}
    }
  }
 
  render () {
    return this.props.children;
  }
}
// 消费者
import zhCN from 'zh-cn';

class Search extends React.Component {
  static propTypes = {
    locale: PropTypes.object
  };
  static defaultProps = {
    locale: zhCN.Search
  };
  // 声明需要使用的Context属性
  static contextTypes = {
    nextLocale: PropTypes.object
  };
  render() {
    const locale = Object.assign({}, nextLocale['Search'], locale);
    return (
      <div>
	<input />
	<button>{locale.search}</button>
	<button>{locale.clear}</button>
      </div>
    );
  }
}

export default Search;

这样,直接给<ConfigProvider>传递国际化参数,就可以改变其子组件所使用的语言包。

数据传递的问题解决了,按照这个思路对组件进行改造就可以完美支持一键切换语言了~ 事实上,这个解决方案通用性很强,只要子组件(包括自定义组件)都按上面的方式进行改造,就可以支持语言包的切换。

但同时我们也发现,改造工作高度重复,都是新增contextTypes静态属性、对props和context上的locale进行merge。有没有对开发者(基础组件开发者、业务组件开发者)更友好的方式来降低这部分重复性工作呢?

2.子组件的统一处理

Fusion为Util类组件ConfigProvider增加了一个静态方法ConfigProvider.config(Component),在这个函数里进行对于locale的改造工作,它返回一个新的受控制的高阶组件(HOC)NewComponent。

NewComponent 相当于被 ConfigProvider 代理了一层。

在ConfigProvider.config()这个函数里

  • 为组件新增contextTypes静态属性,以便接收来自父组件的context;
  • 为组件props、context传入的locale进行merge,以便分发处理语言包文案;

这样,只要子组件经过该函数处理,就可以让ConfigProvider“遥控”语言包切换

import zhCN from 'zh-cn';

class Search extends React.Component {
  static propTypes = {
    locale: PropTypes.object
  };
  static defaultProps = {
    locale: zhCN.Search
  };
  render() {
    return (
      <div>
	<input />
	<button>{locale.search}</button>
	<button>{locale.clear}</button>
      </div>
    );
  }
}
// 经过统一处理
export default ConfigProvider.config(Search);

ConfigProvider.config(Component)的语言包文案分发处理逻辑简化如下:

// ConfigProvider.jsx
function config(Component) {
   class ConfigedComponent extends React.Component {
        static propTypes = {
            ...(Component.propTypes || {}),
            locale: PropTypes.object,
        };
        static contextTypes = {
            ...(Component.contextTypes || {}),
            nextLocale: PropTypes.object,
        };
        render() {
            // 组件props上直接设置
            const { locale } = this.props;
            // ConfigProvider"遥控"设置
            const { nextLocale = {} } = this.context;
            // 组件上直接设置语言包,优先级高于在父组件ConfigProvider上设置。
            const newLocale = Object.assign({},
                nextLocale[Component.displayName],
                locale
            );
            
            return (
                <Component locale={newLocale}/>
            );
        }
    }
	return ConfigedComponent;
}

这样就基本完成了组件库的多语言能力建设,这也是Fusion Next组件库的多语言支持的思路。

除此之外,ConfigProvider还有内置了其他通用能力,例如组件的镜像反转RTL,pure render开关、修改样式的默认前缀等,更多可以查看 ConfigProvider源代码使用文档 了解。

相关链接