Спросите свой вопрос и получите сводку документа, используя эту страницу и выбранного вами поставщика AI
История версий
- "Initial history"v9.4.129.08.2026
Содержимое этой страницы было переведено с помощью ИИ.
Смотреть последнюю версию оригинального контента на английскомЕсли у вас есть идея по улучшению этой документации, не стесняйтесь внести свой вклад, подав запрос на вытягивание на GitHub.
Ссылка на документацию GitHubКопировать Markdown документа в буфер обмена
Переведите ваше htmx приложение с помощью Intlayer | Internationalization (i18n)
htmx не отображает никаких собственных элементов контента. Каждый текст, который видит пользователь, — это HTML, созданный вашим сервером, и каждый swap — это отдельный HTTP-запрос. Интернационализация htmx-приложения — это поэтому ответственность сервера: локаль должна разрешаться при каждом запросе, и каждый фрагмент должен быть отрендерен на этой локали.
Intlayer решает эту проблему через свои backend-интеграции, которые определяют локаль для каждого запроса и предоставляют ваше объявленное содержимое обработчику, который создает HTML.
Содержание
Три правила i18n в htmx-приложении
Одна страница может вызвать десятки свопов. Каждый из них — это отдельный запрос без памяти о странице, которая его инициировала. Если locale находится в переменной, установленной во время начального рендеринга, каждый фрагмент после неё попадает на язык по умолчанию.
Middleware Intlayer разрешает locale из самого запроса, поэтому фрагмент, поданный на десятой минуте, отвечает на том же языке, что и страница, поданная в нулевую минуту.
С htmx работают два носителя. Cookie (INTLAYER_LOCALE) автоматически отправляется браузером на каждый запрос, включая htmx запросы. Заголовок (x-intlayer-locale) может быть прикреплен к htmx запросам с помощью атрибута hx-headers. По умолчанию читаются оба.
Переведённое значение, интерполированное в фрагмент, это разметка. Экранируйте его, точно так же, как вы делали бы с любым другим динамическим значением, чтобы перевод, содержащий <, не мог нарушить документ, в который он вставляется.
Пошаговое руководство
Смотрите Шаблон приложения на GitHub.
Установка зависимостей
Установите
intlayerплюс интеграцию для вашего сервера.bashКопировать кодКопировать код в буфер обмена
bashКопировать кодКопировать код в буфер обмена
bashКопировать кодКопировать код в буфер обмена
bashКопировать кодКопировать код в буфер обмена
Express и Fastify читают куки локали через собственные парсеры куков, поэтому они должны быть установлены вместе. Hono и Elysia анализируют куки изначально.
htmx сам по себе - это единый тег скрипта, добавляемый на шаге 4.
Конфигурация вашего проекта
Создайте
intlayer.config.tsв корне вашего проекта:intlayer.config.tsКопировать кодКопировать код в буфер обмена
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;Для полного списка опций см. документацию конфигурации.
Объявите Ваш Контент
Объявите каждый label, который сервер будет рендерить, включая те, которые появляются только внутри фрагмента:
src/app.content.tsКопировать кодКопировать код в буфер обмена
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ ru: "Язык", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ ru: "Товары в вашей корзине: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ ru: "Добавить товар", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", ar: "أضف منتجًا", }), }, } satisfies Dictionary; export default appContent;Объявления контента могут находиться в любом месте внутри
contentDir(по умолчанию./src) и совпадать с.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}. См. документацию по объявлению контента.Зарегистрировать middleware Intlayer
Middleware разрешает locale каждого запроса и предоставляет его вашим обработчикам.
src/index.tsКопировать кодКопировать код в буфер обмена
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // Cookie parser должен работать первым: `express-intlayer` читает locale // cookie через `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());Разрешённый locale находится на
res.locals.locale.src/index.tsКопировать кодКопировать код в буфер обмена
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);Разрешённая локаль находится на
req.intlayer.locale.src/index.tsКопировать кодКопировать код в буфер обмена
import { Hono } from "hono"; import { intlayer } from "hono-intlayer"; const app = new Hono(); app.use("*", intlayer());Разрешённая локаль — это
c.get("locale").src/index.tsКопировать кодКопировать код в буфер обмена
import { Elysia } from "elysia"; import { intlayer } from "elysia-intlayer"; const app = new Elysia().use(intlayer());Разрешённая локаль — это
intlayer!.localeна контексте маршрута.По умолчанию локаль берётся из cookie
INTLAYER_LOCALE, затем из заголовкаx-intlayer-locale, затем из согласованияAccept-Language.Отрендеривать фрагменты с локалью запроса
Напишите ваши рендереры фрагментов как чистые функции локали и передайте разрешённую middleware локаль. Передача её явно связывает фрагмент с запросом, который его запросил, независимо от того, на каком сервере вы находитесь.
src/views.tsКопировать кодКопировать код в буфер обмена
import { currency, getIntlayer, type Locale } from "intlayer"; const HTML_ENTITIES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** Экранирует переведенное значение, чтобы оно не могло вырваться из разметки. */ 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>`; };Обслуживайте его из маршрута:
src/index.tsКопировать кодКопировать код в буфер обмена
app.post("/cart/items", (req, res) => { // Получаем количество товаров из тела запроса const itemCount = Number(req.body?.itemCount ?? 0) + 1; // Отправляем HTML-ответ с отрендеренной корзиной res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsКопировать кодКопировать код в буфер обмена
fastify.post("/cart/items", async (req, reply) => { // Получаем количество товаров из тела запроса и увеличиваем на 1 const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; // Отправляем HTML-ответ с отрендеренной корзиной return reply .type("text/html") .send(renderCart(req.intlayer.locale, itemCount)); });src/index.tsКопировать кодКопировать код в буфер обмена
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.tsКопировать кодКопировать код в буфер обмена
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" }, }); });Тот же фрагмент теперь отвечает на французском для посетителя, чье cookie говорит
fr, и на арабском для того, чье cookie говоритar, без изменений в вызываемой разметке.Serve the first page
Render the
<body>на его собственном, так что переключатель локали на шаге 7 может заменить его целиком, затем оберните его в документ, который загружает htmx:src/views.tsКопировать кодКопировать код в буфер обмена
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>`;getHTMLTextDirвозвращаетltr,rtlилиautoдля локали, что обеспечивает корректное отображение арабского и иврита.Переключение языка
Переключение языка — это обычный запрос. Сервер сохраняет выбор в cookie, который читает middleware, затем возвращает страницу, отрендеренную в новой локали.
Отобразите переключатель в виде
select, который отправляет сам себя и заменяет весь<body>, чтобы статические метки вокруг ваших фрагментов тоже изменились:src/views.tsКопировать кодКопировать код в буфер обмена
import { getIntlayer, getLocaleName, type Locale, locales } from "intlayer"; const renderLocaleSwitcher = (locale: Locale): string => { // Получить содержимое приложения для текущей локали const content = getIntlayer("app", locale); // Создать опции 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)записывает каждый язык на текущий отображаемый язык. Передайте второй аргумент без значения, чтобы записать каждый на его собственный язык.Обработайте post, проверив значение, установив cookie и вернув новое тело:
src/index.tsКопировать кодКопировать код в буфер обмена
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.tsКопировать кодКопировать код в буфер обмена
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("Unknown locale"); } return reply .setCookie("INTLAYER_LOCALE", requestedLocale, { sameSite: "lax", path: "/", }) .type("text/html") .send(renderBody(requestedLocale, 0)); });src/index.tsКопировать кодКопировать код в буфер обмена
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("Unknown locale", 400); } // Установка cookie с выбранной локалью setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); return c.html(renderBody(requestedLocale, 0)); });src/index.tsКопировать кодКопировать код в буфер обмена
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" }, }); });isDeclaredLocaleсужает произвольную строку до одной из ваших настроенных локалей, поэтому неожиданное значение никогда не достигает ваших renderers.Синхронизировать lang и dir после замены
НеобязательноSwap может заменить
<body>, но никогда<html>вокруг него. Отрендерьтеlangиdirна заменяемом body и скопируйте их обратно на корневой элемент один раз из head:src/views.tsКопировать кодКопировать код в буфер обмена
Без этого переключение на арабский язык будет отображаться справа налево внутри body, но документ по-прежнему сообщает вспомогательным технологиям и краулерам о предыдущем языке.
Отправлять локаль как заголовок вместо cookie
НеобязательноЕсли cookie вам не подходит, прикрепляйте локаль к каждому htmx запросу с помощью
hx-headersна элементе-предке. Потомки наследуют её:htmlКопировать кодКопировать код в буфер обмена
Middleware по умолчанию читает
x-intlayer-locale. Вы можете переименовать оба носителя в вашей конфигурации:intlayer.config.tsКопировать кодКопировать код в буфер обмена
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { // ... Other configuration options routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
Настройка TypeScript
Включите автоматически сгенерированные типы, чтобы необъявленный ключ вызывал ошибку компиляции, а не пустую строку во время выполнения.
Копировать код в буфер обмена
Git Configuration
Рекомендуется игнорировать файлы, сгенерированные Intlayer:
Копировать код в буфер обмена
VS Code Extension
Чтобы улучшить опыт разработки с Intlayer, вы можете установить официальное расширение Intlayer VS Code Extension.
Установите из VS Code Marketplace
Это расширение предоставляет:
- Автодополнение для ключей переводов.
- Обнаружение ошибок в реальном времени для отсутствующих переводов.
- Встроенные предпросмотры переведённого контента.
- Быстрые действия для легкого создания и обновления переводов.
Для получения дополнительной информации об использовании расширения см. документацию расширения Intlayer VS Code Extension.
Идите дальше
Чтобы пойти дальше, вы можете экстернализировать свой контент с помощью CMS, чтобы переводчики могли изменять копию без развёртывания.
Часто задаваемые вопросы
Потому что запрос фрагмента не содержал locale. htmx запросы независимы от страницы, которая их выдала, поэтому locale должен передаваться на каждом запросе через cookie INTLAYER_LOCALE или заголовок x-intlayer-locale, установленный с помощью hx-headers. Убедитесь, что cookie parser запускается перед middleware Intlayer на Express и Fastify, иначе cookie никогда не будет прочитан и каждый запрос вернётся к Accept-Language.
Передавайте его. Интеграции предоставляют разрешённую локаль (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), и передача её в getIntlayer делает каждый renderer чистой функцией локали. Это легче тестировать, и это сохраняет портативность ваших fragment renderers, если вы смените сервер.
Нет. Всё, что видит посетитель, создаётся сервером, поэтому в браузере нечего переводить. Это также причина, по которой стоимость веса страницы i18n в приложении htmx близка к нулю: каталог никогда не отправляется клиенту.
Обслуживайте ваши страницы с префиксом локали (/fr/cart) и читайте локаль из пути в вашем обработчике маршрута, а не из cookie, для полного рендеринга страницы. Фрагменты могут продолжать использовать cookie или заголовок. См. конфигурация для опций маршрутизации и пользовательские переписи URL.
getHTMLTextDir(locale) возвращает ltr, rtl или auto. Установите это на документе для начального рендеринга и переприменяйте после замены, как показано на шаге 8. Используйте логические свойства CSS (margin-inline-start вместо margin-left), чтобы ваша разметка соответствовала этому.
Да, для всего, что вы интерполируете в строку шаблона, точно так же как для любого другого динамического значения. Контент из CMS или от переводчика – это не разметка, которую вы контролируете. Шаг 5 показывает минимальный экранировщик.
