如果为React应用添加国际化

2018-07-12 admin

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

  • 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 <div itemprop="articleBody" class="entry-content article-content" data-v-41d33d72="" 和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之后才部分支持它。

下面是主流浏览器支持情况: QQ--20180710153948

如果你想覆盖那些不支持 Intl API 的浏览器,你需要一个 ployfillIntl.js

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

首先,Intl.js本身体积很大,需要考虑将它只提供给不支持 Intl API 的浏览器,以减小整体js包的大小。

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

5、结论

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

这里是完整的代码:my repository

原文链接:https://www.ajiehome.com/2018/07/10/ru-he-shi-xian-reactguo-ji-hua/

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-35826.html

文章标题:如果为React应用添加国际化

相关文章
H5即将迎来黄金时代 轻应用再成行业焦点
鎽樿�侊細浠庣伀閫熻交搴旂敤鑾峰緱鎶曡祫绛変腑涓嶉毦鐪嬪嚭锛孒TML5鍗冲皢杩庢潵榛勯噾鏃朵唬銆傝秺鏉ヨ秺澶氱殑浼佷笟鎴栧垱涓氳€呭紑濮嬫秹瓒矵5锛岃�╄交搴旂敤鍐嶆�℃垚涓鸿�屼笟鐨勭劍鐐癸紝鎺ヤ笅鏉ュ皢鏈夋洿澶欻5寮曟搸浠ュ強鏇村�欻5...
2015-11-12
何为闭包?有关JS闭包的一些理解
简单一点的说:闭包就是能够读取其他函数内部变量的函数。那如何实现读取其它函数内部变量呢,大家都知道在JavaScript中内部函数可以访问其父函数中的变量,那如果将内部函数返回是不是代表能够通过它访问其父函数中的变量了呢,闭包的原理事实上就...
2015-11-11
关于Ajax应用开发需要注意的事项
接触Ajax,那时候的Ajax支持还不是很好,都要涉及底层,没有现成的框架给你调用。现在把常见的问题列举如下。 [b]1、编码问题[/b] 注意AJAX要取的文件是UTF-8编码的。GB2312编码传回BROWSE后中文会乱码。如果用VBS...
2015-11-11
2015年将会有大量基于HTML5和JS的WEB应用
随着HTML5的定稿,以及JS的迅速发展,我们有理由相信,在接下来的一年里,将会涌现出大量的WEB应用,网站的表现形式将不再仅仅局限于过去的形式,必将在2015年引来一次重大改革! ...
2015-11-12
WebSocket断开原因分析,再也不怕为什么又断开了
阅读原文:https://wdd.js.org/websocket-… 1. 把错误打印出来 WebSocket断开的原因有很多,最好在WebSocket断开时,将错误打印出来。 在线demo地址:https://wdd.js.org/we...
2018-04-25
HTML5移动应用开发的12大特性
1.离线缓存为HTML5开发移动应用提供了基础 HTML5 Web Storage API可以看做是加强版的cookie,不受数据大小限制,有更好的弹性以及架构,可以将数据写入到本机的ROM中,还可以在关闭浏览器后再次打开时恢复数据,以减少...
2015-11-11
freemarker判断对象是否为空的方法
FreeMarker与Web容器无关,即在Web运行时,它并不知道Servlet或HTTP。它不仅可以用作表现层的实现技术,而且还可以用于生成XML,JSP或Java 等。 freemarker中显示某对象使用${name}. 但如果nam...
2017-03-27
为什么AngularJS能够成功?
AngularJS 为什么成功了? 写在前面的话 继上一篇总结之后, 觉得必须补充一下 AngularJS 与 Ionic 的技术性话题 于是, 连夜写了这第一篇. 讲述了 AngularJS 与其他对手之间的优与缺. 我有任何理解错误, ...
2015-11-12
线程有什么用处? 为什么有些东西注定不会流行
多线程的领域也许只有一个: 图形学. 我们以一个游戏来说明 @ |___|___|___|___|___ @是一个玩家, 往前走, 每一个___是1米. 每当@走到1米的时候, 会绘制一个蘑菇*给玩家看. @|___*___|___|___...
2015-11-12
YouTube抛弃Flash,将HTML5视频设为默认
YouTube在今天宣布,它终于停止继续使用Adobe Flash作为默认设置了。YouTube网站现在将在Google Chrome、微软的IE 11、苹果的Safari 8浏览器,以及Mozilla的Firefox浏览器的测试版中使用H...
2015-11-12
回到顶部