國際化通常可分爲如下幾個挑戰:javascript
一、檢測用戶的語言環境;前端
二、翻譯UI元素、標題和提示;java
三、提供特定於地區的內容,如日期、貨幣和數字。node
在本文中,我將只關注前端部分。咱們將開發一個簡單的通用React應用程序: react-i18n ,在此基礎上提供全面的國際化支持。react
react-i18n 技術架構:webpack
一、用 Express
做爲web服務器nginx
二、webpack
用於構建客戶端 JavaScript
git
三、使用 Babel
將 ES6
翻譯爲 ES5
github
四、 React
實現UI。web
使用 better-npm-run
編寫跨平臺的腳本,使用 nodemon
啓動web服務器,用 webpackage-dev-server
做爲靜態服務器。
服務器應用程序的入口點是 server.js
。在這裏,咱們加載 Babel
和 Babel-polyfill
, ES6
編寫其他的服務器代碼。服務器端業務邏輯在 src/server.jsx
中實現。在這裏,咱們正在設置一個 Express web
服務器, 監聽端口 3001
。 components/App.jsx
做爲程序的入口。
有兩種可能的解決方案:
一、出於某種緣由,包括 Skype
和 NBA
在內的大多數流行網站都使用IP地理定位來查找用戶的位置,並據此猜想用戶的語言。這種方法不只在實現方面代價高昂,並且並不十分準確。
例如,用戶每每常常出去旅行,這意味着一個位置並不必定表明用戶想要的語言環境。
二、咱們將使用第二個解決方案並在服務器端處理HTTP頭接受語言,並根據用戶的系統語言設置提取用戶的 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
響應頭中去,用於全部後續請求。
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));
複製代碼
此處作了如下幾件事情: 一、應用在啓動時將有所的 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>
複製代碼
根據語言環境,您的數據將以不一樣的方式表示。例如,俄語將顯示 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>
複製代碼
做爲前端開發人員,咱們必須考慮到瀏覽器和平臺的多樣性。 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。
本文爲您提供了構建國際化的 React
應用所須要的全部知識:包括如何檢測用戶的語言環境,將其保存到 cookie
中; 提供用戶切換語言環境的選項,並能正確的顯示貨幣、日期時間和數字。
這裏是完整的代碼:my repository