Nextjs14 App router 使用next-intl实现优雅的国际化

1,388 阅读1分钟

安装next-inti

pnpm install next-intl

目录结构

image.png

首先需要在next.config.ts 去引入一下next-intl

import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {};

export default withNextIntl(nextConfig);

在app目录下新建 config.ts

//  app/config.ts
export const locales = ["en", "cn"] as const;

export type Locales = typeof locales;

在app目录下新建messages语言存放文件夹

image.png

image.png

在app目录下新建 i18n.ts

//  app/config.ts
import { getRequestConfig } from "next-intl/server";
import { notFound } from "next/navigation";
import { locales } from "./config";

export default getRequestConfig(async ({ locale }) => {
  if (!locales.includes(locale as any)) notFound();
  return {
    messages: (await import(`./messages/${locale}.json`)).default,
  };
});

  • getRequestConfig 来自 next-intl/server:这个函数是用于在服务器端渲染(SSR)期间动态加载语言特定消息的关键函数。

  • locales 来自 ./config:这个变量可能从一个单独的配置文件中导入支持的语言环境的数组(例如,['en', 'es', 'fr']

  • return { messages: (await import(./messages/${locale}.json)).default };:这是配置的核心。它定义了如何加载特定于语言的消息:

  • messages:这个键由 next-intl 库用于访问加载的消息。

  • await import(./messages/${locale}.json):这个动态地导入一个包含指定 locale 的翻译消息的 JSON 文件。文件路径通过将 ./messages/locale 连接并添加 .json 扩展名来构建。

在app目录下新建 middleware.ts

import createMiddleware from "next-intl/middleware";
import { locales } from "./config";

export default createMiddleware({
  locales,
  defaultLocale: "en",
});

export const config = {
  // Match only internationalized pathnames
  matcher: ["/", "/(cn|en)/:path*"],
};
  • 定义支持语言: 通过 locales 数组指定应用程序支持的语言。

  • 设置默认语言: 如果用户未设置语言偏好,则使用 defaultLocale 作为默认语言。

  • 匹配国际化路径: 中间件只处理配置的路径,确保只有需要国际化的页面被处理。

  • 处理语言切换: 当用户访问不同的语言路径时,中间件会根据路径中的语言代码来切换语言。

image.png

使用国际化

我们需要在app路由下面新建[locale目录] 用来存放所有的目录文件 把最外面的 以及layout.tsx 移入进来

image.png

重新启动项目,会发现项目的路径会添加上国际化的标识

使用next-intl 的useTranslations 方法去替换原本语言

import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";

export default function Home({ params: { locale } }: Props) {
  const t = useTranslations();
  return (
    <div>
      <Button size="sm" variant="secondary">
        {t("file")}
      </Button>
    </div>
  );
}

使用Link的时候会自动添加上国际化的前缀

当我们要使用next的Link组件进行路由跳转的时候,会发现没有带上国际化的前缀,这个时候就要用next-intl提供的路由方法去替换原生的方法

//app/config.ts

import { Pathnames, LocalePrefix } from "next-intl/routing";

export const locales = ["en", "cn"] as const;

export type Locales = typeof locales;

export const pathnames: Pathnames<Locales> = {
  "/": "/",
  "/pathnames": "/pathnames",
};

export const localePrefix: LocalePrefix<Locales> = "always";

添加navigation.ts去替换原生的路由跳转方法

import { createNavigation } from "next-intl/navigation";
import { localePrefix, locales, pathnames } from "./config";

export const { Link, getPathname, redirect, usePathname, useRouter } =
  createNavigation({
    locales,
    pathnames,
    localePrefix,
  });

在之前的page.tsx 中使用

import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";
import { Link } from "@/navigation";

export default function Home({ params: { locale } }: Props) {
  const t = useTranslations();

  return (
    <div>
      <Button size="sm" variant="secondary">
        {t("file")}
      </Button>
      <Link href="/editor/123">跳转</Link>
    </div>
  );
}

PixPin_2024-12-14_12-48-05.gif

编写国家化切换组件

"use client";

import { cn } from "@/lib/utils";
import { usePathname, useRouter } from "next/navigation";

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useLocale } from "next-intl";
import { locales } from "@/config";
const langIcon = (
  <svg
    viewBox="0 0 24 24"
    focusable="false"
    width="1em"
    height="1em"
    fill="currentColor"
    aria-hidden="true"
  >
    <path d="M0 0h24v24H0z" fill="none" />
    <path
      d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z "
      className="css-c4d79v"
    />
  </svg>
);
const LANG_MAP = {
  en: {
    label: "English",
    icon: "🇺🇸",
  },
  cn: {
    label: "中文",
    icon: "🇨🇳",
  },
} as const;

export default function LocaleSwitch() {
  const currentLocale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  const changeLocale = (locale: string) => {
    if (locale === currentLocale) return;
    const newPath = pathname.replace(`/${currentLocale}`, `/${locale}`);
    return router.push(newPath);
  };

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>{langIcon}</DropdownMenuTrigger>
      <DropdownMenuContent className="w-44">
        {locales.map((locale) => (
          <DropdownMenuItem
            key={locale}
            onClick={() => changeLocale(locale)}
            className={cn(currentLocale === locale && "font-bold")}
          >
            <div className="flex items-center">
              <span className="mr-2">{LANG_MAP[locale].icon}</span>
              <span>{LANG_MAP[locale].label}</span>
            </div>
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

recording.gif

网站title的也随着国际化以及打包分别打包出对应语言的包

import { Inter } from "next/font/google";
import "../globals.css";
import {
  getMessages,
  getTranslations,
  setRequestLocale,
} from "next-intl/server";
import { NextIntlClientProvider } from "next-intl";
import { locales } from "@/config";

const inter = Inter({ subsets: ["latin"] });

export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

export async function generateMetadata({
  params: { locale },
}: {
  params: { locale: string };
}) {
  const t = await getTranslations({
    locale,
  });

  return {
    title: t("title"),
  };
}


export default async function RootLayout({
  children,
  params: { locale },
}: Readonly<{
  children: React.ReactNode;
  params: { locale: string };
}>) {
  setRequestLocale(locale);
  const messages = await getMessages();
  return (
    <html lang={locale}>
      <body className={inter.className}>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";
import { Link } from "@/navigation";
import { setRequestLocale } from "next-intl/server";

type Props = {
  params: {
    locale: string;
  };
};

export default function Home({ params: { locale } }: Props) {
  setRequestLocale(locale);
  const t = useTranslations();

  return (
    <div>
      <Button size="sm" variant="secondary">
        {t("file")}
      </Button>
      <Link href="/editor/123">跳转</Link>
    </div>
  );
}

recording.gif

image.png

项目地址 github.com/luabuCN/can…