为React应用添加国际化支持

国际化一般可分为以下几个挑战:

  • 1、检测用户的语言环境;

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

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

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

react-i18n 技术架构:

1、用 Express 作为web服务器

2、webpack 用于构建客户端 JavaScript

3、使用 BabelES6 翻译为 ES5

4、 React 实现UI。

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

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

1、检测用户的语言环境

有两种可能的解决方案:

1、出于某种原因,包括 SkypeNBA 在内的大多数流行网站都使用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 来设置应用支持的语种: EnglishRussian 。同时实现一个 detectLocale 的函数,用来 cookie 中读取 locale , 如果未读取到,就读取 Accept-Language 请求头, 如果最后都失败了,就使用默认的 en

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

2、翻译页面元素

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

此处做了以下几件事情: 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.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>
复制代码

4、问题

作为前端开发人员,我们必须考虑到浏览器和平台的多样性。 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

5、结论

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

这里是完整的代码:my repository

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值