next-intl 改造 i18n 步骤

next.config.ts 中添加 next-intl 插件

export default withNextIntl(nextConfig);

i18n/request.ts 配置请求时如何获取语言设置、加载翻译资源

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

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

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

requestLocale 首选语言环境 path > cookie > accept-language > defaultLocale
可以从本地或远程加载翻译资源

i18n/routing.ts 定义支持的语言环境

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

export const routing = defineRouting({
  locales: ["en", "zh"],
  defaultLocale: "en",
  localePrefix: "as-needed", // 默认语言不显示前缀 /en
});

i18n/navigation.ts 导出国际化版本的导航 api

import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";

export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

使用这里的导航以便自动处理多语言路径

proxy.ts 中添加 i18n 中间件

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

export const config = {
  matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)", // 仅匹配页面路由
};

const handleI18nRouting = createMiddleware(routing);

export default async function (request: NextRequest) {
  const response = handleI18nRouting(request);
  // ...其他中间件

  return response;
}

messages/en.json 翻译文件

{
  "Layout": {
    "title": "Scott Tang",
    "blog": "Blog",
    "about": "About",
    "present": "Copyright ©2016 present <link>Scott Tang</link>"
  },
  "HomePage": {
    "description": "This is my portfolio, blog, and personal website."
  }
}

将所有页面移到 app/[locale]/ 下,支持动态语言路由

app/[locale]/layout.tsx - 集成 NextIntlClientProvider + 生成多语言静态参数([locale])

import { Locale, NextIntlClientProvider } from "next-intl";
import { routing } from "@/i18n/routing";

// /[locale] 动态路由参数
export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

// provider
<NextIntlClientProvider></NextIntlClientProvider>;

根 app/layout.tsx - 简化为传递子组件

export default function RootLayout({ children }: Props) {
  return children;
}

根 app/page.tsx - 简化为空组件,默认 / -> /en

// 不需要首页
export default function RootPage() {}

因为 localePrefix: "as-needed", 首页 / 渲染的默认语言 /en

components/LocaleSwitch.tsx - 语言切换组件

import { useRouter, usePathname } from "@/i18n/navigation";

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

// 切换语言只需 router.replace
// next-intl 内部会设置 cookie 以便下次访问时默认语言
router.replace(pathname, { locale: newLocale });

如果不用 [locale] 动态路由,只用 cookie 记录语言,切换语言需调用服务端 Action 进行 set-cookie 触发重新渲染
next.js 会将 set-cookie 的 action 标记为"需要触发重新渲染"

由于 next-intl 中间件默认会使用 header() 获取首选语言,因此会导致页面渲染变为动态渲染,如果不希望如此, 则需要在 layout 和 page 使用 Trans 之前手动调用 setRequestLocale(locale); 在 next.js 支持 rootParams 之后会解决此问题

使用场景

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

setRequestLocale(locale); // 静态渲染

const t = useTranslations("HomePage"); // 客户端组件 内使用
const t = await getTranslations("Layout"); // 服务端组件/函数 内使用