i18n در Next.js 15 با next-intl + RTL (راهنمای ۲۰۲۶)

راه‌اندازی کامل بین‌المللی‌سازی در Next.js 15 App Router با next-intl، تشخیص خودکار زبان، رندر استاتیک، hreflang و پشتیبانی RTL برای فارسی و عربی همراه با مثال‌های واقعی.

i18n در Next.js 15 + RTL (۲۰۲۶)

به‌روزرسانی: ۶ ژوئن ۲۰۲۶

برای پیاده‌سازی بین‌المللی‌سازی (i18n) در Next.js 15 App Router باید از یک سگمنت پویا به نام [locale] در ریشه‌ی پوشه‌ی app به همراه یک Middleware برای تشخیص و هدایت کاربر استفاده کنید، چون App Router برخلاف Pages Router دیگر تنظیم داخلی i18n در next.config.js ندارد. کتابخانه‌ی پیشنهادی رسمی next-intl است که از Server Components، رندر استاتیک، فرمت ICU و پشتیبانی RTL به‌صورت کامل پشتیبانی می‌کند و این مقاله گام‌به‌گام آن را پیاده‌سازی می‌کند.

  • در App Router تنظیم i18n در next.config.js منسوخ است؛ روش رسمی استفاده از سگمنت پویا app/[locale]/... است.
  • کتابخانه‌ی next-intl 3.x با Server Components و رندر استاتیک سازگار است و جایگزین مدرن next-i18next محسوب می‌شود.
  • Middleware با استفاده از هدر Accept-Language و کتابخانه‌ی @formatjs/intl-localematcher زبان کاربر را تشخیص می‌دهد.
  • برای پشتیبانی RTL کافی است dir و lang روی تگ <html> بر اساس locale تنظیم شوند.
  • متادیتای چندزبانه با generateMetadata و تگ‌های hreflang از طریق alternates.languages اضافه می‌شود تا SEO چندزبانه به‌درستی کار کند.
  • برای رندر استاتیک، generateStaticParams باید تمام locale‌ها را برگرداند و setRequestLocale در هر صفحه فراخوانی شود.

آیا Next.js 15 از i18n پشتیبانی می‌کند؟

بله، Next.js 15 از بین‌المللی‌سازی پشتیبانی می‌کند، اما نه به شکلی که توسعه‌دهندگان Pages Router به آن عادت داشتند. راستش وقتی پروژه‌ی قبلی‌مان را از Pages Router به App Router مهاجرت دادیم، نصف روز را صرف این کردیم که بفهمیم چرا تنظیمات i18n در next.config.js اصلاً اعمال نمی‌شود. در نسخه‌های قدیمی، یک شیء i18n در فایل next.config.js با لیست locale‌ها تعریف می‌شد و فریم‌ورک خودش هدایت کاربر و کوکی NEXT_LOCALE را مدیریت می‌کرد. در App Router این رفتار حذف شده است (به‌صورت کامل، نه اینکه deprecate شده باشد) و طبق مستندات رسمی Next.js توسعه‌دهنده باید مسیریابی locale را به‌صورت دستی با یک سگمنت پویا پیاده کند.

دلیل این تغییر بنیادی، معماری Server Components است. در App Router هر صفحه می‌تواند به‌صورت استاتیک، پویا یا جریانی رندر شود و فریم‌ورک به یک «بافت زبان» نیاز دارد که بتواند آن را در زمان build، در زمان درخواست و در زمان revalidation هم‌زمان حفظ کند. این کار با سگمنت پویای [locale] به‌مراتب تمیزتر از تنظیمات گلوبال انجام می‌شود. در عمل، ساختار پروژه‌ی شما به این شکل درمی‌آید:

app/
├── [locale]/
│   ├── layout.tsx
│   ├── page.tsx
│   └── dashboard/
│       └── page.tsx
├── middleware.ts
└── i18n/
    ├── request.ts
    └── routing.ts
messages/
├── en.json
├── fa.json
└── ar.json

این الگو با الگوهای واکشی داده در Next.js 15 سازگار است؛ هر fetch داخل یک سگمنت locale می‌تواند هم‌چنان از کشینگ Data Cache بهره ببرد.

انتخاب استراتژی مسیریابی: prefix یا domain

خب، قبل از نوشتن یک خط کد، باید تصمیم بگیرید که URL‌های چندزبانه چه شکلی داشته باشند. سه استراتژی رایج وجود دارد و هرکدام مزایا و معایب خودش را دارد. این جدول مقایسه‌ی سریعی برای انتخاب درست در اختیار شما می‌گذارد:

