方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【React】i18nライブラリーを導入しプロジェクトを多言語対応する手順

react-i18n-multiple-language
この記事は約11分で読めます。

はじめに

1時間の音声を15秒で文字起こしし、かつ、自然な会話形式に自動変換する「インタビューAI」を開発中。

現在、日本語で日本国内向けに提供開始しているが、海外展開も進めていきたいので、サービスを多言語対応したいと思う。今回は、Reactプロジェクトを多言語対応する手順を記載。

フロントエンドの英語対応を進めるために、次のように進める。

i18nライブラリーの導入

Reactのプロジェクトでの多言語対応には、react-i18nextのような国際化ライブラリが便利。まず、以下のコマンドでreact-i18nexti18nextをfrontend/ディレクトリにインストールする。

また、今回は、ユーザーのブラウザの表示言語に基づいて、自動的に言語を切り替えたいので、i18nextで提供されている、ブラウザの設定を検出して言語を自動的に設定するためのプラグインであるi18next-browser-languagedetectorもインストールする

ShellScript
npm install react-i18next i18next i18next-browser-languagedetector

多言語化対応のディレクトリ構造(フロントエンド)

src/locales/配下に多言語対応のための翻訳ファイル(例えばen.jsonja.json)を格納し、アプリ全体で使えるようにする

ShellScript
project-root/
  └── src/
      ├── components/
      ├── locales/        <-- ここに言語ファイルを格納
           ├── en.json   <-- 英語の翻訳ファイル
           └── ja.json   <-- 日本語の翻訳ファイル
      ├── App.tsx
      ├── i18n.js         <-- i18nの設定ファイル
      └── index.tsx

i18nの設定ファイルの作成

次に、i18n設定ファイルを作成し、プロジェクト全体で使用できるように設定する。例えば、src/i18n.jsに以下のように記述する。

TypeScript
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from './locales/en.json';
import ja from './locales/ja.json';

i18n
  .use(LanguageDetector) // 言語検出機能を使用
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: en },
      ja: { translation: ja },
    },
    fallbackLng: 'en', // 言語が検出されない場合のフォールバック
    interpolation: { escapeValue: false },
    detection: {
      order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'], // 言語の検出順序
      caches: ['localStorage', 'cookie'], // 言語を保存する場所
    },
  });

export default i18n;

この設定で、ユーザーのブラウザの表示言語を自動的に検出し、それに基づいて初期の言語を設定することができる。例えば、日本語環境でブラウザを使用している場合は自動的にjaに設定される。

次にlocales/en.jsonlocales/ja.jsonに言語ごとのテキストを記述する。

言語ファイルの作成

次に、各言語のテキストをlocalesフォルダに作成する。例えばen.jsonファイルを以下のように作成する。

en.json:

TypeScript
{
  "welcome": "Welcome",
  "logout": "Logout",
  "delete_account": "Delete Account",
  "transcription": "Transcription",
  "summary": "Summary",
  "title_header": "Title & Headings"
}

ja.json:

TypeScript
{
  "welcome": "ようこそ",
  "logout": "ログアウト",
  "delete_account": "退会",
  "transcription": "文字起こし",
  "summary": "要約",
  "title_header": "タイトル・小見出し"
}

アプリ全体にi18nを適用

src/index.jsまたはsrc/index.tsxでi18nをインポートし、アプリ全体に適用する。

TypeScript
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './i18n';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

既存のコンポーネントを翻訳対応に変更

次に、各ファイルの翻訳対応を行う。試しに、まず、App.tsxの「文字起こし」「要約」「タイトル・小見出しのボタン」を英語対応する。

App.tsxにおいて、以下の変更を加える。

  1. react-i18nextをインポートして、useTranslationフックを使う。
  2. 各テキストをt()を使用して翻訳対応する。
TypeScript
import React from 'react';
import { useTranslation } from 'react-i18next';
// その他のインポートは省略

