はじめに
最近、Reactを勉強しているので、しばらくはReactの記事を書いていきたいと思います! 今回は、Reactでi18n対応する方法について紹介します。
react-intlとは?
ReactやVue.jsなどで、i18n対応ができるパッケージです。総称してFormat.jsと呼ばれていますが、実際に使用するパッケージは
react-intl
になります。GithubのStart数も多く、人気のパッケージです。
https://formatjs.io/
react-intlの導入
まずは、create-react-appします。
1 | $ npx create-react-app formatjs-study --template typescript |
次に、Format.jsを追加します。また、今回はCSSフレームワークとしてMUIを使用するので、併せて追加します。
1 | $ yarn add react-intl @mui/material @emotion/react @emotion/styled |
使い方
IntlProviderの実装
react-intlは、Reactコンテキストとして実装されており、i18n対応したい箇所を
<IntlProvider>
コンポーネントで囲う必要があります。IntlProviderは、ロケールや翻訳メッセージなどを持ち、配下の
<Formatted*>
コンポーネントで使用されます。
一般的には、Reactコンポーネントのルートに近い位置に
<IntlProvider>
コンポーネントを置き、アプリケーション全体で使えるようにした方が良いそうなので、そのように実装してみます。
index.tsxを編集します。
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 | import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; import { IntlProvider } from "react-intl"; import ja from "./lang/ja.json"; import en from "./lang/en.json"; function selectMessages() { switch (navigator.language) { case "en": return en; case "ja": return ja; default: return en; } } ReactDOM.render( <React.StrictMode> <IntlProvider locale={navigator.language} messages={selectMessages()}> <App /> </IntlProvider> </React.StrictMode>, document.getElementById("root") ); |
IntlProviderに渡しているPropsについて説明します。
-
locale
ja、enなどのロケールが入ります。今回は、navigator.language
から取得しているため、ブラウザの言語設定が入ります。後ほど説明する日時フォーマットは、ここのlocaleによって適切にフォーマットされます。 -
messages
翻訳メッセージを渡します。今回はselectMessages()
で、navigator.language
によってメッセージを切り替えています。メッセージ自体は、JSONファイルに定義して、importしています。
メッセージ定義
今回は、JSONファイルにメッセージを定義していきます。構造はフラットにする必要があるため、構造を示す場合には
.
などで区切ります。
src/lang/en.json
1 2 3 | { "app.hello": "hello {name}!" } |
src/lang/ja.json
1 2 3 | { "app.hello": "こんにちは、{name}さん!" } |
<Foramatted*>コンポーネントの使用
IntlProviderの配下で、実際にi18nしていくには、
<Formatted*>
コンポーネントを使います。
メッセージ
先ほど定義したメッセージを表示させるには、
<FormattedMessage>
を使用します。
1 | <FormattedMessage id="app.hello" values={{ name: "太郎" }} /> |
「こんにちは、太郎さん!」と表示されました。
この他にも、様々なMessage Syntaxに対応しているので、よく使いそうなものを紹介します。
複数形対応
複数形を表現するには、
plural
フォーマットを使用します。
1 2 3 4 | { ・・・中略・・・ "app.cat": "You have {catCount, plural, =0 {no cats} one { 1 cat} other {{catCount} cats}}" } |
翻訳メッセージ内に
{key, plural, matches}
という形式で表します。
- key
複数化を判断するための数値を受け取ります。 - plural
複数形フォーマットを指定するため、plural
を指定します。 - mathces
keyの値によって表示させたいメッセージを指定します。書式は条件 {文言}
です。条件には、次のようなものがあります。- one: 単数
- other: 複数
- =value: 特定の数値
英語や日本語以外では、上記以外の条件を使うこともあるようです。詳しくはこちらの公式リファレンスが参考になります。
https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format
日時
まず、日付を表示させてみます。
1 2 3 4 5 6 7 | <FormattedDate value={new Date()} year="numeric" weekday="short" month="long" day="numeric" /> |
これで、「2021年11月30日(火)」と表示されました。プロパティを渡さなくすれば、表示させないようにすることもできます。
また、
year
,
weekday
,
month
,
day
などのプロパティを全て渡さなければ、デフォルトである「2021/12/1」と表示されるようになります。
次に、時間を表示させてみます。
1 2 3 4 5 6 | <FormattedTime value={new Date()} hour="numeric" minute="numeric" second="numeric" /> |
こちらは、「19:21:54」と表示されました。もし秒数が不要であれば、
second=
を削除すれば表示されなくなります。(その場合、デフォルト設定と同じになるため、hour, minute, secondを全て削除しても構いません。)
<FormattedDate>
と
<FormattedTime>
は、使用箇所でいちいち定義するより、いずれもラップしたコンポーネントを作り、アプリケーション全体で共通化した方が良いと思いました。日時のフォーマットは、アプリケーション全体で統一感を持たせた方が良いと思うためです。
タイムゾーンについて
<FormattedDate>
や
<FormattedTime>
は、valueに渡されたDateオブジェクトをもとにフォーマットを行います。そのため、ISO8606形式の日付を
Date.parse()
でパースすれば、ブラウザのタイムゾーンに変換して表示させることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <FormattedDate value={Date.parse("2021-01-01T00:00:00.000+00:00")} year="numeric" weekday="short" month="short" day="numeric" /> <br /> <FormattedTime value={Date.parse("2021-01-01T00:00:00.000+00:00")} hour="numeric" minute="numeric" second="numeric" /> |
UTC時間で0時を指定していますが、ブラウザで表示してみると「2021年1月1日(金) 9:00:00」と表示され、ブラウザのタイムゾーンに変換されていることが分かります。
ちなみに、Chromeでタイムゾーンを変更するには、Developer Toolsを開き、「…」からSensorsを開きます。Locationを変更してからリロードすると、タイムゾーンによって時刻の表示が変わることが確認できます。
数値(通貨など)
数値のフォーマットを行うには、
<FormattedNumber>
を使います。
まず、通貨の表示をやってみます。
1 2 3 4 5 6 | <p> <FormattedNumber value={10} style="currency" currency="USD" /> </p> <p> <FormattedNumber value={1000} style="currency" currency="JPY" /> </p> |
USDは「$ 10.00」、JPYは「\ 1,000」と表示されました。このように、通貨に合わせてフォーマットされます。
その他にも、
style="unit"
とすることで、様々な単位の数値を出力するこどがてきます。
1 2 3 | <p> <FormattedNumber value={10} style="unit" unit="megabyte" /> </p> |
たとえば、上記の場合は「10 MB」と表示されます。その他にも様々な単位が用意されています。詳しくは下記のを参照してください。
https://formatjs.io/docs/polyfills/intl-numberformat#SupportedUnits
命令型APIの呼び出し
ここまでに紹介した、
Formatted**
は Reactコンポーネントとして描画するほかに、命令型APIとしても同じ機能が用意されています。
useIntl
フックが用意されているので、これを使います。
1 2 3 4 5 6 7 8 9 10 11 | function App() { const intl = useIntl(); return ( <div className="App"> <Snackbar open={true} message={intl.formatMessage({ id: "app.hello" }, { name: "太郎" })} /> </div> ) } |
例えば、MUIのSnackbarでは、表示するメッセージをstringで渡す必要があります。そこで、
intl.formatMessage()
を使うと、
<FormattedMessage>
と同じ結果を得られます。
基本的には、
format**
という関数名で、先ほど紹介したReactコンポーネントと同じ結果を得ることができます。
言語の変更
ここまでのサンプルでは、
navigator.language
でブラウザの言語を取得していましたが、今度は言語をアプリケーション内で変更できるようにしたいと思います。
今回は、
IntlProvider
をラップしたReact コンテキストを実装することで、localeやmessagesを切り替えできるようにしたいと思います。
まずは、React コンテキストから実装していきます。
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 38 | import React, { createContext, useState, VFC } from "react"; import ja from "./lang/ja.json"; import en from "./lang/en.json"; import { IntlProvider } from "react-intl"; interface Props { children: React.ReactNode; } interface Lang { switchLang(lang: string): void; } export const Context = createContext<Lang>({} as Lang); export const IntlProviderWrapper: VFC<Props> = (props) => { const langs = [ { locale: "ja", messages: ja }, { locale: "en", messages: en }, ]; const [lang, setLang] = useState(langs[0]); const switchLang = (locale: string) => { const changeLang = langs.find((lang) => lang.locale === locale); if (changeLang == null) { return; } setLang(changeLang); }; return ( <Context.Provider value={{ switchLang }}> <IntlProvider {...lang}>{props.children}</IntlProvider> </Context.Provider> ); }; export default IntlProviderWrapper; |
まず、
const langs
に言語を定義していきます。ここで定義したプロパティは、そのまま
IntlProvider
に渡すため、
IntlProvider
のプロパティのインターフェイスに合わせておきます。
また、
switchLang
関数を公開することで、子コンポーネントから言語を切り替えることができるようになっています。
switchLang
関数では、指定されたlocaleを探し、
setLang
しています。
そして、
<IntlProvider {...lang}>
とすることで、選択された言語情報を渡し、変更があったときに再描画されるようにしています。
最後に、index.tsxを修正します。
1 2 3 4 5 6 7 8 | ReactDOM.render( <React.StrictMode> <IntlProviderWrapper> <App /> </IntlProviderWrapper> </React.StrictMode>, document.getElementById("root") ); |
先ほど作成した
<IntlProviderWrapper>
で、
<App />
を囲います。これで、アプリケーション全体で、どこからでも言語を変更できるようになりました。
それでは、さっそく言語を変更してみましょう。
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 | import React, { useContext } from "react"; import "./App.css"; import { FormattedMessage, } from "react-intl"; import Button from "@mui/material/Button"; import { Context } from "./IntlProviderWrapper"; function App() { const { switchLang } = useContext(Context); const intl = useIntl(); return ( <div className="App"> <p> <Button variant="contained" onClick={() => switchLang("ja")}> <FormattedMessage id="app.lang.change.ja" /> </Button> </p> <p> <Button variant="contained" onClick={() => switchLang("en")}> <FormattedMessage id="app.lang.change.en" /> </Button> </p> </div> ); } |
先ほど公開した
switchLang
関数を呼び出し、言語を変更することができました。
さいごに
react-intlを使うと、i18nだけでなく、日時や単位のフォーマットもできることが分かりました。