PJCHENder 未整理筆記

[Note] react-intl 起步走(getting started)

2019-07-02

[Note] react-intl 起步走(getting started)

@(其他-程式力)[javascript, i18]

keywords: i18n, Internationalization, intl, format

這篇文章主要說明如何透過 React Intl 來實作網頁上的 i18n,先備知識必須要已經基本會使用 React。

⚠️ 文章使用的是 react-intl @ 2.9,部分功能在 3.0 後不再支援

開始使用 react-intl

Intl Provider Component

我們直接用 create-react-app 的頁面來做練習:

create-react-app

建立 React 專案

直接透過 create-react-app 建立 React 專案:

1
2
$ create-react-app react-intl-sandbox
$ cd react-intl-sandbox

安裝 react-intl

1
$ npm install react-intl

接著就可以透過 npm run start 啟動專案。

使用 IntlProvider

IntlProvider @ react-intl components

<IntlProvider> 包在最外層,讓所有內部 component 都可以存取到它提供的屬性與方法。這裡方便操作,把它多包成一個名為 Root 的 functional component:

imgur

取得使用者語系並帶入 IntlProvider

接著:

  • 透過 navigator.language 取得使用者在瀏覽器上所使用的語系,並且將它代入 IntlProvider 的 local 屬性中
  • 為了讓語系變動後可以重新轉譯 React 元件,因此也將取得的語系帶入 key 屬性中(參考:dynamic language selection
  • 若有需要可以定義 defaultLocale 屬性

react-intl

這時候打開瀏覽器會看到錯誤訊息:

react-intl

表示它載不到預設的地區資料。

載入需要的語系資料

Basic Internationalization Principles @ Format.js

在 react-intl 中已經定義好許多地區的語系資料,這些資料會定義某個語系是否有其父子語系的關係,例如 zh-TW 是在 zh-Hant 底下,而 zh-Hant 又在 zh 底下這種關係。除此之外,裡面還定義了關於「時間」、「複數型態」時要怎麼呈現的資訊。

關於各地區的語系,最標準的可以參考 BCP 47 所定義的 locale code 規格文件。

react-intl 所定義好的語系檔都在安裝好的套件內,路徑為 node_modules/react-intl/locale-data/,裡面列出了各語系的定義:

react-intl

例如說這裡我們可以載入英文、中文和日文的語系資料,並且透過 addLocaleData 將語系檔載入:

react-intl

⚠️ 在 react-intl 3 之後不在支援 addLocaleData 的方法,而是直接使用原生的 Intl API,關於使用方式可以參考 Migrate to using native Intl APIs

定義多語系字典檔

做好上述設定後,就可以來定義多語系用的字典檔。假設我們要做 i18n 的內容是範例頁面中的「Learn React」這個部分:

create-react-app

這時候就可以先定義這個字典檔。首先在 ./src 資料夾內再新增一個 i18n 的資料夾,裡面分別放入 en.js, zh.js, ja.js

react-intl

接著分別定義每支字典檔的內容,這裡的 app.learn 可以視為 id,之後的使用 react-intl 提供的方法或元件時,他會根據你提供的這個 id 來帶入不同的內容,而 { } 刮起來的 name 表示它是動態的變數:

1
2
3
// ./src/i18n/en.js
const en = { 'app.learn': 'Learn {name}' };
export default en;
1
2
3
// ./src/i18n/zh.js
const zh_TW = { 'app.learn': '學習 {name}' };
export default zh_TW;
1
2
3
// ./src/i18n/ja.js
const ja_JP = { 'app.learn': '学び {name}' };
export default ja_JP;

撰寫切換語言的功能

因為一開始使用的是 functional component,想要添加狀態(state)的話,可以匯入 useState 近來用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ./src/index.js
import React, { useState } from 'react';

// ...
const Root = () => {
// 使用 useState 定義 locale 這個 state
const [locale, setLocale] = useState(navigator.language);

return (
<IntlProvider
locale={locale}
key={locale}
defaultLocale="en"
>
<!-- 將 setLocale 的方法傳到 App 內 -->
<App setLocale={setLocale} />
</IntlProvider>
);
};

在 App.js 的地方,撰寫三個按鈕可以來切換 locale 的狀態:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ./src/App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

// 從 props 取得從 index 傳入的 setLocale 方法
function App({ setLocale }) {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<div>
<!-- 建立三顆按鈕可以切換 local 的狀態 -->
<button onClick={() => setLocale('en')}>英文</button>
<button onClick={() => setLocale('zh-Hant')}>中文</button>
<button onClick={() => setLocale('ja')}>日文</button>
</div>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

export default App;

這時候的畫面會像這樣:

react-intl

文字訊息多語系

String Formatting Components @ react-intl

這裡說明的是最一般用來轉換不同語系字串的作法,react-intl 提供了:

根據不同的語言傳入不同的語系檔

可以切換 locale 的狀態後,最後就可以根據使用者選擇的語言傳入不同的語系檔。

先將剛剛撰寫好的語系檔載入:

1
2
3
4
5
// ./src/index.js
// ...
import en from './i18n/en.js';
import zh from './i18n/zh.js';
import ja from './i18n/ja.js';

接著定義 messages 這個變數,透過 if 判斷使用者切換到的語言為何,最後將 messages 傳入 <IntlProvider>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ./src/index.js
import React, { useState } from 'react';
// ...

import en from './i18n/en.js';
import zh from './i18n/zh.js';
import ja from './i18n/ja.js';

addLocaleData([...zhLocaleData, ...enLocaleData, ...jaLocaleData]);

const Root = () => {
const [locale, setLocale] = useState(navigator.language);
let messages;

// 根據使用者選擇的語系 locale 切換使用不同的 messages
if (locale.includes('zh')) {
messages = zh;
} else if (locale.includes('ja')) {
messages = ja;
} else {
messages = en;
}

return (
<!-- 將 messages 傳入 IntlProvider -->
<IntlProvider
locale={locale}
key={locale}
defaultLocale="en"
messages={messages}
>
<App setLocale={setLocale} />
</IntlProvider>
);
};

ReactDOM.render(<Root />, document.getElementById('root'));

在元件中使用語系檔

index.js 中,使用者在切換語言後,我們會提供不同的語系檔到 <IntlProvider>messages 屬性內。現在,在 App.js 中就可以使用 react-intl 提供的 <FormattedMessage> 方法來顯示。

<FormattedMessage /> 中,id 會對應到先前寫在 ./src/i18n/ 資料夾內的語系檔中定義的屬性 app.learn,而 values 是因為在語系檔中有定義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ./src/App.js
// ...
// 匯入 FormattedMessage 元件
import { FormattedMessage } from 'react-intl';

function App({ setLocale }) {
return (
<div className="App">
<header className="App-header">
<!-- ... -->
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
<!-- 使用 FormattedMessage -->
<FormattedMessage id="app.learn" values={{ name: 'React' }} />
</a>
</header>
</div>
);
}

這時候就完成了簡單的 i18n 語系替換:

react-intl

時間日期多語系

Date Formatting Components @ react-intl

在 react-intl 中也提供了時間、日期的多語系功能,包含:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {
FormattedDate,
FormattedTime,
} from 'react-intl';

function App({ setLocale }) {
return (
<div className="App">
<header className="App-header">
<!-- ... -->
<br />
<!-- 使用多語系的日期格式 -->
<FormattedDate
value={new Date()}
year="numeric"
month="long"
day="numeric"
weekday="long"
/>
<!-- 使用多語系的時間格式 -->
<FormattedTime value={new Date()} />
</header>
</div>
);
}

react-intl

完成的結果如下:

react-intl

如果有需要也可以使用 <FormattedRelativeTime>,這個方法會回傳距今的時間:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ./src/App.js
// ⚠️ 在 2.x 版時使用的是 FormattedRelative
import {
FormattedRelative,
} from 'react-intl';

function App({ setLocale }) {
return (
<div className="App">
<header className="App-header">
<!-- ... -->
<br />
<!-- ... -->
<FormattedRelative value={new Date() - 60 * 10}/>
</header>
</div>
);
}

⚠️ 在 v2.x 前使用的是 <FormattedRelative>

顯示的結果如下:

react-intl

數值多語系

Number Formatting Component @ react-intl

若有需要針對數值進行多語系的切換,react-intl 則提供了:

更新到最新 beta 版

務必參考 Upgrade Guide @ react-intl

安裝當前最新版本(非穩定版):

1
2
# 撰寫本文時最新的版本為 v3.0.0-beta-11
$ npm install react-intl@next

注意事項:

  • <FormattedRelative> 改為 <FormattedRelativeTime>,且 value 的使用方式不同,改成是用「差距(delta)」,參考 FormattedRelativeTime
  • 改用瀏覽器原生的語系資料,移除 addLocaleData 的方法,參考 Migrate to using native Intl APIs

因此,我們不需要在 addLocaleData

react-intl

掃描二維條碼,分享此文章