Đặ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
Lịch sử phiên bản
- "Initial history"v9.4.129/8/2026
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 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 ứng dụng htmx của bạn bằng Intlayer | Quốc tế hóa (i18n)
htmx không render bất kỳ nội dung nào của riêng nó. Mọi nhãn mà khách truy cập đọc được đều là HTML mà máy chủ của bạn tạo ra, và mọi swap là một yêu cầu HTTP riêng biệt. Quốc tế hóa một ứng dụng htmx do đó là một mối quan tâm của máy chủ: locale phải được giải quyết trên mỗi yêu cầu, và mỗi fragment phải được render ở locale đó.
Intlayer giải quyết điều này thông qua các backend integrations của nó, chúng phát hiện locale cho mỗi yêu cầu và expose nội dung khai báo của bạn cho handler xây dựng HTML.
Mục lục
Ba quy tắc của i18n trong một ứng dụng htmx
Một trang có thể kích hoạt hàng chục swaps. Mỗi cái là một yêu cầu mới không có bộ nhớ về trang đã phát hành nó. Nếu locale nằm trong một biến được đặt trong quá trình render ban đầu, mọi fragment sau đó sẽ quay lại ngôn ngữ mặc định.
Middleware Intlayer giải quyết locale từ chính yêu cầu đó, vì vậy một fragment được phục vụ tại phút mười trả lời cùng ngôn ngữ với trang được phục vụ tại phút không.
Hai trình vận chuyển hoạt động với htmx. Một cookie (INTLAYER_LOCALE) được gửi bởi trình duyệt tự động trên mỗi yêu cầu, bao gồm các yêu cầu htmx. Một header (x-intlayer-locale) có thể được đính kèm vào các yêu cầu htmx với thuộc tính hx-headers. Cả hai đều được đọc theo mặc định.
Một giá trị được dịch nội suy vào một fragment là markup. Escape nó, giống như bạn sẽ làm với bất kỳ giá trị động nào khác, vì vậy một bản dịch chứa < không thể phá vỡ tài liệu mà nó được hoán đổi vào.
Hướng Dẫn Từng Bước
Xem Application Template trên GitHub.
Cài đặt Dependencies
Cài đặt
intlayercùng với integration cho server của bạn.bashSao chép mãSao chép mã vào clipboard
bashSao chép mãSao chép mã vào clipboard
bashSao chép mãSao chép mã vào clipboard
bashSao chép mãSao chép mã vào clipboard
Express và Fastify đọc cookie locale thông qua các cookie parser của riêng họ, vì vậy chúng phải được cài đặt cùng với. Hono và Elysia parse cookies một cách native.
htmx chính nó là một single script tag, được thêm vào bước 4.
Cấu hình dự án của bạn
Tạo một
intlayer.config.tsở root của dự án:intlayer.config.tsSao chép mãSao chép mã vào clipboard
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { internationalization: { locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH, Locales.ARABIC], defaultLocale: Locales.ENGLISH, }, }; export default config;Để xem danh sách đầy đủ các tùy chọn, hãy xem tài liệu cấu hình.
Khai báo Nội dung của bạn
Khai báo mọi nhãn mà máy chủ sẽ hiển thị, bao gồm cả những nhãn chỉ xuất hiện bên trong một fragment:
src/app.content.tsSao chép mãSao chép mã vào clipboard
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ vi: "Ngôn ngữ", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ vi: "Mục trong giỏ hàng của bạn: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ vi: "Thêm một mục", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;Các khai báo nội dung có thể nằm ở bất kỳ đâu trong
contentDir(theo mặc định là./src) và khớp với.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}. Xem tài liệu khai báo nội dung.Đăng ký middleware Intlayer
Middleware giải quyết locale của mỗi request và hiển thị nó cho các handler của bạn.
src/index.tsSao chép mãSao chép mã vào clipboard
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // Cookie parser phải chạy trước: `express-intlayer` đọc locale // cookie thông qua `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());Locale đã được giải quyết là trên
res.locals.locale.src/index.tsSao chép mãSao chép mã vào clipboard
import cookie from "@fastify/cookie"; import formbody from "@fastify/formbody"; import Fastify from "fastify"; import { intlayer } from "fastify-intlayer"; const fastify = Fastify(); await fastify.register(cookie); await fastify.register(formbody); await fastify.register(intlayer);Locale được phân giải nằm trên
req.intlayer.locale.src/index.tsSao chép mãSao chép mã vào clipboard
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());Locale được phân giải là
c.get("locale").src/index.tsSao chép mãSao chép mã vào clipboard
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());Locale đã được phân giải là
intlayer!.localetrên bối cảnh route.Theo mặc định, locale được lấy từ cookie
INTLAYER_LOCALE, sau đó là headerx-intlayer-locale, sau đó là thương lượngAccept-Language.Render các fragment với locale của request
Viết các renderer fragment của bạn dưới dạng pure function của một locale, và truyền locale mà middleware đã phân giải. Truyền nó một cách rõ ràng giúp giữ một fragment được liên kết với request đã yêu cầu nó, bất kể bạn đang ở server nào.
src/views.tsSao chép mãSao chép mã vào clipboard
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Thoát một giá trị đã dịch để nó không thể thoát ra khỏi markup. */ const escapeHtml = (value: string): string => value.replace( /[&<>"']/g, (character) => HTML_ENTITIES[character] ?? character ); export const renderCart = (locale: Locale, itemCount: number): string => { const content = getIntlayer("app", locale); return `<section id="cart"> <p>${escapeHtml(String(content.cartSummary({ count: itemCount })))}</p> <p>${escapeHtml(currency(itemCount * 12.5, { locale, currency: "EUR" }))}</p> <button hx-post="/cart/items" hx-vals='{"itemCount": ${itemCount}}' hx-target="#cart" hx-swap="outerHTML" >${escapeHtml(String(content.addItem))}</button> </section>`; };Phục vụ nó từ một route:
src/index.tsSao chép mãSao chép mã vào clipboard
app.post("/cart/items", (req, res) => { const itemCount = Number(req.body?.itemCount ?? 0) + 1; res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsSao chép mãSao chép mã vào clipboard
fastify.post("/cart/items", async (req, reply) => { const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; return reply .type("text/html") .send(renderCart(req.intlayer.locale, itemCount)); });src/index.tsSao chép mãSao chép mã vào clipboard
app.post("/cart/items", async (c) => { const body = await c.req.parseBody(); const itemCount = Number(body["itemCount"] ?? 0) + 1; return c.html(renderCart(c.get("locale"), itemCount)); });src/index.tsSao chép mãSao chép mã vào clipboard
app.post("/cart/items", ({ body, intlayer }) => { const itemCount = Number((body as { itemCount?: string })?.itemCount ?? 0) + 1; return new Response(renderCart(intlayer!.locale, itemCount), { headers: { "content-type": "text/html" }, }); });Đoạn code tương tự hiện đã trả lời bằng tiếng Pháp cho một khách thăm có cookie
fr, và bằng tiếng Ả Rập cho một khách có cookiear, mà không có thay đổi nào trong markup gọi.Phục vụ trang đầu tiên
Render
<body>riêng biệt, để công tắc locale trong bước 7 có thể hoán đổi toàn bộ, sau đó bọc nó trong tài liệu mà tải htmx:src/views.tsSao chép mãSao chép mã vào clipboard
import { getHTMLTextDir, getIntlayer, type Locale } from "intlayer"; export const renderBody = (locale: Locale, itemCount: number): string => { const content = getIntlayer("app", locale); return `<body lang="${locale}" dir="${getHTMLTextDir(locale)}"> <main> <h1>${escapeHtml(String(content.pageTitle))}</h1> ${renderLocaleSwitcher(locale)} ${renderCart(locale, itemCount)} </main> </body>`; }; export const renderPage = (locale: Locale, itemCount: number): string => `<!doctype html> <html lang="${locale}" dir="${getHTMLTextDir(locale)}"> <head> <meta charset="utf-8" /> <title>${escapeHtml(String(getIntlayer("app", locale).pageTitle))}</title> <script src="https://unpkg.com/htmx.org@2.0.4"></script> </head> ${renderBody(locale, itemCount)} </html>`;getHTMLTextDirtrả vềltr,rtlhoặcautocho locale, đó là những gì làm cho tiếng Ả Rập và tiếng Do Thái hiển thị bố cục một cách chính xác.Chuyển đổi ngôn ngữ
Chuyển đổi ngôn ngữ là một yêu cầu như bất kỳ yêu cầu nào khác. Server lưu trữ lựa chọn trong cookie mà middleware đọc, sau đó trả về trang được hiển thị lại trong locale mới.
Hiển thị bộ chọn ngôn ngữ dưới dạng
selecttự gửi và thay thế toàn bộ<body>, để các nhãn tĩnh xung quanh các fragment của bạn cũng thay đổi:src/views.tsSao chép mãSao chép mã vào clipboard
import { getIntlayer, getLocaleName, type Locale, locales } from "intlayer"; const renderLocaleSwitcher = (locale: Locale): string => { const content = getIntlayer("app", locale); const options = locales .map( (availableLocale: Locale) => `<option value="${availableLocale}"${availableLocale === locale ? " selected" : ""}>${escapeHtml(getLocaleName(availableLocale, locale))}</option>` ) .join(""); return `<form> <label for="locale">${escapeHtml(String(content.localeLabel))}</label> <select id="locale" name="locale" hx-post="/locale" hx-trigger="change" hx-target="body" hx-swap="outerHTML" >${options}</select> </form>`; };getLocaleName(availableLocale, locale)ghi mỗi ngôn ngữ bằng ngôn ngữ hiện được hiển thị. Không truyền đối số thứ hai để ghi mỗi ngôn ngữ bằng chính ngôn ngữ của nó.Xử lý post bằng cách xác thực giá trị, đặt cookie và trả về body mới:
src/index.tsSao chép mãSao chép mã vào clipboard
import { isDeclaredLocale } from "intlayer"; app.post("/locale", (req, res) => { const requestedLocale = String(req.body?.locale); if (!isDeclaredLocale(requestedLocale)) { res.status(400).send("Unknown locale"); return; } res.cookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }); res.type("html").send(renderBody(requestedLocale, 0)); });src/index.tsSao chép mãSao chép mã vào clipboard
import { isDeclaredLocale } from "intlayer"; fastify.post("/locale", async (req, reply) => { const requestedLocale = String((req.body as { locale?: string })?.locale); if (!isDeclaredLocale(requestedLocale)) { return reply.status(400).send("Unknown locale"); } return reply .setCookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }) .type("text/html") .send(renderBody(requestedLocale, 0)); });src/index.tsSao chép mãSao chép mã vào clipboard
import { setCookie } from "hono/cookie"; import { isDeclaredLocale } from "intlayer"; app.post("/locale", async (c) => { // Phân tích body từ request const body = await c.req.parseBody(); const requestedLocale = String(body["locale"]); // Kiểm tra xem locale có được khai báo không if (!isDeclaredLocale(requestedLocale)) { return c.text("Unknown locale", 400); } // Đặt cookie cho locale setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); return c.html(renderBody(requestedLocale, 0)); });src/index.tsSao chép mãSao chép mã vào clipboard
import { isDeclaredLocale } from "intlayer"; app.post("/locale", ({ body, cookie, status }) => { const requestedLocale = String((body as { locale?: string })?.locale); if (!isDeclaredLocale(requestedLocale)) { return status(400, "Ngôn ngữ không xác định"); } cookie["INTLAYER_LOCALE"]!.set({ value: requestedLocale, sameSite: "lax", path: "/", }); return new Response(renderBody(requestedLocale, 0), { headers: { "content-type": "text/html" }, }); });isDeclaredLocalethu hẹp một chuỗi tùy ý thành một trong các ngôn ngữ được cấu hình của bạn, do đó một giá trị không mong muốn không bao giờ đạt đến các renderer của bạn.Giữ lang và dir đồng bộ sau khi hoán đổi
Tùy chọnMột swap có thể thay thế
<body>, nhưng không bao giờ thay thế<html>xung quanh nó. Renderlangvàdirtrên body được swap và sao chép chúng trở lại phần tử gốc một lần, từ head:src/views.tsSao chép mãSao chép mã vào clipboard
Không có điều này, một sự chuyển đổi sang tiếng Ả Rập sẽ render từ phải sang trái bên trong body trong khi tài liệu vẫn quảng cáo ngôn ngữ trước đó cho công nghệ hỗ trợ và cho các crawler.
Gửi locale như một header thay vì một cookie
Tùy chọnNếu cookie không phù hợp với bạn, hãy đính kèm locale vào mọi yêu cầu htmx bằng
hx-headerstrên một phần tử tổ tiên. Các phần tử con sẽ kế thừa nó:htmlSao chép mãSao chép mã vào clipboard
Middleware đọc
x-intlayer-localetheo mặc định. Bạn có thể đổi tên cả hai carrier trong cấu hình của bạn:intlayer.config.tsSao chép mãSao chép mã vào clipboard
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Các tùy chọn cấu hình khác routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
Cấu hình TypeScript
Bao gồm các loại được tự động tạo để một khóa không khai báo là lỗi biên dịch thay vì một chuỗi trống tại thời gian chạy.
Sao chép mã vào clipboard
Cấu hình Git
Nên bỏ qua các tệp được tạo bởi Intlayer:
Sao chép mã vào clipboard
VS Code Extension
Để cải thiện trải nghiệm phát triển với Intlayer, bạn có thể cài đặt Intlayer VS Code Extension chính thức.
Cài đặt từ VS Code Marketplace
Tiện ích mở rộng này cung cấp:
- Tự động hoàn thành cho các khóa dịch.
- Phát hiện lỗi thời gian thực cho các dịch bị thiếu.
- Xem trước nội tuyến của nội dung đã dịch.
- Hành động nhanh để dễ dàng tạo và cập nhật các bản dịch.
Để biết thêm chi tiết về cách sử dụng tiện ích mở rộng, hãy tham khảo tài liệu Intlayer VS Code Extension.
Đi xa hơn
Để đi xa hơn, bạn có thể ngoại hóa nội dung của mình bằng cách sử dụng CMS, vì vậy các nhà dịch có thể thay đổi nội dung mà không cần triển khai.
Các Câu Hỏi Thường Gặp
Vì yêu cầu fragment không mang theo locale. htmx requests là độc lập với trang phát hành chúng, vì vậy locale phải được truyền trên mỗi yêu cầu, thông qua cookie INTLAYER_LOCALE hoặc header x-intlayer-locale được đặt với hx-headers. Kiểm tra rằng cookie parser chạy trước Intlayer middleware trên Express và Fastify, nếu không cookie sẽ không bao giờ được đọc và mọi yêu cầu sẽ quay lại Accept-Language.
Hãy truyền nó. Các integrations expose locale được resolved (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), và việc truyền nó tới getIntlayer làm cho mỗi renderer trở thành một pure function của một locale. Điều đó dễ dàng hơn để test, và nó giữ cho fragment renderers của bạn portable nếu bạn thay đổi server.
Không. Mọi thứ mà một visitor nhìn thấy được produced bởi server, vì vậy không có gì để translate trong browser. Đó cũng là lý do tại sao page weight cost của i18n trong một htmx app gần như bằng không: không có catalog nào được shipped tới client.
Phục vụ các trang của bạn dưới một tiền tố locale (/fr/cart) và đọc locale từ đường dẫn trong trình xử lý route của bạn, thay vì từ cookie, để render toàn bộ trang. Các fragment có thể tiếp tục sử dụng cookie hoặc header. Xem configuration để biết các tùy chọn định tuyến và custom URL rewrites.
getHTMLTextDir(locale) trả về ltr, rtl hoặc auto. Đặt nó trên document cho lần render ban đầu, và áp dụng lại sau khi swap như bước 8 chỉ ra. Sử dụng các thuộc tính CSS logic (margin-inline-start thay vì margin-left) để bố cục của bạn tuân theo.
Có, đối với bất kỳ thứ gì bạn nội suy vào một template string, giống như đối với bất kỳ giá trị động nào khác. Nội dung đến từ CMS hoặc từ một người dịch không phải là markup mà bạn kiểm soát. Bước 5 cho thấy một hàm escape tối thiểu.
