国际化一般可分为以下几个挑战:
-
1、检测用户的语言环境;
-
2、翻译UI元素、标题和提示;
-
3、提供特定于地区的内容,如日期、货币和数字。
在本文中,我将只关注前端部分。我们将开发一个简单的通用React应用程序: react-i18n ,在此基础上提供全面的国际化支持。
react-i18n 技术架构:
1、用 Express
作为web服务器
2、webpack
用于构建客户端 JavaScript
3、使用 Babel
将 ES6
翻译为 ES5
4、 React
实现UI。
使用 better-npm-run
编写跨平台的脚本,使用 nodemon
启动web服务器,用 webpackage-dev-server
作为静态服务器。
服务器应用程序的入口点是 server.js
。在这里,我们加载 Babel
和 Babel-polyfill
, ES6
编写其余的服务器代码。服务器端业务逻辑在 src/server.jsx
中实现。在这里,我们正在设置一个 Express web
服务器, 监听端口 3001
。 components/App.jsx
作为程序的入口。
1、检测用户的语言环境
有两种可能的解决方案:
1、出于某种原因,包括 Skype
和 NBA
在内的大多数流行网站都使用IP地理定位来查找用户的位置,并据此猜测用户的语言。这种方法不仅在实现方面代价高昂,而且并不十分准确。
例如,用户往往经常出去旅行,这意味着一个位置并不一定代表用户想要的语言环境。
2、我们将使用第二个解决方案并在服务器端处理HTTP头接受语言,并根据用户的系统语言设置提取用户的 Accept-Language
,它是由每个现代浏览器在一个页面请求中发送的出去的。
Accept-Language请求头
Accept-Language
请求头允许客户端声明它可以理解的自然语言,以及优先选择的区域方言。
Accept-Language
请求头提供一组自然语言,首选它们作为对请求的响应。每个语言范围可以被赋予一个关联的“quality”值,它表示用户对该范围指定的语言的偏好的估计。该值默认为 q=1
。例如,accept -language: da, en-gb;q=0.8, en;q=0.7
表示“我更喜欢丹麦语,但会接受英式英语和其他类型的英语”。“一个语言范围匹配一个语言标记,如果它恰好等于标记,或者它恰好等于标记的前缀,那么前缀后面的第一个标记字符就是-。
值得一提的是,这种方法还不完善。例如,用户可能通过网吧或公共计算机访问您的网站。要解决这个问题,只需要在页面增加能够快速切换语言的按钮,让用户手动去选择所期待的。
如何检测用户的语言环境
这是一个基于 Node.js
的web服务器。我们使用的是 accept-language
包,它从HTTP头中提取位置,并在您的网站支持的位置中找到最相关的。如果没有找到,那么您将回到网站的默认语言环境。对于返回的用户,我们将检查cookie的值。
让我们完成以下依赖包的安装:
npm install --save accept-language npm install --save cookie-parser js-cookie
复制代码
src/server.jsx
中这样写:
import cookieParser from 'cookie-parser';
import acceptLanguage from 'accept-language';
acceptLanguage.languages(['en', 'ru']);
const app = express();
app.use(cookieParser());
function detectLocale(req) {
const cookieLocale = req.cookies.locale;
return acceptLanguage.get(cookieLocale || req.headers['accept-language']) || 'en';
}
…
app.use((req, res) => {
const locale = detectLocale(req);
const componentHTML = ReactDom.renderToString(<App />);
res.cookie('locale', locale, { maxAge: (new Date() * 0.001) + (365 * 24 * 3600) });
return res.end(renderHTML(componentHTML));
});
复制代码
我们通过引入 accept-language
来设置应用支持的语种: English
和 Russian
。同时实现一个 detectLocale
的函数,用来 cookie
中读取 locale
, 如果未读取到,就读取 Accept-Language
请求头, 如果最后都失败了,就使用默认的 en
。
在处理请求之后,我们将检测到的语言环境以 cookie
的形式添加到 HTTP
响应头中去,用于所有后续请求。
2、翻译页面元素
React Intl 是一个比较流行和成熟的 React
应用国际化实现方案。它所有的库都使用相同的方法:提供 higher-order components
(高阶组件来自于在React中广泛使用的函数编程设计模式),它注入国际化函数,通过React的上下文特性来处理消息、日期、数字和货币。
首先,需要提供国际化依赖的 Provider
,需要我们稍微修改一下 src/server.jsx
和 src/client.jsx
这两个文件:
npm install --save react-intl
复制代码
src/server.jsx
如下:
import { IntlProvider } from 'react-intl';
…
const componentHTML = ReactDom.renderToString(
<IntlProvider locale={locale}>
<App />
</IntlProvider>
);
…
复制代码
src/client.jsx
如下:
import { IntlProvider } from 'react-intl';
import Cookie from 'js-cookie';
const locale = Cookie.get('locale') || 'en';
…
ReactDOM.render(
<IntlProvider locale={locale}>
<App />
</IntlProvider>,
document.getElementById('react-view')
);
复制代码
至此,所有 IntlProvider
子组件都能访问到提供的国际化函数了。让我们添加一些翻译文本,并新增一个按钮来更改语言环境。这时我们有 FormattedMessage
和 formatMessage
函数可以使用,二者的不同之处在于 FormattedMessage
会将渲染的内容包裹在一个 span
元素中。通常这种情况只适合于文本,而不适合 HTML
属性值:alt
和 title
。
src/components/App.jsx文件:
import { FormattedMessage } from 'react-intl';
…
<h1><FormattedMessage id="app.hello_world" defaultMessage="Hello World!" description="Hello world header greeting" /></h1>
复制代码
id
在整个应用内必须保证是唯一的,因此规定一些命名规则是非常有用的。我通常喜欢这样的格式: componentName.someUniqueIdWithInComponent
。 defaultMessage
用于应用程序的默认语言环境,description
为转换器提供一些上下文。
重启 nodeman
并刷新页面,页面会出现“Hello World”。在开发者工具查看页面元素时,发现文本包裹在一个 span
标签中。在这种情况下,这不是一个问题,但有时我们更倾向于只获取文本,而不需要任何附加标记。为此,我们需要直接访问 React Intl
提供的国际化对象。
返回 src/components/App.jsx
文件:
import { FormattedMessage, intlShape, injectIntl, defineMessages } from 'react-intl';
const propTypes = {
intl: intlShape.isRequired,
};
const messages = defineMessages({
helloWorld2: {
id: 'app.hello_world2',
defaultMessage: 'Hello World 2!',
},
});
export default class extends Component {
class App extends Component {
render() {
return (
<div className="App">
<h1>
<FormattedMessage
id="app.hello_world"
defaultMessage="Hello World!"
description="Hello world header greeting"
/>
</h1>
<h1>{this.props.intl.formatMessage(messages.helloWorld2)}</h1>
</div>
);
}
}
App.propTypes = propTypes;
export default injectIntl(App);
复制代码
首先,我们必须使用 injectIntl
,它包装我们的app组件并注入 intl
对象。为了获得翻译后的消息,我们必须调用 formatMessage
方法,并将消息对象作为参数传递。此消息对象必须具有惟一的 id
和 defaultValue
属性。
React
最棒的地方是它的生态系统。让我们向我们的项目添加 babel-plugin-reactor-intl
,它将从组件中提取格式消息并构建翻译字典。我们将把这本字典转交给译者,他们不需要任何编程技能来完成他们的工作。
npm install --save-dev babel-plugin-react-intl
复制代码
.babelrc
:
{
"presets": [
"es2015",
"react",
"stage-0"
],
"env": {
"development": {
"plugins":[
["react-intl", {
"messagesDir": "./build/messages/"
}]
]
}
}
}
复制代码
重新启动nodemon,您将看到在项目的根目录中已经创建了一个 build/messages
文件夹。我们需要将所有这些文件合并成一个JSON。可以参考我的代码。将其保存为 script/translate.js
。
向 package.json
新增一个 script
命令:
"scripts": {
…
"build:langs": "babel scripts/translate.js | node",
…
}
复制代码
运行这个脚本:
npm run build:langs
复制代码
你应该看到 build/lang
文件夹中生成了一个 en.json
,包含以下内容:
{
"app.hello_world": "Hello World!",
"app.hello_world2": "Hello World 2!"
}
复制代码
现在有趣的部分出现了。
在服务器端,我们可以将所有翻译加载到内存中,并为每个请求提供相应地服务。但是,对于客户端,这种方法不适用。我们将发送一次带翻译的JSON文件,客户端将自动为所有组件应用提供的文本,因此客户端只获得它需要的内容。
让我们将输出内容复制到 public/assets
文件夹。
ln -s ../../build/lang/en.json public/assets/en.json
复制代码
注意:如果是window环境,就不能这么使用了,需要手动复制
cp ../../build/lang/en.json public/assets/en.json
复制代码
接下来我们需要调整服务器和客户端代码。
首先是 src/server.jsx
import { addLocaleData, IntlProvider } from 'react-intl';
import fs from 'fs';
import path from 'path';
import en from 'react-intl/locale-data/en';
import ru from 'react-intl/locale-data/ru';
addLocaleData([…ru, …en]);
const messages = {};
const localeData = {};
['en', 'ru'].forEach((locale) => {
localeData[locale] = fs.readFileSync(path.join(__dirname, `../node_modules/react-intl/locale-data/${locale}.js`)).toString();
messages[locale] = require(`../public/assets/${locale}.json`);
});
--- function renderHTML(componentHTML) {
function renderHTML(componentHTML, locale) {
…
<script type="application/javascript" src="${assetUrl}/public/assets/bundle.js"></script>
<script type="application/javascript">${localeData[locale]}</script>
…
--- <IntlProvider locale={locale}>
<IntlProvider locale={locale} messages={messages[locale]}>
…
--- return res.end(renderHTML(componentHTML));
return res.end(renderHTML(componentHTML, locale));
复制代码
此处做了以下几件事情: 1、应用在启动时将有所的 locale
配置信息加载到内存中,用于货币、日期和数字格式的显示;
2、扩展 renderHTML
方法,将特定于语言环境的 JavaScript
插入到生成的 HTML
标记中;
3、向 IntlProvider
提供翻译后的消息;
对于客户端,首先需要安装一个库来执行AJAX请求。我更喜欢使用 isomorphic-fetch
,因为我们很可能还需要从第三方api请求数据,isomorphic-fetch
在客户端和服务器环境中都可以很好地做到这一点。
npm install --save isomorphic-fetch
复制代码
src/client.jsx
修改如下:
import { addLocaleData, IntlProvider } from 'react-intl';
import fetch from 'isomorphic-fetch';
const locale = Cookie.get('locale') || 'en';
fetch(`/public/assets/${locale}.json`)
.then((res) => {
if (res.status >= 400) {
throw new Error('Bad response from server');
}
return res.json();
})
.then((localeData) => {
addLocaleData(window.ReactIntlLocaleData[locale]);
ReactDOM.render(
<IntlProvider locale={locale} messages={localeData}>
…
);
}).catch((error) => {
console.error(error);
});
复制代码
为了客户端能正确的加载 locale
文件,还需调整 src/server.jsx
:
app.use(cookieParser());
app.use('/public/assets', express.static('public/assets'));
复制代码
在生产环境中,通常使用
nginx
提供对静态资源的访问。
客户端在初始化JavaScript之后, client.jsx
将从 cookie
中获取语言环境,并请求相应的 JSON
翻译文件。
打开开发人员工具中的“Network”选项卡,检查我们的客户端是否已成功获取JSON。
为了便于测试,增加一个切换语种的组件 src/components/LocaleButton.jsx
:
import React, { Component, PropTypes } from 'react';
import Cookie from 'js-cookie';
const propTypes = {
locale: PropTypes.string.isRequired,
};
class LocaleButton extends Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
Cookie.set('locale', this.props.locale === 'en' ? 'ru' : 'en');
window.location.reload();
}
render() {
return <button onClick={this.handleClick}>{this.props.locale === 'en' ? 'Russian' : 'English'};
}
}
LocaleButton.propTypes = propTypes;
export default LocaleButton;
复制代码
src/components/App.jsx
增加 LocaleButton
的引用:
import LocaleButton from './LocaleButton';
...
<h1>{this.props.intl.formatMessage(messages.helloWorld2)}</h1>
<LocaleButton locale={this.props.intl.locale} />
...
复制代码
一旦用户更改了他们的语言环境,我们将重新加载页面,同时重新加载新的
locale
文件。
目前为止我们学习了如何检测用户的语言环境以及如何显示翻译的消息。在进入最后一部分之前,让我们讨论另外两个重要的主题。
多元化和模板
在英语中,大多数单词可能有两种形式: “one apple,”、“many apples”。在其他语言中,事情要复杂得多。例如俄语中,就有四种不同的表示形式。 React Intl
能够帮助我们相应地处理多元化问题。它还支持模板,因此您可以提供在渲染过程中插入模板的变量。
src/components/App.jsx
中:
const messages = defineMessages({
counting: {
id: 'app.counting',
defaultMessage: 'I need to buy {count, number} {count, plural, one {apple} other {apples}}'
},
…
<LocaleButton locale={this.props.intl.locale} />
<div>{this.props.intl.formatMessage(messages.counting, { count: 1 })}</div>
<div>{this.props.intl.formatMessage(messages.counting, { count: 2 })}</div>
<div>{this.props.intl.formatMessage(messages.counting, { count: 5 })}</div>
复制代码
3、Date and times formatting
根据语言环境,您的数据将以不同的方式表示。例如,俄语将显示 500,00 $
和 10.12.2016
,而美式英语将显示 $500.00
和 12/10/2016
。
React Intl
为这样的数据展示提供了相应的组件:
import {
FormattedDate,
FormattedRelative,
FormattedNumber,
FormattedMessage,
intlShape,
injectIntl,
defineMessages,
} from 'react-intl';
<div>{this.props.intl.formatMessage(messages.counting, { count: 5 })}</div>
<div><FormattedDate value={Date.now()} /></div>
<div><FormattedNumber value="1000" currency="USD" currencyDisplay="symbol" style="currency" /></div>
<div><FormattedRelative value={Date.now()} /></div>
复制代码
4、问题
作为前端开发人员,我们必须考虑到浏览器和平台的多样性。 React Intl
使用浏览器 Intl API
来处理DateTime
和 Number
格式。尽管这些 Intl API
早在2012年就提出了,但并不是所有现代浏览器都支持它, 甚至Safari也只有在iOS 10之后才部分支持它。
下面是主流浏览器支持情况:
如果你想覆盖那些不支持 Intl API
的浏览器,你需要一个 ployfill
: Intl.js
这并不是一个完美的解决方案:
首先,Intl.js本身体积很大,需要考虑将它只提供给不支持 Intl API
的浏览器,以减小整体js包的大小。
第二个问题 Intl.js 并不是完全正确的,这意味着服务器和客户端之间的数据和数字表示可能不同,这将再次破坏服务器端渲染。请参阅relevant GitHub issue。
5、结论
本文为您提供了构建国际化的 React
应用所需要的所有知识:包括如何检测用户的语言环境,将其保存到 cookie
中; 提供用户切换语言环境的选项,并能正确的显示货币、日期时间和数字。
这里是完整的代码:my repository