• AaronConlon.dev
  • Talk
  • Weekly
  • 工具
  • 编程
  • 翻译
  • 资讯
  • 通识

Next.js15 + i18n 初体验

2024年10月24上次更新于 将近 2 年前
编程

我需要为我的 nextjs 项目添加 i18n 功能,从而更好地进行推广和服务。于是我尝试使用 next-intl库来完成这个功能,本文记录了我接入这个库的整个过程。

版本信息

  • nextjs 15

    • app 路由

    • 子域名方案

  • next-intl 3.23.4

起步

首先安装依赖 next-intl,在根目录下创建对应的目录:


├── messages

│   ├── en.json (1)

│   └── ...

├── next.config.mjs (2)

└── src

    ├── i18n

    │   ├── routing.ts (3)

    │   └── request.ts (5)

    ├── middleware.ts (4)

    └── app

        └── [locale]

            ├── layout.tsx (6)

            └── page.tsx (7)

其次,创建翻译文件 messages/en.json和其他语言的翻译文件:


// en.json

{

  "HomePage": {

    "title": "Aaron's Chrome Extension Store",

    "description": "A collection of Chrome extensions created by Aaron.",

    "hello": "Hello, world!"

  },

  "Metadata": {

    "title": "Aaron's Chrome Extension Store",

    "description": "A curated store of excellent extensions maintained and independently developed by Aaron.",

    "keywords": "Chrome extensions, Aaron, extension store"

  }

}



// zh.json

{

  "HomePage": {

    "title": "亚伦的 Chrome 扩展商店",

    "description": "亚伦创建的 Chrome 扩展集合。",

    "hello": "你好,世界!"

  },

  "Metadata": {

    "title": "亚伦的 Chrome 扩展商店",

    "description": "亚伦精心维护和独立开发的优秀扩展程序商店。",

    "keywords": "Chrome 扩展, 亚伦, 扩展商店"

  },

  "Locales": {

    "en": "英语",

    "es": "西班牙语",

    "fr": "法语",

    "zh": "中文"

  },

  "Common": {

    "language": "语言"

  }

}

再次,配置 next.config.ts文件:


import createNextIntlPlugin from 'next-intl/plugin';

 

const withNextIntl = createNextIntlPlugin();

 

/** @type {import('next').NextConfig} */

const nextConfig = {};

 

export default withNextIntl(nextConfig);

然后,配置 i18n的路由文件 src/i18n/routing.ts:


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

import {createNavigation} from 'next-intl/navigation';

 

export const routing = defineRouting({

  // A list of all locales that are supported

  locales: ['en', 'zh'],

 

  // Used when no locale matches

  defaultLocale: 'en'

});

 

// Lightweight wrappers around Next.js' navigation APIs

// that will consider the routing configuration

export const {Link, redirect, usePathname, useRouter} =

  createNavigation(routing);

再然后,创建中间件 src/middleware.ts:


import createMiddleware from 'next-intl/middleware';

import {routing} from './i18n/routing';

 

export default createMiddleware(routing);

 

export const config = {

  // Match only internationalized pathnames

  matcher: ['/', '/(zh|en)/:path*']

};

由 next-intl提供的创建中间件的函数配合多语言的路由,即可创建一个中间件。与此同时,导出的 config对象限定了这个中间件的匹配范围。

然后,我们还需要创建一个 src/i18n/request.ts文件来提供一个函数来获取语言和对应语言的翻译文件。


import {getRequestConfig} from 'next-intl/server';

import {routing} from './routing';

 

export default getRequestConfig(async ({requestLocale}) => {

  // This typically corresponds to the `[locale]` segment

  let locale = await requestLocale;

 

  // Ensure that a valid locale is used

  if (!locale || !routing.locales.includes(locale as any)) {

    locale = routing.defaultLocale;

  }

 

  return {

    locale,

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

  };

});

接下来,创建 src/app/[locale]/layout.tsx布局组件:


import { routing } from '@/i18n/routing';

import { NextIntlClientProvider } from 'next-intl';

import { getMessages } from 'next-intl/server';

import localFont from "next/font/local";

import { notFound } from 'next/navigation';

import "../globals.css";

import SiteHeader from '../view/SiteHeader';



const geistSans = localFont({

  src: "../fonts/GeistVF.woff",

  variable: "--font-geist-sans",

  weight: "100 900",

});

