海外项目的 React 国际化开发实践

微信扫一扫,分享到朋友圈

海外项目的 React 国际化开发实践

总篇106篇 2020年第30篇

前言

互联网产品进军海外市场,必然要进行国际化及本地化,使产品能够支持多语言访问并适应本地的用户习惯。

对于前端开发人员,可以在开源社区找到许多成熟的插件来满足国际化需求。但多数插件仅仅提供文本翻译、日期或货币格式化的能力,我们面临的更多挑战是如何合理的规划多语言定义的配置文件、命名规范等,进而提升开发效率及页面性能。

近半年时间,有幸参与到公司海外项目的前端研发工作,并负责国际化的调研、集成与优化,在不断的摸索过程中,积累了一些实践经验。

国际化与本地化

关于 国际化
(i18n)与  本地化
(l10n),可能很多人会混淆;虽然两者的区别十分微妙,但却很重要。

我们来看下
维基百科 [1]

的定义:

国际化是指在设计软件,将软件与特定语言及地区脱钩的过程;当软件被移植到不同的语言及地区时,软件本身不用做内部工程上的改变或修正。

本地化是指当移植软件时,加上与特定区域设置有关的信息和翻译文件的过程。

如果某一产品将推广到不同的国家市场进行运行,首先要让其适应不同的文化并被不同的受众接受,同时技术层面上保证同一套系统能够适应不同国家的产品需求,系统本身与特定地区解耦,此过程即为 国际化

由于不同地区文化的差异,我们需要在阅读习惯、操作体验上进行不同的呈现,包括日期、货币等展示格式的不同,对这些差异所做的工作即为 本地化

由此可见, 国际化
先于  本地化
执行,本地化过程实际上是在完成国际化之后,使该产品适应特定的目标市场。

对前端技术而言,只能解决国际化及本地化中的部分问题,即:


1.
多语言支持(国际化)


2.
数字、日期、货币等根据当地习惯展示(本地化)

技术选型

目前市面上有一些流行的国际化解决方案,其中包括:



i18next



vue-i18n



react-intl



angular-translate



i18n-js

由于项目使用 React 框架开发,因此选择了
react-intl [2]

作为国际化方案,主要基于以下几点考量:



快速集成,支持 Hooks 语法。



基于原生 
Intl [3]

 API,支持 IE11(时间、货币、数字)。



支持 NodeJS(服务端渲染)。



具有强大的社区支持。

基本使用

方便起见,我们采用
create-react-app [4]

脚手架进行 Demo 项目的创建,命令如下:

npx create-react-app i18n-demo

完成项目初始化后,进入 i18n-demo 项目目录,安装 react-intl:

yarn add react-intl

src
中创建一个名为  i18n
的文件夹,用于存放语言配置文件,采用 JSON 格式,并根据不同的语言标识进行命名,如图所示:

定义一个 ID
为 hello 的配置,并为中文和英文赋予不同的文案内容以适应不同地区的受众。

下面我们对 App.js
中代码进行改造,实现基本的多语言展示与切换功能,代码如下:

import React, { useState } from 'react';
import { IntlProvider, useIntl } from 'react-intl';
import './App.css';



// 引入语言配置文件
const zhJson = require('./i18n/zh.json');
const enJson = require('./i18n/en.json');



// Hi组件
const Hi = () => {
const { formatMessage: t } = useIntl();
return <>
{t({ id: 'hello' })} React
</>
}



// 项目根组件
function App() {
const [locale, setLocale] = useState('zh');
const messages = locale === 'zh' ? zhJson : enJson;



// 切换当前语言
const onSwitch = (lang) => {
return () => {
setLocale(lang);
}
}



return (
<IntlProvider locale={locale} messages={messages}>
<div className="App">
<header className="App-header">
<Hi />
<div>
<button onClick={onSwitch('zh')}>简体中文</button>
<br />
<button onClick={onSwitch('en')}>English</button>
</div>
</header>
</div>
</IntlProvider>
);
}



export default App;



