Đặ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-intl với Intlayer | Quốc tế hóa (i18n)
Hướng dẫn này sẽ dẫn bạn qua các thực hành tốt nhất của next-intl trong ứng dụng Next.js 15 (App Router), và chỉ cách tích hợp Intlayer lên trên để quản lý dịch thuật và tự động hóa mạnh mẽ.
Xem so sánh tại next-i18next vs next-intl vs Intlayer.
- Dành cho người mới: làm theo từng bước để có một ứng dụng đa ngôn ngữ hoạt động.
- Dành cho lập trình viên trung cấp: chú ý tối ưu payload và tách biệt server/client.
- Dành cho lập trình viên cao cấp: lưu ý về static generation, middleware, tích hợp SEO và các hook tự động hóa.
Những nội dung chúng ta sẽ đề cập:
- Cài đặt và cấu trúc file
- Tối ưu cách tải các thông điệp
- Sử dụng component phía client và server
- Metadata, sitemap, robots cho SEO
- Middleware cho định tuyến locale
- Thêm Intlayer lên trên (CLI và tự động hóa)
Cài đặt ứng dụng của bạn sử dụng next-intl
Cài đặt các dependencies của next-intl -
npm install next-intl.├── locales│ ├── en│ │ ├── common.json│ │ └── about.json│ ├── fr│ │ ├── common.json│ │ └── about.json│ └── es│ ├── common.json│ └── about.json└── src ├── i18n.ts ├── middleware.ts ├── app │ └── [locale] │ ├── layout.tsx │ └── about │ └── page.tsx └── components ├── ClientComponentExample.tsx └── ServerComponent.tsxCài đặt và Tải Nội dung
Chỉ tải các namespace mà các route của bạn cần và xác thực locale sớm. Giữ các component phía server đồng bộ khi có thể và chỉ gửi các thông điệp cần thiết đến client.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) { // Chỉ tải các namespace mà layout/pages của bạn cần const [common, about] = await Promise.all([ import(`../locales/${locale}/common.json`).then((m) => m.default), import(`../locales/${locale}/about.json`).then((m) => m.default), ]); return { common, about } as const;}export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale as any)) notFound(); return { messages: await loadMessages(locale), };});Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { ReactNode } from "react";import { locales } from "@/i18n";import { getLocaleDirection, unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const { locale } = params; // Đặt locale yêu cầu đang hoạt động cho lần render server này (RSC) unstable_setRequestLocale(locale); const dir = getLocaleDirection(locale); return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({ params,}: { params: { locale: string };}) { const { locale } = params; // Các thông điệp được tải phía server. Chỉ đẩy những gì cần thiết cho client. const messages = await getMessages(); const clientMessages = pick(messages, ["common", "about"]); // Dịch/định dạng chỉ dành riêng cho phía server const tAbout = await getTranslations("about"); const tCounter = await getTranslations("about.counter"); const format = await getFormatter(); const initialFormattedCount = format.number(0); return ( <NextIntlClientProvider locale={locale} messages={clientMessages}> <main> <h1>{tAbout("title")}</h1> <ClientComponentExample /> <ServerComponent formattedCount={initialFormattedCount} label={tCounter("label")} increment={tCounter("increment")} /> </main> </NextIntlClientProvider> );}Sử dụng trong một component phía client
Hãy lấy ví dụ về một component phía client hiển thị bộ đếm.
Dịch thuật (dạng được tái sử dụng; tải chúng vào messages của next-intl theo cách bạn muốn)
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
{ "counter": { "label": "Bộ đếm", "increment": "Tăng" }}Sao chép đoạn mã vào khay nhớ tạm (clipboard)
{ "counter": { "label": "Compteur", "increment": "Incrémenter" }}Component phía client
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
"use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => { // Phạm vi trực tiếp tới đối tượng lồng nhau const t = useTranslations("about.counter"); const format = useFormatter(); const [count, setCount] = useState(0); return ( <div> <p>{format.number(count)}</p> <button aria-label={t("label")} onClick={() => setCount((count) => count + 1)} > {t("increment")} </button> </div> );};Đừng quên thêm message "about" vào messages phía client của trang (chỉ bao gồm các namespace mà client của bạn thực sự cần).
Sử dụng trong component phía server
Component UI này là một component phía server và có thể được render bên dưới một component phía client (page → client → server). Giữ nó đồng bộ bằng cách truyền các chuỗi đã được tính trước.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
type ServerComponentProps = { formattedCount: string; label: string; increment: string;};const ServerComponent = ({ formattedCount, label, increment,}: ServerComponentProps) => { return ( <div> <p>{formattedCount}</p> <button aria-label={label}>{increment}</button> </div> );};Ghi chú:
- Tính toán formattedCount phía server (ví dụ: const initialFormattedCount = format.number(0)).
- Tránh truyền các hàm hoặc đối tượng không thể tuần tự hóa vào các component phía server.
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, url)]) ); return { title: t("title"), description: t("description"), alternates: { canonical: localizedPath(locale, url), languages: { ...languages, "x-default": url }, }, };}// ... Phần còn lại của mã trangSao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export default function sitemap(): MetadataRoute.Sitemap { const aboutLanguages = Object.fromEntries( locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [ { url: formatterLocalizedPath(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages: aboutLanguages }, }, ];}Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => "/" + locale + path),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...withAllLocales("/dashboard"), ...withAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: origin, sitemap: origin + "/sitemap.xml", };}Middleware cho định tuyến locale
Thêm một middleware để xử lý phát hiện và định tuyến locale:
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({ locales: [...locales], defaultLocale, localeDetection: true,});export const config = { // Bỏ qua API, các phần nội bộ của Next và tài nguyên tĩnh matcher: ["/((?!api|_next|.*\\..*).*)"],};Thực hành tốt nhất
- Đặt thuộc tính lang và dir cho html: Trong src/app/[locale]/layout.tsx, tính toán dir thông qua getLocaleDirection(locale) và đặt <html lang={locale} dir={dir}>.
- Phân tách messages theo namespace: Tổ chức JSON theo locale và namespace (ví dụ: common.json, about.json).
- Giảm thiểu tải trọng cho client: Trên các trang, chỉ gửi các namespace cần thiết đến NextIntlClientProvider (ví dụ, pick(messages, ['common', 'about'])).
- Ưu tiên các trang tĩnh: Xuất export const dynamic = 'force-static' và tạo các tham số tĩnh cho tất cả các locales.
- Các component server đồng bộ: Truyền các chuỗi đã được tính trước (nhãn đã dịch, số đã định dạng) thay vì các cuộc gọi async hoặc các hàm không thể tuần tự hóa.
Triển khai Intlayer trên nền next-intl
Cài đặt các phụ thuộc của intlayer:
npm install intlayer @intlayer/sync-json-plugin -DTạo file cấu hình intlayer:
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
import { type IntlayerConfig, Locales } from "intlayer";import { syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = { internationalization: { locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH], defaultLocale: Locales.ENGLISH, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ // Giữ cấu trúc thư mục theo namespace đồng bộ với Intlayer syncJSON({ source: ({ key, locale }) => `./locales/${locale}/${key}.json`, }), ],};export default config;Thêm các script vào package.json:
Sao chép đoạn mã vào khay nhớ tạm (clipboard)
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}Ghi chú:
- intlayer fill: sử dụng nhà cung cấp AI của bạn để điền các bản dịch còn thiếu dựa trên các locale đã cấu hình.
- intlayer test: kiểm tra các bản dịch bị thiếu hoặc không hợp lệ (sử dụng trong CI).
Bạn có thể cấu hình các đối số và nhà cung cấp; xem thêm tại Intlayer CLI.