はじめに
1時間の音声を15秒で文字起こしし、かつ、自然な会話形式に自動変換する「インタビューAI」を開発中。
現在、日本語で日本国内向けに提供開始しているが、海外展開も進めていきたいので、サービスを多言語対応したいと思う。今回は、Reactプロジェクトを多言語対応する手順を記載。
フロントエンドの英語対応を進めるために、次のように進める。
i18nライブラリーの導入
Reactのプロジェクトでの多言語対応には、react-i18next
のような国際化ライブラリが便利。まず、以下のコマンドでreact-i18next
とi18next
をfrontend/ディレクトリにインストールする。
また、今回は、ユーザーのブラウザの表示言語に基づいて、自動的に言語を切り替えたいので、i18next
で提供されている、ブラウザの設定を検出して言語を自動的に設定するためのプラグインであるi18next-browser-languagedetector
もインストールする
npm install react-i18next i18next i18next-browser-languagedetector
多言語化対応のディレクトリ構造(フロントエンド)
src/locales/
配下に多言語対応のための翻訳ファイル(例えばen.json
とja.json
)を格納し、アプリ全体で使えるようにする
project-root/
└── src/
├── components/
├── locales/ <-- ここに言語ファイルを格納
│ ├── en.json <-- 英語の翻訳ファイル
│ └── ja.json <-- 日本語の翻訳ファイル
├── App.tsx
├── i18n.js <-- i18nの設定ファイル
└── index.tsx
i18nの設定ファイルの作成
次に、i18n設定ファイルを作成し、プロジェクト全体で使用できるように設定する。例えば、src/i18n.js
に以下のように記述する。
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.json
とlocales/ja.json
に言語ごとのテキストを記述する。
言語ファイルの作成
次に、各言語のテキストをlocales
フォルダに作成する。例えばen.json
ファイルを以下のように作成する。
en.json:
{
"welcome": "Welcome",
"logout": "Logout",
"delete_account": "Delete Account",
"transcription": "Transcription",
"summary": "Summary",
"title_header": "Title & Headings"
}
ja.json:
{
"welcome": "ようこそ",
"logout": "ログアウト",
"delete_account": "退会",
"transcription": "文字起こし",
"summary": "要約",
"title_header": "タイトル・小見出し"
}
アプリ全体にi18nを適用
src/index.js
またはsrc/index.tsx
でi18nをインポートし、アプリ全体に適用する。
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
において、以下の変更を加える。
react-i18next
をインポートして、useTranslation
フックを使う。- 各テキストを
t()
を使用して翻訳対応する。
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」になる。
ついでに、ナビゲーションのボタン(プラン・使い方・お問い合わせ・ログイン・ログアウト・退会)も多言語対応しておく。
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に切り替わっている。
あとは、他の日本語の部分も翻訳ファイルにまとめて、多言語対応したいと思う。