在根组件 App
中使用 IntlProvider 
上下文 [5]

容器组件来包裹其他子组件,并且对  locale
和  messages
两个属性进行了赋值, locale
为当前的语言环境,默认设置为  zh
messages
为当前语言环境下的文案配置列表,也就是在第一步中定义的语言配置文件,我们在代码中将其进行了引入。

自定义 Hi
组件,使用  formatMessage
API 来翻译文案,通过传入参数  id
来展示对应配置文件中的内容。

App
中实现切换功能,当我们点击 简体中文 或 English 按钮时, Hi
组件中文案将切换为  你好 React
和  Hello React

以上是一个简化版的实现,功能上虽然能够实现语言切换,同时也暴露出一些问题:


1.
ID
命名问题,首先要保证唯一,其次也不能太长,当我们有成百上千个文案要去配置,这显然会成为一个棘手的问题。下一章节将进行详细探讨。


2.
需要根据当前用户所处地域、设备的语言设置或者 URL 判断使用哪一种语言,可自行实现,不做重点讨论。

命名规范

下图是一段
TikTok [6]

网站的多语言配置文件

可以看到,ID 过长并且带有一些特殊字符(空格、{}等),一方面要写很长的 ID 去保证唯一性,开发效率不高,另一方面也会增加代码体积,这显然不是我们想要的效果。

按组件定义

对于 React 项目,组件化开发方式,如果将多语言配置在组件级别,便能够快速查找和定义,如下图的目录结构:

首先将 App.js 中的 Hi
组件独立出来,在其内部定义使用到的多语言。

上文中提到赋值 messages
属性,该属性需要传入当前环境的全部配置。那么问题来了,我们如何将各个组件的配置文件进行合并呢?

我们采用一个简单的方式,通过 webpack 提供的 require.context
进行处理,将  App.js
改造如下(部分代码省略):

import Hi from './comps/Hi';



const getMessages = (locale) => {
const msgJson = {};
const ctx = require.context('./', true, /i18n\/.*\.json$/);
ctx.keys().forEach((file) => {
// 文件名(区域标识)
const langKey = file.replace(/.*\/i18n\/(.*).json$/, '$1');



// JSON文件内容
const langValue = ctx(file);



// 合并
msgJson[langKey] = msgJson[langKey] || {};
msgJson[langKey] = { ...msgJson[langKey], ...langValue };
});
return msgJson[locale];
};



function App() {
const messages = getMessages(locale);
}

遍历当前文件所属目录(即 src),查找所有 i18n 文件夹下定义的 JSON 文件,通过正则匹配语言环境并进行合并,将合并后的内容赋值到 msgJson
变量,最后得到的内容格式如下:

{

zh: {

"hello": "你好"

},

en: {

"hello": "Hello"

}

}

目前,只有 Hi
组件中定义了  ID
为  hello
的配置,由于  ID
是唯一的,因此其他组件中必须保证不使用  hello
进行定义,不然在合并时后者会被覆盖掉,这就涉及到对命名空间的有效处理。或许可以像 TikTok 那样,把  ID
定义为不易产生冲突的形式,但如此处理,一方面会造成严重的代码冗余,另一方面其他开发人员并不清楚你的  ID
定义,只有运行时出现文案翻译错误后才会发现,降低了开发效率。

目录层级命名

既然配置文件已经定义在组件内部,如果在合并时为现有 ID
增加目录层级前缀,例如  hello
转换为  comps.hi.hello
(文件目录:comps/Hi),形成一个目录级别的命名空间,便不会出现  ID
冲突的问题了,岂不快哉。

心动不如行动!我们将 getMessages
方法进行改造,使其能够自动为  ID
添加目录前缀:

