ファランクスブログ

© 2026 all rights reserved.
  1. blog
  2. nextjs-18n
  • インストール
  • ルーティングあり
  • defaultLocale
  • 優先言語の取得
  • proxy.ts
  • ルーティング無し
  • 初回リクエスト時のlocaleセット
  • localeの変更
  • localeに応じたmetadata
  • jsonファイルの分割
  • Link

Nextjs:next-intl(i18n)

2026年3月14日

Nextjs16以降のCache Componentsが有効だと警告が出る

  • 対策:https://next-intl.dev/docs/routing/setup#static-rendering

    • この対策はルーティングありの場合には有効だが、ルーティング無しだと警告が消えない

インストール

https://next-intl.dev/docs/getting-started/app-router

pnpm add next-intl

2つの実装方法: with i18n routing = ルーティングあり / without i18n routing = ルーティングなし

  • ルーティングあり(URLに /ja や /en が表示)は、ルーティングなしと比べると組み込むのが簡単

  • i18nという単語はnext-intlに固有ではない。i18n = internationalization(国際化)

ルーティングあり

defaultLocale

// routing.ts
import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  locales: ["en", "ja"],

  defaultLocale: "en",
});
  • routing.tsに書く defaultLocaleは「この言語をデフォルトに」という意味ではなく、「ブラウザの優先言語(accept-language)がlocales配列に含まれていない場合の代替」という意味でのデフォルト

  • 例:

    • Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en-US;q=0.7,en;q=0.6 だと、ブラウザの優先言語の3番目にrouting.tsのlocales配列に含まれる ja がヒットするので、localeは ja になる

    • Accept-Language: zh-CN,zh;q=0.9 だとlocales配列でヒットしないのでdefaultLocaleの en になる

  • *ルーティングなしの場合、このような内部的な最適化は無いので自分で行う必要がある

優先言語の取得

(知識として)

