Задайте питання та отримайте підсумок документа, вказавши цю сторінку та обраного вами постачальника штучного інтелекту
Історія версій
- "Початкова історія"v9.4.129.08.2026
Вміст цієї сторінки перекладено за допомогою штучного інтелекту.
Переглянути останню версію оригінального вмісту англійськоюЯкщо у вас є ідея щодо покращення цієї документації, будь ласка, долучіться, надіславши pull request на GitHub.
Посилання на документацію на GitHubСкопіювати документацію у форматі Markdown в буфер обміну
Перекладіть вашу htmx програму за допомогою Intlayer | Internationalization (i18n)
htmx не отримує власного контенту. Кожен напис, який читає відвідувач, — це HTML, який виробив ваш сервер, і кожна заміна є окремим HTTP-запитом. Інтернаціоналізація htmx додатка — це тому серверна справа: локаль повинна бути визначена на кожному запиті, і кожен фрагмент повинен бути відрендерений цією мовою.
Intlayer охоплює це через свої backend інтеграції, які виявляють локаль для кожного запиту і показують ваш оголошений контент обробнику, який формує HTML.
Зміст
Три правила i18n у htmx додатку
Одна сторінка може ініціювати десятки swap'ів. Кожен з них — це свіжий запит без пам'яті про сторінку, яка його видала. Якщо локаль живе у змінній, встановленій під час початкового рендерингу, кожен фрагмент після нього повертається до мови за замовчуванням.
Middleware Intlayer розв'язує локаль із самого запиту, тому фрагмент, поданий на десятій хвилині, відповідає тією ж мовою, що й сторінка, подана на нульовій хвилині.
Два переносники працюють з htmx. Cookie (INTLAYER_LOCALE) автоматично відправляється браузером при кожному запиті, включаючи запити htmx. Заголовок (x-intlayer-locale) може бути прикріплений до запитів htmx за допомогою атрибута hx-headers. Обидва читаються за замовчуванням.
Перекладене значення, інтерпольоване у фрагмент, є розміткою. Екранізуйте його точно так само, як ви робили б з будь-яким іншим динамічним значенням, тому переклад, що містить <, не може розірвати документ, у який він вставляється.
Покрокове керівництво
Див. Application Template на GitHub.
Встановлення залежностей
Встановіть
intlayerплюс інтеграцію для вашого сервера.bashКопіювати кодСкопіюйте код у буфер обміну
bashКопіювати кодСкопіюйте код у буфер обміну
bashКопіювати кодСкопіюйте код у буфер обміну
bashКопіювати кодСкопіюйте код у буфер обміну
bashКопіювати кодСкопіюйте код у буфер обміну
Express та Fastify читають куки локалі через власні парсери cookies, тому їх потрібно встановити разом. Hono та Elysia розбирають cookies нативно.
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({ uk: "Мова", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ uk: "Товари у вашому кошику: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ uk: "Додати товар", 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 розпізнає локаль кожного запиту та надає її доступ до ваших обробників.
src/index.tsКопіювати кодСкопіюйте код у буфер обміну
import cookieParser from "cookie-parser"; import express from "express"; import { intlayer } from "express-intlayer"; const app = express(); // The cookie parser has to run first: `express-intlayer` reads the locale // cookie through `req.cookies`. app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());Розпізнана локаль знаходиться на
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());Розраховане locale доступне як
intlayer!.localeна контексті маршруту.За замовчуванням locale беруть з cookies
INTLAYER_LOCALE, потім із заголовкаx-intlayer-locale, потім із переговорівAccept-Language.Рендеризуйте фрагменти з locale запиту
Напишіть ваші рендери фрагментів як чисті функції locale та передайте locale, яке 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; res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsКопіювати кодСкопіюйте код у буфер обміну
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.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, без змін у викликаному розмітці.Служба першої сторінки
Рендеріть
<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для locale, що забезпечує правильне відображення арабської та іврит мов.Змінити мову
Зміна мови — це запит як і будь-який інший. Сервер зберігає вибір у 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); 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звужує довільний рядок до одного з ваших налаштованих локалей, тому неочікуване значення ніколи не потрапляє до ваших рендерерів.Синхронізуйте lang і dir після заміни
Необов'язковоОбмін може замінити
<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 = { // ... Інші параметри конфігурації 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, щоб перекладачі змінювали копію без розгортання.
Часто задавані запитання
Тому що запит фрагмента не мав локалі. htmx запити незалежні від сторінки, яка їх видала, тому локаль повинна передаватися на кожному з них через cookie INTLAYER_LOCALE або заголовок x-intlayer-locale, встановлений за допомогою hx-headers. Переконайтеся, що парсер cookie запускається перед middleware Intlayer на Express та Fastify, інакше cookie ніколи не читається і кожен запит повертається до Accept-Language.
Передавайте його. Інтеграції надають розпізнану локаль (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale), а передача її до getIntlayer робить кожен renderer чистою функцією локалі. Це простіше тестувати, і це робить ваші fragment renderers портативними, якщо ви зміните server.
Ні. Все, що бачить відвідувач, створюється сервером, тому в браузері нічого не потрібно перекладати. Саме тому вартість ваги сторінки для i18n в htmx додатку близька до нуля: жоден каталог ніколи не відправляється клієнту.
Подавайте свої сторінки з префіксом локалі (/fr/cart) і читайте локаль зі шляху у вашому обробнику маршруту, а не з cookie, для повного рендерингу сторінки. Фрагменти можуть продовжити використовувати cookie або заголовок. Див. configuration для параметрів маршрутизації та custom URL rewrites.
getHTMLTextDir(locale) повертає ltr, rtl або auto. Установіть його на документ для першого рендерингу та переналаштуйте його після заміни, як показано на кроці 8. Використовуйте логічні властивості CSS (margin-inline-start замість margin-left), щоб ваш макет слідував за ними.
Так, для будь-чого, що ви інтерполюєте в рядок шаблону, точно як і для будь-якого іншого динамічного значення. Вміст, який надходить від CMS або від перекладача, — це не розмітка, яку ви контролюєте. Крок 5 показує мінімальний екранувач.