const getMessages = (locale) => {
const msgJson = {};
const ctx = require.context('./', true, /i18n\/.*\.json$/);
ctx.keys().forEach((file) => {



// 生成命名空间
const namespace = file.replace(/^\.\/(.*)\/i18n\/.*$/, '$1')
.split('/')
.map(str => str.toLowerCase())
.join('.');



// 文件名(区域标识)
const langKey = file.replace(/.*\/i18n\/(.*).json$/, '$1');



// JSON文件内容
const langValue = ctx(file);



// 使用带有命名空间的ID,组合为新对象
const newValue = {};
for (const key in langValue) {
newValue[`${namespace}.${key}`] = langValue[key];
}



// 合并
msgJson[langKey] = msgJson[langKey] || {};
msgJson[langKey] = { ...msgJson[langKey], ...newValue }; // 使用newValue
});
return msgJson[locale];
};

现在 hello
转换为了  comps.hi.hello
,那么在使用  formatMessage
翻译时,需要将  ID
参数做相应调整:

const Hi = () => {
const { formatMessage: t } = useIntl();
return (<>
{t({ id: 'comps.hi.hello' })} React
</>)
}

加上了命名空间,无论内部定义怎样的 ID
,开发者也无需担心命名冲突的问题了。

货币

在不同的区域需要展示不同的货币符号,例如 1000 元人民币展示为 ¥1,000
,1000 英镑为  £1,000

react-intl 提供了 formatNumber
API进行货币的格式化,使用方式如下:

formatNumber(1000, {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 0,
useGrouping: true
})


style
:指定数字的格式样式。


decimal


数字

currenc
y

货币
percent 百分比




currency

ISO的货币代码 [7]

,没有默认值;如果 style 为 “currency”,则必须提供货币代码。

CNY 人民
EUR 欧元
GBP 英镑


useGrouping
:是否使用分组分隔符,如千位分隔符或千/万/亿分隔符。默认为 true。



    •
minimumFractionDigits
:保留的小数点位数。默认为 2。

在实际的开发中,我们需要进行全局的格式化配置,可以通过配置 IntlProvider
上的  formats
属性进行实现。

改造 App.js ,创建当前语言环境的 formats
并进行配置:

function App() {
const [locale, setLocale] = useState('zh');
const [messages, setMessages] = useState({});
/* 代码省略... */



// 货币格式化
const formats = {
number: {
currency: {
style: 'currency',
currency: {
zh: 'CNY',
en: 'GBP',
}[locale],
minimumFractionDigits: 0,
},
},
}



/* 代码省略... */



return (
<IntlProvider locale={locale} messages={messages} formats={formats}>
{/* 代码省略... */}
</IntlProvider>
);
}

自定义 formats
后,可以更加方便的使用货币格式化,代码如下:

formatNumber(1000, {
format: 'currency'
})

总结

进行国际化,重要的是要提供一个全面且面向未来的解决方案,即要选择合适并且具有扩展性的第三方库,同时也要提前制定命名和使用规范,当我们的项目逐渐变得庞大,修缮工作便不那么容易进行,未雨绸缪是良好的开发习惯。

希望该文章对大家的国际化开发之路有所帮助,如有错误之处,敬请指正。

References

[1]
维基百科:  https://zh.wikipedia.org/wiki/%E5%9B%BD%E9%99%85%E5%8C%96%E4%B8%8E%E6%9C%AC%E5%9C%B0%E5%8C%96

[2]
react-intl:  https://formatjs.io/docs/react-intl/

[3]
Intl:  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl

[4]
create-react-app:  https://github.com/facebook/create-react-app

[5]
上下文:  https://reactjs.org/docs/context.html

[6]
TikTok:  https://www.tiktok.com/foryou/?lang=en

[7]
ISO的货币代码:  https://baike.baidu.com/item/ISO%E8%B4%A7%E5%B8%81%E4%BB%A3%E7%A0%81/12678908?fr=aladdin

微信扫一扫,分享到朋友圈

海外项目的 React 国际化开发实践

快手寻求香港IPO:抖音最大竞争对手寻求500亿美元的估值

上一篇

猴子偷走iPhone疯狂自拍 失主找回手机后发现海量照片

下一篇

你也可能喜欢

海外项目的 React 国际化开发实践

长按储存图像,分享给朋友