最近、Reactを勉強しているので、しばらくはReactの記事を書いていきたいと思います! 今回は、Reactでi18n対応する方法について紹介します。
ReactやVue.jsなどで、i18n対応ができるパッケージです。総称してFormat.jsと呼ばれていますが、実際に使用するパッケージは react-intl
になります。GithubのStart数も多く、人気のパッケージです。
https://formatjs.io/
まずは、create-react-appします。
$ npx create-react-app formatjs-study --template typescript
次に、Format.jsを追加します。また、今回はCSSフレームワークとしてMUIを使用するので、併せて追加します。
$ yarn add react-intl @mui/material @emotion/react @emotion/styled
react-intlは、Reactコンテキストとして実装されており、i18n対応したい箇所を <IntlProvider>
コンポーネントで囲う必要があります。IntlProviderは、ロケールや翻訳メッセージなどを持ち、配下の <Formatted*>
コンポーネントで使用されます。
一般的には、Reactコンポーネントのルートに近い位置に <IntlProvider>
コンポーネントを置き、アプリケーション全体で使えるようにした方が良いそうなので、そのように実装してみます。
index.tsxを編集します。
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
navigator.language
から取得しているため、ブラウザの言語設定が入ります。後ほど説明する日時フォーマットは、ここのlocaleによって適切にフォーマットされます。messages
selectMessages()
で、 navigator.language
によってメッセージを切り替えています。メッセージ自体は、JSONファイルに定義して、importしています。今回は、JSONファイルにメッセージを定義していきます。構造はフラットにする必要があるため、構造を示す場合には .
などで区切ります。
src/lang/en.json
{ "app.hello": "hello {name}!" }
src/lang/ja.json
{ "app.hello": "こんにちは、{name}さん!" }
IntlProviderの配下で、実際にi18nしていくには、 <Formatted*>
コンポーネントを使います。
先ほど定義したメッセージを表示させるには、 <FormattedMessage>
を使用します。
<FormattedMessage id="app.hello" values={{ name: "太郎" }} />
「こんにちは、太郎さん!」と表示されました。
この他にも、様々なMessage Syntaxに対応しているので、よく使いそうなものを紹介します。
複数形を表現するには、 plural
フォーマットを使用します。
{ ・・・中略・・・ "app.cat": "You have {catCount, plural, =0 {no cats} one { 1 cat} other {{catCount} cats}}" }
翻訳メッセージ内に{key, plural, matches}
という形式で表します。
plural
を指定します。条件 {文言}
です。条件には、次のようなものがあります。 英語や日本語以外では、上記以外の条件を使うこともあるようです。詳しくはこちらの公式リファレンスが参考になります。
https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format
まず、日付を表示させてみます。
<FormattedDate value={new Date()} year="numeric" weekday="short" month="long" day="numeric" />
これで、「2021年11月30日(火)」と表示されました。プロパティを渡さなくすれば、表示させないようにすることもできます。
また、 year
, weekday
, month
, day
などのプロパティを全て渡さなければ、デフォルトである「2021/12/1」と表示されるようになります。
次に、時間を表示させてみます。
<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()
でパースすれば、ブラウザのタイムゾーンに変換して表示させることができます。
<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>
を使います。
まず、通貨の表示をやってみます。
<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"
とすることで、様々な単位の数値を出力するこどがてきます。
<p> <FormattedNumber value={10} style="unit" unit="megabyte" /> </p>
たとえば、上記の場合は「10 MB」と表示されます。その他にも様々な単位が用意されています。詳しくは下記のを参照してください。
https://formatjs.io/docs/polyfills/intl-numberformat#SupportedUnits
ここまでに紹介した、Formatted**
は Reactコンポーネントとして描画するほかに、命令型APIとしても同じ機能が用意されています。 useIntl
フックが用意されているので、これを使います。
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 コンテキストから実装していきます。
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を修正します。
ReactDOM.render( <React.StrictMode> <IntlProviderWrapper> <App /> </IntlProviderWrapper> </React.StrictMode>, document.getElementById("root") );
先ほど作成した <IntlProviderWrapper>
で、 <App />
を囲います。これで、アプリケーション全体で、どこからでも言語を変更できるようになりました。
それでは、さっそく言語を変更してみましょう。
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だけでなく、日時や単位のフォーマットもできることが分かりました。