Ajukan pertanyaan Anda dan dapatkan ringkasan dokumen dengan merujuk halaman ini dan penyedia AI pilihan Anda
Riwayat Versi
- "Initial history"v9.4.129/8/2026
Konten halaman ini diterjemahkan menggunakan AI.
Lihat versi terakhir dari konten aslinya dalam bahasa InggrisJika Anda memiliki ide untuk meningkatkan dokumentasi ini, silakan berkontribusi dengan mengajukan pull request di GitHub.
Tautan GitHub ke dokumentasiSalin Markdown dokumentasi ke clipboard
Terjemahkan aplikasi htmx Anda menggunakan Intlayer | Internationalization (i18n)
htmx tidak merender konten apa pun dari dirinya sendiri. Setiap label yang dibaca pengunjung adalah HTML yang dihasilkan server Anda, dan setiap swap adalah permintaan HTTP yang terpisah. Menginternasionalisasi aplikasi htmx adalah oleh karena itu tanggung jawab server: locale harus diselesaikan pada setiap permintaan, dan setiap fragment harus dirender dalam locale tersebut.
Intlayer mencakup ini melalui integrasi backend-nya, yang mendeteksi locale per permintaan dan mengekspos konten yang Anda deklarasikan ke handler yang membangun HTML.
Daftar Isi
Tiga aturan i18n dalam aplikasi htmx
Satu halaman dapat memicu puluhan swap. Setiap satu adalah permintaan baru tanpa memori dari halaman yang mengeluarkannya. Jika locale berada dalam variabel yang diatur selama render awal, setiap fragment setelahnya kembali ke bahasa default.
Middleware Intlayer menyelesaikan locale dari permintaan itu sendiri, sehingga fragment yang dikirimkan pada menit kesepuluh menjawab dalam bahasa yang sama dengan halaman yang dikirimkan pada menit nol.
Dua pembawa bekerja dengan htmx. Cookie (INTLAYER_LOCALE) dikirimkan oleh browser secara otomatis pada setiap permintaan, termasuk yang htmx. Header (x-intlayer-locale) dapat dilampirkan ke permintaan htmx dengan atribut hx-headers. Keduanya dibaca secara default.
Nilai yang diterjemahkan dan diinterpolasi ke dalam fragmen adalah markup. Escape-nya, persis seperti yang Anda lakukan untuk nilai dinamis lainnya, jadi terjemahan yang mengandung < tidak dapat merusak dokumen tempat fragmen tersebut ditukar.
Panduan Langkah demi Langkah
Lihat Application Template di GitHub.
Install Dependencies
Pasang
intlayerplus integrasi untuk server Anda.bashSalin kodeSalin kode ke clipboard
bashSalin kodeSalin kode ke clipboard
bashSalin kodeSalin kode ke clipboard
bashSalin kodeSalin kode ke clipboard
bashSalin kodeSalin kode ke clipboard
Express dan Fastify membaca cookie locale melalui parser cookie mereka sendiri, jadi cookie-cookie tersebut harus dipasang bersama. Hono dan Elysia mem-parse cookie secara native.
htmx itu sendiri adalah tag script tunggal, ditambahkan di step 4.
Konfigurasi proyek Anda
Buat
intlayer.config.tsdi root proyek Anda:intlayer.config.tsSalin kodeSalin kode ke 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;Untuk daftar lengkap opsi, lihat dokumentasi konfigurasi.
Deklarasikan Konten Anda
Deklarasikan setiap label yang akan dirender server, termasuk yang hanya muncul di dalam fragment:
src/app.content.tsSalin kodeSalin kode ke clipboard
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ id: "Bahasa", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ id: "Item dalam keranjang Anda: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ id: "Tambahkan item", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;Deklarasi konten dapat berada di mana saja di bawah
contentDir(secara default./src) dan cocok dengan.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}. Lihat dokumentasi deklarasi konten.Daftarkan middleware Intlayer
Middleware menyelesaikan locale dari setiap request dan membuatnya dapat diakses oleh handlers Anda.
src/index.tsSalin kodeSalin kode ke clipboard
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // Cookie parser harus berjalan pertama: `express-intlayer` membaca locale // cookie melalui `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());Locale yang sudah diselesaikan berada di
res.locals.locale.src/index.tsSalin kodeSalin kode ke 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 yang diselesaikan ada di
req.intlayer.locale.src/index.tsSalin kodeSalin kode ke clipboard
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());Locale yang diselesaikan adalah
c.get("locale").src/index.tsSalin kodeSalin kode ke clipboard
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());Locale yang telah diselesaikan adalah
intlayer!.localepada konteks rute.Secara default, locale diambil dari cookie
INTLAYER_LOCALE, kemudian headerx-intlayer-locale, kemudian negosiasiAccept-Language.Render fragments dengan locale permintaan
Tulis renderer fragment Anda sebagai fungsi murni dari sebuah locale, dan berikan locale yang telah diselesaikan middleware. Meneruskannya secara eksplisit membuat fragment tetap terikat pada permintaan yang memintanya, di server mana pun Anda berada.
src/views.tsSalin kodeSalin kode ke clipboard
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Mengamankan nilai yang diterjemahkan agar tidak dapat keluar dari 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>`; };Sajikan dari rute:
src/index.tsSalin kodeSalin kode ke clipboard
app.post("/cart/items", (req, res) => { // Menambah jumlah item dari body permintaan const itemCount = Number(req.body?.itemCount ?? 0) + 1; // Mengirim respons HTML dengan keranjang yang dirender res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsSalin kodeSalin kode ke clipboard
fastify.post("/cart/items", async (req, reply) => { // Menambah jumlah item dari body permintaan const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; // Mengirim respons HTML dengan keranjang yang dirender return reply .type("text/html") .send(renderCart(req.intlayer.locale, itemCount)); });src/index.tsSalin kodeSalin kode ke 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.tsSalin kodeSalin kode ke 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" }, }); });Fragment yang sama sekarang menjawab dalam bahasa Prancis untuk pengunjung yang cookie-nya mengatakan
fr, dan dalam bahasa Arab untuk yang cookie-nya mengatakanar, tanpa perubahan pada markup yang dipanggil.Sajikan halaman pertama
Render
<body>sendirian, jadi penyaklah locale di step 7 dapat menukarnya sepenuhnya, kemudian bungkus dalam dokumen yang memuat htmx:src/views.tsSalin kodeSalin kode ke 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>`;getHTMLTextDirmengembalikanltr,rtlatauautountuk locale, yang membuat Arabic dan Hebrew ditampilkan dengan benar.Ubah bahasa
Mengubah bahasa adalah permintaan seperti yang lain. Server menyimpan pilihan dalam cookie yang dibaca middleware, kemudian mengembalikan halaman yang di-render ulang dalam locale baru.
Render the switcher as a
selectthat posts itself and swaps the whole<body>, so the static labels around your fragments change too:src/views.tsSalin kodeSalin kode ke 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)menulis setiap bahasa dalam bahasa yang sedang ditampilkan. Jangan lewatkan argumen kedua untuk menulis masing-masing dalam bahasa mereka sendiri.Tangani post dengan memvalidasi nilai, mengatur cookie, dan mengembalikan body baru:
src/index.tsSalin kodeSalin kode ke 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.tsSalin kodeSalin kode ke 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.tsSalin kodeSalin kode ke clipboard
import { setCookie } from "hono/cookie"; import { isDeclaredLocale } from "intlayer"; app.post("/locale", async (c) => { const body = await c.req.parseBody(); const requestedLocale = String(body["locale"]); if (!isDeclaredLocale(requestedLocale)) { return c.text("Locale tidak dikenal", 400); } setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); return c.html(renderBody(requestedLocale, 0)); });src/index.tsSalin kodeSalin kode ke 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, "Unknown locale"); } cookie["INTLAYER_LOCALE"]!.set({ value: requestedLocale, sameSite: "lax", path: "/", }); return new Response(renderBody(requestedLocale, 0), { headers: { "content-type": "text/html" }, }); });isDeclaredLocalemempersempit string arbitrer ke salah satu locale yang dikonfigurasi, sehingga nilai yang tidak terduga tidak pernah mencapai renderer Anda.Jaga lang dan dir tetap sinkron setelah swap
OpsionalSwap dapat mengganti
<body>, namun tidak pernah<html>di sekitarnya. Renderlangdandirpada body yang di-swap dan salin kembali ke elemen root sekali saja, dari head:src/views.tsSalin kodeSalin kode ke clipboard
Tanpa ini, beralih ke Bahasa Arab akan merender dari kanan ke kiri di dalam body sementara dokumen masih mengumumkan bahasa sebelumnya ke teknologi asistif dan ke crawler.
Kirim locale sebagai header daripada cookie
OpsionalJika cookie tidak sesuai untuk Anda, lampirkan locale ke setiap htmx request dengan
hx-headerspada elemen ancestor. Descendants akan mewarisinya:htmlSalin kodeSalin kode ke clipboard
Middleware membaca
x-intlayer-localesecara default. Anda dapat mengganti kedua carrier dalam konfigurasi Anda:intlayer.config.tsSalin kodeSalin kode ke clipboard
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Opsi konfigurasi lainnya routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
Konfigurasi TypeScript
Sertakan tipe yang dihasilkan secara otomatis sehingga kunci yang tidak dideklarasikan menjadi kesalahan kompilasi daripada string kosong saat runtime.
Salin kode ke clipboard
Konfigurasi Git
Disarankan untuk mengabaikan file yang dihasilkan oleh Intlayer:
Salin kode ke clipboard
Ekstensi VS Code
Untuk meningkatkan pengalaman pengembangan Anda dengan Intlayer, Anda dapat menginstal Intlayer VS Code Extension resmi.
Instal dari VS Code Marketplace
Ekstensi ini menyediakan:
- Autocompletion untuk kunci terjemahan.
- Deteksi kesalahan real-time untuk terjemahan yang hilang.
- Preview inline dari konten yang diterjemahkan.
- Aksi cepat untuk dengan mudah membuat dan memperbarui terjemahan.
Untuk detail lebih lanjut tentang cara menggunakan ekstensi, lihat dokumentasi Intlayer VS Code Extension.
Lanjutkan Lebih Jauh
Untuk lanjutkan lebih jauh, Anda dapat meneksternalisasi konten Anda menggunakan CMS, sehingga penerjemah dapat mengubah teks tanpa deployment.
Pertanyaan yang Sering Diajukan
Karena permintaan fragment tidak membawa locale. Permintaan htmx independen dari halaman yang mengeluarkannya, jadi locale harus berpindah di setiap satu, melalui cookie INTLAYER_LOCALE atau header x-intlayer-locale yang diatur dengan hx-headers. Periksa bahwa cookie parser berjalan sebelum middleware Intlayer di Express dan Fastify, jika tidak cookie tidak pernah dibaca dan setiap permintaan kembali ke Accept-Language.
Berikan locale ke getIntlayer. Integrasi mengekspos locale yang sudah diselesaikan (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), dan menyerahkannya ke getIntlayer membuat setiap renderer menjadi fungsi murni dari sebuah locale. Itu lebih mudah untuk diuji, dan membuat renderer fragment Anda portable jika Anda mengubah server.
Tidak. Semua yang dilihat pengunjung diproduksi oleh server, jadi tidak ada yang perlu diterjemahkan di browser. Itulah juga mengapa biaya berat halaman i18n dalam aplikasi htmx hampir nol: tidak ada katalog yang pernah dikirim ke klien.
Layani halaman Anda di bawah awalan lokal (/fr/cart) dan baca lokal dari jalur di penangani rute Anda, bukan dari cookie, untuk rendering halaman lengkap. Fragment dapat terus menggunakan cookie atau header. Lihat konfigurasi untuk opsi routing dan penulisan ulang URL kustom.
getHTMLTextDir(locale) mengembalikan ltr, rtl atau auto. Tetapkan pada dokumen untuk rendering awal, dan terapkan kembali setelah swap seperti yang ditunjukkan pada langkah 8. Gunakan properti logis CSS (margin-inline-start daripada margin-left) sehingga tata letak Anda mengikutinya.
Ya, untuk apa pun yang Anda interpolasi ke dalam template string, persis seperti nilai dinamis lainnya. Konten yang berasal dari CMS atau dari penerjemah bukanlah markup yang Anda kontrol. Langkah 5 menunjukkan escaper minimal.
