此教程适用于比较简单的项目实现,如果你是刚入门next,并且不想用太复杂的方式去实现一个多语言项目,那么这个教程就挺适合你的。
此教程适用于app目录的next项目。
先贴一下参阅的连接:
实现思路
结合文件结构解说一下大致逻辑:

- 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下的因项目而异,可以看个参考:

/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文件复制进去翻译,一般我的项目都是够用的,如果体验下来觉得还有改进的地方,想提一些建议的朋友可以直接联系网站里的邮箱,看到了的话考虑改进~