const App: React.FC = () => {
  const { t } = useTranslation();

  // 他のコードはそのまま
  return (
    <Elements stripe={stripePromise}>
      <div className="app-container">
        <Navbar
          currentUser={currentUser}
          onLogout={handleLogout}
          onDeleteAccount={handleDeleteAccount}
        />

        <div className="container mx-auto mt-20 pt-5 px-4">
          {!location.pathname.includes("/login") &&
            !location.pathname.includes("/upgrade-plan") && (
              <div className="flex justify-center mb-12 mt-4">
                <div className="flex space-x-4">
                  <Link to="/transcription">
                    <Button
                      variant={
                        activeService === "transcription"
                          ? "primary"
                          : "outline"
                      }
                      className="w-auto"
                      onClick={() => setActiveService("transcription")}
                    >
                      {t('transcription')}
                    </Button>
                  </Link>

                  <Link to="/summary">
                    <Button
                      variant={
                        activeService === "summary" ? "primary" : "outline"
                      }
                      className="w-auto"
                      onClick={() => setActiveService("summary")}
                    >
                      {t('summary')}
                    </Button>
                  </Link>

                  <Link to="/title-header-generator">
                    <Button
                      variant={
                        activeService === "title-header-generator"
                          ? "primary"
                          : "outline"
                      }
                      className="w-auto"
                      onClick={() => setActiveService("title-header-generator")}
                    >
                      {t('title_header')}
                    </Button>
                  </Link>
                </div>
              </div>
            )}
        {/* その他のコード */}
        </div>
      </div>
    </Elements>
  );
};

このようにして、各コンポーネントでuseTranslationを使用し、テキストを言語ファイルから取得するようにする。

Navbarに言語切り替え機能の実装

ブラウザの言語を自動的に設定した後でも、ユーザーが手動で言語を切り替えたい場合には、Navbarに言語選択のドロップダウンメニューを追加する。以下のように、手動で言語を切り替えるボタンを追加。

<select>タグのvalue属性にi18n.languageを設定することで、ユーザーがページをロードした際に、ブラウザで検出された言語がドロップダウンで自動的に選択されるようにする。例えば、ブラウザが英語設定であれば、ドロップダウンのデフォルトの選択肢も「English」になる。

ついでに、ナビゲーションのボタン(プラン・使い方・お問い合わせ・ログイン・ログアウト・退会)も多言語対応しておく。

TypeScript
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { AiOutlineClose, AiOutlineMenu } from "react-icons/ai";
import { Link } from "react-router-dom";
import { Button } from "./ui/button";

type NavbarProps = {
  currentUser: any;
  onLogout: () => void;
  onDeleteAccount: () => void;
};