const geistMono = localFont({

  src: "../fonts/GeistMonoVF.woff",

  variable: "--font-geist-mono",

  weight: "100 900",

});



export default async function LocaleLayout({

  children,

  params

}: {

  children: React.ReactNode;

  params: Promise<{ locale: string }>;

}) {

  const { locale } = await params;

  // Ensure that the incoming `locale` is valid

  if (!routing.locales.includes(locale as any)) {

    notFound();

  }



  // Providing all messages to the client

  // side is the easiest way to get started

  const messages = await getMessages();

  return (

    <html lang={locale}>

      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>

        <NextIntlClientProvider messages={messages}>

          <SiteHeader />

          {children}

        </NextIntlClientProvider>

      </body>

    </html>

  );

}

这里我们提供了一个 NextIntlClientProvider组件,将翻译信息传递了下去。

其中 <SiteHeader>单独拆成一个组件,是因为 useTranslations钩子必须在非异步组件内使用。

这里我将默认的 src/app/layout.tsx删掉了,然后在 src/app/page.tsx中做了重定向:


import { redirect } from 'next/navigation';



// This page only renders when the app is built statically (output: 'export')

export default function RootPage() {

  redirect('/en');

}

访问根目录时自动重定向到 /en路由下,并且经过中间件的处理。

进入到不同的路由下会使得当前的语言被写入到客户端 cookie中的 NEXT_LOCALE字段,下次请求时带上此字段,next-intl就可以确定语言了。

编写页面

现在来编写 src/app/[locale]/page.tsx:


import { useTranslations } from 'next-intl';



export default function HomePage() {

  const t = useTranslations('homepage');

  return (

    <div className='flex flex-col items-center m-12 gap-4'>

      <h1>{t('hello')}</h1>

    </div>

  );

}

此时,我们进入 /en和 /zh两个路由下的页面如下:

中文:

英文:

静态渲染

目前版本的 next-intl仅在服务器组件中使用 useTranslations来实现多语言,未来将会消除这个限制,让客户端组件也能自由切换多语言。

在静态渲染时,next-intl提供了一个临时的 api 来启用多语言功能。

由于我们使用了 [locale]动态路由,因此我们需要通过 generateStaticParams函数,将所有可能的语言路由传给 nextjs,以便于在构建时渲染。

在 app/[locale]/layout.tsx组件中添加代码:


export function generateStaticParams() {

  return routing.locales.map((locale) => ({locale}));

}

显然 routing来源于我们的 i18n/routing,声明了我们支持的哪些语言。

此外,我们还需要在布局组件中显式地声明启用静态渲染多语言:


...

...



export default async function LocaleLayout({

  children,

  params

}: {

  children: React.ReactNode;

  params: Promise<{ locale: string }>;

}) {

  const { locale } = await params;

  ...

  // Enable static rendering

  setRequestLocale(locale);

  ...

}

我们需要在要启用静态渲染的每个页面和每个布局中调用此函数,因为 Next.js 可以独立渲染布局和页面。

setRequestLocale函数让 nextjs知悉当前页面的语言。

这听起来有点模糊,目前 nextjs并没有提供任何 API 来从路由参数中读取类似 locale 这样的内容。而语言是 next-intl的关键信息,这部分功能只能通过 next-intl来增强。

关键点:

  1. 通常在根布局(root layout)调用一次就足够了

  2. 如果页面需要在生成静态参数或元数据时使用翻译,那么该页面也需要调用

  3. 普通组件(包括服务端组件和客户端组件)不需要调用

  4. 这个函数的调用应该总是在使用任何翻译功能之前

举个例子:


app/

├── [locale]/           # 语言路由必须在这一层

│   ├── layout.tsx     # ✅ 在这里调用 setRequestLocale

│   ├── page.tsx

│   └── about/

│       └── page.tsx   # ❌ 不需要重复调用,已被 [locale]/layout.tsx 覆盖

│

└── layout.tsx         # ❌ 在这里调用无效,因为在语言路由之外

在 metadata 中使用多语言

在完成了上述的配置后,我们可以在 generateMetadata函数的参数中得到 locale,在此函数内部使用 getTranslations函数获取翻译功能的 t函数。


