XQuery 是 XML 數據的查詢語言,類似於 SQL 是資料庫的查詢語言。它被設計用於查詢 XML 數據。 XQuery 示例 for $x in doc("books.xml")/bookstore/book where $x/price > 30 order by $x/title retu ...
最近在用next寫一個多語言的項目,找了好久沒找到簡單實現的教程,實踐起來感覺都比較複雜,最後終於是在官方文檔找到了,結合網上找到的代碼demo,終於實現了,在這裡簡單總結一下。
此教程適用於比較簡單的項目實現,如果你是剛入門next,並且不想用太複雜的方式去實現一個多語言項目,那麼這個教程就挺適合你的。
此教程適用於app目錄的next項目。
先貼一下參閱的連接:
實現思路
結合文件結構解說一下大致邏輯:
i18n-config.ts
只是一個全局管理多語言簡寫的枚舉文件,其他文件可以引用這個文件,這樣就不會出現不同文件對不上的情況。middleware.ts
做了一層攔截,在用戶訪問localhost:3000
的時候能通過請求頭判斷用戶常用的語言,配合app目錄多出來的[lang]
目錄,從而實現跳轉到localhost:3000/zh
這樣。dictionaries
文件夾下放各語言的json欄位,通過欄位的引用使頁面呈現不同的語種。
事實上每個頁面的layout.tsx
和page.tsx
都會將語言作為參數傳入,在對應的文件里,再調用get-dictionaries.ts
文件里的方法就能讀取到對應的json文件里的內容了。
大致思路是這樣,下麵貼對應的代碼。
/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
,需要先安裝兩個依賴,這兩個依賴用於判斷用戶常用的語言:
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
下的因項目而異,可以看個參考:
文件以語言簡寫命名,/i18n-config.ts
里的locales
有什麼語言,這裡就有多少個對應的文件就行了。
/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();
實際使用可以做個參考:
到這裡其實就實現了,但是下麵的事情需要註意:
如果你的項目有集成了第三方需要配知道middleware的地方,比如clerk,需要調試一下是否衝突。
如果你不知道clerk是什麼,那麼下麵可以不用看,下麵將以clerk為例,描述一下可能遇到的問題和解決方案。
Clerk適配
clerk是一個可以快速登錄的第三方庫,用這個庫可以快速實現用戶登錄的邏輯,包括Google、GitHub、郵箱等的登錄。
clerk允許你配置哪些頁面是公開的,哪些頁面是需要登錄之後才能看的,如果用戶沒登錄,但是卻訪問了需要登錄的頁面,就會返回401,跳轉到登錄頁面。
就是這裡衝突了,因為我們實現多語言的邏輯是,用戶訪問localhost:3000
的時候判斷用戶常用的語言,從而實現跳轉到localhost:3000/zh
這樣。
這兩者實現都在middleware.ts
文件中,上面這種配置會有衝突,這兩者只有一個能正常跑通,而我們想要的效果是兩者都能跑通,既能自動跳轉到登錄頁面,也能自動跳轉到常用語言頁面。
技術問題定位:這是因為你重寫了middleware方法,導致不會執行Clerk的authMiddleware方法,視覺效果上,就是多語言導致了Clerk不會自動跳轉登錄。
所以要把上面的middleware方法寫到authMiddleware方法里的beforeAuth里去,Clerk官方有說明: Clerk authMiddleware說明
所以現在/middleware.ts文件內的內容變成了:
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了,大功告成。