0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【実録】個人開発アプリの日本人ユーザーが増えたので、`react-i18next` で多言語対応したら爆速で実装できた話

Posted at

👋 はじめに

こんにちは、keni (@keni_1997)です。
個人で「Pomodoro Flow」というポモドーロタイマーアプリを開発・運営しています。

hero-image.png

もともと海外ユーザー向けに作ったアプリだったのですが、ある日アナリティクスを見てみると、ありがたいことに日本からのアクセスも少しばかり増えていました。 本当に嬉しい限りです…!

しかし、喜びと同時にある問題に気づきました。

「UI、全部英語のままだ…!せっかく使ってくれているのに申し訳ない…!」

すぐに日本語対応を決意したものの、正直なところ多言語対応って、なんだか面倒なイメージがありませんか?

  • 「設定が複雑そう…」
  • 「すべてのテキストを置き換えるのが大変…」
  • 「将来、言語を追加するときに破綻しそう…」

そんな不安を抱えながらリサーチしてたどり着いたのが、今回紹介する react-i18next でした。

結論から言うと、導入も実装も驚くほど簡単で、半日程度で対応完了しました
まぁ、日本語だけ対応したから、すぐに完了したってのはありますが😅

この記事では、私と同じように「多言語対応、やらなきゃな…」と思っているReact開発者に向けて、react-i18next を使った爆速実装の手順を、実体験のコードを元にお伝えします!

🎯 対象読者

  • React / TypeScript で開発している方
  • 個人開発アプリやサービスをグローバルに展開したい方
  • 多言語対応の実装に苦手意識を持っている方

🚀 この記事を読めば…

  • react-i18next を使ったモダンな多言語対応フローがわかる!
  • 型安全性を保ちながら、メンテナンスしやすい実装方法が身につく!
  • 「多言語対応、チョロいな?」と思えるようになる!かも

💻 開発環境

  • React: 19.0.0
  • TypeScript: ~5.7.2
  • vite: ^6.3.0
  • i18next: ^23.11.5
  • react-i18next: ^15.5.2

1. 3つの神器をインストール 🛠️

まずは、多言語対応を支える3つのライブラリをインストールします。

npm install i18next react-i18next i18next-browser-languagedetector
  • i18next: 多言語対応のコアエンジン。彼がいないと始まりません。
  • react-i18next: i18next をReactで直感的に使えるようにする最高の相棒。
  • i18next-browser-languagedetector: ユーザーのブラウザ言語を嗅ぎつけてくれる賢い探偵役です。

2. 設計図と翻訳辞書を用意する 🗺️

次に、i18next の設定ファイルと、各言語の翻訳ファイル(辞書)を作成します。
「最初に仕組みさえ作れば、あとは辞書を充実させるだけ」という状態を目指します。src/i18n ディレクトリにすべてまとめましょう。

ディレクトリ構成
src
└── i18n
    ├── config.ts       # 全体の司令塔!i18nextの設定ファイル
    └── locales
        ├── en
        │   └── translation.ts  # 英語の辞書
        └── ja
            └── translation.ts  # 日本語の辞書

① 翻訳辞書(translation.ts)の作成

英語と日本語の翻訳ファイルを作成します。ここで超重要ポイントが、オブジェクトの末尾に as const を付けることです。これにより、キーの入力補完が効いたり、存在しないキーを呼び出すとエラーになったり、TypeScriptの恩恵を最大限に享受できます。

src/i18n/locales/ja/translation.ts

export const jaTranslation = {
  common: {
    appName: 'Pomodoro Flow',
    loading: '読み込み中…',
    language: '言語',
  },
  // ... 他の翻訳
} as const; // これが命!

export default jaTranslation;

src/i18n/locales/en/translation.ts

export const enTranslation = {
  common: {
    appName: 'Pomodoro Flow',
    loading: 'Loading…',
    language: 'Language',
  },
  // ... 他の翻訳
} as const; // これが命!

export default enTranslation;

② 司令塔(config.ts)の作成

i18next を初期化し、アプリ全体の設定を行います。

src/i18n/config.ts

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import enTranslation from './locales/en/translation';
import jaTranslation from './locales/ja/translation';