ویژگیPrefix مسیر (/fa/about)دامنه‌ی جدا (about.fa)کوکی (بدون پیشوند)
سئوعالیعالیضعیف (محتوای کلوک)
پیچیدگی پیاده‌سازیکمزیاد (DNS و SSL)کم
اشتراک‌گذاری لینکصریح استصریح استمبهم
کش CDNسادهسادهپیچیده (Vary)
هزینه‌ی هاستینگیک دامنهچند دامنهیک دامنه
پیشنهاد برایاکثر پروژه‌هابرندهای محلی مجزااپ داخلی

برای ۹۰٪ پروژه‌ها prefix مسیر انتخاب درست است: ساده، SEO-friendly و قابل کش در Vercel Edge Network. در ادامه‌ی این راهنما همین استراتژی را پیاده‌سازی می‌کنیم. اگر مخاطب شما در دو منطقه‌ی جغرافیایی کاملاً جدا هستند (مثلاً example.de و example.fr) دامنه‌ی جدا منطقی است، اما نگهداری آن چند برابر سخت‌تر است.

نصب و راه‌اندازی next-intl

برای پروژه‌های جدید، کتابخانه‌ی next-intl در نسخه‌ی ۳.x استاندارد عملی است چون با Server Components، رندر استاتیک، فرمت ICU و TypeScript به‌خوبی سازگار است. طبق مستندات رسمی next-intl نصب در سه گام انجام می‌شود.

گام اول: نصب پکیج:

npm install next-intl

گام دوم: تعریف routing در i18n/routing.ts:

import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['en', 'fa', 'ar'],
  defaultLocale: 'en',
  localePrefix: 'as-needed',
});

گام سوم: پلاگین را در next.config.ts فعال کنید:

import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin('./i18n/request.ts');

export default withNextIntl({
  // سایر تنظیمات Next.js
});

سپس فایل‌های ترجمه را در پوشه‌ی messages/ با نام en.json، fa.json و ar.json بسازید. فرمت JSON تو در تو پیشنهاد می‌شود تا namespace‌ها واضح باشند:

// messages/fa.json
{
  "HomePage": {
    "title": "به Next.js Launchpad خوش آمدید",
    "cta": "شروع آموزش"
  },
  "Common": {
    "loading": "در حال بارگذاری..."
  }
}

Middleware و تشخیص خودکار زبان

Middleware قلب پیاده‌سازی i18n در App Router است. وظیفه‌ی آن این است که وقتی کاربر روی /about کلیک می‌کند، بر اساس هدر Accept-Language یا کوکی قبلی، او را به نسخه‌ی locale درست هدایت کند. next-intl یک Middleware آماده ارائه می‌دهد:

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};

الگوریتم انتخاب زبان از کتابخانه‌ی @formatjs/intl-localematcher استفاده می‌کند که پیاده‌سازی استاندارد BCP 47 است. اگر کاربر هدر Accept-Language: fa-IR,fa;q=0.9,en;q=0.8 بفرستد، Middleware ابتدا fa-IR را امتحان می‌کند، سپس fa و در نهایت به en برمی‌گردد.

اگر تنظیمات localePrefix را روی 'as-needed' بگذارید، URLهای زبان پیش‌فرض بدون پیشوند می‌مانند (/about) و URLهای سایر زبان‌ها پیشوند می‌گیرند (/fa/about). این بهترین حالت برای SEO سایت‌هایی است که زبان اصلی‌شان بازار اصلی محسوب می‌شود.

ترجمه در Server Components و Client Components

یکی از مهم‌ترین مزایای next-intl 3.x این است که برخلاف نسخه‌های قدیمی react-i18next، می‌توانید در Server Components مستقیماً ترجمه بگیرید بدون اینکه کل درخت را به Client Component تبدیل کنید. این کار حجم JavaScript ارسالی به مرورگر را به‌شدت کاهش می‌دهد. ابتدا فایل i18n/request.ts را بسازید:

import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }
  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

سپس در یک Server Component از getTranslations استفاده می‌کنید:

// app/[locale]/page.tsx
import { getTranslations, setRequestLocale } from 'next-intl/server';

export default async function HomePage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  setRequestLocale(locale);
  const t = await getTranslations('HomePage');

  return (
    <main>
      <h1>{t('title')}</h1>
      <button>{t('cta')}</button>
    </main>
  );
}

در Client Components از hook به نام useTranslations استفاده می‌کنید — همان API، اما در سمت مرورگر:

'use client';
import { useTranslations } from 'next-intl';

export function LangSwitcher() {
  const t = useTranslations('Common');
  return <span>{t('loading')}</span>;
}

برای ساخت فرم‌های چندزبانه می‌توانید این الگو را با Server Actions در Next.js 15 و Drizzle ORM ترکیب کنید تا پیام‌های اعتبارسنجی هم به زبان کاربر برگردند.

پشتیبانی RTL برای فارسی و عربی

