作成日時:2023年10月10日 23:47
更新日時:2023年10月12日 23:43
Next.js
国際化
Tutorial
先日、next-i18nextを使って国際化対応を実装することがありました。
Next.js13.4以降、色々と変わって国際化対応するドキュメントも一部古かったりなど、
実装までにちょっと苦労したのもあり、今後のために記しておきます。
HTNCodeのGitHubの公開リポジトリにもあげておきましたので、ご参考になれば幸いです。
https://github.com/HTNCode/next-i18next-app-test
※CSSは今回の実装に関係ないので適当です。あしからず。
色々探してみたんですが、この動画が一番わかりやすかったです。
チュートリアル形式で説明してくれてます。
■Internationalization in NextJs 13@Hamed Bahramさんの動画
https://www.youtube.com/watch?v=hA0Wp3KQYGU
以下環境にて実装。
"react": "^18",
"react-dom": "^18",
"next": "13.5.4"
必要なものをインストールします。
npm i @formatjs/intl-localematcher next-i18next @types/negotiator server-only
さっそく実装していきます。
// layout.tsx
import "./globals.css";
import { Inter } from "next/font/google";
import { Locale, i18n } from "../../i18n.config";
import Header from "./components/Header";
import { Metadata } from "next";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "next-i18next-app",
description: "This is test.",
};
export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ lang: locale }));
}
export default function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { lang: Locale; pathname: string };
}) {
return (
<html lang={params.lang}>
<body className={inter.className}>
<Header lang={params.lang} />
<main>{children}</main>
</body>
</html>
);
}
//page.tsx
import { Locale } from "../../i18n.config";
import { getDictionary } from "../libs/dictionary";
export default async function Home({
params: { lang },
}: {
params: { lang: Locale };
}) {
const { page } = await getDictionary(lang);
if (!page) return null;
return (
<>
<div className="main_container">
<section>
<h1>{page.home.title}</h1>
<p>{page.home.description}</p>
</section>
</div>
</>
);
}
// Header.tsx
import { getDictionary } from "@/app/libs/dictionary";
import { Locale } from "@/i18n.config";
import Link from "next/link";
import LocaleSwitcher from "./locale-switcher";
export default async function Header({ lang }: { lang: Locale }) {
const { navigation } = await getDictionary(lang);
return (
<header>
<nav className="navbar">
<ul className="menu">
<li>
<Link href={`/${lang}`}>{navigation.home}</Link>
</li>
<li>
<Link href={`/${lang}`}>{navigation.about}</Link>
</li>
</ul>
<LocaleSwitcher />
</nav>
</header>
);
}
// Context.Provider.ts
"use client";
import React from "react";
import { Locale } from "../../../i18n.config";
export const LangContext = React.createContext<Locale>("ja");
// locale-switcher.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { i18n } from "../../../i18n.config";
export default function LocaleSwitcher() {
const pathName = usePathname();
const redirectedPathName = (locale: string) => {
if (!pathName) return "/";
const segments = pathName.split("/");
segments[1] = locale;
return segments.join("/");
};
return (
<ul className="localSwitcher">
{i18n.locales.map((locale) => {
return (
<li key={locale}>
<Link href={redirectedPathName(locale)}>{locale}</Link>
</li>
);
})}
</ul>
);
}
// dictionary.ts
import "server-only";
import type { Locale } from "../../i18n.config";
const dictionaries = {
en: () =>
import("../../dictionaries/en.json").then((module) => module.default),
ja: () =>
import("../../dictionaries/ja.json").then((module) => module.default),
};
export const getDictionary = async (locale: Locale) => {
const dictionaryLoader = dictionaries[locale];
if (typeof dictionaryLoader !== "function") {
locale = "ja";
}
return dictionaries[locale]();
};
// en.json
{
"page": {
"home": {
"title": "next-i18next-app",
"description": "This is test."
}
},
"navigation": {
"home": "HOME",
"about": "ABOUT"
}
}
// ja.json
{
"page": {
"home": {
"title": "next-i18nextのアプリ",
"description": "これはテストです。"
}
},
"navigation": {
"home": "ホーム",
"about": "私たちについて"
}
}
// i18n.config.ts
export const i18n = {
defaultLocale: "ja",
locales: ["ja", "en"],
} as const;
export type Locale = (typeof i18n)["locales"][number];
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { i18n } from "./i18n.config";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
function getLocale(request: NextRequest): string | undefined {
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
// @ts-ignore locales are readonly
const locales: string[] = i18n.locales;
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
const locale = matchLocale(languages, locales, i18n.defaultLocale);
return locale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(`/${locale}/${pathname}`, request.url)
);
}
}
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
Vercelにデプロイする際、srcディレクトリを使用していたのも影響していたのかもしれませんが、
うまくリダイレクトが動作しないことがありました。
必要に応じてVercel側でリダイレクト処理を設定するのもありかと思い、備忘録として残しておきます。
vercel.jsonファイルを作成し、以下記述を行ってデプロイするだけです。
// vercel.json
{
"redirects": [
{
"source": "/",
"destination": "https://sample.com/ja"
}
]
}
以上、Next.js13.5でのnext-i18nextを使った国際化対応方法でした。
HTNCode