Trang chủSandboxTrưng bàyỨng dụngTài liệuBlog
    • EnglishTiếng Anh
      EN
    • русскийTiếng Nga
      RU
    • 日本語Tiếng Nhật
      JA
    • françaisTiếng Pháp
      FR
    • 한국어Tiếng Hàn
      KO
    • 中文Tiếng Trung
      ZH
    • españolTiếng Tây Ban Nha
      ES
    • DeutschTiếng Đức
      DE
    • العربيةTiếng Ả Rập
      AR
    • italianoTiếng Italy
      IT
    • British EnglishTiếng Anh (Anh)
      EN-GB
    • portuguêsTiếng Bồ Đào Nha
      PT
    • हिन्दीTiếng Hindi
      HI
    • TürkçeTiếng Thổ Nhĩ Kỳ
      TR
    • polskiTiếng Ba Lan
      PL
    • IndonesiaTiếng Indonesia
      ID
    • Tiếng ViệtTiếng Việt
      VI
    • українськаTiếng Ukraina
      UK
    /
    Lọc tài liệu theo framework
    Alt+←
    Tại sao Intlayer?
    Bắt đầu
    Khái niệm
    • Intlayer làm việc như thế nào
    • Cấu hình
    • TestFillBuildWatchExtractLoginPushPullConfigurationListVersionEditorLiveDebugDoc ReviewDoc TranslateSDK
    • Editor visual
    • CMS
    • Tích hợp CI/CD
    • DịchSố nhiềuLiệt kêĐiều kiệnGiới tínhChènTệpNestingMarkdownHTMLLấy hàm
    • File cho mỗi ngôn ngữ
    • Biên dịch
    • Tự động điền
    • Kiểm tra
    • Tối ưu hóa gói
    Môi trường
    • Next.js 14 và App Router
      Next.js 15
      Next.js không locale URL
      Next.js và Page Router
      Trình biên dịch
    • Tanstack Start Solid
    • Astro và React
      Astro và Svelte
      Astro và Vue
      Astro và Solid
      Astro và Preact
      Astro và Lit
      Astro và Vanilla JS
    • React Router v7
      React Router v7 (fs-routes)
      Compiler
    • Nuxt và Vue
    • Vite và Solid
    • SvelteKit
    • Vite và Preact
    • Vite và Vanilla JS
    • Vite và Lit
    • Angular 19 (Webpack)
      Analog
    • React CRA
    • React Native và Expo
    • Express.js
      NestJS
      Fastify
      Hono
      Adonis
    • Lynx và React
    Plugins
    • JSON
    • gettext (.po)
    Mở rộng VS Code
    Tác nhân
    • MCP Server
    • Kỹ năng tác nhân
    Phiên bản
    • v8
    • v7
    • v6
    Benchmark
    • Next.js
    • TanStack
    • Vue
    • Solid
    • Svelte
    Blog
    Đặt câu hỏi
    1. Documentation
    2. Next intl
    Ngày tạo:2025-10-05Cập nhật lần cuối:2025-10-05
    Xem mẫu ứng dụng trên GitHub

    Trang này có một mẫu ứng dụng có sẵn.

    Tham chiếu tài liệu này tới trợ lý AI yêu thích của bạn
    ChatGPT
    Claude
    DeepSeek
    Google AI mode
    Gemini
    Perplexity
    Mistral
    Grok

    Đặ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

    Nộ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 Anh
    Chỉnh sửa tài liệu này

    Nế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ệu
    Sao chép

    Sao 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 -

    bash
    Sao chép mã

    Sao chép đoạn mã vào khay nhớ tạm (clipboard)

    npm install next-intl
    bash
    Sao chép mã

    Sao chép đoạn mã vào khay nhớ tạm (clipboard)

    .├── 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.tsx

    Cà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.

    src/i18n.ts
    Sao chép mã

    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)) notFound();  return {    messages: await loadMessages(locale),  };});
    src/app/[locale]/layout.tsx
    Sao chép mã

    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>  );}
    src/app/[locale]/about/page.tsx
    Sao chép mã

    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)

    locales/en/about.json
    Sao chép mã

    Sao chép đoạn mã vào khay nhớ tạm (clipboard)

    {  "counter": {    "label": "Bộ đếm",    "increment": "Tăng"  }}
    locales/fr/about.json
    Sao chép mã

    Sao chép đoạn mã vào khay nhớ tạm (clipboard)

    {  "counter": {    "label": "Compteur",    "increment": "Incrémenter"  }}

    Component phía client

    src/components/ClientComponentExample.tsx
    Sao chép mã

    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.

    src/components/ServerComponent.tsx
    Sao chép mã

    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.
    src/app/[locale]/about/layout.tsx
    Sao chép mã

    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ã trang
    src/app/sitemap.ts
    Sao chép mã

    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 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 },    },  ];}
    src/app/robots.ts
    Sao chép mã

    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:

    src/middleware.ts
    Sao chép mã

    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:

    bash
    Sao chép mã

    Sao chép đoạn mã vào khay nhớ tạm (clipboard)

    npm install intlayer @intlayer/sync-json-plugin --save-dev

    Tạo file cấu hình intlayer:

    intlayer.config.ts
    Sao chép mã

    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({      format: "icu",      source: ({ key, locale }) => `./locales/${locale}/${key}.json`,    }),  ],};export default config;

    Thêm các script vào package.json:

    package.json
    Sao chép mã

    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.

    Tại sao Intlayer?
    Alt+→

    Trong trang này

      Các cuộc thảo luận là ẩn danh và được xem xét thường xuyên để giải quyết các vấn đề phổ biến. Hãy thoải mái chia sẻ ý tưởng tính năng, phản hồi về tài liệu hoặc bất cứ điều gì liên quan đến Intlayer, chúng tôi sử dụng thông tin này để định hình lộ trình và cải thiện sản phẩm.

      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.tsx
      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)) notFound();  return {    messages: await loadMessages(locale),  };});
      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>  );}
      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>  );}
      {  "counter": {    "label": "Bộ đếm",    "increment": "Tăng"  }}
      {  "counter": {    "label": "Compteur",    "increment": "Incrémenter"  }}
      "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>  );};
      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>  );};
      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ã trang
      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 },    },  ];}
      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",  };}
      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|.*\\..*).*)"],};
      npm install intlayer @intlayer/sync-json-plugin --save-dev
      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({      format: "icu",      source: ({ key, locale }) => `./locales/${locale}/${key}.json`,    }),  ],};export default config;
      {  "scripts": {    "i18n:fill": "intlayer fill",    "i18n:test": "intlayer test"  }}