多國語言支持指南 - 使用 next-intl 在 Next.js 中實現國際化

__Notes
Next.jsInternationalizationi18nnext-intlFrontend

什麼是 next-intl?

next-intl 是一個專為 Next.js 設計的國際化(i18n)解決方案,它整合了 Next.js 的 App Router,讓開發者能夠輕鬆地為应用添加多語言支持。透過 next-intl,可以使用動態路由或子域名路由來管理不同語言的內容,並提供豐富的本地化功能,如翻譯管理、日期和時間格式化等。

為什麼選擇 next-intl?

  • 簡單易用:與 Next.js 無縫集成,設定與使用都非常直觀。
  • 高效性能:支持靜態渲染和服務器端渲染,確保應用性能。
  • 靈活配置:支持多種路由策略,包括前綴路由和域名路由。
  • 豐富功能:內建翻譯、日期時間格式化、數字格式化等多種本地化功能。

如何在 Next.js 中設置 next-intl

以下是使用 next-intl 在 Next.js 中實現國際化的步驟:

1. 安裝 next-intl

首先,確保您已經有一個使用 App Router 的 Next.js 應用。然後,運行以下命令來安裝 next-intl

bash
npm install next-intl

2. 配置文件結構

按照 next-intl 的推薦,創建以下文件結構:

folder
├── messages
│   ├── en.json
│   └── zh-TW.json
├── next.config.mjs
└── src
    ├── i18n
    │   ├── routing.ts
    │   └── request.ts
    ├── middleware.ts
    └── app
        └── [locale]
            ├── layout.tsx
            └── page.tsx

3. 創建翻譯文件

messages 資料夾中,為每種語言創建對應的 JSON 文件。例如,en.jsonzh-TW.json

messages/en.json
json
{
    "HomePage": {
        "title": "Hello World!",
        "about": "Go to the about page"
    }
}
messages/zh-TW.json
json
{
    "HomePage": {
        "title": "你好,世界!",
        "about": "前往關於頁面"
    }
}

4. 配置 next.config.mjs

設置 next-intl 插件以提供請求特定的 i18n 配置:

src/app/layout.tsx
typescript
import createNextIntlPlugin from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin();
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 其他 Next.js 配置
};
 
export default withNextIntl(nextConfig);

5. 配置路由

src/i18n/routing.ts 中定義支持的語言和默認語言:

src/i18n/routing.ts
typescript
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
 
export const routing = defineRouting({
  locales: ['en', 'zh-TW'],
  defaultLocale: 'en'
});
 
export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

6. 設置中間件

src/middleware.ts 中設置中間件以處理語言協商和路由重寫:

src/middleware.ts
typescript
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
 
export default createMiddleware(routing);
 
export const config = {
  matcher: ['/((?!api|_next/static|favicon.ico).*)'],
};

7. 配置請求

src/i18n/request.ts 中設置請求配置,以便在服務器組件中使用本地化訊息:

src/i18n/request.ts
typescript
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
 
export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;
 
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }
 
  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});

8. 配置布局

src/app/[locale]/layout.tsx 中配置 NextIntlClientProvider 以提供翻譯訊息給客戶端組件:

src/app/[locale]/layout.tsx
typescript
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
 
export default async function LocaleLayout({
  children,
  params: { locale }
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  if (!routing.locales.includes(locale as any)) {
    notFound();
  }
 
  const messages = await getMessages();
 
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

9. 配置頁面

src/app/[locale]/page.tsx 中使用 useTranslations 來顯示翻譯內容:

src/app/[locale]/page.tsx
typescript
import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/routing';
 
export default function HomePage() {
  const t = useTranslations('HomePage');
  return (
    <div>
      <h1>{t('title')}</h1>
      <Link href="/about">{t('about')}</Link>
    </div>
  );
}

10. 設置靜態參數

為了支持靜態渲染,添加 generateStaticParams 函數:

src/app/[locale]/layout.tsx
typescript
import { routing } from '@/i18n/routing';
import { setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
 
export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}
 
export default async function LocaleLayout({ children, params: { locale } }) {
  if (!routing.locales.includes(locale as any)) {
    notFound();
  }
 
  setRequestLocale(locale);
 
  const messages = await getMessages();
 
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

11. 更新 metadata

確保每個頁面的 metadata 支持多語言:

src/app/[locale]/page.tsx
typescript
import { getTranslations } from 'next-intl/server';
 
export async function generateMetadata({ params: { locale } }) {
  const t = await getTranslations({ locale, namespace: 'Metadata' });
 
  return {
    title: t('title')
  };
}

參考來源