Đặt câu hỏi và nhận tóm tắt tài liệu bằng cách tham chiếu trang này và nhà cung cấp AI bạn chọn
Bằng cách tích hợp Intlayer MCP Server vào trợ lý AI ưa thích của bạn, bạn có thể truy xuất toàn bộ tài liệu trực tiếp từ ChatGPT, DeepSeek, Cursor, VSCode, v.v.
Xem tài liệu MCP ServerNội dung của trang này đã được dịch bằng AI.
Xem phiên bản mới nhất của nội dung gốc bằng tiếng AnhNếu bạn có ý tưởng để cải thiện tài liệu này, vui lòng đóng góp bằng cách gửi pull request trên GitHub.
Liên kết GitHub tới tài liệuSao chép Markdown của tài liệu vào bộ nhớ tạm
Dịch website Next.js 15 sử dụng next-i18next với Intlayer | Quốc tế hóa (i18n)
Hướng dẫn này dành cho ai
- Junior: Làm theo chính xác các bước và sao chép các khối mã. Bạn sẽ có một ứng dụng đa ngôn ngữ hoạt động.
- Mid-level: Sử dụng các danh sách kiểm tra và các lưu ý về thực hành tốt nhất để tránh những sai sót phổ biến.
- Senior: Lướt qua cấu trúc tổng thể, các phần SEO và tự động hóa; bạn sẽ tìm thấy các thiết lập mặc định hợp lý và các điểm mở rộng.
Những gì bạn sẽ xây dựng
- Dự án App Router với các tuyến đường được địa phương hóa (ví dụ: /, /fr/...)
- Cấu hình i18n với các locale, locale mặc định, hỗ trợ RTL
- Khởi tạo i18n phía server và một provider phía client
- Các bản dịch theo namespace được tải theo yêu cầu
- SEO với hreflang, sitemap và robots được địa phương hóa
- Middleware cho việc định tuyến theo locale
- Tích hợp Intlayer để tự động hóa quy trình dịch thuật (kiểm thử, điền AI, đồng bộ JSON)
Lưu ý: next-i18next được xây dựng dựa trên i18next. Hướng dẫn này sử dụng các primitive của i18next tương thích với next-i18next trong App Router, đồng thời giữ cho kiến trúc đơn giản và sẵn sàng cho môi trường production. Để so sánh rộng hơn, xem next-i18next vs next-i18next vs Intlayer.
1) Cấu trúc dự án
Cài đặt các dependencies của next-i18next -
npm install next-i18next i18next react-i18next i18next-resources-to-backendBắt đầu với một cấu trúc rõ ràng. Giữ các thông điệp được tách theo locale và namespace.
.├── i18n.config.ts└── src ├── locales │ ├── en │ │ ├── common.json │ │ └── about.json │ └── fr │ ├── common.json │ └── about.json ├── app │ ├── i18n │ │ └── server.ts │ └── [locale] │ ├── layout.tsx │ └── about.tsx └── components ├── I18nProvider.tsx ├── ClientComponent.tsx └── ServerComponent.tsxDanh sách kiểm tra (trung cấp/cao cấp):
- Giữ một file JSON cho mỗi namespace mỗi locale
- Không tập trung quá nhiều thông điệp; sử dụng các namespace nhỏ, theo trang/tính năng
- Tránh import tất cả các locale cùng lúc; chỉ tải những gì bạn cần
2) Cài đặt dependencies
bashpnpm add i18next react-i18next i18next-resources-to-backendNếu bạn dự định sử dụng các API hoặc cấu hình tương tác của next-i18next, cũng cần:
pnpm add next-i18next3) Cấu hình i18n cốt lõi
Định nghĩa các locales, locale mặc định, RTL, và các hàm trợ giúp cho các đường dẫn/URL có địa phương hóa.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export const rtlLocales = ["ar", "he", "fa", "ur"] as const;export const isRtl = (locale: string) => (rtlLocales as readonly string[]).includes(locale);export function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";export function abs(locale: string, path: string) { return ORIGIN + localizedPath(locale, path);}Ghi chú chuyên sâu: Nếu bạn sử dụng next-i18next.config.js, hãy giữ cho nó đồng bộ với i18n.config.ts để tránh lệch pha.
4) Khởi tạo i18n phía server
Khởi tạo i18next trên server với backend động chỉ nhập khẩu các JSON locale/namespace cần thiết.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";// Tải tài nguyên JSON từ src/locales/<locale>/<namespace>.jsonconst backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));export async function initI18next( locale: string, namespaces: string[] = ["common"]) { const i18n = createInstance(); await i18n .use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i18n;}Ghi chú giữa: Giữ danh sách namespace ngắn cho mỗi trang để giới hạn payload. Tránh các gói “bắt tất cả” toàn cục.
5) Provider phía client cho các component React
Bao bọc các component phía client với một provider phản chiếu cấu hình phía server và chỉ tải các namespace được yêu cầu.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
"use client";import * as React from "react";import { I18nextProvider } from "react-i18next";import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";const backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));type Props = { locale: string; namespaces?: string[]; resources?: Record<string, any>; // { ns: bundle } children: React.ReactNode;};export default function I18nProvider({ locale, namespaces = ["common"], resources, children,}: Props) { const [i18n] = React.useState(() => { const i = createInstance(); i.use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, resources: resources ? { [locale]: resources } : undefined, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i; }); return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;}Mẹo cho người mới: Bạn không cần phải truyền tất cả các thông điệp đến client. Hãy bắt đầu chỉ với các namespace của trang.
6) Bố cục và đường dẫn địa phương hóa
Đặt ngôn ngữ và hướng, đồng thời tạo trước các đường dẫn theo từng locale để ưu tiên render tĩnh.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { ReactNode } from "react";import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";export const dynamicParams = false;export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const locale: Locale = (locales as readonly string[]).includes(params.locale) ? (params.locale as any) : defaultLocale; const dir = isRtl(locale) ? "rtl" : "ltr"; return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}7) Ví dụ trang với sử dụng server + client
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import I18nProvider from "@/components/I18nProvider";import { initI18next } from "@/app/i18n/server";import type { Locale } from "@/i18n.config";import ClientComponent from "@/components/ClientComponent";import ServerComponent from "@/components/ServerComponent";// Ép buộc render tĩnh cho trangexport const dynamic = "force-static";export default async function AboutPage({ params: { locale },}: { params: { locale: Locale };}) { const namespaces = ["common", "about"] as const; const i18n = await initI18next(locale, [...namespaces]); const tAbout = i18n.getFixedT(locale, "about"); return ( <I18nProvider locale={locale} namespaces={[...namespaces]}> <main> <h1>{tAbout("title")}</h1> <ClientComponent /> <ServerComponent t={tAbout} locale={locale} count={0} /> </main> </I18nProvider> );}Bản dịch (mỗi JSON cho một namespace dưới src/locales/...):
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
{ "title": "Giới thiệu", "description": "Mô tả trang giới thiệu", "counter": { "label": "Bộ đếm", "increment": "Tăng" }}Sao chép đoạn mã vào khay nhớ tạm (clipboard)
{ "title": "À propos", "description": "Description de la page À propos", "counter": { "label": "Compteur", "increment": "Incrémenter" }}Component phía client (chỉ tải namespace cần thiết):
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
"use client";import React, { useState } from "react";import { useTranslation } from "react-i18next";const ClientComponent = () => { const { t, i18n } = useTranslation("about"); const [count, setCount] = useState(0); const numberFormat = new Intl.NumberFormat(i18n.language); return ( <div> <p>{numberFormat.format(count)}</p> <button aria-label={t("counter.label")} onClick={() => setCount((c) => c + 1)} > {t("counter.increment")} </button> </div> );};export default ClientComponent;Đảm bảo trang/provider chỉ bao gồm các namespace bạn cần (ví dụ: about). Nếu bạn sử dụng React < 19, hãy ghi nhớ các bộ định dạng nặng như Intl.NumberFormat.
Synchronous server component được nhúng dưới một client boundary:
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
type ServerComponentProps = { t: (key: string) => string; locale: string; count: number;};const ServerComponent = ({ t, locale, count }: ServerComponentProps) => { const formatted = new Intl.NumberFormat(locale).format(count); return ( <div> <p>{formatted}</p> <button aria-label={t("counter.label")}>{t("counter.increment")}</button> </div> );};export default ServerComponent;8) SEO: Metadata, Hreflang, Sitemap, Robots
Dịch nội dung là một cách để mở rộng phạm vi tiếp cận. Thiết lập SEO đa ngôn ngữ một cách kỹ lưỡng.
Các thực hành tốt nhất:
- Đặt lang và dir tại phần gốc
- Thêm alternates.languages cho mỗi locale (+ x-default)
- Liệt kê các URL đã dịch trong sitemap.xml và sử dụng hreflang
- Loại trừ các khu vực riêng tư đã được địa phương hóa (ví dụ: /fr/admin) trong robots.txt
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; // Nhập đúng gói JSON từ src/locales const messages = (await import("@/locales/" + locale + "/about.json")) .default; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, "/about")]) ); return { title: messages.title, description: messages.description, alternates: { canonical: localizedPath(locale, "/about"), languages: { ...languages, "x-default": "/about" }, }, };}export default async function AboutPage() { return <h1>About</h1>;}Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export default function sitemap(): MetadataRoute.Sitemap { const languages = Object.fromEntries( locales.map((locale) => [locale, abs(locale, "/about")]) ); return [ { url: abs(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", // tần suất thay đổi: hàng tháng priority: 0.7, // độ ưu tiên alternates: { languages }, // các phiên bản ngôn ngữ thay thế }, ];}Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { MetadataRoute } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";const ORIGIN = "https://example.com";const expandAllLocales = (path: string) => [ localizedPath(defaultLocale, path), ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => localizedPath(locale, path)),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...expandAllLocales("/dashboard"), ...expandAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: ORIGIN, sitemap: ORIGIN + "/sitemap.xml", };}9) Middleware cho định tuyến locale
Phát hiện locale và chuyển hướng đến đường dẫn có locale nếu thiếu.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import { NextResponse, type NextRequest } from "next/server";import { defaultLocale, locales } from "@/i18n.config";const PUBLIC_FILE = /\.[^/]+$/; // loại trừ các file có phần mở rộngexport function middleware(request: NextRequest) { const { pathname } = request.nextUrl; if ( pathname.startsWith("/_next") || pathname.startsWith("/api") || pathname.startsWith("/static") || PUBLIC_FILE.test(pathname) ) { return; } const hasLocale = locales.some( (locale) => pathname === "/" + locale || pathname.startsWith("/" + locale + "/") ); if (!hasLocale) { const locale = defaultLocale; const url = request.nextUrl.clone(); url.pathname = "/" + locale + (pathname === "/" ? "" : pathname); return NextResponse.redirect(url); }}export const config = { matcher: [ // Khớp tất cả các đường dẫn ngoại trừ những đường dẫn bắt đầu bằng các từ này và các tệp có phần mở rộng "/((?!api|_next|static|.*\\..*).*)", ],};10) Hiệu suất và các thực hành tốt nhất cho trải nghiệm nhà phát triển (DX)
- Đặt thuộc tính lang và dir cho html: Đã thực hiện trong src/app/[locale]/layout.tsx.
- Phân tách thông điệp theo namespace: Giữ các gói nhỏ (common.json, about.json, v.v.).
- Giảm thiểu payload phía client: Trên các trang, chỉ truyền các namespace cần thiết cho provider.
- Ưu tiên các trang tĩnh: Sử dụng export const dynamic = 'force-static' và generateStaticParams cho từng locale.
- Đồng bộ các component server: Truyền các chuỗi/định dạng đã tính trước thay vì gọi async khi render.
- Ghi nhớ các thao tác nặng: Đặc biệt trong mã client cho các phiên bản React cũ hơn.
- Bộ nhớ đệm và header: Ưu tiên tĩnh hoặc revalidate thay vì render động khi có thể.
11) Kiểm thử và CI
- Thêm unit test cho các component sử dụng t để đảm bảo các key tồn tại.
- Xác thực rằng mỗi namespace có cùng các khóa trên tất cả các locale.
- Hiển thị các khóa còn thiếu trong quá trình CI trước khi triển khai.
Intlayer sẽ tự động hóa phần lớn công việc này (xem phần tiếp theo).
12) Thêm Intlayer lên trên (tự động hóa)
Intlayer giúp bạn giữ đồng bộ các bản dịch JSON, kiểm tra các khóa còn thiếu và điền bằng AI khi cần.
Cài đặt các dependencies của intlayer:
npm install intlayer @intlayer/sync-json-plugin -DSao chép đoạn mã vào khay nhớ tạm (clipboard)
import { type IntlayerConfig, Locales } from "intlayer";import { locales, defaultLocale } from "@/i18n";import { syncJSON } from "@intlayer/sync-json";export const locales = [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH];const config: IntlayerConfig = { internationalization: { locales, defaultLocale, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ syncJSON({ source: ({ locale }) => `./locales/${locale}.json`, }), ],};export default config;Thêm các script cho package:
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}Các luồng phổ biến:
- pnpm i18n:test trong CI để làm cho build thất bại khi thiếu key
- pnpm i18n:fill chạy cục bộ để đề xuất bản dịch AI cho các key mới được thêm vào
Bạn có thể cung cấp các đối số CLI; xem thêm tại Tài liệu CLI của Intlayer.
13) Khắc phục sự cố
- Không tìm thấy khóa: Đảm bảo trang/nhà cung cấp liệt kê đúng các namespace và tệp JSON tồn tại trong src/locales/<locale>/<namespace>.json.
- Ngôn ngữ sai/lỗi nháy tiếng Anh: Kiểm tra kỹ việc phát hiện locale trong middleware.ts và lng của nhà cung cấp.
- Vấn đề bố cục RTL: Xác nhận dir được lấy từ isRtl(locale) và CSS của bạn tuân thủ [dir="rtl"].
- Thiếu các alternate SEO: Xác nhận alternates.languages bao gồm tất cả các locale và x-default.
- Gói quá lớn: Chia nhỏ các namespace hơn nữa và tránh nhập toàn bộ cây locales trên client.
14) Tiếp theo
- Thêm nhiều locale và namespace hơn khi các tính năng phát triển
- Địa phương hóa các trang lỗi, email và nội dung được điều khiển bởi API
- Mở rộng quy trình làm việc của Intlayer để tự động mở PR cập nhật bản dịch
Nếu bạn muốn một mẫu khởi đầu, hãy thử template: https://github.com/aymericzip/intlayer-next-i18next-template.