React Router v7で言語切り替えコンポーネントを構築する方法
同じページにとどまりながら言語を切り替える
問題
ユーザーは言語切り替え時に現在のコンテキストが保持されることを期待しています。製品ページ、ヘルプ記事、アカウント設定を閲覧している際に、英語からスペイン語に切り替えると、同じページがスペイン語で表示されるべきです。しかし、多くの実装では言語選択をホームページへのリダイレクトを伴うナビゲーションイベントとして扱い、ユーザーは元いた場所に戻るために再度ナビゲートする必要があります。これによりユーザーのフローが中断され、特にコンテンツが豊富なアプリケーションでナビゲーション階層の深い位置にいる場合、フラストレーションが生じます。
根本的な原因は、言語切り替え機能が現在のページに基づいて動的にURLを構築するのではなく、ハードコードされた遷移先URLを使用していることにあります。現在のURL構造を読み取って変換しなければ、言語変更時にユーザーのアプリケーション内の位置を維持することができません。
解決策
現在のURLを読み取り、アクティブなロケールパラメータと残りのパスセグメントを抽出する言語切り替えコンポーネントを構築します。サポートされている各言語に対して、ロケールセグメントのみを置き換え、他のすべてのパスセグメントとクエリパラメータを維持した新しいURLを生成します。これらのURLをリンクとしてレンダリングすることで、ユーザーはアプリケーション内の位置を失うことなく言語を切り替えることができます。
このアプローチでは、ロケールをナビゲーション先ではなくURL構造内の置換可能なパラメータとして扱い、/en/products/shoesから/es/products/shoesへの切り替え時にユーザーのコンテキストが保持されるようにします。
ステップ
1. ロケール対応URLを構築するヘルパー関数を作成する
現在のパス名とターゲットロケールを受け取り、ロケールセグメントを置き換えて新しいパスを構築する関数を定義します。
export function buildLocalePath(
currentPath: string,
newLocale: string,
): string {
const segments = currentPath.split("/").filter(Boolean);
if (segments.length === 0) {
return `/${newLocale}`;
}
segments[0] = newLocale;
return `/${segments.join("/")}`;
}
この関数はパス名をセグメントに分割し、最初のセグメントをターゲットロケールに置き換えてパスを再構築します。ルートパスなどのエッジケースを処理し、ロケールが常に最初のセグメントになるようにします。
2. サポートするロケールを定義する
アプリケーションがサポートするすべての言語をリストアップする設定オブジェクトを作成します。
export const locales = [
{ code: "en", label: "English" },
{ code: "es", label: "Español" },
{ code: "fr", label: "Français" },
{ code: "de", label: "Deutsch" },
];
この設定は、言語切替機能に表示する言語の信頼できる情報源として機能し、各ロケールにユーザーフレンドリーなラベルを提供します。
3. 言語切替コンポーネントを構築する
現在の位置を読み取り、アクティブなロケールを判断し、他のサポートされているすべての言語へのリンクをレンダリングするコンポーネントを作成します。
import { Link, useLocation, useParams } from "react-router";
import { locales, buildLocalePath } from "./i18n-config";
export function LanguageSwitcher() {
const location = useLocation();
const params = useParams();
const currentLocale = params.locale || "en";
return (
<nav aria-label="Language switcher">
<ul>
{locales.map((locale) => {
const isActive = locale.code === currentLocale;
const newPath = buildLocalePath(location.pathname, locale.code);
return (
<li key={locale.code}>
{isActive ? (
<span aria-current="true">{locale.label}</span>
) : (
<Link to={newPath}>{locale.label}</Link>
)}
</li>
);
})}
</ul>
</nav>
);
}
このコンポーネントは、useLocationを使用して現在のパス名にアクセスし、useParamsを使用してURLからアクティブなロケールを抽出します。サポートされている各ロケールに対して、ヘルパー関数を使用して新しいパスを生成し、現在の言語に対してはリンクまたは非インタラクティブな要素をレンダリングします。
4. クエリパラメータとハッシュフラグメントを保持する
言語を切り替える際にクエリ文字列とURLフラグメントを維持するようにヘルパー関数を拡張します。
export function buildLocalePath(
currentPath: string,
search: string,
hash: string,
newLocale: string,
): string {
const segments = currentPath.split("/").filter(Boolean);
if (segments.length === 0) {
return `/${newLocale}${search}${hash}`;
}
segments[0] = newLocale;
return `/${segments.join("/")}${search}${hash}`;
}
この更新版は、locationオブジェクトからsearchとhashプロパティを受け取り、生成されたパスに追加することで、フィルター、ソートパラメータ、アンカーリンクが言語切替時に維持されるようにします。
5. 拡張されたヘルパーを使用するようにコンポーネントを更新する
スイッチャーを修正して、完全な位置情報をヘルパー関数に渡すようにします。
import { Link, useLocation, useParams } from "react-router";
import { locales, buildLocalePath } from "./i18n-config";
export function LanguageSwitcher() {
const location = useLocation();
const params = useParams();
const currentLocale = params.locale || "en";
return (
<nav aria-label="Language switcher">
<ul>
{locales.map((locale) => {
const isActive = locale.code === currentLocale;
const newPath = buildLocalePath(
location.pathname,
location.search,
location.hash,
locale.code,
);
return (
<li key={locale.code}>
{isActive ? (
<span aria-current="true">{locale.label}</span>
) : (
<Link to={newPath}>{locale.label}</Link>
)}
</li>
);
})}
</ul>
</nav>
);
}
このコンポーネントは現在、location.searchとlocation.hashをヘルパーに渡すことで、スペイン語に切り替える際に/en/products?category=shoes#reviewsのようなURLが/es/products?category=shoes#reviewsになることを保証します。