برای زبان‌های راست‌به‌چپ مثل فارسی، عربی و عبری باید دو چیز روی تگ <html> تنظیم شود: lang و dir. در App Router این کار در app/[locale]/layout.tsx انجام می‌شود:

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';

const RTL_LOCALES = ['fa', 'ar', 'he'];

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  if (!routing.locales.includes(locale as any)) notFound();
  setRequestLocale(locale);
  const messages = await getMessages();
  const dir = RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr';

  return (
    <html lang={locale} dir={dir}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

برای استایل‌دهی RTL، Tailwind CSS از نسخه‌ی ۳.۳ به بعد از modifierهای rtl: و ltr: پشتیبانی می‌کند. مثال:

<div className="ml-4 rtl:ml-0 rtl:mr-4">...</div>

بهتر است به‌جای margin از CSS logical properties استفاده کنید: ms-4 (margin-inline-start) به‌صورت خودکار در RTL به سمت راست می‌چسبد و در LTR به سمت چپ. این الگو در Tailwind 4 به نام‌های ps-*، pe-*، ms-* و me-* در دسترس است.

رندر استاتیک با locale

یکی از سؤال‌های پرتکرار این است: «اگر URL ما شامل [locale] پویا است، آیا می‌توانیم صفحه را استاتیک رندر کنیم؟» پاسخ بله است، اما شرط دارد. باید در هر صفحه دو کار انجام دهید: ۱) تابع generateStaticParams را صادر کنید که لیست locale‌ها را برمی‌گرداند، و ۲) در ابتدای کامپوننت setRequestLocale(locale) را فراخوانی کنید تا next-intl بداند کدام پیام‌ها را بدون نیاز به request زنده بارگذاری کند.

// app/[locale]/about/page.tsx
import { setRequestLocale, getTranslations } from 'next-intl/server';
import { routing } from '@/i18n/routing';

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

export default async function AboutPage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  setRequestLocale(locale);
  const t = await getTranslations('About');
  return <article>{t('content')}</article>;
}

اگر setRequestLocale را فراموش کنید، Next.js صفحه را پویا (dynamic) رندر می‌کند و پیامی در لاگ build می‌بینید که می‌گوید "Couldn't determine locale, falling back to dynamic rendering". خودم یک بار این هشدار را نادیده گرفتم و بعد متوجه شدم نیمی از صفحات سایت در production استاتیک نیستند. این مهم‌ترین تله‌ی performance در i18n App Router است. این الگو با Partial Prerendering (PPR) در Next.js 15 هم سازگار است و می‌توانید بخش‌های استاتیک locale را با Suspense boundary از بخش‌های پویا جدا کنید.

SEO چندزبانه: hreflang و metadata

برای SEO چندزبانه، Google انتظار دارد تگ‌های <link rel="alternate" hreflang="..."> در head صفحه باشند تا بفهمد کدام URL کدام زبان را پوشش می‌دهد. در Next.js 15 این کار از طریق Metadata API و فیلد alternates.languages انجام می‌شود:

// app/[locale]/page.tsx
import { getTranslations } from 'next-intl/server';
import type { Metadata } from 'next';

export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string }>;
}): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'HomePage' });
  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      canonical: `/${locale}`,
      languages: {
        'en': '/en',
        'fa': '/fa',
        'ar': '/ar',
        'x-default': '/en',
      },
    },
  };
}

قانون مهم: مقدار x-default باید به نسخه‌ی پیش‌فرض (معمولاً انگلیسی) اشاره کند و هر زبان باید به URL کامل آن زبان لینک شود. اگر این تگ را اضافه نکنید، Google ممکن است محتوای فارسی شما را به‌عنوان «نسخه‌ی تکراری انگلیسی» علامت بزند و از ایندکس حذف کند.

next-intl در مقایسه با next-i18next

next-i18next از سال ۲۰۲۰ کتابخانه‌ی محبوب i18n برای Next.js بود، اما عمدتاً برای Pages Router طراحی شده است. در App Router از Server Components به‌صورت بومی پشتیبانی نمی‌کند و نیازمند wrapper‌های اضافی است. next-intl از ابتدا برای معماری جدید نوشته شده و در مستندات رسمی Next.js به‌عنوان گزینه‌ی پیشنهادی معرفی شده است.

تفاوت‌های کلیدی:

  • Server Components: next-intl کاملاً سازگار است؛ next-i18next نیازمند راه‌حل‌های دور زدنی.
  • اندازه‌ی bundle: next-intl تقریباً ۸ کیلوبایت gzip، next-i18next حدود ۲۰ کیلوبایت با وابستگی به i18next.
  • فرمت پیام: next-intl از ICU MessageFormat پشتیبانی می‌کند (pluralization، select، number/date formatting). next-i18next از فرمت i18next استفاده می‌کند که قواعد جمع متفاوتی دارد.
  • TypeScript: next-intl کلیدهای ترجمه را به‌صورت type-safe expose می‌کند.