export const supportedLanguages = ['en', 'ja'] as const;

const resources = {
  en: { translation: enTranslation },
  ja: { translation: jaTranslation },
};

void i18n
  .use(LanguageDetector)
  .use(initReactI18next) // Reactとの連携を有効化
  .init({
    resources,
    fallbackLng: 'en', // もし翻訳が見つからなければ英語で表示
    supportedLngs: supportedLanguages,
    interpolation: {
      escapeValue: false, // Reactなら不要
    },
    detection: {
      // ?lang=ja > localStorage > ブラウザ設定 の順で言語を判定
      order: ['querystring', 'localStorage', 'navigator'],
      caches: ['localStorage'],
    },
  });

export default i18n;

3. i18next有効化

設定は完了です!最後に、アプリのルートで i18next を有効にします。

src/main.tsx

import { Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';

import i18n from './i18n/config';
import './index.css';
import { Router } from './Router';

createRoot(document.getElementById('root')!).render(
  <I18nextProvider i18n={i18n}>
    {/* 翻訳読み込み中のローディング画面などを表示できる */}
    <Suspense fallback={<div>Loading...</div>}>
      <Router />
    </Suspense>
  </I18nextProvider>
);

<I18nextProvider> でアプリをラップするだけ。これだけで、アプリ内のどのコンポーネントからでも翻訳機能が呼び出せるようになります。

4. いざ、翻訳!コンポーネントの書き換え方

準備はすべて整いました。あとはコンポーネント内のハードコードされた文字列を置き換えていくだけです。

useTranslation フックが主役

useTranslation フックを呼び出すと、翻訳のための t 関数が手に入ります。

import { useTranslation } from 'react-i18next';

export const MyComponent = () => {
  const { t } = useTranslation();

  return (
    <>
      {/* 変更前:<h1>Task List</h1> */}
      <h1>{t('taskList.title')}</h1>

      {/* 変更前:<p>Add a new task</p> */}
      <p>{t('taskList.addButton')}</p>
    </>
  );
};

どうでしょう? t('キー名') の形に書き換えるだけなので、非常に直感的です。

動的な値もお手の物

t 関数の第二引数を使えば、{{変数}} の形で動的な値を埋め込めます。

// 辞書の定義 (ja)
// "tour": { "step": "ステップ {{current}} / {{total}}" }

const { t } = useTranslation();
const [index, setIndex] = useState(0);
const total = 9;

// "ステップ 1 / 9" と表示される
return <div>{t('tour.step', { current: index + 1, total })}</div>;

5. ユーザー自身で言語を切り替えられるようにする

自動検出も便利ですが、ユーザーが手動で言語を選べるスイッチも用意しておくと、とても親切です。
これも驚くほど簡単です。

src/components/LanguageSwitcher.tsx

import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { supportedLanguages, type SupportedLanguage } from '../i18n/config';

export const LanguageSwitcher = () => {
  const { i18n, t } = useTranslation();

  const handleChange = useCallback(
    (event: React.ChangeEvent<HTMLSelectElement>) => {
      const nextLang = event.target.value as SupportedLanguage;
      // この一行で、アプリ全体の言語が切り替わる!
      void i18n.changeLanguage(nextLang);
    },
    [i18n],
  );

  return (
    <select value={i18n.resolvedLanguage} onChange={handleChange}>
      {supportedLanguages.map((lng) => (
        <option key={lng} value={lng}>
          {t(`common.languages.${lng}`)}
        </option>
      ))}
    </select>
  );
};

i18n.changeLanguage() を呼び出すだけで、I18nextProvider 以下のコンポーネントがすべて再レンダリングされ、表示が切り替わります。まさに魔法のようです。

🎉 おわりに

react-i18next を使った多言語対応、いかがでしたでしょうか?

この記事が、あなたのアプリを世界に羽ばたかせるきっかけになれば、これ以上嬉しいことはありません。

もしよろしければ、今回実装した「Pomodoro Flow」も触ってみて、フィードバックなどいただけると励みになります!

最後までお読みいただき、ありがとうございました!

📚 参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?