クライアント:

  useEffect(() => {
    const lang = navigator.languages[0];
  • navigator.languagesでブラウザの優先言語の配列を取得(['ja', 'en-US', 'en'] )

サーバー:

export default function middleware(request: NextRequest) {
  const acceptLangs = request.headers.get("accept-language");
  • acceptLangsの中身:

    • [ ja,en-US;q=0.9,en;q=0.8 ] (1に近いほど優先)

    • 数値が設定されていないものは1扱い(上記だと ja )

ja-JP や en-US という文字列自体はブラウザ間で違いはない。

proxy.ts

docsのproxy.ts はnext-intl の処理だけをmiddlewareにする構成なので、他のmiddleware処理もできるようにする

import createMiddleware from "next-intl/middleware";
import { routing } from "./next-intl/i18n/routing";
import { NextRequest } from "next/server";

const intlMiddleware = createMiddleware(routing);

export default async function middleware(req: NextRequest) {
  return intlMiddleware(req);
}

ルーティング無し

intl-provider.tsx:

import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";

export const LocaleProvider = async ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const locale = await getLocale();
  const messages = await getMessages();
  return (
    <NextIntlClientProvider locale={locale} messages={messages}>
      {children}
    </NextIntlClientProvider>
  );
};
  • 現状 Cache Componentsが有効の場合、layout.tsxのexport defaultにasyncを含めないようするに必要があるので、NextIntlProviderは別のファイルに分けて<Suspense>で囲い警告が出ないようにする。

request.ts:

import { getRequestConfig } from "next-intl/server";
import { cookies } from "next/headers";

export default getRequestConfig(async () => {
  const store = await cookies();
  const locale = store.get("locale")?.value || "ja";
  const messages = (await import(`./messages/${locale}.json`)).default;

  return {
    locale,
    messages,
  };
});
  • paramsからlocaleを取得できないので、ルーティングありで書いていた({requestLocale}}は機能しない。cookieからlocaleを取得するように変更する。

proxy.ts :

  • createMiddleware()があると、ルート("/")が404になるので外す

初回リクエスト時のlocaleセット

ルーティングなしの場合、localeのcookieへのセットを自分で行う必要がある。

  • クライアント(useEffect)側でやると、default-localeと異なる言語設定のユーザーは初回リクエスト時に一瞬違う言語がUIに表示されるので、UXが悪くなる。

  • サーバーサイド(middleware)で初回cookieのセットは行う方がいい。

  • 以下はjaとenだけ用意し、enに該当しない場合はjaに流すケース

クライアント:

"use client";
import { Locale, useLocale } from "next-intl";
import { useEffect } from "react";

export default function LocaleSwitcher({
  changeLocale,
}: {
  changeLocale: (locale: Locale) => void;
}) {
  const locale = useLocale();
  const nextLocale = locale === "ja" ? "en" : "ja";

  useEffect(() => {
    const langs = navigator.languages.map((lang) => lang.split("-")[0]);
    let prefLang: "ja" | "en" = "ja";

    if (langs.includes("en")) {
      prefLang = "en";
    }

    document.cookie = `locale=${prefLang}; path=/; max-age=31536000`;
  }, []);

  return (
    <Button
      variant={"link"}
      className="flex items-center gap-1"
      onClick={() => changeLocaleAction(nextLocale)}
    >
      <EarthIcon size={14} strokeWidth={0.75} />
      <p className="text-xs">{nextLocale.toUpperCase()}</p>
    </Button>
  );
}

サーバー:

import { NextRequest, NextResponse } from "next/server";

export function middleware(req: NextRequest) {
  const res = NextResponse.next();

  // セットされていない場合
  if (!req.cookies.get("locale")) {
    const acceptLang = req.headers.get("accept-language") || "";
    const langs = acceptLang.split(",").map(lang => lang.split(";")[0].split("-")[0]);

    const prefLang: "ja" | "en" = langs.includes("en") ? "en" : "ja";

    res.cookies.set({
      name: "locale",
      value: prefLang,
      path: "/",
      maxAge: 60 * 60 * 24 * 365,
    });
  }

  return res;
}

localeの変更

"use server";
import { Locale } from "next-intl";
import { cookies } from "next/headers";

export const changeLocaleAction = async (locale: Locale) => {
  const store = await cookies();
  store.set("locale", locale);
};
  • クライアント側でdocument.cookie の値を変更しても、cookieの値が変わるだけでページの内容はリロードするまで変わらない。

  • buttonのonClick等でserverActonを呼び出すことでRSCが再取得されるようにする。

localeに応じたmetadata

layout.tsx :

export const generateMetadata = async () => {
  const t = await getTranslations("metadata");
  return {
    title: t("title"),
    description: t("description"),
    ...
  };
};

jsonファイルの分割

request.ts:

import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: {
      home: {
        ...(await import(`./messages/${locale}.json`)).default,
      },
      test: {
        ...(await import(`./messages/${locale}/test.json`)).default,
      },
    },
  };
});
  • 以下のようにしても機能するが、jsonファイル間でキーが被る可能性がある

    messages: {
      ...(await import(`./messages/${locale}.json`)).default,
      ...(await import(`./messages/${locale}/test.json`)).default,
    },

page.tsx:

const Page = () => {
  const t = useTranslations("test");
  return (
    <div>
      {t("title")}
      <hr />
      {t("metadata.description")}
    </div>
  );
};

Link

NextjsのLink(next/link)ではなくnext-intlのLinkを使うとlocaleが自動付与される。

import Link from "next/link";

const Footer = async () => {
  const locale = await getLocale();

  return (
      <Link href={`/${locale}/terms`}>terms</Link>|
  • NextjsのLinkはlocaleの取得プロセスが必要

next-intlのLink:

import { Link } from "@/i18n/routing";

const Footer = () => {
  return (
      <Link href={`/terms`}>terms</Link>|

ルーティングなしの場合:

  • i18nのLinkを使うとlocaleが含まれたURLになるので表示がずれるため、NextjsのLinkを使う。

    • NextjsのLinkを使えばいいので必要はないが、routing.tsにlocalePrefix: "never",の行を足すとi18nのLinkを使ってもURLにlocaleが含まれなくなる。

import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  defaultLocale: "ja",
  locales: ["en", "ja"],
  localePrefix: "never",
});

nextjs
/
other
  • インストール
  • ルーティングあり
  • defaultLocale
  • 優先言語の取得
  • proxy.ts
  • ルーティング無し
  • 初回リクエスト時のlocaleセット
  • localeの変更
  • localeに応じたmetadata
  • jsonファイルの分割
  • Link