استخدم مساعدك المفضل للملخص واستخدم هذه الصفحة والموفر AI الذي تريده
تاريخ الإصدارات
- "Initial history"v9.4.129/8/2026
تمت ترجمة محتوى هذه الصفحة باستخدام الذكاء الاصطناعي.
اعرض آخر نسخة المحتوى الأصلي باللغة الإنكليزيةإذا كان لديك فكرة لتحسين هذه الوثيقة، فلا تتردد في المساهمة من خلال تقديم طلب سحب على GitHub.
رابط GitHub للتوثيقنسخ الـ Markdown من المستند إلى الحافظة
ترجمة تطبيق htmx باستخدام Intlayer | الدولية (i18n)
htmx لا يعرض أي محتوى خاص به. كل تسمية يقرأها الزائر هي HTML أنتجها الخادم، وكل تبديل هو طلب HTTP منفصل. لذا فإن دولي (i18n) لتطبيق htmx يعتبر مسؤولية الخادم: يجب حل locale على كل طلب، وكل جزء يجب أن يتم تصييره في هذا locale.
يغطي Intlayer هذا من خلال تكاملاته الخلفية، التي تكتشف locale لكل طلب وتعرض محتوى معلن عليه للمعالج الذي ينشئ HTML.
جدول المحتويات
القواعد الثلاث للدولي (i18n) في تطبيق htmx
صفحة واحدة يمكن أن تؤدي إلى عشرات عمليات الاستبدال. كل واحدة منها طلب جديد بدون ذاكرة عن الصفحة التي أصدرتها. إذا كانت اللغة موجودة في متغير تم تعيينه أثناء العرض الأولي، فإن كل جزء بعده سيعود إلى اللغة الافتراضية.
يقوم middleware Intlayer بحل اللغة من الطلب نفسه، لذلك يجيب الجزء المقدم في الدقيقة العاشرة باللغة ذاتها التي تم تقديم الصفحة بها في الدقيقة الصفر.
يعمل حاملان مع htmx. يتم إرسال ملف تعريف الارتباط (INTLAYER_LOCALE) من قبل المتصفح تلقائياً في كل طلب، بما في ذلك طلبات htmx. يمكن إرفاق رأس (x-intlayer-locale) بطلبات htmx باستخدام السمة hx-headers. يتم قراءة كليهما بشكل افتراضي.
القيمة المترجمة المدرجة في مقطع هي markup. قم بـ escape لها، تماماً كما تفعل مع أي قيمة ديناميكية أخرى، لذلك لا يمكن لترجمة تحتوي على < أن تكسر المستند الذي يتم تبديله فيه.
دليل خطوة بخطوة
انظر إلى نموذج التطبيق على GitHub.
تثبيت المتطلبات
قم بتثبيت
intlayerبالإضافة إلى التكامل الخاص بخادمك.bashنسخ الكودنسخ الكود إلى الحافظة
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;للحصول على القائمة الكاملة للخيارات، راجع توثيق التكوين.
أعلن عن محتواك
أعلن عن كل التسميات التي سيعيدها الخادم، بما في ذلك تلك التي تظهر فقط داخل جزء:
src/app.content.tsنسخ الكودنسخ الكود إلى الحافظة
import { insert, t, type Dictionary } from "intlayer"; const appContent = { key: "app", content: { pageTitle: "Intlayer + htmx", localeLabel: t({ ar: "اللغة", en: "Language", fr: "Langue", es: "Idioma", }), cartSummary: insert( t({ ar: "المنتجات في سلتك: {{count}}", en: "Items in your cart: {{count}}", fr: "Articles dans votre panier : {{count}}", es: "Artículos en tu carrito: {{count}}", }) ), addItem: t({ ar: "أضف منتجًا", en: "Add an item", fr: "Ajouter un article", es: "Añadir un artículo", }), }, } 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(); // يجب تشغيل محلل الكوكيز أولاً: `express-intlayer` يقرأ كوكي اللغة // من خلال `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 من ملف تعريف
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) => { // الحصول على عدد العناصر من جسم الطلب وإضافة 1 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" }, }); });الآن يجيب نفس المقطع باللغة الفرنسية للزائر الذي تقول ملفات تعريفه
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للـ locale، وهذا ما يجعل العربية والعبرية تتصرف بشكل صحيح.تبديل اللغة
تبديل اللغة هو طلب مثل أي طلب آخر. يقوم الخادم بتخزين الاختيار في ملف تعريف الارتباط الذي يقرأه 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(""); // إرجاع نموذج HTML يحتوي على محول اللغة 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)تكتب كل لغة باللغة المعروضة حالياً. لا تمرر أي وسيط ثاني لكتابة كل واحدة بلغتها الخاصة بدلاً من ذلك.تعامل مع المنشور من خلال التحقق من القيمة وتعيين ملف تعريف الارتباط وإرجاع النص الجديد:
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) => { // استخراج الـ body من الطلب const body = await c.req.parseBody(); // الحصول على اللغة المطلوبة من الـ body const requestedLocale = String(body["locale"]); // التحقق من أن اللغة المطلوبة معلنة في الإعدادات if (!isDeclaredLocale(requestedLocale)) { return c.text("Unknown locale", 400); } // تعيين ملف تعريف الارتباط للغة المطلوبة setCookie(c, "INTLAYER_LOCALE", requestedLocale, { sameSite: "Lax", path: "/", }); // إرجاع صفحة HTML بالـ render للغة المطلوبة 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 متزامنة بعد استبدال
اختيارييمكن للـ swap أن يستبدل
<body>، لكن ليس العنصر<html>حوله. قم بتصييرlangوdirعلى الـ body المستبدل وانسخهما مرة أخرى إلى العنصر الجذر مرة واحدة، من الـ head:src/views.tsنسخ الكودنسخ الكود إلى الحافظة
بدون هذا، عند التبديل إلى اللغة العربية، سيتم العرض من اليمين إلى اليسار داخل الـ body بينما لا يزال المستند يعلن اللغة السابقة لتكنولوجيا المساعدة وللزحافات.
إرسال المنطقة الإقليمية كرأس بدلاً من ملف تعريف ارتباط
اختياريإذا لم تناسبك ملف تعريف الارتباط (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
يُنصح بتجاهل الملفات المولدة بواسطة Intlayer:
نسخ الكود إلى الحافظة
VS Code Extension
لتحسين تجربة التطوير الخاصة بك مع Intlayer، يمكنك تثبيت Intlayer VS Code Extension الرسمية.
التثبيت من VS Code Marketplace
توفر هذا الامتداد:
- إكمال تلقائي لمفاتيح الترجمة.
- كشف الأخطاء في الوقت الفعلي للترجمات المفقودة.
- معاينات مضمنة للمحتوى المترجم.
- إجراءات سريعة لإنشاء وتحديث الترجمات بسهولة.
للحصول على مزيد من التفاصيل حول كيفية استخدام الامتداد، راجع وثائق امتداد Intlayer VS Code.
المزيد
للمتابعة، يمكنك إضفاء طابع خارجي على محتواك باستخدام CMS، حتى يتمكن المترجمون من تغيير النسخ دون نشر.
الأسئلة الشائعة
لأن طلب الجزء لم يحمل أي locale. طلبات htmx مستقلة عن الصفحة التي أصدرتها، لذا يجب أن ينتقل locale عليها، من خلال ملف تعريف الارتباط INTLAYER_LOCALE أو رأس x-intlayer-locale معين مع hx-headers. تحقق من أن محلل ملفات تعريف الارتباط يعمل قبل middleware الـ Intlayer على Express و Fastify، وإلا فلن يتم قراءة ملف تعريف الارتباط أبداً وسيعود كل طلب إلى Accept-Language.
مررها. التكاملات تكشف عن locale المحلول (res.locals.locale, req.intlayer.locale, c.get("locale"), intlayer!.locale)، وتمريره إلى getIntlayer يجعل كل renderer دالة نقية من locale. هذا أسهل للاختبار، ويحافظ على portability لـ fragment renderers الخاصة بك إذا قمت بتغيير الخادم.
لا. كل شيء يراه الزائر يتم إنتاجه بواسطة الخادم، لذا لا يوجد شيء للترجمة في المتصفح. هذا أيضاً هو السبب في أن تكلفة وزن الصفحة للـ i18n في تطبيق htmx قريبة جداً من الصفر: لا يتم إرسال أي كتالوج إلى العميل أبداً.
قدم صفحاتك تحت بادئة لغة (/fr/cart) واقرأ اللغة من المسار في معالج المسار الخاص بك، بدلاً من ملف تعريف الارتباط، لعرض الصفحة الكاملة. يمكن للأجزاء أن تستمر في استخدام ملف تعريف الارتباط أو رأس الطلب. انظر إلى المكونات الإضافية للإعدادات لخيارات التوجيه وإعادات كتابة عناوين URL المخصصة.
getHTMLTextDir(locale) يُرجع ltr أو rtl أو auto. قم بتعيينه على المستند للعرض الأولي، وأعد تطبيقه بعد التبديل كما توضح الخطوة 8. استخدم خصائص CSS المنطقية (margin-inline-start بدلاً من margin-left) حتى يتبع تخطيطك.
نعم، لأي شيء تقحمه في سلسلة نص template، تماماً كما هو الحال مع أي قيمة ديناميكية أخرى. المحتوى القادم من CMS أو من مترجم ليس markup تتحكم فيه. الخطوة 5 توضح escaper بسيط.
