隨著國際化發展,多語言的需求越來越常見,單一的語言已經遠不能滿足需求了。作為一個組件庫,支持多語言也是基本能力。

多語言功能的本質其實是文本的替換,一個辭彙「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/l

由於語言包結構的調整,需要同時修改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參數,在語言切換時改變值的內容。

並且語言的設置大都是以項目(或者頁面)為單位的,有沒有更聰明、對開發者更友好的使用方式呢?

一鍵設置語言

如果你使用過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開關、修改樣式的默認前綴等,更多可以查看代碼 和 文檔 了解。

相關鏈接

  • Fusion 多語言切換demo: codepen.io/aboutblank/p
  • Fusion ConfigProvider: fusion.design/component
  • github: github.com/alibaba-fusi

推薦閱讀:

相关文章