المؤلف:
    إنشاء:2026-09-13آخر تحديث:2026-09-13

    next-intl مقابل Intlayer | مقارنة أداء التدويل (i18n) في React وNext.js

    مكتبة next-intl هي الخيار الافتراضي لتدويل تطبيقات Next.js App Router اليوم: تكامل محكم مع التوجيه، دعم كامل لـ ICU MessageFormat، وتجربة مطور مألوفة لأي شخص استخدم أنظمة i18n الكلاسيكية.

    أما Intlayer فتعيد التفكير في المشكلة من أساسها: لا توجد قواميس مركزية، ولا حاجة لمطابقة مساحات الأسماء (namespaces) مع المسارات يدوياً. يتم الإعلان عن المحتوى بجانب كل مكون، بينما يتكفل مترجم وقت البناء بحزم ما تحتاجه كل صفحة فقط.

    تقارن هذه المقالة المكتبتين بناءً على بيانات مستخرجة من Benchmark Bloom، وهو مشروع اختبار مفتوح المصدر يبني التطبيق نفسه مع كل مكتبة ويسجل ما يقوم المتصفح بتنزيله وتنفيذه فعلياً.

    باختصار (tl;dr): تضيف next-intl ما لا يقل عن +12.6 كيلوبايت gzip في كل صفحة لمجرد وقت التشغيل (runtime)، وتسرب ~90% من سلاسل الصفحات الأخرى في إعداداتها القياسية (static وdynamic). يتطلب التخلص من هذا التسرب تقسيم الكتالوجات إلى مساحات أسماء واختيارها يدوياً لكل صفحة، وهي مهمة معقدة يتجنبها معظم المطورين. في المقابل، يضمن مترجم Intlayer تسريباً بنسبة 0%، ومكونات أصغر بثلاث مرات، و+0.3 كيلوبايت فقط فوق التطبيق الأساسي، كل ذلك بدون أي إعدادات يدوية إضافية.

    نظرة عامة

    • next-intl - معيار مجتمع Next.js. قواميس JSON مركزية لكل لغة، دعم كامل لـ ICU MessageFormat، وتكامل وثيق مع معالجة طلبات Next.js ونظام التوجيه الخاص به.
    • Intlayer - نموذج محتوى يتمحور حول المكونات. توضع ملفات .content.ts بجانب مكوناتها، ويقوم مترجم وقت البناء بتقليم الشجرة (tree-shaking) والتحميل الكسول للمحتوى لكل مكون ولكل لغة، مع توليد أنواع TypeScript صارمة تلقائياً.
    المكتبةنجوم GitHubإجمالي التعديلاتآخر تعديلالإصدار الأولإصدار NPMتنزيلات NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast Commitأبريل 2024npmnpm downloads
    amannn/next-intlGitHub Repo starsGitHub commit activityLast Commitمارس 2021npmnpm downloads
    يتم تحديث الشارات تلقائياً.

    مقارنة الميزات جنباً إلى جنب

    الميزةIntlayer (react-intlayer / next-intlayer)next-intl (next-intl / use-intl)
    الترجمات بجانب المكونات✅ نعم، ملف .content.ts بجوار كل مكون❌ قواميس JSON مركزية في مجلد messages/
    التكامل مع TypeScript✅ أنواع صارمة مولدة تلقائياً من المحتوى⚠️ مدعوم عبر إعدادات global.d.ts يدوية لمسارات الرسائل
    اكتشاف الترجمات المفقودة✅ خطأ TypeScript + خطأ/تحذير وقت البناء⚠️ وقت التشغيل يعيد المفتاح أو يرمي خطأ حسب الإعدادات
    المحتوى الغني (JSX / Markdown / مكونات)✅ دعم مباشر⚠️ عبر t.rich() مع توفير مكونات التحويل
    دعم ICU MessageFormat⚠️ قيد التطوير✅ نعم، دعم كامل لمعيار ICU
    مكونات الخادم المتزامنةuseIntlayer من next-intlayer/server يعمل في أي مكون خادم متزامن❌ يتطلب تمرير الترجمات عبر الـ props من خادم غير متزامن
    تقليم الشجرة (Tree-shaking)✅ تلقائي لكل مكون ولكل لغة⚠️ يتطلب تقسيماً يدوياً لمساحات الأسماء واستخدام pick()
    التحميل الكسول (Lazy loading)✅ سطر إعداد واحد (importMode: 'dynamic')⚠️ يتطلب استيراداً ديناميكياً يدوياً في getRequestConfig
    محرر مرئي / CMS✅ محرر مرئي مجاني + CMS اختياري❌ لا يوجد
    ترجمة مدعومة بالذكاء الاصطناعي✅ مدمجة وتستخدم مفاتيحك الخاصة❌ لا يوجد
    خادم MCP ومهارات الوكلاء✅ نعم❌ لا يوجد

    الاختبار

    ما تم قياسه

    تبني مجموعة Benchmark Bloom نفس التطبيق مع كل مكتبة: 10 صفحات (الرئيسية، من نحن، المدونة، الوظائف، اتصل بنا، الأسئلة الشائعة، الأسعار، المنتجات، الإعدادات، الفريق)، و10 لغات (en، fr، es، de، it، pt، zh، ja, ko, ru)، مع مكونات متطابقة ومحتوى متطابق. يتم قياس الصفحات باللغتين en وfr. يتم تطبيق كل مكتبة في ما يصل إلى أربع استراتيجيات تحميل:

    الاستراتيجيةالوصفمن يستخدمها
    staticيتم تجميع كل لغة وكل صفحة معاً وتحميلها دفعة واحدةالنماذج الأولية السريعة، الأكواد المولدة بالذكاء
    dynamicيتم تحميل لغة العرض النشطة فقط، ولكن لجميع الصفحات دفعة واحدةمعظم المشاريع
    scoped-staticمساحات أسماء لكل مسار، بدون تحميل كسولنادرة
    scoped-dynamicمساحات أسماء لكل مسار + تحميل كسول. يتم إرسال الصفحة الحالية باللغة الحالية فقطالتطبيقات ذات الميزانيات الصارمة في الأداء

    لا تملك Intlayer متغيراً "مخصص النطاق" (scoped): يقوم المترجم تلقائياً بتحديد نطاق المحتوى لكل مكون، لذلك فإن صفي static وdynamic محصوران بالفعل.

    لكل بناء، تسجل المجموعة:

    • حجم المكتبة (Lib size): حجم gzip لمكون فارغ يستورد مكتبة i18n فقط.
    • JS للصفحة (Page JS): متوسط حجم JavaScript بتنسيق gzip الذي تم تنزيله لكل صفحة.
    • نسبة تسرب اللغة (Locale leak %): نسبة السلاسل التي تنتمي إلى لغة لا يعرضها المستخدم.
    • نسبة تسرب الصفحة (Page leak %): نسبة السلاسل التي تنتمي إلى صفحة لا يتصفحها المستخدم.
    • متوسط حجم المكون (Component avg): متوسط حجم gzip لكل مكون تم تجميعه بمعزل.
    • تفاعلية E2E: الوقت المنقضي بين اختيار لغة جديدة وتحديث html[lang] في الـ DOM.
    • الترطيب (Hydration): مدة مرحلة ترطيب React.
    الأرقام أدناه مأخوذة من اختبار بتاريخ 2026-09-12 باستخدام next-intl 4.14.2 وintlayer 9.5.1.

    النتائج على Next.js (App Router)

    المكتبةالاستراتيجيةحجم المكتبة (gz)متوسط JS للصفحة (gz)تسرب اللغةتسرب الصفحةمتوسط المكون (gz)تفاعلية E2Eالترطيب
    base (بدون i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    next-intlayerstatic5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayerdynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms
    @intlayer/next-intl (توافق)static8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intl (توافق)dynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms

    كيف تقرأ هذه النتائج

    • تكلفة وقت التشغيل. يزن التطبيق الأساسي 141.0 كيلوبايت لكل صفحة. ترفعه next-intl إلى 153.6 كيلوبايت (+12.6 كيلوبايت gzip في كل صفحة)، بينما يرفعه Intlayer إلى 141.3 كيلوبايت فقط (+0.3 كيلوبايت).
    • التسريب. في الإعدادين الأكثر استخداماً (static وdynamic)، ترسل next-intl ما يقرب من ~90% من سلاسل الصفحات الأخرى في كل صفحة، لأن ملف en.json كاملاً يدخل في مزود العميل. يتطلب الوصول إلى 0% تقسيماً يدوياً دقيقاً. أما Intlayer فيحقق 0% تلقائياً.
    • حجم المكون. المكون الذي يستدعي useTranslations() يُترجم في المتوسط إلى 21.8 كيلوبايت؛ والمكون نفسه مع useIntlayer() يبلغ 6.9 كيلوبايت فقط. وفي وضع scoped-static تقفز مكونات next-intl إلى 80.1 كيلوبايت لأن كل مكون يُضمن مساحة أسمائه داخلياً.

    النتائج على TanStack Start (use-intl)

    المكتبةالاستراتيجيةحجم المكتبة (gz)متوسط JS للصفحة (gz)تسرب اللغةتسرب الصفحةمتوسط المكون (gz)تفاعلية E2E
    base (بدون i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms
    @intlayer/use-intl (توافق)dynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms

    كيف تقرأ هذه النتائج

    • يرسل الإعداد البسيط لـ use-intl ما مقداره 68.8 كيلوبايت أكثر من JavaScript لكل صفحة مقارنة بالتطبيق الأساسي.
    • في وضع dynamic، يصل use-intl إلى 119.4 كيلوبايت، لكنه لا يزال يحمل تسرباً للصفحات بنسبة 89.8%.
    • يظهر الفارق المعماري بوضوح في حجم المكونات: 76-87 كيلوبايت مع use-intl مقابل 6-8 كيلوبايت مع Intlayer.
    • تبديل اللغة أسرع بمرتين إلى أربع مرات مع Intlayer (3 مللي ثانية مقابل 7-21 مللي ثانية).

    لماذا هذا الفارق؟ الكتالوجات المركزية مقابل القواميس المجمعة

    تتبع next-intl النموذج التقليدي: ملف JSON واحد لكل لغة، يُحمّل في getRequestConfig، ويُمرر إلى NextIntlClientProvider، ويُقرأ عبر t("namespace.key").

    bash
    .
    ├── messages
       ├── en.json
       └── fr.json
    └── src
        ├── i18n
       ├── request.ts
       └── routing.ts
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    └── page.tsx
    

    لا يمكن لوقت التشغيل معرفة المفاتيح التي ستستخدمها الصفحة فعلياً، لذا فإن الخيار الآمن هو إرسال الكتالوج بأكمله.

    بينما تقلب Intlayer هذه المسؤولية؛ حيث يتم الإعلان عن المحتوى بجانب المكون المعني مباشرة:

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    ├── page.tsx
                    └── page.content.ts
        └── components
            └── Counter
                ├── index.tsx
                └── index.content.ts
    

    في وقت البناء، يرى المترجم أي مكون يستورد أي قاموس، ويحزم هذه القواميس فقط للغة النشطة، ويسقط أي محتوى غير مستخدم تلقائياً.

    للحصول على أرقام صف dynamic، اضبط dictionary.importMode: 'dynamic' في intlayer.config.ts. راجع دليل تحسين الحزمة.

    تجربة المطور

    مكون العميل (Client Component)

    next-intl

    messages/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/Counter.tsx
    "use client";
    
    import { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    export const Counter = () => {
      const t = useTranslations("counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button aria-label={t("label")} onClick={() => setCount((c) => c + 1)}>
            {t("increment")}
          </button>
        </div>
      );
    };
    

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ en: "Counter", fr: "Compteur" }),
        increment: t({ en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    "use client";
    
    import { useState } from "react";
    import { useIntlayer } from "next-intlayer";
    import { useNumber } from "next-intlayer/format";
    
    export const Counter = () => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((c) => c + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    مكونات الخادم المتزامنة (Server Components)

    غالباً ما تكون عناصر واجهة المستخدم المشتركة (شريط التنقل، التذييل، البطاقات) مكونات خادم تُعرض كأبناء لمكونات العميل، لذا لا يمكن أن تكون غير متزامنة (async).

    next-intl

    src/components/ServerCounter.tsx
    type ServerCounterProps = {
      t: (key: string) => string;
      formattedCount: string;
    };
    
    export const ServerCounter = ({ t, formattedCount }: ServerCounterProps) => (
      <div>
        <p>{formattedCount}</p>
        <button aria-label={t("label")}>{t("increment")}</button>
      </div>
    );
    

    Intlayer

    src/components/ServerCounter.tsx
    import { useIntlayer } from "next-intlayer/server";
    import { useNumber } from "next-intlayer/server/format";
    
    export const ServerCounter = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    البيانات الوصفية (Metadata)

    next-intl

    src/app/[locale]/about/page.tsx
    import type { Metadata } from "next";
    import { getTranslations } from "next-intl/server";
    import { routing } from "@/i18n/routing";
    
    const localizedPath = (locale: string, path: string) =>
      locale === routing.defaultLocale ? path : `/${locale}${path}`;
    
    export const generateMetadata = async ({
      params,
    }: {
      params: Promise<{ locale: string }>;
    }): Promise<Metadata> => {
      const { locale } = await params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const languages = Object.fromEntries(
        routing.locales.map((l) => [l, localizedPath(l, "/about")])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    };
    

    Intlayer

    src/app/[locale]/about/page.tsx
    import { getIntlayer, getMultilingualUrls } from "intlayer";
    import type { Metadata } from "next";
    import type { LocalPromiseParams } from "next-intlayer";
    
    export const generateMetadata = async ({
      params,
    }: LocalPromiseParams): Promise<Metadata> => {
      const { locale } = await params;
      const metadata = getIntlayer("about-metadata", locale);
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    

    الاحتفاظ بـ API الخاص بـ next-intl مع مخرجات Intlayer

    لا يتعين عليك إعادة كتابة مكوناتك للحصول على أرقام الأداء الموضحة أعلاه. حزمة @intlayer/next-intl هي محول متوافق مباشرة: يحتفظ بـ useTranslations وgetTranslations وuseFormatter وt.rich() وصيغ الجمع في ICU، ويقدمها من قواميس Intlayer التي يترجمها مترجم Intlayer.

    next.config.ts
    import type { NextConfig } from "next";
    import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
    
    const withIntlayer = createNextIntlPlugin();
    
    const nextConfig: NextConfig = {};
    
    export default withIntlayer(nextConfig);
    

    في الاختبار، تحسن بناء التوافق لنفس التطبيق من 153.6 كيلوبايت إلى 147.5 كيلوبايت لكل صفحة، ومن 21.8 كيلوبايت إلى 8.1 كيلوبايت لكل مكون، ومن تسرب يقارب 90% إلى 0%، مع بقاء كود التطبيق دون أي تعديل. ويمكن لملفات messages/{locale}.json الحالية أن تظل مصدر الحقيقة عبر إضافة مزامنة JSON.

    راجع دليل الانتقال من next-intl لاتباع الخطوات التفصيلية.

    متى تختار أياً منهما؟

    • اختر next-intl إذا كنت بحاجة إلى معيار مجتمع Next.js الواسع، أو تعتمد بشكل كبير على ICU MessageFormat، أو كان تطبيقك صغيراً إلى متوسط الحجم، أو كنت مدمجاً بالفعل مع منصات ترجمة مركزية (Crowdin، Phrase، Lokalise...).
    • اختر Intlayer إذا كنت تريد محتوى بنطاق المكونات، وTypeScript صارماً، وأخطاء للمفاتيح المفقودة عند البناء، وتقليم الشجرة والتحميل الكسول دون أي جهد، ومكونات خادم متزامنة، وأدوات تحرير مدمجة (محرر مرئي، CMS، ترجمة بالذكاء الاصطناعي، خادم MCP).
    • اختر @intlayer/next-intl إذا كنت تستخدم next-intl بالفعل وتريد مكاسب الحزمة والأداء دون إعادة كتابة التطبيق.

    مقارنات ذات صلة

    نجوم GitHub

    تعد نجوم GitHub مؤشراً قوياً على شعبية المشروع وثقة المجتمع وأهميته على المدى الطويل.

    رسم بياني لتاريخ النجوم

    الخاتمة

    next-intl مكتبة قوية ومصانة جيداً، ويؤكد الاختبار أنها خيار جيد على Next.js. لكن نموذج الكتالوج المركزي يضع كل عبء التحسين على عاتق المطور: الإعداد البسيط يسرب نحو 90% من محتوى الصفحات الأخرى، ووقت التشغيل وحده يكلف +12.6 كيلوبايت gzip في كل صفحة.

    ينقل Intlayer هذا العمل بأكمله إلى المترجم. القواميس لكل مكون، والتحميل الكسول لكل لغة، وتطهير المحتوى غير المستخدم تصبح جميعها مخرجات بناء تلقائية. والنتيجة على نفس التطبيق: +0.3 كيلوبايت لكل صفحة، 0% تسرب، ومكونات أصغر بـ 3 مرات، وتبديل لغة أسرع بمرتين إلى 4 مرات على TanStack Start.

    جميع البيانات الأولية والتطبيقات وسيناريوهات الاختبار متاحة في مستودع Benchmark Bloom. يمكنك تشغيلها بنفسك.

    راجع وثيقة 'لماذا Intlayer؟' لمزيد من التفاصيل.

    التعليقات

    لا توجد تعليقات بعد. كن أول من يشارك أفكاره.

    مقالات ذات صلة

    آخر المقالات