React ベースの Web フォームを国際(多言語)化対応してみた

react-i18next と react-router-dom を活用して React ベースの Web フォームに i18n 対応を追加したお話
2021.09.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

最近、React(+ React Hook Form)と Material-UI を組み合わせた Web フォームアプリの開発を行っておりますが、とある要件にて日本語だけでなく英語にも対応した Web フォームが必要になったため、色々と調べた結果 react-i18next と react-router-dom を活用して国際(多言語)化の対応を行いました。実際に動作するサンプルと合わせて、導入時のポイントを共有したいと思います。

なお、ソースコードは下記の GitHub リポジトリで公開しております。

サンプル

以下、CodeSandbox を利用して公開しているサンプルフォームです。

ナビゲーションバーに表示されている URL https://uld2s.csb.app/ の末尾に en を追記することで Web フォームで表示される言語設定を切り替えることが可能になっております。

Basic フォームで、NEXT ボタンをクリックすると英文のエラーメッセージも確認できます。

1-2.png

react-i18next と react-router-dom で国際(多言語)化対応を実装してみた

i18n 化をする際にやったことは、以下のとおりです。

モジュールをインストール

react-i18next を利用するために i18next と合わせてモジュールをインストールしておきます。

# npm
$ npm install react-i18next i18next --save

詳細については、公式のドキュメントをご確認ください。

また、今回は React フォームアプリのルーティングパスで言語切替を実装するために react-router-dom を活用しているため、こちらのモジュールも合わせてインストールします。

$ npm install react-router-dom

辞書ファイルを作成

src/i18n.js ファイルの resources として英語(en)および日本語(ja)の翻訳テキストを持たせておきました。

src/i18n.js

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

const resources = {
    en: {
        translation: {
            "basicStepLabel": "Basic",
            "optionalStepLabel": "Optional",
            "confirmStepLabel": "Confirm",
            "errorRequired": "This field is required",
            :
            snip
            :
        }
    },
    ja: {
        translation: {
            "basicStepLabel": "基本項目",
            "optionalStepLabel": "任意項目",
            "confirmStepLabel": "入力確認",
            "errorRequired": "必須項目です",
            :
            snip
            :
        }
    }
};

i18n
    .use(initReactI18next)
    .init({
        resources,
        lng: "ja",
        debug: true,
        interpolation: {
            escapeValue: false,
        }
    });

export default i18n;

次に、src/index.js 内で i18n をインポートしておきます。

index.js

import './i18n';

利用するコンポーネント側では、t() 関数と先程準備した辞書のキー名を組み合わせて翻訳テキストを取得するように実装を変更します。

src/components/Content.js

import { useTranslation } from 'react-i18next';

:
snip
:

function getSteps({ t }) {
    return [
        t('basicStepLabel'),
        t('optionalStepLabel'),
        t('confirmStepLabel')
    ];
}

function Content() {
    const { t, i18n } = useTranslation();
    :
    snip
    :
    const steps = getSteps({t});

あとは、ユーザーが目にする各種テキスト表示箇所(インプットフォームのラベルやプレースホルダー、エラーメッセージ等のテキスト情報)を上記のような実装に順次置き換えていくだけです。

言語を切り替えるためのルーティングパスを設定

各種テキストの表示箇所を、言語設定に従い動的に切り替えるための下準備が整ったら言語設定を切り替えるための機能を追加していきます。本記事では、App.js 側にルーティングの設定を仕込んでおきルーティングパスの文字列で言語を切り替えれるようにします。

App.js

import './App.css';
import { Box, CssBaseline } from '@material-ui/core';
import Header from './Header';
import Footer from './Footer';
import Form from './components/Content';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";


function App() {
  return (
    <Box>
      <Header />
      <div style={{ padding: 30 }}>
        <Router>
          <Switch>
            <Route exact path="/" component={Form} />
            <Route exact path="/:lang" component={Form} />
          </Switch>
        </Router>
      </div>
      <Footer />
      <CssBaseline />
    </Box>
  );
}
export default App;

App コンポーネントを呼び出す際のパスとして、/en が指定されると lang に en が渡されるイメージです。 次に Form コンポーネントの実体である、src/components/Content.js 内で useParams() を利用して lang を取得します。

src/components/Content.js

import { useTranslation } from 'react-i18next';
import { useParams } from "react-router-dom";
:
snip
:
function Content() {
    const { t, i18n } = useTranslation();
    :
    snip
    :
    const { lang } = useParams();
    useEffect(() => {
        if (lang === 'en') {
            i18n.changeLanguage('en');
        } else {
            i18n.changeLanguage('ja');
        }
    }, [lang, i18n]);

lang が en で指定されていた場合に i18n.changeLanguage('en'); を呼び出し、言語設定を日本語から英語に変更します。 react-i18next と react-router-dom を活用することで、割と簡単に i18n 化の仕組みが導入できました。

yup のSchema 定義を動的に生成する(オプション)

この Web フォームでは、yup というライブラリを利用してバリデーション機能を提供しています。 当初、Web フォーム上にトグルスイッチを設けて、言語設定を切り替える機能を提供しようと試行錯誤していましたが、yup で利用している Schema 定義が静的に宣言されていると react-i18next の言語設定を動的に変更してもエラーメッセージの言語が変更されませんでした。コンポーネントが呼び出されたタイミングの言語設定に基づいて Schema が静的に作成されることが原因と思われます。

変更前

Basic.js

import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

function Basic(props) {
    const basicSchema = Yup.object().shape({
        checkBox: Yup.boolean()
            .oneOf([true], 'チェックが必要です'),
        textBox: Yup.string()
            .required('必須項目です')
            .max(10, '10文字以内で入力してください')
            .matches(/^[a-zA-Z0-9!-/:-@¥[-`{-~ ]*$/, "半角英数字記号以外は使用できません"),
        pullDown: Yup.string()
            .oneOf(['one', 'two', 'three'], 'いずれかを選択してください'),
    });

そこで、Schema 定義を動的に生成するように変更することで、意図した動作(エラーメッセージも言語設定に基づいた表示)になりました。

変更後

Basic.js

import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

const getBasicSchema = ({ t }) => {
    return Yup.object().shape({
        checkBox: Yup.boolean().oneOf([true], t('checkRequired')),
        textBox: Yup.string().required(t('errorRequired')).max(10, t('limitCharacter')).matches(/^[a-zA-Z0-9!-/:-@¥[-`{-~ ]*$/, t('errorMatchTextBox')),
        pullDown: Yup.string().oneOf(['one', 'two', 'three'], t('selectRequired')),
    });  
}

function Basic(props) {
    const { t } = useTranslation();
    const basicSchema = getBasicSchema({t});
    const { control, handleSubmit, setValue, formState:{ errors } } = useForm({
        mode: 'onBlur',
        defaultValues: {
            checkBox: false,
            textBox: "",
            pullDown: "",
        },
        resolver: yupResolver(basicSchema)
    });

色々と試行錯誤して得られた知見だったので、参考情報として共有します。

さいごに

今回は利用していませんが、i18next-browser-languagedetector というプラグインを導入するとユーザーが利用しているブラウザの各種情報から言語を検出することができる機能なども提供されているようです。(すごく便利ですね)

これからの時代、国際化(英語化)対応の要件が無くても、幅広いユーザーをターゲットとするサービスであれば、i18n化対応の仕組みを事前に導入しておくのが多様性に優れた良いデザイン(サービス設計)なのかもしれないなと思いました。

ではでは