此教程适用于比较简单的项目实现,如果你是刚入门next,并且不想用太复杂的方式去实现一个多语言项目,那么这个教程就挺适合你的。

此教程适用于app目录的next项目。

先贴一下参阅的连接:

教程来源于官方

可参阅的代码demo

实现思路

结合文件结构解说一下大致逻辑:

Next.js项目App目录i18n结构
  • i18n-config.ts只是一个全局管理多语言简写的枚举文件,其他文件可以引用这个文件,这样就不会出现不同文件对不上的情况。
  • middleware.ts做了一层拦截,在用户访问localhost:3000的时候能通过请求头判断用户常用的语言,配合app目录多出来的[lang]目录,从而实现跳转到localhost:3000/zh这样。
  • dictionaries文件夹下饭各语言的json字段,通过字段的引用使页面呈现不同的语种。

事实上layout.tsx和page.tsx都会将语言作为参数传入,在对应的文件里,再调用get-dictionaries里的方法就能读取到对应的json文件里的内容了。

具体代码

大致思路如上所述,下面贴对应的代码。

/i18n-config.ts的代码:

// /i18n-config.ts

export const i18n = {

   defaultLocale: "en",

   // locales: ["en", "zh", "es", "hu", "pl"],

   locales: ["en", "zh"],

} as const;

export type Locale = (typeof i18n)["locales"][number];

安装依赖:

npm install @formatjs/intl-localematcher
npm install negotiator

然后才是/middleware.ts的代码

// /middleware.ts

 

import {NextResponse} from "next/server";

import type {NextRequest} from "next/server";

 

import {i18n} from "./i18n-config";

 

import {match as matchLocale} from "@formatjs/intl-localematcher";

// @ts-ignore

import Negotiator from "negotiator";

 

function getLocale(request: NextRequest): string | undefined {

   // Negotiator expects plain object so we need to transform headers

   const negotiatorHeaders: Record<string, string> = {};

   request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

 

   // @ts-ignore locales are readonly

   const locales: string[] = i18n.locales;

 

   // Use negotiator and intl-localematcher to get best locale

   let languages = new Negotiator({headers: negotiatorHeaders}).languages(

       locales,

   );

 

   const locale = matchLocale(languages, locales, i18n.defaultLocale);

 

   return locale;

}

 

export function middleware(request: NextRequest) {

   const pathname = request.nextUrl.pathname;

 

   // // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.

   // // If you have one

   if (

       [

           '/manifest.json',

           '/favicon.ico',

           '/logo.svg',

           '/logo.png',

           '/sitemap.xml'

       ].includes(pathname)

   )

       return

 

 

   // Check if there is any supported locale in the pathname

   const pathnameIsMissingLocale = i18n.locales.every(

       (locale) =>

           !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,

   );

 

   // Redirect if there is no locale

   if (pathnameIsMissingLocale) {

       const locale = getLocale(request);

 

       // e.g. incoming request is /products

       // The new URL is now /en-US/products

       return NextResponse.redirect(

           new URL(

               `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,

               request.url,

           ),

       );

   }

}

 

export const config = {

   // Matcher ignoring `/_next/` and `/api/`

   matcher: ["/((?!ap

/dictionaries下的因项目而异,可以看个参考:

dictionaries目录的demo


/get-dictionaries.ts的代码

 

// /get-dictionaries.ts

 

import "server-only";

import type { Locale } from "./i18n-config";

 

// We enumerate all dictionaries here for better linting and typescript support

// We also get the default import for cleaner types

const dictionaries = {

 en: () => import("./dictionaries/en.json").then((module) => module.default),

 zh: () => import("./dictionaries/zh.json").then((module) => module.default),

};

 

export const getDictionary = async (locale: Locale) => dictionaries[locale]?.() ?? dictionaries.en();
实际使用可以做个参考:


 

实际使用的参考

这样就OK了,大功告成。

关于多语言json文件的管理和翻译

上面介绍的这种方法,需要通过json去管理多语言。事实上在做了多个多语言的项目之后,每次管理这些json是让我很难受的。

每次zh.json多了一些键值对,我都要翻译对应的en.json, ja.json, ru.json 等等。

用gpt翻译的话,翻译到第一个语种还好,翻译到后面的语种gpt就已经忘记了前面的原文了。

用机器翻译的话,又识别不了json格式,得手动把value值复制出来再粘贴回去,真的会死人。

基于这块的考虑我做了个专门针对这种情况的翻译器,有需要的朋友可以体验一下json翻译器

对应的还有markdown翻译器

markdown就更长了,gpt会遗忘上下文,机器翻译长度要求你分成几段,而且翻译出来的结果会丢失一些markdown语法。

这个翻译器会考虑长度问题,可以直接把一整个json或者markdown文件复制进去翻译,一般我的项目都是够用的,如果体验下来觉得还有改进的地方,想提一些建议的朋友可以直接联系网站里的邮箱,看到了的话考虑改进~