Faça sua pergunta e obtenha um resumo do documento referenciando esta página e o provedor AI de sua escolha
Histórico de versões
- "Histórico inicial"v9.4.129/08/2026
O conteúdo desta página foi traduzido com uma IA.
Veja a última versão do conteúdo original em inglêsSe você tiver uma ideia para melhorar esta documentação, sinta-se à vontade para contribuir enviando uma pull request no GitHub.
Link do GitHub para a documentaçãoCopiar o Markdown do documento para a área de transferência
Traduza sua aplicação htmx usando Intlayer | Internacionalização (i18n)
htmx não renderiza conteúdo próprio. Todo rótulo que um visitante lê é HTML que seu servidor produziu, e cada swap é uma solicitação HTTP separada. Internacionalizar um aplicativo htmx é, portanto, uma preocupação do servidor: a locale tem que ser resolvida em cada solicitação, e cada fragmento tem que ser renderizado nessa locale.
Intlayer cobre isso através de suas integrações de backend, que detectam a locale por solicitação e expõem seu conteúdo declarado ao handler que constrói o HTML.
Índice de Conteúdos
As três regras de i18n em um aplicativo htmx
Uma única página pode acionar dezenas de swaps. Cada um é uma requisição nova sem memória da página que o emitiu. Se a locale vive em uma variável definida durante a renderização inicial, cada fragment após ela volta ao idioma padrão.
O middleware Intlayer resolve a locale a partir da própria requisição, então um fragment servido no minuto dez responde no mesmo idioma que a página servida no minuto zero.
Dois carriers funcionam com htmx. Um cookie (INTLAYER_LOCALE) é enviado pelo navegador automaticamente em cada requisição, incluindo as do htmx. Um header (x-intlayer-locale) pode ser anexado às requisições htmx com o atributo hx-headers. Ambos são lidos por padrão.
Um valor traduzido interpolado em um fragmento é markup. Escape-o, exatamente como você faria com qualquer outro valor dinâmico, para que uma tradução contendo < não possa quebrar o documento no qual ele é trocado.
Guia Passo a Passo
Veja Modelo de Aplicação no GitHub.
Instalar Dependências
Instale
intlayermais a integração para seu servidor.bashCopiar códigoCopiar o código para a área de transferência
bashCopiar códigoCopiar o código para a área de transferência
bashCopiar códigoCopiar o código para a área de transferência
bashCopiar códigoCopiar o código para a área de transferência
bashCopiar códigoCopiar o código para a área de transferência
Express e Fastify leem o cookie de locale através dos seus próprios parsers de cookies, portanto esses têm que ser instalados juntamente. Hono e Elysia analisam cookies nativamente.
htmx em si é uma única tag de script, adicionada no passo 4.
Configuração do seu projeto
Crie um
intlayer.config.tsna raiz do seu projeto:intlayer.config.tsCopiar códigoCopiar o código para a área de transferência
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 a lista completa de opções, consulte a documentação de configuração.
Declare Your Content
Declare every label the server will render, including the ones that only ever appear inside a fragment:
src/app.content.tsCopiar códigoCopiar o código para a área de transferência
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ pt: "Idioma", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ pt: "Itens no seu carrinho: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ pt: "Adicionar um item", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;As declarações de conteúdo podem estar em qualquer lugar dentro de
contentDir(por padrão./src) e corresponder a.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}. Consulte a documentação de declaração de conteúdo.Registrar o middleware do Intlayer
O middleware resolve a locale de cada requisição e a expõe aos seus handlers.
src/index.tsCopiar códigoCopiar o código para a área de transferência
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // O cookie parser precisa rodar primeiro: `express-intlayer` lê a locale // do cookie através de `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());A locale resolvida está em
res.locals.locale.src/index.tsCopiar códigoCopiar o código para a área de transferência
</chunk> 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);O locale resolvido está em
req.intlayer.locale.src/index.tsCopiar códigoCopiar o código para a área de transferência
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());O locale resolvido é
c.get("locale").src/index.tsCopiar códigoCopiar o código para a área de transferência
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());O locale resolvido é
intlayer!.localeno contexto da rota.Por padrão, o locale é obtido do cookie
INTLAYER_LOCALE, depois do headerx-intlayer-locale, e depois da negociaçãoAccept-Language.Renderizar fragmentos com o locale da requisição
Escreva seus renderizadores de fragmentos como funções puras de um locale, e passe o locale que o middleware resolveu. Passá-lo explicitamente mantém um fragmento vinculado à requisição que o pediu, seja qual for o servidor em que você está.
src/views.tsCopiar códigoCopiar o código para a área de transferência
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Escapa um valor traduzido para que não possa sair da marcação. */ 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>`; };Entregue-o a partir de uma rota:
src/index.tsCopiar códigoCopiar o código para a área de transferência
app.post("/cart/items", (req, res) => { // Obtém o número de itens do corpo da requisição, padrão é 0 const itemCount = Number(req.body?.itemCount ?? 0) + 1; // Retorna o carrinho renderizado em HTML res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsCopiar códigoCopiar o código para a área de transferência
fastify.post("/cart/items", async (req, reply) => { // Obtém o número de itens do corpo da requisição, padrão é 0 const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; // Retorna o carrinho renderizado em HTML return reply .type("text/html") .send(renderCart(req.intlayer.locale, itemCount)); });src/index.tsCopiar códigoCopiar o código para a área de transferência
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 o código para a área de transferência
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" }, }); });O mesmo fragmento agora responde em francês para um visitante cujo cookie diz
fr, e em árabe para um cujo cookie dizar, sem nenhuma alteração na marcação chamadora.Servir a primeira página
Renderize o
<body>por si só, para que o alternador de locale na etapa 7 possa trocá-lo integralmente, depois envolva-o no documento que carrega o htmx:src/views.tsCopiar códigoCopiar o código para a área de transferência
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>`;getHTMLTextDirretornaltr,rtlouautopara o locale, o que faz com que Árabe e Hebraico sejam renderizados corretamente.Alternar o idioma
Alternar idioma é uma requisição como qualquer outra. O servidor armazena a escolha no cookie que o middleware lê e então retorna a página renderizada novamente no novo locale.
Renderize o seletor como um
selectque se submete e troca todo o<body>, para que os rótulos estáticos ao redor de seus fragmentos também mudem:src/views.tsCopiar códigoCopiar o código para a área de transferência
import { getIntlayer, getLocaleName, type Locale, locales } from "intlayer"; const renderLocaleSwitcher = (locale: Locale): string => { // Obtém o conteúdo internacionalizado para a localidade atual const content = getIntlayer("app", locale); // Mapeia cada localidade disponível para uma opção select 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)escreve cada idioma no idioma atualmente exibido. Não passe um segundo argumento para escrever cada um em seu próprio idioma.Manipule o post validando o valor, configurando o cookie e retornando o novo body:
src/index.tsCopiar códigoCopiar o código para a área de transferência
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 o código para a área de transferência
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 desconhecida"); } return reply .setCookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }) .type("text/html") .send(renderBody(requestedLocale, 0)); });src/index.tsCopiar códigoCopiar o código para a área de transferência
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"]); // Valida se a locale solicitada é uma das locales configuradas if (!isDeclaredLocale(requestedLocale)) { return c.text("Unknown locale", 400); } // Define o cookie da locale setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); return c.html(renderBody(requestedLocale, 0)); });src/index.tsCopiar códigoCopiar o código para a área de transferência
import { isDeclaredLocale } from "intlayer"; app.post("/locale", ({ body, cookie, status }) => { const requestedLocale = String((body as { locale?: string })?.locale); // Verifica se a locale solicitada é uma locale declarada if (!isDeclaredLocale(requestedLocale)) { return status(400, "Unknown locale"); } // Define o cookie da locale do Intlayer cookie["INTLAYER_LOCALE"]!.set({ value: requestedLocale, sameSite: "lax", path: "/", }); // Retorna a resposta HTML renderizada com a nova locale return new Response(renderBody(requestedLocale, 0), { headers: { "content-type": "text/html" }, }); });isDeclaredLocalereduz uma string arbitrária para uma de suas locales configuradas, garantindo que um valor inesperado nunca atinja seus renderers.Manter lang e dir sincronizados após uma troca
OpcionalUma troca pode substituir o
<body>, nunca o<html>ao seu redor. Renderizelangedirno body trocado e copie-os de volta para o elemento raiz uma vez, a partir do head:src/views.tsCopiar códigoCopiar o código para a área de transferência
Sem isso, uma troca para árabe renderiza da direita para a esquerda dentro do body enquanto o documento ainda anuncia o idioma anterior para tecnologia assistiva e crawlers.
Enviar a localização como um header em vez de um cookie
OpcionalSe um cookie não se adequar a você, anexe a localidade a cada requisição htmx com
hx-headersem um elemento ancestral. Os descendentes herdam:htmlCopiar códigoCopiar o código para a área de transferência
O middleware lê
x-intlayer-localepor padrão. Você pode renomear ambos os transportadores na sua configuração:intlayer.config.tsCopiar códigoCopiar o código para a área de transferência
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Outras opções de configuração routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
Configurar TypeScript
Inclua os tipos gerados automaticamente para que uma chave não declarada seja um erro de compilação em vez de uma string vazia em tempo de execução.
Copiar o código para a área de transferência
Configuração do Git
É recomendado ignorar os arquivos gerados pelo Intlayer:
Copiar o código para a área de transferência
Extensão VS Code
Para melhorar sua experiência de desenvolvimento com Intlayer, você pode instalar a Extensão Oficial Intlayer para VS Code.
Instale do VS Code Marketplace
Esta extensão fornece:
- Autocompletar para chaves de tradução.
- Detecção de erros em tempo real para traduções ausentes.
- Visualizações inline do conteúdo traduzido.
- Ações rápidas para criar e atualizar traduções facilmente.
Para mais detalhes sobre como usar a extensão, consulte a documentação da Extensão Intlayer VS Code.
Ir Além
Para ir além, você pode externalizar seu conteúdo usando o CMS, para que tradutores alterem o conteúdo sem necessidade de deployment.
Perguntas Frequentes
Porque a solicitação do fragmento não continha nenhuma locale. As solicitações htmx são independentes da página que as emitiu, então a locale deve viajar em cada uma, através do cookie INTLAYER_LOCALE ou um header x-intlayer-locale definido com hx-headers. Verifique se o parser de cookie é executado antes do middleware Intlayer no Express e Fastify, caso contrário, o cookie nunca é lido e toda solicitação volta para Accept-Language.
Passe-o. As integrações expõem o locale resolvido (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), e passá-lo para getIntlayer faz de cada renderer uma função pura de um locale. Isso é mais fácil de testar, e mantém seus fragment renderers portáveis se você mudar de servidor.
Não. Tudo o que um visitante vê é produzido pelo servidor, portanto não há nada para traduzir no navegador. É também por isso que o custo de peso da página de i18n em um app htmx é próximo a zero: nenhum catálogo é jamais enviado para o cliente.
Sirva suas páginas sob um prefixo de locale (/fr/cart) e leia a locale do caminho em seu manipulador de rota, em vez de do cookie, para a renderização de página completa. Fragmentos podem continuar usando o cookie ou o header. Consulte configuração para as opções de roteamento e reescritas de URL personalizadas.
getHTMLTextDir(locale) retorna ltr, rtl ou auto. Configure-o no documento para a renderização inicial e reaplique-o após uma troca conforme a etapa 8 mostra. Use propriedades CSS lógicas (margin-inline-start em vez de margin-left) para que seu layout siga.
Sim, para qualquer coisa que você interpole em uma string de template, exatamente como para qualquer outro valor dinâmico. Conteúdo vindo do CMS ou de um tradutor não é markup que você controla. O passo 5 mostra um escapador minimal.
