このページとあなたの好きなAIアシスタントを使ってドキュメントを要約します
バージョン履歴
- "Initial history"v9.4.12026/8/29
このページのコンテンツはAIを使用して翻訳されました。
英語の元のコンテンツの最新バージョンを見るこのドキュメントを改善するアイデアがある場合は、GitHubでプルリクエストを送信することで自由に貢献してください。
ドキュメントへのGitHubリンクドキュメントのMarkdownをクリップボードにコピー
Intlayerを使用してhtmxアプリケーションを翻訳する | 国際化(i18n)
htmxはそれ自体のコンテンツをレンダリングしません。訪問者が読むすべてのラベルはサーバーが生成したHTMLであり、すべてのスワップは別のHTTPリクエストです。したがって、htmxアプリの国際化はサーバーの関心事です。ロケールは各リクエストで解決される必要があり、各フラグメントはそのロケールでレンダリングされる必要があります。
Intlayerはバックエンド統合を通じてこれをカバーし、リクエストごとにロケールを検出し、HTMLを構築するハンドラーに対して宣言されたコンテンツを公開します。
目次
htmxアプリでのi18nの3つのルール
A single page can trigger dozens of swaps. Each one is a fresh request with no memory of the page that issued it. If the locale lives in a variable set during the initial render, every fragment after it falls back to the default language.
The Intlayer middleware resolves the locale from the request itself, so a fragment served at minute ten answers in the same language as the page served at minute zero.
Two carriers work with htmx. A cookie (INTLAYER_LOCALE) is sent by the browser automatically on every request, including htmx ones. A header (x-intlayer-locale) can be attached to htmx requests with the hx-headers attribute. Both are read by default.
フラグメントに挿入された翻訳値はマークアップです。他の動的な値と同じように、それをエスケープしてください。そうすれば、< を含む翻訳が、それが交換されるドキュメントを破壊することはありません。
ステップバイステップガイド
Application Template を GitHub で参照してください。
依存関係をインストール
intlayerとサーバーの統合をインストールします。bashコードをコピーコードをクリップボードにコピー
bashコードをコピーコードをクリップボードにコピー
bashコードをコピーコードをクリップボードにコピー
bashコードをコピーコードをクリップボードにコピー
bashコードをコピーコードをクリップボードにコピー
ExpressとFastifyはそれぞれのcookieパーサーを通じてlocale cookieを読み取るため、これらと一緒にインストールする必要があります。HonoとElysiaはネイティブにcookieを解析します。
htmx自体は単一のscriptタグであり、ステップ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;全オプションのリストについては、設定ドキュメントを参照してください。
コンテンツを宣言する
サーバーがレンダリングするすべてのラベルを宣言します。フラグメント内にのみ表示されるものも含めて:
src/app.content.tsコードをコピーコードをクリップボードにコピー
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ ja: "言語", en: "Language", fr: "Langue", es: "Idioma", ar: "اللغة", }), cartSummary: insert( t({ ja: "カート内のアイテム: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", ar: "المنتجات في سلتك: {{count}}", }) ), addItem: t({ ja: "アイテムを追加", 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}にマッチします。コンテンツ宣言ドキュメンテーションを参照してください。Intlayer ミドルウェアを登録する
各リクエストの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`は // `req.cookies`を通じてlocale cookieを読み込みます。 app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(intlayer());解決されたlocaleは
res.locals.localeにあります。src/index.tsコードをコピーコードをクリップボードにコピー
typescript fileName="src/index.ts" codeFormat={["typescript", "esm"]} 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です。デフォルトでは、ロケールは
INTLAYER_LOCALEクッキーから取得され、次にx-intlayer-localeヘッダー、その後Accept-Languageネゴシエーションから取得されます。リクエストロケールでフラグメントをレンダリングする
フラグメントレンダラーをロケールの純粋な関数として記述し、ミドルウェアが解決したロケールを渡します。明示的に渡すことで、フラグメントがどのサーバー上にあるかに関わらず、それを要求したリクエストに関連付けられます。
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) => { // リクエストボディからitemCountを取得し、デフォルト値は0 const itemCount = Number(req.body?.itemCount ?? 0) + 1; // renderCart関数を使用してHTMLを生成し、レスポンスを返す res.type("html").send(renderCart(res.locals.locale, itemCount)); });src/index.tsコードをコピーコードをクリップボードにコピー
fastify.post("/cart/items", async (req, reply) => { // リクエストボディからitemCountを取得し、デフォルト値は0 const itemCount = Number((req.body as { itemCount?: string })?.itemCount ?? 0) + 1; // renderCart関数を使用して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" }, }); });同じフラグメントは、クッキーが
frであるビジターに対してはフランス語で、クッキーが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を返します。これにより、アラビア語とヘブライ語が正しくレイアウトされるようになります。言語を切り替える
言語の切り替えは、他のリクエストと同様です。サーバーはこの選択をミドルウェアが読み込むクッキーに保存し、その後、ページを新しいロケールで再レンダリングして返します。
<select>を使用してスイッチャーをレンダリングし、自身をpostして全体の<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)は、現在表示されている言語で各言語を記述します。2番目の引数を渡さない場合は、代わりに各言語をそれ自身の言語で記述します。POSTを処理するには、値を検証し、cookieを設定し、新しいbodyを返します:
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) => { // ボディをパースしてrequestLocaleを取得 const body = await c.req.parseBody(); const requestedLocale = String(body["locale"]); // 宣言されたロケールかどうかをチェック if (!isDeclaredLocale(requestedLocale)) { return c.text("Unknown locale", 400); } // クッキーを設定 setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); // HTMLレスポンスを返す 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は任意の文字列をあなたが設定したロケールの1つに絞り込むため、予期しない値がレンダラーに到達することはありません。スワップ後に lang と dir を同期させた状態に保つ
オプションswapは
<body>を置き換えることができますが、その周りの<html>は置き換えられません。swappedされたbodyにlangとdirをレンダリングし、headからroot要素に一度コピーして戻します:src/views.tsコードをコピーコードをクリップボードにコピー
これがない場合、アラビア語への切り替えはbody内で右から左へレンダリングされますが、ドキュメントは前の言語を支援技術とクローラーに引き続き広告します。
localeをクッキーの代わりにヘッダーとして送信する
オプションCookieが適切でない場合は、祖先要素の
hx-headersを使用して、すべてのhtmxリクエストにlocaleを付与します。子孫はそれを継承します:htmlコードをコピーコードをクリップボードにコピー
ミドルウェアはデフォルトで
x-intlayer-localeを読み取ります。設定でキャリアの名前を変更できます:intlayer.config.tsコードをコピーコードをクリップボードにコピー
import { Locales, type IntlayerConfig } from "intlayer"; // Intlayerの設定 const config: IntlayerConfig = { // ... その他の設定オプション routing: { storage: [ { type: "header", name: "my-locale-header" }, { type: "cookie", name: "my-locale-cookie" }, ], }, }; export default config;
TypeScriptの設定
自動生成された型を含めることで、実行時に空の文字列になるのではなく、宣言されていないキーがコンパイルエラーになります。
コードをクリップボードにコピー
Git 設定
Intlayer によって生成されたファイルを無視することをお勧めします:
コードをクリップボードにコピー
VS Code Extension
Intlayer を使用した開発体験を向上させるために、公式の Intlayer VS Code Extension をインストールできます。
このエクステンションは以下を提供します:
- 翻訳キーの自動補完。
- 欠落している翻訳のリアルタイムエラー検出。
- 翻訳済みコンテンツのインラインプレビュー。
- 翻訳を簡単に作成・更新するクイックアクション。
エクステンションの使用方法の詳細については、Intlayer VS Code エクステンションのドキュメントを参照してください。
さらに進める
さらに進めるには、CMSを使用してコンテンツを外部化できます。これにより、翻訳者はデプロイメントなしでコピーを変更できます。
よくある質問
フラグメントリクエストにロケールが含まれていなかったため。htmxリクエストはそれを発行したページから独立しているため、INTLAYER_LOCALE cookieまたはhx-headersで設定されたx-intlayer-localeヘッダーを通じて、各リクエストにロケールを含める必要があります。ExpressおよびFastifyでcookie parserがIntlayer middlewareの前に実行されていることを確認してください。そうでない場合、cookieは読み込まれず、すべてのリクエストがAccept-Languageにフォールバックします。
それを渡してください。インテグレーションは解決されたロケール(res.locals.locale、req.intlayer.locale、c.get("locale")、intlayer!.locale)を公開しており、getIntlayerに渡すことで各レンダラーをロケールの純粋な関数にします。これはテストしやすく、サーバーを変更した場合でもフラグメントレンダラーをポータブルに保つことができます。
いいえ。訪問者が見るものはすべてサーバーによって生成されるため、ブラウザで翻訳するものはありません。これはまた、htmxアプリのi18nのページ重量コストがほぼゼロである理由です。カタログがクライアントに送信されることはありません。
ロケールプレフィックス(/fr/cart)の下でページを提供し、Cookie ではなくルートハンドラーのパスからロケールを読み取ります。これはフルページレンダリングの場合です。フラグメントは引き続き Cookie またはヘッダーを使用できます。configurationでルーティングオプションを、custom URL rewritesを参照してください。
getHTMLTextDir(locale) は ltr、rtl、または auto を返します。初期レンダリングではドキュメントに設定し、ステップ 8 に示すようにスワップ後に再度適用します。CSS 論理プロパティ(margin-left ではなく margin-inline-start)を使用して、レイアウトが従うようにします。
はい、テンプレート文字列に挿入するすべてのものに対して、他の動的値とまったく同じようにエスケープする必要があります。CMSまたは翻訳者から来るコンテンツは、あなたが制御するマークアップではありません。ステップ5は最小限のエスケーパーを示しています。
