Pose una domanda e ottieni un riassunto del documento facendo riferimento a questa pagina e al provider AI di tua scelta
Cronologia delle versioni
- "Initial history"v9.4.129/08/2026
Il contenuto di questa pagina è stato tradotto con un'IA.
Vedi l'ultima versione del contenuto originale in ingleseSe hai un’idea per migliorare questa documentazione, non esitare a contribuire inviando una pull request su GitHub.
Collegamento GitHub alla documentazioneCopia il Markdown del documento nella porta-documenti
Traduci la tua applicazione htmx usando Intlayer | Internazionalizzazione (i18n)
htmx non esegue il rendering di alcun contenuto proprio. Ogni etichetta che un visitatore legge è HTML prodotto dal tuo server, e ogni swap è una richiesta HTTP separata. Internazionalizzare un'app htmx è quindi una preoccupazione del server: la locale deve essere risolta su ogni richiesta, e ogni frammento deve essere renderizzato in quella locale.
Intlayer copre questo attraverso le sue integrazioni backend, che rilevono la locale per ogni richiesta ed espongono i contenuti dichiarati al handler che costruisce l'HTML.
Indice dei contenuti
Le tre regole dell'i18n in un'app htmx
Una singola pagina può attivare dozzine di swap. Ognuno è una richiesta nuova senza memoria della pagina che l'ha generata. Se la locale vive in una variabile impostata durante il rendering iniziale, ogni frammento successivo ricade al linguaggio predefinito.
Il middleware di Intlayer risolve la locale dalla richiesta stessa, quindi un frammento servito al minuto dieci risponde nella stessa lingua della pagina servita al minuto zero.
Due vettori funzionano con htmx. Un cookie (INTLAYER_LOCALE) viene inviato automaticamente dal browser ad ogni richiesta, incluse quelle htmx. Un header (x-intlayer-locale) può essere allegato alle richieste htmx con l'attributo hx-headers. Entrambi vengono letti per impostazione predefinita.
Une valeur traduite interpolée dans un fragment est du markup. Échappez-la, exactement comme vous le feriez pour toute autre valeur dynamique, afin qu'une traduction contenant < ne puisse pas casser le document dans lequel elle est échangée.
Guide étape par étape
Vedi Application Template su GitHub.
Installare le dipendenze
Installa
intlayerpiù l'integrazione per il tuo server.bashCopiare il codiceCopiare il codice nella clipboard
bashCopiare il codiceCopiare il codice nella clipboard
bashCopiare il codiceCopiare il codice nella clipboard
bashCopiare il codiceCopiare il codice nella clipboard
bashCopiare il codiceCopiare il codice nella clipboard
Express e Fastify leggono il cookie della locale attraverso i loro parser di cookie, quindi devono essere installati insieme. Hono ed Elysia analizzano i cookie nativamente.
htmx stesso è un singolo script tag, aggiunto nel passaggio 4.
Configurazione del vostro progetto
Crea un
intlayer.config.tsalla radice del tuo progetto:intlayer.config.tsCopiare il codiceCopiare il codice nella 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;Per l'elenco completo delle opzioni, consulta la documentazione di configurazione.
Dichiara il Tuo Contenuto
Dichiara ogni etichetta che il server renderizzerà, incluse quelle che appaiono solo all'interno di un frammento:
src/app.content.tsCopiare il codiceCopiare il codice nella clipboard
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ it: "Lingua", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ it: "Articoli nel tuo carrello: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ it: "Aggiungi un articolo", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;Le dichiarazioni di contenuto possono trovarsi ovunque sotto
contentDir(per impostazione predefinita./src) e corrispondere a.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}. Vedi la documentazione sulle dichiarazioni di contenuto.Registra il middleware di Intlayer
Il middleware risolve la locale di ogni richiesta e la espone ai tuoi handler.
src/index.tsCopiare il codiceCopiare il codice nella clipboard
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // Il cookie parser deve essere eseguito per primo: `express-intlayer` legge la locale // del cookie attraverso `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());La locale risolta è su
res.locals.locale.src/index.tsCopiare il codiceCopiare il codice nella 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);La locale risolta si trova su
req.intlayer.locale.src/index.tsCopiare il codiceCopiare il codice nella clipboard
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());La locale risolta è
c.get("locale").src/index.tsCopiare il codiceCopiare il codice nella clipboard
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());La locale risolta è
intlayer!.localenel contesto della route.Per impostazione predefinita, la locale viene presa dal cookie
INTLAYER_LOCALE, quindi dall'headerx-intlayer-locale, quindi dalla negoziazioneAccept-Language.Rendering dei fragment con la locale della richiesta
Scrivi i tuoi renderer di fragment come funzioni pure di una locale e passa la locale risolta dal middleware. Passarla esplicitamente mantiene un fragment legato alla richiesta che lo ha chiesto, qualunque server tu stia utilizzando.
src/views.tsCopiare il codiceCopiare il codice nella clipboard
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Escapa un valore tradotto in modo che non possa fuoriuscire dal 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>`; };Servirlo da una route:
src/index.tsCopiare il codiceCopiare il codice nella clipboard
app.post("/cart/items", (req, res) => { // Ottiene il numero di elementi dal corpo della richiesta const itemCount = Number(req.body?.itemCount ?? 0) + 1; // Invia la risposta HTML con il carrello renderizzato res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsCopiare il codiceCopiare il codice nella clipboard
fastify.post("/cart/items", async (req, reply) => { // Ottiene il numero di elementi dal corpo della richiesta const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; // Invia la risposta HTML con il carrello renderizzato return reply .type("text/html") .send(renderCart(req.intlayer.locale, itemCount)); });src/index.tsCopiare il codiceCopiare il codice nella 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.tsCopiare il codiceCopiare il codice nella 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" }, }); });Lo stesso frammento ora risponde in francese per un visitatore il cui cookie dice
fr, e in arabo per uno il cui cookie dicear, senza alcuna modifica al markup chiamante.Servire la prima pagina
Esegui il rendering del
<body>da solo, in modo che il selettore di lingua nel passaggio 7 possa scambiarlo interamente, quindi racchiudilo nel documento che carica htmx:src/views.tsCopiare il codiceCopiare il codice nella 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>`;getHTMLTextDirrestituisceltr,rtloautoper la locale, il che è ciò che consente all'arabo e all'ebraico di essere visualizzati correttamente.Cambia la lingua
Cambiare la lingua è una richiesta come qualsiasi altra. Il server memorizza la scelta nel cookie che il middleware legge, quindi restituisce la pagina sottoposta a nuovo rendering nella nuova locale.
Renderizza lo switcher come un
selectche si invia da solo e sostituisce l'intero<body>, in modo che anche le etichette statiche intorno ai tuoi frammenti cambino:src/views.tsCopiare il codiceCopiare il codice nella 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)scrive ogni lingua nella lingua attualmente visualizzata. Non passare un secondo argomento per scrivere invece ognuna nella propria lingua.Gestisci il post convalidando il valore, impostando il cookie e restituendo il nuovo corpo:
src/index.tsCopiare il codiceCopiare il codice nella 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.tsCopiare il codiceCopiare il codice nella 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("Locale sconosciuto"); } return reply .setCookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }) .type("text/html") .send(renderBody(requestedLocale, 0)); });src/index.tsCopiare il codiceCopiare il codice nella 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 sconosciuto", 400); } setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); return c.html(renderBody(requestedLocale, 0)); });src/index.tsCopiare il codiceCopiare il codice nella 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" }, }); });isDeclaredLocalelimita una stringa arbitraria a una delle tue locale configurate, quindi un valore inaspettato non raggiunge mai i tuoi renderer.Mantieni lang e dir sincronizzati dopo uno swap
OpzionaleUno swap può sostituire il
<body>, mai l'<html>che lo circonda. Renderizzalangedirsul body scambiato e copiali di nuovo sull'elemento radice una volta, dall'head:src/views.tsCopiare il codiceCopiare il codice nella clipboard
Senza questo, uno switch all'arabo renderizza da destra a sinistra all'interno del body mentre il documento continua a pubblicizzare la lingua precedente alla tecnologia assistiva e ai crawler.
Invia la locale come header invece di un cookie
OpzionaleSe un cookie non ti piace, allega la locale a ogni richiesta htmx con
hx-headerssu un elemento antenato. I discendenti l'ereditano:htmlCopiare il codiceCopiare il codice nella clipboard
Il middleware legge
x-intlayer-localeper impostazione predefinita. Puoi rinominare entrambi i carrier nella tua configurazione:intlayer.config.tsCopiare il codiceCopiare il codice nella clipboard
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Altre opzioni di configurazione routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
Configura TypeScript
Includi i tipi generati automaticamente affinché una chiave non dichiarata sia un errore di compilazione piuttosto che una stringa vuota a runtime.
Copiare il codice nella clipboard
Configurazione Git
È consigliato ignorare i file generati da Intlayer:
Copiare il codice nella clipboard
Estensione VS Code
Per migliorare la tua esperienza di sviluppo con Intlayer, puoi installare l'Estensione Intlayer VS Code ufficiale.
Installa dal VS Code Marketplace
Questa estensione fornisce:
- Autocompletamento per le chiavi di traduzione.
- Rilevamento errori in tempo reale per traduzioni mancanti.
- Anteprime inline dei contenuti tradotti.
- Azioni rapide per creare e aggiornare facilmente le traduzioni.
Per ulteriori dettagli su come utilizzare l'estensione, fare riferimento alla documentazione dell'Intlayer VS Code Extension.
Andare oltre
Per andare oltre, puoi esternalizzare il tuo contenuto utilizzando il CMS, in modo che i traduttori possono modificare i contenuti senza una distribuzione.
Domande frequenti
Perché la richiesta del frammento non conteneva nessuna locale. Le richieste htmx sono indipendenti dalla pagina che le ha emesse, quindi la locale deve viaggiare su ognuna di esse, tramite il cookie INTLAYER_LOCALE o un header x-intlayer-locale impostato con hx-headers. Verifica che il parser dei cookie sia eseguito prima del middleware Intlayer su Express e Fastify, altrimenti il cookie non viene mai letto e ogni richiesta ricade su Accept-Language.
Passalo. Le integrazioni espongono la locale risolta (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), e passarla a getIntlayer rende ogni renderer una funzione pura di una locale. Questo è più facile da testare, e mantiene i tuoi fragment renderers portabili se cambi server.
No. Tutto ciò che un visitatore vede è prodotto dal server, quindi non c'è niente da tradurre nel browser. Questo è anche il motivo per cui il costo del peso della pagina per l'i18n in un'app htmx è quasi zero: nessun catalogo viene mai spedito al client.
Servire le tue pagine con un prefisso di locale (/fr/cart) e leggere la locale dal percorso nel tuo route handler, piuttosto che dal cookie, per il rendering completo della pagina. I frammenti possono continuare a utilizzare il cookie o l'header. Vedi configurazione per le opzioni di routing e rewrite URL personalizzati.
getHTMLTextDir(locale) ritorna ltr, rtl o auto. Impostalo sul documento per il rendering iniziale, e riapplicalo dopo uno swap come mostra il passo 8. Usa proprietà CSS logiche (margin-inline-start invece di margin-left) così il tuo layout le segue.
Sì, per qualsiasi cosa tu interpoli in una template string, esattamente come per qualsiasi altro valore dinamico. Il contenuto proveniente dal CMS o da un traduttore non è markup che controlli. Il passaggio 5 mostra un escaper minimalista.