const Navbar: React.FC<NavbarProps> = ({
  currentUser,
  onLogout,
  onDeleteAccount,
}) => {
  const [isOpen, setIsOpen] = useState(false);
  const { t, i18n } = useTranslation(); // i18nのインスタンスとt関数を取得

  const toggleMenu = () => setIsOpen(!isOpen);

  const changeLanguage = (lng: string) => {
    i18n.changeLanguage(lng); // 言語を変更
  };

  return (
    <nav className="p-4 fixed top-0 w-full z-10 bg-white">
      <div className="container mx-auto flex justify-between items-center">
        {/* ロゴ */}
        <Link to="/" className="flex items-center text-brand font-bold text-xl">
          <img
            src="/images/interview-ai-logo.png"
            alt="Logo"
            className="h-6 w-6 mr-2"
          />
          {t("app_name", { defaultValue: "インタビューAI" })}{" "}
          {/* デフォルト値として日本語を設定 */}
        </Link>

        {/* ハンバーガーメニュー(スマホ用) */}
        <div className="md:hidden">
          <button onClick={toggleMenu}>
            {isOpen ? (
              <AiOutlineClose className="h-6 w-6 text-gray-800" />
            ) : (
              <AiOutlineMenu className="h-6 w-6 text-gray-800" />
            )}
          </button>
        </div>

        {/* ナビゲーションリンク */}
        <div
          className={`${
            isOpen ? "block" : "hidden"
          } md:flex flex-col md:flex-row items-center md:items-center absolute md:static top-16 left-0 w-full md:w-auto bg-white md:bg-transparent`}
        >
          <Link
            to="/upgrade-plan"
            className="text-gray-800 hover:text-blue-600 mx-2 py-2 md:py-0"
          >
            {t("plan")}
          </Link>
          <a
            href="https://www.interview-ai.site/how-to-use-interviewai/"
            target="_blank"
            rel="noopener noreferrer"
            className="text-gray-800 hover:text-blue-600 mx-2 py-2 md:py-0"
          >
            {t("how_to_use")}
          </a>
          <a
            href="https://docs.google.com/forms/d/e/1FAIpQLSeGpCKSrHcT6HVzQNOL27-KPPli0LxYCXaphMv6QE8I-rA9KA/viewform"
            target="_blank"
            rel="noopener noreferrer"
            className="text-gray-800 hover:text-blue-600 mx-2 py-2 md:py-0"
          >
            {t("contact")}
          </a>

          {currentUser ? (
            <>
              <Button
                variant="ghost"
                onClick={onLogout}
                className="text-gray-800 hover:bg-gray-600 hover:text-white mx-2 py-2 md:py-0"
              >
                {t("logout")}
              </Button>
              <Button
                variant="ghost"
                onClick={onDeleteAccount}
                className="text-gray-800 hover:bg-gray-600 hover:text-white mx-2 py-2 md:py-0"
              >
                {t("delete_account")}
              </Button>
            </>
          ) : (
            <Link to="/login">
              <Button
                variant="ghost"
                className="text-gray-800 hover:bg-gray-600 hover:text-white mx-2 py-2 md:py-0"
              >
                {t("login")}
              </Button>
            </Link>
          )}

          {/* 言語選択ドロップダウンの追加 */}
          <div className="ml-4">
            <select
              value={i18n.language}
              onChange={(e) => changeLanguage(e.target.value)}
              className="border border-gray-300 rounded p-1"
            >
              <option value="ja">日本語</option>
              <option value="en">English</option>
            </select>
          </div>
        </div>
      </div>
    </nav>
  );
};

export default Navbar;

動作確認

Dockerコンテナの停止とビルド

ちなみに、今回のように新しいライブラリを追加した場合でDocker環境下で動作確認をしたい場合、次のDockerコマンドを順に実行することで、依存関係を正しく反映させることができる。

  • docker-compose down: 現在稼働しているコンテナを停止し、削除する。
  • docker-compose build: 新しいDockerイメージをビルドする。通常、このステップで依存関係がDockerイメージに取り込まれる。
  • docker-compose up: 新たにビルドされたイメージを使ってコンテナを起動。

ただし、一部のデータ(例えば、依存関係や設定ファイルなど)がボリュームにキャッシュされていると、最新のイメージが反映されない場合がある。docker-compose upを行っても、新しいライブラリの依存関係に関するエラーログが生じた場合は、新しい依存関係を反映させるために、下記コマンドを使ってキャッシュや既存のイメージを削除してから、build & upする

docker-compose down --rmi all --volumes --remove-orphans

  • -rmi all:すべてのイメージを削除。
  • -volumes:ボリュームも削除してデータをクリアにする。
  • -remove-orphans:現在必要ない孤立したコンテナも削除。

言語表示ドロップダウンで言語切り替え

トップ画面を開くと、ナビゲーションバーの1番右側に言語表示のドロップダウンメニューが表示されるようになった。デフォルトでブラウザ表示言語の日本語になっている。

ドロップダウンメニューをクリックすると、「日本語」の下に「English」が表示されるのでクリック

今回英語翻訳を記載したナビゲーションと、文字起こし・要約・タイトルコミ出しのボタンが無事英語に切り替わった。

Chromeブラウザの表示言語の優先を日本語から英語に切り替えてみる。

無事、英語が表示された。

言語バーもEnglishに切り替わっている。

あとは、他の日本語の部分も翻訳ファイルにまとめて、多言語対応したいと思う。

タイトルとURLをコピーしました