اگر پروژه‌ی موجودی روی Pages Router دارید، مهاجرت فوری ضروری نیست؛ اما برای پروژه‌های جدید قطعاً next-intl انتخاب بهتری است.

خطاهای رایج و راه‌حل آن‌ها

در پیاده‌سازی i18n با Next.js 15 چند تله‌ی تکراری وجود دارد که توسعه‌دهندگان تازه‌کار اغلب در آن‌ها گیر می‌کنند:

۱. Hydration mismatch به‌خاطر تاریخ و عدد

دقیقاً همین باگ را موقع راه‌اندازی نسخه‌ی فارسی یک داشبورد گرفتم: اگر تاریخ یا عدد را با toLocaleString() در سرور و کلاینت با locale متفاوت فرمت کنید، React خطای hydration می‌دهد (سرور ۱۲٬۳۴۵ می‌نویسد و کلاینت 12,345). راه‌حل: همیشه از useFormatter() یا getFormatter() از next-intl استفاده کنید که locale یکسانی در هر دو طرف اعمال می‌کنند. اگر همچنان با خطاهای hydration روبه‌رو هستید، راهنمای میدلور پیشرفته در Next.js 15 الگوهای دیباگ مفیدی دارد.

۲. کوکی NEXT_LOCALE نادیده گرفته می‌شود

اگر در حال مهاجرت از Pages Router هستید، next-intl به‌طور پیش‌فرض از کوکی NEXT_LOCALE پشتیبانی می‌کند، اما باید localeDetection را در routing روشن کنید. در غیر این صورت ترجیح کاربر بین session‌ها حفظ نمی‌شود.

۳. کلیدهای ترجمه‌ی ازدست‌رفته در production

در development، یک کلید گمشده فقط در کنسول هشدار می‌دهد، اما در production می‌تواند کل صفحه را خراب کند. توصیه می‌شود از TypeScript declaration برای ترجمه‌ها استفاده کنید و در CI با اسکریپتی همه‌ی فایل‌های locale را برای کلیدهای گمشده اسکن کنید.

۴. کارایی Middleware روی Vercel Edge

هر درخواست از Middleware عبور می‌کند و این یعنی @formatjs/intl-localematcher روی هر pageview اجرا می‌شود. اگر ترافیک شما بالاست، locale‌های پشتیبانی‌شده را به حداقل ضروری محدود کنید و از localePrefix: 'always' استفاده کنید تا redirect کمتری اتفاق بیفتد.

سوالات متداول

چگونه در Next.js 15 App Router زبان کاربر را تشخیص دهیم؟

با استفاده از Middleware و کتابخانه‌ی @formatjs/intl-localematcher که هدر Accept-Language مرورگر را تجزیه می‌کند و نزدیک‌ترین زبان پشتیبانی‌شده را انتخاب می‌کند. next-intl این منطق را به‌صورت آماده در createMiddleware ارائه می‌دهد.

آیا تنظیم i18n در next.config.js در App Router کار می‌کند؟

خیر، تنظیم i18n در next.config.js فقط مخصوص Pages Router است. در App Router این تنظیم نادیده گرفته می‌شود و باید از سگمنت پویای [locale] با Middleware استفاده کنید.

بهترین کتابخانه‌ی i18n برای Next.js 15 چیست؟

next-intl 3.x به‌عنوان گزینه‌ی پیشنهادی مستندات رسمی Next.js معرفی شده است چون با Server Components، رندر استاتیک و فرمت ICU سازگار است. next-i18next همچنان برای پروژه‌های Pages Router کار می‌کند اما برای پروژه‌های جدید توصیه نمی‌شود.

چگونه پشتیبانی RTL را برای فارسی و عربی فعال کنیم؟

در app/[locale]/layout.tsx روی تگ <html> ویژگی‌های lang={locale} و dir={isRTL ? 'rtl' : 'ltr'} را ست کنید. برای استایل‌دهی از CSS logical properties (margin-inline-start) یا modifierهای rtl: در Tailwind استفاده کنید.

آیا می‌توان صفحات چندزبانه را به‌صورت استاتیک رندر کرد؟

بله، با صادر کردن generateStaticParams که لیست تمام locale‌ها را برمی‌گرداند و فراخوانی setRequestLocale(locale) در ابتدای هر صفحه. این کار به Next.js اجازه می‌دهد همه‌ی نسخه‌های زبانی را در زمان build تولید کند.

Editorial Team
درباره نویسنده Editorial Team

Our team of expert writers and editors.