Stellen Sie Ihre Frage und erhalten Sie einen Resümee des Dokuments, indem Sie diese Seite und den AI-Anbieter Ihrer Wahl referenzieren
Versionshistorie
- "Erste Geschichte"v9.4.129.8.2026
Der Inhalt dieser Seite wurde mit einer KI übersetzt.
Den englischen Originaltext ansehenWenn Sie eine Idee haben, um diese Dokumentation zu verbessern, zögern Sie bitte nicht, durch das Einreichen eines Pull-Requests auf GitHub beizutragen.
GitHub-Link zur DokumentationMarkdown des Dokuments in die Zwischenablage kopieren
Übersetzen Sie Ihre htmx-Anwendung mit Intlayer | Internationalisierung (i18n)
htmx rendert keinen eigenen Inhalt. Jedes Label, das ein Besucher liest, ist HTML, das Ihr Server produziert hat, und jeder Swap ist eine separate HTTP-Anfrage. Die Internationalisierung einer htmx-App ist daher eine Server-Aufgabe: Das Locale muss bei jeder Anfrage aufgelöst werden, und jedes Fragment muss in diesem Locale gerendert werden.
Intlayer behandelt dies durch seine Backend-Integrationen, die das Locale pro Anfrage erkennen und Ihren deklarierten Inhalt dem Handler bereitstellen, der das HTML erstellt.
Inhaltsverzeichnis
Die drei Regeln der i18n in einer htmx-App
Eine einzelne Seite kann Dutzende von Swaps auslösen. Jeder ist eine neue Anfrage ohne Erinnerung an die Seite, die sie ausgelöst hat. Wenn das Locale in einer Variablen gespeichert ist, die während des initialen Renderings festgelegt wird, greift jedes Fragment danach auf die Standardsprache zurück.
Die Intlayer-Middleware löst das Locale aus der Anfrage selbst auf, sodass ein Fragment, das in Minute zehn bereitgestellt wird, in der gleichen Sprache antwortet wie die Seite, die in Minute null bereitgestellt wurde.
Zwei Träger funktionieren mit htmx. Ein Cookie (INTLAYER_LOCALE) wird vom Browser automatisch bei jeder Anfrage, einschließlich htmx-Anfragen, gesendet. Ein Header (x-intlayer-locale) kann htmx-Anfragen mit dem Attribut hx-headers angehängt werden. Beide werden standardmäßig gelesen.
Ein übersetzter Wert, der in ein Fragment interpoliert wird, ist Markup. Escape es genau wie jeden anderen dynamischen Wert, damit eine Übersetzung mit < das Dokument, in das es ausgetauscht wird, nicht beschädigen kann.
Schritt-für-Schritt-Anleitung
Siehe Application Template auf GitHub.
Abhängigkeiten installieren
Installieren Sie
intlayerplus die Integration für Ihren Server.bashCode kopierenKopieren Sie den Code in die Zwischenablage
bashCode kopierenKopieren Sie den Code in die Zwischenablage
bashCode kopierenKopieren Sie den Code in die Zwischenablage
bashCode kopierenKopieren Sie den Code in die Zwischenablage
bashCode kopierenKopieren Sie den Code in die Zwischenablage
Express und Fastify lesen das Locale-Cookie über ihre eigenen Cookie-Parser, daher müssen diese parallel installiert werden. Hono und Elysia parsen Cookies nativ.
htmx selbst ist ein einzelnes Script-Tag, das in Schritt 4 hinzugefügt wird.
Konfiguration Ihres Projekts
Erstellen Sie eine
intlayer.config.tsim Stammverzeichnis Ihres Projekts:intlayer.config.tsCode kopierenKopieren Sie den Code in die Zwischenablage
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;Die vollständige Liste der Optionen finden Sie in der Konfigurationsdokumentation.
Deklarieren Sie Ihren Inhalt
Deklarieren Sie jedes Label, das der Server rendert, einschließlich derjenigen, die nur in einem Fragment erscheinen:
src/app.content.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ de: "Sprache", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ de: "Artikel in Ihrem Warenkorb: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ de: "Artikel hinzufügen", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;Inhaltsdeklarationen können überall unter
contentDir(standardmäßig./src) leben und mit.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}übereinstimmen. Siehe die Dokumentation zur Inhaltsdeklaration.Registrieren Sie die Intlayer-Middleware
Das Middleware löst das Locale jeder Anfrage auf und stellt es Ihren Handlern zur Verfügung.
src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // Der Cookie-Parser muss zuerst ausgeführt werden: `express-intlayer` liest das Locale // Cookie über `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());Das aufgelöste Locale befindet sich auf
res.locals.locale.src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
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);Das aufgelöste Locale ist auf
req.intlayer.locale.src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());Das aufgelöste Locale ist
c.get("locale").src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());Die aufgelöste Locale befindet sich unter
intlayer!.localeim Route-Kontext.Standardmäßig wird die Locale aus dem
INTLAYER_LOCALECookie entnommen, dann derx-intlayer-localeHeader, dannAccept-LanguageVerhandlung.Fragment mit der Request-Locale rendern
Schreiben Sie Ihre Fragment-Renderer als reine Funktionen einer Locale, und übergeben Sie die Locale, die die Middleware aufgelöst hat. Die explizite Übergabe hält ein Fragment an die Anfrage gebunden, die es angefordert hat, unabhängig davon, auf welchem Server Sie sich befinden.
src/views.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Escaped einen übersetzten Wert, damit dieser nicht aus dem Markup ausbrechen kann. */ 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>`; };Stelle es von einer Route bereit:
src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
app.post("/cart/items", (req, res) => { // Die Artikelanzahl aus dem Request-Body abrufen und um 1 erhöhen const itemCount = Number(req.body?.itemCount ?? 0) + 1; // Das Warenkorb-HTML mit der aktuellen Locale und Artikelanzahl rendern res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
fastify.post("/cart/items", async (req, reply) => { // Die Artikelanzahl aus dem Request-Body abrufen und um 1 erhöhen const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; // Das Warenkorb-HTML mit der aktuellen Locale und Artikelanzahl rendern return reply .type("text/html") .send(renderCart(req.intlayer.locale, itemCount)); });src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
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.tsCode kopierenKopieren Sie den Code in die Zwischenablage
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" }, }); });Das gleiche Fragment antwortet nun auf Französisch für einen Besucher, dessen Cookie
frsagt, und auf Arabisch für einen, dessen Cookiearsagt, ohne Änderung am aufrufenden Markup.Die erste Seite bereitstellen
Rendern Sie den
<body>allein, damit der Locale-Switcher in Schritt 7 ihn vollständig austauschen kann, dann wickeln Sie ihn in das Dokument, das htmx lädt:src/views.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { getHTMLTextDir, getIntlayer, type Locale } from "intlayer"; export const renderBody = (locale: Locale, itemCount: number): string => { // Hole den Inhalt für die aktuelle Locale 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>`;getHTMLTextDirgibtltr,rtloderautofür das Locale zurück, was dafür sorgt, dass Arabisch und Hebräisch korrekt angezeigt werden.Sprache wechseln
Ein Sprachwechsel ist eine Anfrage wie jede andere. Der Server speichert die Auswahl im Cookie, den die Middleware liest, und gibt dann die Seite in den neuen Lokalisierungen neu gerendert zurück.
Rendere den Wechsler als
select, der sich selbst sendet und den ganzen<body>austauscht, sodass auch die statischen Bezeichnungen um deine Fragmente herum wechseln:src/views.tsCode kopierenKopieren Sie den Code in die Zwischenablage
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)schreibt jede Sprache in der aktuell angezeigten Sprache. Übergeben Sie kein zweites Argument, um jede stattdessen in ihrer eigenen Sprache zu schreiben.Behandeln Sie den POST, indem Sie den Wert validieren, das Cookie setzen und den neuen Body zurückgeben:
src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
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.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { isDeclaredLocale } from "intlayer"; fastify.post("/locale", async (req, reply) => { // Extrahieren Sie die angeforderte Sprache aus dem Request-Body const requestedLocale = String((req.body as { locale?: string })?.locale); // Überprüfen Sie, ob die angeforderte Sprache deklariert ist if (!isDeclaredLocale(requestedLocale)) { return reply.status(400).send("Unknown locale"); } // Setzen Sie das Cookie und geben Sie den neuen Body zurück return reply .setCookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }) .type("text/html") .send(renderBody(requestedLocale, 0)); });src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { setCookie } from "hono/cookie"; import { isDeclaredLocale } from "intlayer"; app.post("/locale", async (c) => { // Body parsen const body = await c.req.parseBody(); // Angeforderte Locale aus dem Body extrahieren const requestedLocale = String(body["locale"]); // Überprüfen, ob die angeforderte Locale deklariert ist if (!isDeclaredLocale(requestedLocale)) { return c.text("Unknown locale", 400); } // Cookie für die Locale setzen setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); // HTML mit der angeforderten Locale zurückgeben return c.html(renderBody(requestedLocale, 0)); });src/index.tsCode kopierenKopieren Sie den Code in die Zwischenablage
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" }, }); });isDeclaredLocalegrenzt einen beliebigen String auf eine deiner konfigurierten Locales ein, sodass ein unerwarteter Wert niemals deine Renderer erreicht.Lang und dir nach einem Swap synchron halten
OptionalEin Swap kann den
<body>ersetzen, niemals das<html>um ihn herum. Renderlangunddirauf dem ausgetauschten body und kopiere sie danach einmal vom head auf das root-Element zurück:src/views.tsCode kopierenKopieren Sie den Code in die Zwischenablage
Ohne dies wird ein Wechsel zu Arabisch innerhalb des body von rechts nach links gerendert, während das Dokument die vorherige Sprache gegenüber unterstützender Technologie und Crawlern weiterhin bewirbt.
Die Locale als Header statt als Cookie senden
OptionalWenn ein Cookie nicht für Sie geeignet ist, hängen Sie das Locale an jede htmx-Anfrage mit
hx-headersauf einem übergeordneten Element an. Untergeordnete Elemente erben es:htmlCode kopierenKopieren Sie den Code in die Zwischenablage
Die Middleware liest standardmäßig
x-intlayer-locale. Sie können beide Träger in Ihrer Konfiguration umbenennen:intlayer.config.tsCode kopierenKopieren Sie den Code in die Zwischenablage
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Andere Konfigurationsoptionen routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
TypeScript konfigurieren
Fügen Sie die autogenerierten Typen ein, damit ein nicht deklarierter Schlüssel ein Kompilierungsfehler ist statt einer leeren Zeichenkette zur Laufzeit.
Kopieren Sie den Code in die Zwischenablage
Git-Konfiguration
Es wird empfohlen, die von Intlayer generierten Dateien zu ignorieren:
Kopieren Sie den Code in die Zwischenablage
VS Code Extension
Um Ihre Entwicklungserfahrung mit Intlayer zu verbessern, können Sie die offizielle Intlayer VS Code Extension installieren.
Aus dem VS Code Marketplace installieren
Diese Erweiterung bietet:
- Autovervollständigung für Übersetzungsschlüssel.
- Echtzeit-Fehlerdetection für fehlende Übersetzungen.
- Inline-Vorschau von übersetztem Inhalt.
- Schnellaktionen zur einfachen Erstellung und Aktualisierung von Übersetzungen.
Weitere Informationen zur Verwendung der Erweiterung finden Sie in der Intlayer VS Code Extension-Dokumentation.
Noch weiter gehen
Um noch weiter zu gehen, können Sie Ihren Inhalt mit dem CMS externalisieren, damit Übersetzer Inhalte ohne Deployment ändern können.
Häufig gestellte Fragen
Weil die Fragment-Anfrage keine Sprache mitgebracht hat. htmx-Anfragen sind unabhängig von der Seite, die sie ausgelöst hat, daher muss die Sprache bei jeder Anfrage über den INTLAYER_LOCALE-Cookie oder einen x-intlayer-locale-Header mitgegeben werden, der mit hx-headers gesetzt wird. Überprüfen Sie, dass der Cookie-Parser vor der Intlayer-Middleware auf Express und Fastify ausgeführt wird, sonst wird der Cookie nie gelesen und jede Anfrage fällt auf Accept-Language zurück.
Übergeben Sie es. Die Integrationen zeigen das aufgelöste Locale (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), und das Übergeben an getIntlayer macht jeden Renderer zu einer reinen Funktion eines Locales. Das ist leichter zu testen und hält Ihre Fragment-Renderer portabel, falls Sie den Server wechseln.
Nein. Alles, was ein Besucher sieht, wird vom Server erzeugt, es gibt also nichts, das im Browser übersetzt werden muss. Das ist auch der Grund, warum die Seitengewicht-Kosten von i18n in einer htmx-App nahe bei null liegen: Kein Katalog wird jemals an den Client verschickt.
Stellen Sie Ihre Seiten unter einem Locale-Präfix (/fr/cart) bereit und lesen Sie die Locale aus dem Pfad in Ihrem Route-Handler, anstatt aus dem Cookie, für das vollständige Seiten-Rendering. Fragmente können weiterhin das Cookie oder den Header verwenden. Siehe Konfiguration für die Routing-Optionen und benutzerdefinierte URL-Umschreibungen.
getHTMLTextDir(locale) gibt ltr, rtl oder auto zurück. Setzen Sie es für das ursprüngliche Rendering auf das Dokument und wenden Sie es nach einem Swap wie in Schritt 8 erneut an. Verwenden Sie logische CSS-Eigenschaften (margin-inline-start anstelle von margin-left), damit sich Ihr Layout entsprechend anpasst.
Ja, für alles, das du in einen Template-String interpolierst, genau wie für jeden anderen dynamischen Wert. Inhalte aus dem CMS oder von einem Übersetzer sind kein Markup, das du kontrollierst. Schritt 5 zeigt einen minimalen Escaper.
