Haz tu pregunta y obtén un resumen del documento referenciando esta página y el proveedor AI de tu elección
Historial de versiones
- "Historial inicial"v9.4.129/8/2026
El contenido de esta página ha sido traducido con una IA.
Ver la última versión del contenido original en inglésSi tienes una idea para mejorar esta documentación, no dudes en contribuir enviando una pull request en GitHub.
Enlace de GitHub a la documentaciónCopiar el Markdown del documento a la portapapeles
Traduce tu aplicación htmx usando Intlayer | Internacionalización (i18n)
htmx no renderiza contenido propio. Cada etiqueta que ve un visitante es HTML que tu servidor produjo, y cada intercambio es una solicitud HTTP separada. Internacionalizar una aplicación htmx es por lo tanto una preocupación del servidor: la locale tiene que resolverse en cada solicitud, y cada fragmento tiene que renderizarse en esa locale.
Intlayer cubre esto a través de sus integraciones de backend, que detectan la locale por solicitud y exponen tu contenido declarado al controlador que construye el HTML.
Tabla de Contenidos
Las tres reglas de i18n en una aplicación htmx
Una sola página puede desencadenar docenas de intercambios. Cada uno es una solicitud nueva sin memoria de la página que la emitió. Si la configuración regional vive en una variable establecida durante la representación inicial, cada fragmento después de ella vuelve al idioma predeterminado.
El middleware de Intlayer resuelve la configuración regional de la solicitud misma, por lo que un fragmento servido en el minuto diez responde en el mismo idioma que la página servida en el minuto cero.
Dos portadores funcionan con htmx. Una cookie (INTLAYER_LOCALE) es enviada automáticamente por el navegador en cada solicitud, incluyendo las de htmx. Un encabezado (x-intlayer-locale) puede adjuntarse a las solicitudes de htmx con el atributo hx-headers. Ambos se leen por defecto.
Un valor traducido interpolado en un fragmento es markup. Escápalo, exactamente como lo harías con cualquier otro valor dinámico, para que una traducción que contenga < no pueda romper el documento en el que se intercambia.
Guía Paso a Paso
Consulta la Plantilla de Aplicación en GitHub.
Instalar Dependencias
Instala
intlayermás la integración para tu servidor.bashCopiar códigoCopiar el código al portapapeles
bashCopiar códigoCopiar el código al portapapeles
bashCopiar códigoCopiar el código al portapapeles
bashCopiar códigoCopiar el código al portapapeles
bashCopiar códigoCopiar el código al portapapeles
Express y Fastify leen la cookie de locale a través de sus propios analizadores de cookies, por lo que deben instalarse junto con ellos. Hono y Elysia analizan cookies de forma nativa.
htmx en sí es una única etiqueta de script, agregada en el paso 4.
Configuración de tu proyecto
Crea un
intlayer.config.tsen la raíz de tu proyecto:intlayer.config.tsCopiar códigoCopiar el código al portapapeles
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;Para obtener la lista completa de opciones, consulta la documentación de configuración.
Declarar tu contenido
Declara cada etiqueta que el servidor renderizará, incluyendo las que solo aparecen dentro de un fragmento:
src/app.content.tsCopiar códigoCopiar el código al portapapeles
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ es: "Idioma", en: "Language", fr: "Langue", ar: "اللغة", }), cartSummary: insert( t({ es: "Artículos en tu carrito: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ es: "Añadir un artículo", en: "Add an item", fr: "Ajouter un article", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;Las declaraciones de contenido pueden vivir en cualquier lugar bajo
contentDir(por defecto./src) y coincidir.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}. Consulta la documentación de declaración de contenido.Registrar el middleware de Intlayer
El middleware resuelve la configuración regional de cada solicitud y la expone a tus manejadores.
src/index.tsCopiar códigoCopiar el código al portapapeles
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // El analizador de cookies debe ejecutarse primero: `express-intlayer` lee la configuración regional // cookie a través de `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());La configuración regional resuelta está en
res.locals.locale.src/index.tsCopiar códigoCopiar el código al portapapeles
</budget:token_budget> 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 configuración regional resuelta está en
req.intlayer.locale.src/index.tsCopiar códigoCopiar el código al portapapeles
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());La configuración regional resuelta es
c.get("locale").src/index.tsCopiar códigoCopiar el código al portapapeles
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());La configuración regional resuelta es
intlayer!.localeen el contexto de la ruta.Por defecto, la configuración regional se toma de la cookie
INTLAYER_LOCALE, luego del encabezadox-intlayer-locale, luego de la negociaciónAccept-Language.Renderizar fragmentos con la configuración regional de la solicitud
Escribe tus renderizadores de fragmentos como funciones puras de una configuración regional, y pasa la configuración regional que el middleware resolvió. Pasarla explícitamente mantiene un fragmento vinculado a la solicitud que lo pidió, sin importar en qué servidor estés.
src/views.tsCopiar códigoCopiar el código al portapapeles
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Escapa un valor traducido para que no pueda salir del marcado. */ 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>`; };Sírvelo desde una ruta:
src/index.tsCopiar códigoCopiar el código al portapapeles
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.tsCopiar códigoCopiar el código al portapapeles
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.tsCopiar códigoCopiar el código al portapapeles
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.tsCopiar códigoCopiar el código al portapapeles
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" }, }); });El mismo fragmento ahora responde en francés para un visitante cuya cookie dice
fr, y en árabe para uno cuya cookie dicear, sin cambios en el marcado de llamada.Servir la primera página
Renderiza el
<body>por sí solo, para que el cambiador de idioma en el paso 7 pueda intercambiarlo completamente, luego envuélvelo en el documento que carga htmx:src/views.tsCopiar códigoCopiar el código al portapapeles
import { getHTMLTextDir, getIntlayer, type Locale } from "intlayer"; export const renderBody = (locale: Locale, itemCount: number): string => { // Obtener el contenido internacionalizado para la locale especificada 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>`;getHTMLTextDirdevuelveltr,rtloautopara la locale, lo que permite que el árabe y el hebreo se muestren correctamente.Cambiar el idioma
Cambiar el idioma es una solicitud como cualquier otra. El servidor almacena la selección en la cookie que lee el middleware, y luego devuelve la página renderizada nuevamente en la nueva locale.
Renderiza el selector como un
selectque se envía a sí mismo e intercambia todo el<body>, para que las etiquetas estáticas alrededor de tus fragmentos también cambien:src/views.tsCopiar códigoCopiar el código al portapapeles
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)escribe cada idioma en el idioma actualmente mostrado. No pases un segundo argumento para escribir cada uno en su propio idioma en su lugar.Maneja la publicación validando el valor, estableciendo la cookie y devolviendo el nuevo cuerpo:
src/index.tsCopiar códigoCopiar el código al portapapeles
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.tsCopiar códigoCopiar el código al portapapeles
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("Idioma desconocido"); } return reply .setCookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }) .type("text/html") .send(renderBody(requestedLocale, 0)); });src/index.tsCopiar códigoCopiar el código al portapapeles
import { setCookie } from "hono/cookie"; import { isDeclaredLocale } from "intlayer"; app.post("/locale", async (c) => { // Analizar el cuerpo de la solicitud const body = await c.req.parseBody(); // Obtener la locale solicitada del cuerpo const requestedLocale = String(body["locale"]); // Verificar si la locale está declarada if (!isDeclaredLocale(requestedLocale)) { return c.text("Locale desconocida", 400); } // Establecer la cookie de locale setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); // Retornar la respuesta HTML renderizada return c.html(renderBody(requestedLocale, 0)); });src/index.tsCopiar códigoCopiar el código al portapapeles
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" }, }); });isDeclaredLocaleestrecha una cadena arbitraria a uno de tus locales configurados, por lo que un valor inesperado nunca llega a tus renderizadores.Mantener lang y dir sincronizados después de un swap
OpcionalUn swap puede reemplazar el
<body>, nunca el<html>que lo rodea. Renderizalangydiren el body intercambiado y cópialos de vuelta al elemento raíz una vez, desde el head:src/views.tsCopiar códigoCopiar el código al portapapeles
Sin esto, un cambio al árabe renderiza de derecha a izquierda dentro del body mientras el documento aún anuncia el idioma anterior a la tecnología de asistencia y a los crawlers.
Enviar la configuración regional como encabezado en lugar de una cookie
OpcionalSi una cookie no te conviene, adjunta la locale a cada solicitud htmx con
hx-headersen un elemento ancestro. Los descendientes la heredan:htmlCopiar códigoCopiar el código al portapapeles
El middleware lee
x-intlayer-localepor defecto. Puedes renombrar ambos portadores en tu configuración:intlayer.config.tsCopiar códigoCopiar el código al portapapeles
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Otras opciones de configuración routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
Configurar TypeScript
Incluye los tipos autogenerados para que una clave no declarada sea un error de compilación en lugar de una cadena vacía en tiempo de ejecución.
Copiar el código al portapapeles
Configuración de Git
Se recomienda ignorar los archivos generados por Intlayer:
Copiar el código al portapapeles
Extensión de VS Code
Para mejorar tu experiencia de desarrollo con Intlayer, puedes instalar la Extensión oficial de Intlayer para VS Code.
Instalar desde VS Code Marketplace
Esta extensión proporciona:
- Autocompleción para claves de traducción.
- Detección de errores en tiempo real para traducciones faltantes.
- Vistas previas en línea del contenido traducido.
- Acciones rápidas para crear y actualizar traducciones fácilmente.
Para más detalles sobre cómo usar la extensión, consulta la documentación de la extensión Intlayer VS Code.
Ir más allá
Para ir más allá, puedes externalizar tu contenido usando el CMS, para que los traductores cambien el contenido sin necesidad de una implementación.
Preguntas Frecuentes
Porque la solicitud del fragmento no llevaba ninguna configuración regional. Las solicitudes de htmx son independientes de la página que las emitió, por lo que la configuración regional tiene que viajar en cada una, a través de la cookie INTLAYER_LOCALE o un encabezado x-intlayer-locale establecido con hx-headers. Comprueba que el analizador de cookies se ejecuta antes del middleware de Intlayer en Express y Fastify, de lo contrario la cookie nunca se lee y cada solicitud vuelve a Accept-Language.
Pásalo. Las integraciones exponen la locale resuelta (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), y pasarla a getIntlayer hace que cada renderer sea una función pura de una locale. Eso es más fácil de probar, y mantiene tus fragment renderers portátiles si cambias de servidor.
No. Todo lo que ve un visitante es producido por el servidor, así que no hay nada que traducir en el navegador. Por eso también el costo de peso de la página del i18n en una app htmx es casi cero: ningún catálogo se envía nunca al cliente.
Sirva sus páginas bajo un prefijo de locale (/fr/cart) y lea el locale de la ruta en su controlador de rutas, en lugar de desde la cookie, para el renderizado completo de la página. Los fragmentos pueden seguir utilizando la cookie o el encabezado. Véase configuración para las opciones de enrutamiento y reescrituras de URL personalizadas.
getHTMLTextDir(locale) devuelve ltr, rtl o auto. Establézcalo en el documento para el renderizado inicial y vuelva a aplicarlo después de un intercambio como se muestra en el paso 8. Utilice propiedades lógicas de CSS (margin-inline-start en lugar de margin-left) para que su diseño se ajuste.
Sí, para cualquier cosa que interpoles en una cadena de plantilla, exactamente como para cualquier otro valor dinámico. El contenido proveniente del CMS o de un traductor no es markup que controles. El paso 5 muestra un escapador mínimo.
