カテゴリー: FrontEnd

react-intl (Format.js) を使ってi18n対応

はじめに

最近、Reactを勉強しているので、しばらくはReactの記事を書いていきたいと思います! 今回は、Reactでi18n対応する方法について紹介します。

react-intlとは?

ReactやVue.jsなどで、i18n対応ができるパッケージです。総称してFormat.jsと呼ばれていますが、実際に使用するパッケージは react-intlになります。GithubのStart数も多く、人気のパッケージです。
https://formatjs.io/

react-intlの導入

まずは、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

使い方

IntlProviderの実装

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
    ja、enなどのロケールが入ります。今回は、 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}さん!"
}

<Foramatted*>コンポーネントの使用

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}という形式で表します。

  • key
    複数化を判断するための数値を受け取ります。
  • plural
    複数形フォーマットを指定するため、 pluralを指定します。
  • mathces
    keyの値によって表示させたいメッセージを指定します。書式は 条件 {文言}です。条件には、次のようなものがあります。
    • one: 単数
    • other: 複数
    • =value: 特定の数値

英語や日本語以外では、上記以外の条件を使うこともあるようです。詳しくはこちらの公式リファレンスが参考になります。
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, weekdaymonth, 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

命令型APIの呼び出し

ここまでに紹介した、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だけでなく、日時や単位のフォーマットもできることが分かりました。

おすすめ書籍

カイザー

シェア
執筆者:
カイザー
タグ: React

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

3週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前