🏊‍♀️

Next.js + i18n + hooks で作る言語切り替えコンポーネント

2024/09/19に公開

前提

  • 下記の記事1.のように多言語対応を実現し、記事2.のように言語情報の Redux 化を行った
  • 言語切り替えコンポーネントについては、記事2. で作っていたがベースのみ
  • よって、この記事で言語切り替えコンポーネントの完成までのフローをまとめます

記事

環境

  • Next.js
  • TypeScript
  • App Router
  • Redux
  • Redux Toolkit
  • i18n
  • 日・英の言語対応(デフォルトは日)

言語切り替えコンポーネント本体

結論から、言語切り替えコンポーネントは以下。

  • 現在の言語(currentLocale
  • 言語を切り替えるための関数(toggleLocale

を返してくれるカスタムフックuseLanguageを作成した。

src/components/localeSwitcher/localeSwitcher.tsx
"use client";

import { i18n, type Locale } from "@/i18n/i18n-config";
import { useLanguage } from "@/hooks/useLanguage"; //⭐️

import styles from "./localeSwitcher.module.scss";

export default function LocaleSwitcher() {
  const { currentLocale, toggleLocale } = useLanguage(); //⭐️

  return (
    <ul className={styles.localeSwitcher}>
      {i18n.locales.map((item: Locale) => {
        let isActive = false;
        isActive = currentLocale === item;

        return (
          <li key={item} className={styles.localItem}>
            {isActive ? (
              <span className={styles.current}>{item}</span>
            ) : (
              <button onClick={toggleLocale}>{item}</button>
            )}
          </li>
        );
      })}
    </ul>
  );
}

カスタムフックuseLanguage

カスタムフックuseLanguageの中身の全貌は以下。

src/hooks/useLanguage.ts
src/hooks/useLanguage.ts
import { useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";

// hooks
import { RootState, AppDispatch } from "@/store/store";
import { useAppSelector, useAppDispatch } from "@/hooks/hooks";
import { setLanguage, setPathName } from "@/features/language/languageSlice"; // ⭐️

// i18n
import { i18n, type Locale } from "@/i18n/i18n-config";

export const useLanguage = () => {
  const router = useRouter();
  const pathName = usePathname();

  // 1. Reduxで管理している言語情報
  const dispatch = useAppDispatch<AppDispatch>();
  const currentLocale = useAppSelector(
    (state: RootState) => state.language.language
  );

  // 2. URLのパス名か dispatchが変更されるたびに実行
  useEffect(() => {
    const segments = pathName?.split("/");
    if (segments && i18n.locales.includes(segments[1] as Locale)) {
      dispatch(setLanguage(segments[1] as Locale));
    }
    dispatch(setPathName(pathName || "/"));
  }, [pathName, dispatch]);

  // 3. 言語を切り替えるための関数
  const toggleLocale = () => {
    const newLocale: Locale = currentLocale === "en" ? "ja" : "en";
    const redirectedPathName = (locale: Locale) => {
      if (!pathName) return "/";
      const segments = pathName.split("/");
      segments[1] = locale;
      return segments.join("/");
    };
    router.push(redirectedPathName(newLocale));
    dispatch(setLanguage(newLocale));
  };

  return { currentLocale, toggleLocale };
};

上記src/hooks/useLanguage.ts 内でインポートしている(下記引用)languageSlice(⭐️)の中身は以下。

import { setLanguage, setPathName } from "@/features/language/languageSlice"; // ⭐️
src/features/language/languageSlice.ts
src/features/language/languageSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

// Define a type for the slice state
export interface LanguageState {
  language: "en" | "ja";
  pathName: string | null;
}

// Define the initial state using that type
const initialState: LanguageState = {
  language: "ja",
  pathName: null,
};

export const languageSlice = createSlice({
  name: "language",
  initialState,
  reducers: {
    setLanguage: (state, action: PayloadAction<"en" | "ja">) => {
      state.language = action.payload;
      console.log("current locale:", state.language);
    },
    setPathName: (state, action: PayloadAction<string>) => {
      state.pathName = action.payload;
    },
  },
});

export const { setLanguage, setPathName } = languageSlice.actions;

export default languageSlice.reducer;

状態の取得と初期化

  const router = useRouter();
  const pathName = usePathname();

  // 1. Redux 内で管理している言語情報の dispatch と 現在の言語 を取得
  const dispatch = useAppDispatch<AppDispatch>();
  const currentLocale = useAppSelector(
    (state: RootState) => state.language.language
  );

useEffect

useEffect() 内の処理は、URLのパス名か dispatch が変更されるたびに実行される。

やっていること:

    1. URLを "/" で分割した2番目の値に有効な言語コードが入っているかをチェック
    • YES => 現在の言語として、Reduxストア内の言語設定を更新する
    1. Reduxストアに現在のパス名を保存する
  useEffect(() => {
    const segments = pathName?.split("/");

    // 1. URLを "/" で分割した2番目の値に有効な言語コードが入っているかをチェック
    if (segments && i18n.locales.includes(segments[1] as Locale)) {
      // 1-2. setLanguage アクションをディスパッチし、Reduxストア内の言語設定を更新する
      dispatch(setLanguage(segments[1] as Locale));
    }
    // 2. setPathName アクションをディスパッチし、Reduxストアに現在のパス名を保存する
    dispatch(setPathName(pathName || "/"));
  }, [pathName, dispatch]);

toggleLocale関数

言語を切り替えるための関数。

やっていること:

    1. 新しいロケールの定義
    1. 新しいロケールのURLを再構築し、そのパスに遷移させる
    • 2-0. 上記のために、引数に入るロケールに基づいてURLを再構築する関数を作成
    1. 新しいロケールを現在の言語として Redux ストア内の言語設定を更新する
  const toggleLocale = () => {
    // 1. Redux 内で管理している言語が en だったら、新しいロケールを ja とする。
    const newLocale: Locale = currentLocale === "en" ? "ja" : "en";

    // 2-0. 引数に入るロケールに基づいてURLを再構築する関数
    const redirectedPathName = (locale: Locale) => {
      if (!pathName) return "/";
      const segments = pathName.split("/");
      segments[1] = locale;
      return segments.join("/");
    };

    // 2. 新しいロケールのURLを再構築し、そのパスに遷移させる
    router.push(redirectedPathName(newLocale));

    // 3. setLanguage アクションをディスパッチし、Reduxストア内の言語設定を更新する
    dispatch(setLanguage(newLocale));
  };

戻り値

  return { currentLocale, toggleLocale };

Discussion