为React应用添加国际化支持

国际化通常可分为如下几个挑战:javascript

  • 一、检测用户的语言环境;前端

  • 二、翻译UI元素、标题和提示;java

  • 三、提供特定于地区的内容,如日期、货币和数字。node

在本文中,我将只关注前端部分。咱们将开发一个简单的通用React应用程序: react-i18n ,在此基础上提供全面的国际化支持。react

react-i18n 技术架构:webpack

一、用 Express 做为web服务器nginx

二、webpack 用于构建客户端 JavaScriptgit

三、使用 BabelES6 翻译为 ES5github

四、 React 实现UI。web

使用 better-npm-run 编写跨平台的脚本,使用 nodemon 启动web服务器,用 webpackage-dev-server 做为静态服务器。

服务器应用程序的入口点是 server.js 。在这里,咱们加载 BabelBabel-polyfillES6 编写其他的服务器代码。服务器端业务逻辑在 src/server.jsx 中实现。在这里,咱们正在设置一个 Express web 服务器, 监听端口 3001components/App.jsx做为程序的入口。

一、检测用户的语言环境

有两种可能的解决方案:

一、出于某种缘由,包括 SkypeNBA 在内的大多数流行网站都使用IP地理定位来查找用户的位置,并据此猜想用户的语言。这种方法不只在实现方面代价高昂,并且并不十分准确。

例如,用户每每常常出去旅行,这意味着一个位置并不必定表明用户想要的语言环境。

二、咱们将使用第二个解决方案并在服务器端处理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 来设置应用支持的语种: EnglishRussian 。同时实现一个 detectLocale 的函数,用来 cookie 中读取 locale , 若是未读取到,就读取 Accept-Language 请求头, 若是最后都失败了,就使用默认的 en

在处理请求以后,咱们将检测到的语言环境以 cookie 的形式添加到 HTTP 响应头中去,用于全部后续请求。

二、翻译页面元素

React Intl 是一个比较流行和成熟的 React 应用国际化实现方案。它全部的库都使用相同的方法:提供 higher-order components (高阶组件来自于在React中普遍使用的函数编程设计模式),它注入国际化函数,经过React的上下文特性来处理消息、日期、数字和货币。

首先,须要提供国际化依赖的 Provider,须要咱们稍微修改一下 src/server.jsxsrc/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 子组件都能访问到提供的国际化函数了。让咱们添加一些翻译文本,并新增一个按钮来更改语言环境。这时咱们有 FormattedMessageformatMessage 函数可使用,两者的不一样之处在于 FormattedMessage 会将渲染的内容包裹在一个 span 元素中。一般这种状况只适合于文本,而不适合 HTML 属性值:alttitle

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.someUniqueIdWithInComponentdefaultMessage 用于应用程序的默认语言环境,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 方法,并将消息对象做为参数传递。此消息对象必须具备唯一的 iddefaultValue 属性。

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));
复制代码

此处作了如下几件事情: 一、应用在启动时将有所的 locale 配置信息加载到内存中,用于货币、日期和数字格式的显示;

二、扩展 renderHTML 方法,将特定于语言环境的 JavaScript 插入到生成的 HTML 标记中;

三、向 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>
复制代码

三、Date and times formatting

根据语言环境,您的数据将以不一样的方式表示。例如,俄语将显示 500,00 $10.12.2016 ,而美式英语将显示 $500.0012/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>
复制代码

四、问题

做为前端开发人员,咱们必须考虑到浏览器和平台的多样性。 React Intl 使用浏览器 Intl API 来处理DateTimeNumber 格式。尽管这些 Intl API 早在2012年就提出了,但并非全部现代浏览器都支持它, 甚至Safari也只有在iOS 10以后才部分支持它。

下面是主流浏览器支持状况:

若是你想覆盖那些不支持 Intl API 的浏览器,你须要一个 ployfillIntl.js

这并非一个完美的解决方案:

首先,Intl.js自己体积很大,须要考虑将它只提供给不支持 Intl API 的浏览器,以减少总体js包的大小。

第二个问题 Intl.js 并非彻底正确的,这意味着服务器和客户端之间的数据和数字表示可能不一样,这将再次破坏服务器端渲染。请参阅relevant GitHub issue

五、结论

本文为您提供了构建国际化的 React 应用所须要的全部知识:包括如何检测用户的语言环境,将其保存到 cookie 中; 提供用户切换语言环境的选项,并能正确的显示货币、日期时间和数字。

这里是完整的代码:my repository

相关文章
相关标签/搜索