export async function generateMetadata({ params }: {

  params: Promise<{

    locale: string

  }>

}) {

  const { locale } = await params

  const t = await getTranslations({ locale, namespace: 'Metadata' });



  return {

    title: t('title'),

    description: t('description'),

    keywords: t('keywords'),

  };

}

如此一来便可以在不同语言录音下的页面添加多语言的 metadata 信息了。

启用 TypeScript 支持

next-intl仅需要我们在根目录创建一个global.d.ts文件,声明类型即可:


import en from "./messages/en.json";



type Messages = typeof en;



declare global {

  // Use type safe message keys with `next-intl`

  type IntlMessages = Messages;

}

后续使用t()的时候编辑器就能提供候选补全了。

编写切换语言功能

首先,我们创建一个 SiteHeader组件:


import { useLocale, useTranslations } from "next-intl";

import Link from "next/link";

import { PiGoogleChromeLogoDuotone } from "react-icons/pi";

import { routing } from '../../../i18n/routing';

import LanguageSwitch from "./LanguageSwitch";



export default function SiteHeader() {

  const t = useTranslations('HomePage');

  const locale = useLocale();

  return (

    <div className="mx-auto">

      <div className='flex items-center gap-4 2xl:w-[1360px] p-4'>

        {/* add website header here */}

        <Link href={'/'} locale={locale} className="flex items-center gap-1">

          <PiGoogleChromeLogoDuotone className=' text-xl 2xl:text-3xl text-blue-600' />

          <h1 className="mr-auto">{t('title')}</h1>

        </Link>

        <div className="mr-auto">

          {/* add other nav */}

        </div>

        {/* select to switch language */}

        <LanguageSwitch locales={routing.locales as unknown as string[]} />

      </div>

    </div>

  )

}

以及一个核心切换的客户端组件:


"use client"



import clsx from "clsx";

import { useLocale, useTranslations } from "next-intl";

import Link from "next/link";

import { usePathname } from "next/navigation";

import { LiaLanguageSolid } from "react-icons/lia";



export default function LanguageSwitch({ locales }: { locales: string[] }) {

  const pathname = usePathname()

  const locale = useLocale()



  const t = useTranslations()



  return (

    <div className="relative group">

      <div className="flex items-center gap-1 bg-blue-500 rounded-full px-2 p-0.5 text-white">

        <LiaLanguageSolid /> {t('Common.language')}

      </div>

      <div className="absolute top-[100%] flex-col bg-gray-50/80 inset-x-0 rounded-md p-1 gap-1 border hidden group-hover:flex">

        {locales.map((lang) => (

          <Link className={clsx(lang === locale ? 'text-blue-600 bg-white' : 'text-gray-600', 'text-sm px-1 hover:bg-white')} key={lang} href={pathname.replace(/^\/[^/]+/, `/${lang}`)} locale={lang}>

            {t(`Locales.${lang}`)}

          </Link>

        ))}

      </div>

    </div>



  )

}

最后的效果如下,在鼠标移动到上面时通过 tailwindcss控制切换标签的显示和隐藏,语言数组从配置文件中导出,再使用 Link组件配合 locale属性就能在点击的时候切换到不同的语言路由下了:

最后

好了,今天分享就到这里。

我预计需要一些时间完成这个网站,我打算把自己开发的所有浏览器插件全部放到这上面,再加上用户体系控制,分享一些我开发的免费的简单插件和部分预计可以收费的核心插件。

为了记录这个过程,我会把遇到的问题和解决方案都发布在公众号上,欢迎阅读和分享。

再会。

not-by-ainot-by-ai
已读 0%
文章推荐
文章封面
TypeScript 中的 never、unknown 的使用时机
更新于 2024-09-24
编程
文章封面
为什么我的 Tailwind css 类没生效
更新于 2025-02-13
编程
GitHubTwitter / XMediumDevJuejin

友情链接

Jimmy
Jimmy
老胡
老胡
Submara
Submara
Bruce Song
Bruce Song
Scarsu
Scarsu
宇阳
宇阳
Steven Lynn's Blog
Steven Lynn's Blog
OJ·Jimmy (Other Jimmy)
OJ·Jimmy (Other Jimmy)
liruifengv - Web 开发者,Astro 项目成员,开源爱好者。
liruifengv - Web 开发者,Astro 项目成员,开源爱好者。