最近在用next寫一個(gè)多語言的項(xiàng)目,找了好久沒找到簡單實(shí)現(xiàn)的教程,實(shí)踐起來感覺都比較復(fù)雜,最后終于是在官方文檔找到了,結(jié)合網(wǎng)上找到的代碼demo,終于實(shí)現(xiàn)了,在這里簡單總結(jié)一下。
此教程適用于比較簡單的項(xiàng)目實(shí)現(xiàn),如果你是剛?cè)腴Tnext,并且不想用太復(fù)雜的方式去實(shí)現(xiàn)一個(gè)多語言項(xiàng)目,那么這個(gè)教程就挺適合你的。
此教程適用于app目錄的next項(xiàng)目。
先貼一下參閱的連接:
官方教程: next i18n 文檔
可參閱的代碼demo
實(shí)現(xiàn)思路
結(jié)合文件結(jié)構(gòu)解說一下大致邏輯:
-
i18n-config.ts
只是一個(gè)全局管理多語言簡寫的枚舉文件,其他文件可以引用這個(gè)文件,這樣就不會(huì)出現(xiàn)不同文件對(duì)不上的情況。 -
middleware.ts
做了一層攔截,在用戶訪問localhost:3000
的時(shí)候能通過請(qǐng)求頭判斷用戶常用的語言,配合app目錄多出來的[lang]
目錄,從而實(shí)現(xiàn)跳轉(zhuǎn)到localhost:3000/zh
這樣。 -
dictionaries
文件夾下放各語言的json字段,通過字段的引用使頁面呈現(xiàn)不同的語種。
事實(shí)上每個(gè)頁面的layout.tsx
和page.tsx
都會(huì)將語言作為參數(shù)傳入,在對(duì)應(yīng)的文件里,再調(diào)用get-dictionaries.ts
文件里的方法就能讀取到對(duì)應(yīng)的json文件里的內(nèi)容了。
大致思路是這樣,下面貼對(duì)應(yīng)的代碼。
/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];
/middleware.ts
,需要先安裝兩個(gè)依賴,這兩個(gè)依賴用于判斷用戶常用的語言:
npm install @formatjs/intl-localematcher
npm install negotiator
然后才是/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";
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',
// // Your other files in `public`
// ].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: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
/dictionaries
下的因項(xiàng)目而異,可以看個(gè)參考:
文件以語言簡寫命名,/i18n-config.ts
里的locales
有什么語言,這里就有多少個(gè)對(duì)應(yīng)的文件就行了。
/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();
實(shí)際使用可以做個(gè)參考:
到這里其實(shí)就實(shí)現(xiàn)了,但是下面的事情需要注意:
如果你的項(xiàng)目有集成了第三方需要配知道m(xù)iddleware的地方,比如clerk,需要調(diào)試一下是否沖突。
如果你不知道clerk是什么,那么下面可以不用看,下面將以clerk為例,描述一下可能遇到的問題和解決方案。
Clerk適配
clerk是一個(gè)可以快速登錄的第三方庫,用這個(gè)庫可以快速實(shí)現(xiàn)用戶登錄的邏輯,包括Google、GitHub、郵箱等的登錄。
clerk允許你配置哪些頁面是公開的,哪些頁面是需要登錄之后才能看的,如果用戶沒登錄,但是卻訪問了需要登錄的頁面,就會(huì)返回401,跳轉(zhuǎn)到登錄頁面。
就是這里沖突了,因?yàn)槲覀儗?shí)現(xiàn)多語言的邏輯是,用戶訪問localhost:3000
的時(shí)候判斷用戶常用的語言,從而實(shí)現(xiàn)跳轉(zhuǎn)到localhost:3000/zh
這樣。
這兩者實(shí)現(xiàn)都在middleware.ts
文件中,上面這種配置會(huì)有沖突,這兩者只有一個(gè)能正常跑通,而我們想要的效果是兩者都能跑通,既能自動(dòng)跳轉(zhuǎn)到登錄頁面,也能自動(dòng)跳轉(zhuǎn)到常用語言頁面。
技術(shù)問題定位:這是因?yàn)槟阒貙懥薽iddleware方法,導(dǎo)致不會(huì)執(zhí)行Clerk的authMiddleware方法,視覺效果上,就是多語言導(dǎo)致了Clerk不會(huì)自動(dòng)跳轉(zhuǎn)登錄。
所以要把上面的middleware方法寫到authMiddleware方法里的beforeAuth里去,Clerk官方有說明: Clerk authMiddleware說明
所以現(xiàn)在/middleware.ts文件內(nèi)的內(nèi)容變成了:文章來源:http://www.zghlxwxcb.cn/news/detail-855083.html
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { authMiddleware } from "@clerk/nextjs";
import { i18n } from "./i18n-config";
import { match as matchLocale } from "@formatjs/intl-localematcher";
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 const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
// matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
export default authMiddleware({
publicRoutes: ['/anyone-can-visit-this-route'],
ignoredRoutes: ['/no-auth-in-this-route'],
beforeAuth: (request) => {
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',
'/serviceWorker.js',
'/en/sign-in'
// Your other files in `public`
].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,
),
);
}
}
});
這樣就OK了,大功告成。文章來源地址http://www.zghlxwxcb.cn/news/detail-855083.html
到了這里,關(guān)于next.js app目錄 i18n國際化簡單實(shí)現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!