Zadaj pytanie i otrzymaj streszczenie dokumentu, odwołując się do tej strony i wybranego dostawcy AI
Historia wersji
- "Initial history"v9.4.129.08.2026
Treść tej strony została przetłumaczona przy użyciu sztucznej inteligencji.
Zobacz ostatnią wersję oryginalnej treści w języku angielskimJeśli masz pomysł na ulepszenie tej dokumentacji, zachęcamy do przesłania pull requesta na GitHubie.
Link do dokumentacji na GitHubieKopiuj dokument Markdown do schowka
Przetłumacz swoją aplikację htmx za pomocą Intlayer | Internationalization (i18n)
htmx nie renderuje żadnej zawartości z własnej inicjatywy. Każda etykieta, którą widzi odwiedzający, to HTML wyprodukowany przez serwer, a każda zamiana to osobne żądanie HTTP. Internacjonalizacja aplikacji htmx jest zatem sprawą serwera: locale musi być rozwiązane dla każdego żądania, a każdy fragment musi być renderowany w tym locale.
Intlayer pokrywa to poprzez swoje integracje backendowe, które wykrywają locale dla każdego żądania i ujawniają zadeklarowaną zawartość obsługującemu, który buduje HTML.
Spis treści
Trzy zasady i18n w aplikacji htmx
Pojedyncza strona może wyzwolić dziesiątki swapów. Każdy z nich jest świeżym żądaniem bez pamięci o stronie, która go wydała. Jeśli locale znajduje się w zmiennej ustawionej podczas początkowego renderowania, każdy fragment po nim powraca do języka domyślnego.
Middleware Intlayer rozwiązuje locale z samego żądania, więc fragment wysłużony o dziesiątej minucie odpowiada w tym samym języku co strona wysłużona o zerowej minucie.
Dwa nośniki pracują z htmx. Cookie (INTLAYER_LOCALE) jest wysyłany przez przeglądarkę automatycznie na każde żądanie, w tym te z htmx. Nagłówek (x-intlayer-locale) można dołączyć do żądań htmx za pomocą atrybutu hx-headers. Oba są czytane domyślnie.
Przetłumaczona wartość interpolowana do fragmentu to markup. Uciekaj przed nią, dokładnie tak jak zrobiłbyś to z jakąkolwiek inną wartością dynamiczną, aby tłumaczenie zawierające < nie mogło złamać dokumentu, do którego jest zamieniane.
Przewodnik Krok po Kroku
Zapoznaj się z Szablonem Aplikacji na GitHub.
Zainstaluj Zależności
Zainstaluj
intlayerplus integrację dla twojego serwera.bashKopiuj kodSkopiuj kod do schowka
bashKopiuj kodSkopiuj kod do schowka
bashKopiuj kodSkopiuj kod do schowka
bashKopiuj kodSkopiuj kod do schowka
bashKopiuj kodSkopiuj kod do schowka
Express i Fastify odczytują ciasteczko lokalizacji za pośrednictwem własnych parserów ciasteczek, dlatego te muszą być zainstalowane obok. Hono i Elysia parsują ciasteczka natywnie.
htmx sam w sobie to pojedynczy znacznik skryptu, dodawany w kroku 4.
Konfiguracja projektu
Utwórz plik
intlayer.config.tsw katalogu głównym projektu:intlayer.config.tsKopiuj kodSkopiuj kod do schowka
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;Pełną listę opcji można znaleźć w dokumentacji konfiguracji.
Zadeklaruj swoją zawartość
Zadeklaruj każdą etykietę, którą serwer będzie renderować, w tym te, które pojawiają się tylko wewnątrz fragmentu:
src/app.content.tsKopiuj kodSkopiuj kod do schowka
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ pl: "Język", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ pl: "Przedmioty w koszyku: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ pl: "Dodaj przedmiot", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;Deklaracje treści mogą znajdować się w dowolnym miejscu w
contentDir(domyślnie./src) i powinny pasować do.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}. Zobacz dokumentację deklaracji treści.Zarejestruj middleware Intlayer
Middleware rozwiązuje locale każdego żądania i udostępnia je twoim handlerom.
src/index.tsKopiuj kodSkopiuj kod do schowka
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // Parser ciasteczek musi uruchomić się pierwszy: `express-intlayer` czyta locale // ciasteczko przez `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());Rozwiązany locale znajduje się na
res.locals.locale.src/index.tsKopiuj kodSkopiuj kod do schowka
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);Rozwiązana lokalizacja znajduje się w
req.intlayer.locale.src/index.tsKopiuj kodSkopiuj kod do schowka
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());Rozwiązana lokalizacja to
c.get("locale").src/index.tsKopiuj kodSkopiuj kod do schowka
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());Rozwiązana lokalizacja znajduje się na
intlayer!.localew kontekście trasy.Domyślnie lokalizacja jest pobierana z ciasteczka
INTLAYER_LOCALE, następnie nagłówkax-intlayer-locale, a następnie negocjacjiAccept-Language.Renderuj fragmenty ze zmienną lokalizacją żądania
Napisz swoich renderery fragmentów jako czyste funkcje lokalizacji i przekaż rozwiązaną przez middleware lokalizację. Jawne przekazanie jej utrzymuje fragment związany z żądaniem, które go poprosiło, niezależnie od tego, na którym serwerze się znajdujesz.
src/views.tsKopiuj kodSkopiuj kod do schowka
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Escape'a przetłumaczoną wartość, aby nie mogła wyrwać się z markup'u. */ 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>`; };Serwuj to z trasy:
src/index.tsKopiuj kodSkopiuj kod do schowka
app.post("/cart/items", (req, res) => { // Pobierz liczbę elementów z ciała żądania i zwiększ o 1 const itemCount = Number(req.body?.itemCount ?? 0) + 1; // Wyślij HTML z renderowanym koszkiem res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsKopiuj kodSkopiuj kod do schowka
fastify.post("/cart/items", async (req, reply) => { // Pobierz liczbę elementów z ciała żądania i zwiększ o 1 const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; // Wyślij HTML z renderowanym koszkiem return reply .type("text/html") .send(renderCart(req.intlayer.locale, itemCount)); });src/index.tsKopiuj kodSkopiuj kod do schowka
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.tsKopiuj kodSkopiuj kod do schowka
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" }, }); });Ten sam fragment teraz odpowiada w języku francuskim dla odwiedzającego, którego cookie mówi
fr, i w arabskim dla tego, którego cookie mówiar, bez żadnych zmian w wywoływanym znaczniku.Podaj pierwszą stronę
Renderuj
<body>samodzielnie, aby przełącznik locale w kroku 7 mógł go całkowicie zamienić, a następnie opakuj go w dokument, który ładuje htmx:src/views.tsKopiuj kodSkopiuj kod do schowka
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>`;getHTMLTextDirzwracaltr,rtllubautodla locale, co sprawia, że arabski i hebrajski renderują się poprawnie.Zmień język
Zmiana języka to żądanie jak każde inne. Serwer zapisuje wybór w ciasteczku, które odczytuje middleware, a następnie zwraca stronę re-renderowaną w nowym locale.
Renderuj przełącznik jako
select, który wysyła się sam i zastępuje całe<body>, dzięki czemu zmieniają się również etykiety statyczne wokół twoich fragmentów:src/views.tsKopiuj kodSkopiuj kod do schowka
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)zapisuje każdy język w aktualnie wyświetlanym języku. Nie podawaj drugiego argumentu, aby zamiast tego zapisać każdy w jego własnym języku.Obsługuj post poprzez walidację wartości, ustawienie pliku cookie i zwrócenie nowej treści:
src/index.tsKopiuj kodSkopiuj kod do schowka
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.tsKopiuj kodSkopiuj kod do schowka
import { isDeclaredLocale } from "intlayer"; fastify.post("/locale", async (req, reply) => { const requestedLocale = String((req.body as { locale?: string })?.locale); // Sprawdzenie, czy żądana lokalizacja jest zadeklarowana if (!isDeclaredLocale(requestedLocale)) { return reply.status(400).send("Nieznana lokalizacja"); } return reply .setCookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }) .type("text/html") .send(renderBody(requestedLocale, 0)); });src/index.tsKopiuj kodSkopiuj kod do schowka
import { setCookie } from "hono/cookie"; import { isDeclaredLocale } from "intlayer"; app.post("/locale", async (c) => { // Parsuj treść żądania const body = await c.req.parseBody(); // Pobierz żądaną lokalizację const requestedLocale = String(body["locale"]); // Sprawdzź, czy lokalizacja jest zadeklarowana if (!isDeclaredLocale(requestedLocale)) { return c.text("Nieznana lokalizacja", 400); } // Ustaw cookie z lokalizacją setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); // Zwróć renderowaną stronę HTML return c.html(renderBody(requestedLocale, 0)); });src/index.tsKopiuj kodSkopiuj kod do schowka
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" }, }); });isDeclaredLocalezawęża arbitralny string do jednej z twoich skonfigurowanych lokalizacji, więc nieoczekiwana wartość nigdy nie dotrze do twoich renderów.Utrzymaj lang i dir zsynchronizowane po wymianie
OpcjonalneZamiana może zastąpić
<body>, nigdy nie<html>wokół niego. Renderujlangidirna zamienianym body i skopiuj je z powrotem na element główny raz, z head:src/views.tsKopiuj kodSkopiuj kod do schowka
Bez tego przełączenie na język arabski renderuje się od prawej do lewej wewnątrz body, podczas gdy dokument wciąż ogłasza poprzedni język technologiom pomocniczym i crawlerom.
Wyślij locale jako nagłówek zamiast ciasteczka
OpcjonalneJeśli cookie nie odpowiada Ci, dołącz locale do każdego żądania htmx za pomocą
hx-headersna elemencie ancestor. Descendants go dziedziczą:htmlKopiuj kodSkopiuj kod do schowka
Middleware czyta
x-intlayer-localedomyślnie. Możesz zmienić nazwę obu nośników w Twojej konfiguracji:intlayer.config.tsKopiuj kodSkopiuj kod do schowka
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Pozostałe opcje konfiguracji routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
Konfiguruj TypeScript
Dołącz autogenerowane typy, aby niezadeklarowany klucz był błędem kompilacji, a nie pustym stringiem w runtime'ie.
Skopiuj kod do schowka
Konfiguracja Git
Zaleca się ignorowanie plików generowanych przez Intlayer:
Skopiuj kod do schowka
Rozszerzenie VS Code
Aby ulepszyć doświadczenie programisty z Intlayer, możesz zainstalować oficjalne Rozszerzenie VS Code Intlayer.
Zainstaluj z VS Code Marketplace
To rozszerzenie zapewnia:
- Autouzupełnianie kluczy tłumaczeń.
- Wykrywanie błędów w czasie rzeczywistym dla brakujących tłumaczeń.
- Podglądy bezpośrednie przetłumaczonej zawartości.
- Szybkie akcje do łatwego tworzenia i aktualizacji tłumaczeń.
Aby uzyskać więcej szczegółów na temat korzystania z rozszerzenia, zapoznaj się z dokumentacją rozszerzenia Intlayer VS Code Extension.
Idź dalej
Aby pójść dalej, możesz eksternalizować swoją zawartość za pomocą CMS, aby tłumacze mogli zmieniać kopię bez wdrażania.
Frequently Asked Questions
Ponieważ żądanie fragmentu nie zawierało locale. Żądania htmx są niezależne od strony, która je wysłała, więc locale musi podróżować z każdym z nich, poprzez cookie INTLAYER_LOCALE lub nagłówek x-intlayer-locale ustawiony za pomocą hx-headers. Sprawdź, czy parser cookie działa przed middleware Intlayer na Express i Fastify, w przeciwnym razie cookie nigdy nie zostanie odczytane, a każde żądanie powróci do Accept-Language.
Przekaż ją. Integracje eksponują rozstrzyganą lokalność (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), a przekazanie jej do getIntlayer sprawia, że każdy renderer jest czystą funkcją lokalności. To jest łatwiejsze do testowania i utrzymuje przenośność twoich fragment rendererów, jeśli zmienisz serwer.
Nie. Wszystko, co widzi odwiedzający, jest produkowane przez serwer, więc nie ma nic do przetłumaczenia w przeglądarce. To również dlatego, że koszt wagi strony dla i18n w aplikacji htmx wynosi prawie zero: żaden katalog nigdy nie jest wysyłany do klienta.
Serwuj swoje strony pod prefixem lokalizacji (/fr/cart) i odczytuj lokalę ze ścieżki w handleru trasy, zamiast z ciasteczka, dla pełnego renderowania strony. Fragmenty mogą nadal używać ciasteczka lub nagłówka. Zobacz konfigurację opcji routingu i niestandardowe przepisywanie adresów URL.
getHTMLTextDir(locale) zwraca ltr, rtl lub auto. Ustaw to na dokumencie dla początkowego renderowania i ponownie zastosuj po zamianie, jak pokazuje krok 8. Używaj logicznych właściwości CSS (margin-inline-start zamiast margin-left), aby twój układ podążał za tym.
Tak, dla wszystkiego, co interpolujesz w string szablonowy, dokładnie jak dla każdej innej wartości dynamicznej. Zawartość pochodząca z CMS lub od tłumacza to nie markup, którym możesz manipulować. Krok 5 pokazuje minimalny escaper.
