25

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

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUyMzg4ODk2NQ%3D%3D&%3Bmid=2247485805&%3Bidx=1&%3Bsn=60c0676f5749340212a84db984050783
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

总篇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 格式,并根据不同的语言标识进行命名,如图所示:

e2QVV3n.png!mobile

定义一个 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 为当前的语言环境,默认设置为  zhmessages 为当前语言环境下的文案配置列表,也就是在第一步中定义的语言配置文件,我们在代码中将其进行了引入。

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

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

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

1. ID 命名问题,首先要保证唯一,其次也不能太长,当我们有成百上千个文案要去配置,这显然会成为一个棘手的问题。下一章节将进行详细探讨。 2. 需要根据当前用户所处地域、设备的语言设置或者 URL 判断使用哪一种语言,可自行实现,不做重点讨论。

命名规范

下图是一段 TikTok [6] 网站的多语言配置文件

ne2I3ar.png!mobile

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

按组件定义

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

zumYJzr.png!mobile

首先将 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

QnyQNr.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK