Internationalization in Next.js: 11-Locale Implementation

Build a production-ready 11-locale Next.js app with next-intl v4. Covers routing, RTL support, translation workflows, hreflang SEO, and server/client component patterns.

E
ECOSIRE Research and Development Team
|19 مارچ، 202610 منٹ پڑھیں2.2k الفاظ|

Next.js میں بین الاقوامی کاری: 11-مقامی نفاذ

عالمی سامعین کی تعمیر کے لیے Google Translate کے ذریعے آپ کے سٹرنگ چلانے سے زیادہ کی ضرورت ہے۔ پروڈکشن انٹرنیشنلائزیشن کے نفاذ میں روٹنگ کی حکمت عملی، RTL (دائیں سے بائیں) لے آؤٹ سپورٹ، سرور سائیڈ لوکیل ریزولوشن، ترجمے کی کلیدی تنظیم، SEO hreflang سگنلز، اور ایک پائیدار ترجمہ ورک فلو شامل ہے۔ ان میں سے کسی ایک کو بھی غلط سمجھیں اور آپ کے پاس یا تو عربی/اردو صارفین کے لیے ٹوٹے ہوئے لے آؤٹ ہیں، آپ کی تلاش کی درجہ بندی کو تقسیم کرنے والے hreflang ٹیگ غائب ہیں، یا ترجمہ کی پائپ لائن جو مواد کی ترقی کے وزن میں گر جاتی ہے۔

یہ گائیڈ ECOSIRE.COM پر استعمال ہونے والے مکمل 11-مقامی سیٹ اپ کی دستاویز کرتا ہے — انگریزی کے علاوہ چینی، ہسپانوی، عربی، پرتگالی، فرانسیسی، جرمن، جاپانی، ترکی، ہندی، اور اردو — جو اگلا intl v4.8 پر بنایا گیا ہے۔ یہاں پر ہر پیٹرن کو 5,577 MDX مواد کی فائلوں اور 12,543 ترجمے کی کلیدوں میں پروڈکشن ٹیسٹ کیا جاتا ہے۔

اہم ٹیک ویز

  • localePrefix: 'as-needed' استعمال کریں — انگریزی میں کوئی سابقہ نہیں ہے، دیگر لوکیلز کو /zh/، /ar/، وغیرہ ملتے ہیں۔
  • next-intl v4 routing.ts + navigation.ts پیٹرن استعمال کرتا ہے — کبھی بھی next/navigation سے براہ راست لوکل سے آگاہ اجزاء میں درآمد نہ کریں
  • عربی (ar) اور اردو (ur) کو <html> پر dir="rtl" اور ایک RTL-مطابق فونٹ (Noto Sans Arabic) درکار ہے۔
  • en.json چابیاں نیسٹڈ آبجیکٹ ہونی چاہئیں - فلیٹ ڈاٹ سے الگ کردہ چابیاں نام کی جگہ کی ریزولوشن کو توڑ دیتی ہیں
  • generateMetadata() (کبھی جامد نہیں export const metadata) alternates.languages کے ساتھ ہر صفحہ پر hreflang کے لئے
  • سرور کے اجزاء getTranslations('namespace') استعمال کرتے ہیں، کلائنٹ کے اجزاء useTranslations('namespace') استعمال کرتے ہیں
  • ایڈمن صفحات دو ترجمے کے ہکس استعمال کرتے ہیں: t ماڈیول کے لئے مخصوص کلیدوں کے لئے، tc مشترکہ admin.common کلیدوں کے لئے
  • گوگل ٹرانسلیٹ API کے ساتھ نئی کلیدوں کا خودکار ترجمہ؛ دستی ترجمہ پر ترقی کو کبھی روکیں۔

پروجیکٹ سیٹ اپ

pnpm add next-intl@4

ہر ایپ کے لیے ڈائرکٹری کا ڈھانچہ:

src/
  i18n/
    routing.ts      # defineRouting() — locale list, prefix strategy
    navigation.ts   # createNavigation() — locale-aware Link, useRouter
    request.ts      # getRequestConfig() — server-side message loading
  messages/
    en.json         # Source of truth (~12,543 keys)
    zh.json
    es.json
    ar.json
    pt.json
    fr.json
    de.json
    ja.json
    tr.json
    hi.json
    ur.json

روٹنگ کنفیگریشن

// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const locales = [
  'en', 'zh', 'es', 'ar', 'pt', 'fr', 'de', 'ja', 'tr', 'hi', 'ur',
] as const;

export type Locale = (typeof locales)[number];

export const rtlLocales: Locale[] = ['ar', 'ur'];

export const localeNames: Record<Locale, string> = {
  en: 'English',
  zh: '中文',
  es: 'Español',
  ar: 'العربية',
  pt: 'Português',
  fr: 'Français',
  de: 'Deutsch',
  ja: '日本語',
  tr: 'Türkçe',
  hi: 'हिन्दी',
  ur: 'اردو',
};

export const routing = defineRouting({
  locales,
  defaultLocale: 'en',
  localePrefix: 'as-needed', // /en/blog → /blog, /zh/blog → /zh/blog
});
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';

export const { Link, redirect, useRouter, usePathname, getPathname } =
  createNavigation(routing);

مڈل ویئر / پراکسی

next-intl v4 لوکل کا پتہ لگانے اور روٹنگ کے لیے ایک مڈل ویئر فنکشن استعمال کرتا ہے:

// src/proxy.ts (not middleware.ts — naming matters for Next.js 16)
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
import { NextRequest, NextResponse } from 'next/server';

const intlMiddleware = createMiddleware(routing);

export function middleware(request: NextRequest) {
  // Auth protection for dashboard routes
  const isDashboard = request.nextUrl.pathname.includes('/dashboard');
  const isPortal = request.nextUrl.pathname.includes('/portal');
  const authCookie = request.cookies.get('ecosire_auth');

  if ((isDashboard || isPortal) && !authCookie) {
    const loginUrl = new URL('/auth/login', request.url);
    // Prevent open redirect: only allow relative redirect paths
    const from = request.nextUrl.pathname;
    if (from.startsWith('/') && !from.startsWith('//')) {
      loginUrl.searchParams.set('from', from);
    }
    return NextResponse.redirect(loginUrl);
  }

  return intlMiddleware(request);
}

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

سرور سائیڈ میسج لوڈ ہو رہا ہے۔

// src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  // Validate locale is supported; fall back to default
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

RTL اور فونٹ سپورٹ کے ساتھ لوکل لے آؤٹ

// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing, rtlLocales, type Locale } from '@/i18n/routing';
import { Noto_Sans, Noto_Sans_Arabic } from 'next/font/google';

const notoSans = Noto_Sans({ subsets: ['latin'], variable: '--font-sans' });
const notoArabic = Noto_Sans_Arabic({
  subsets: ['arabic'],
  variable: '--font-arabic',
});

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

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'metadata' });
  return {
    title: { template: `%s | ${t('siteName')}`, default: t('defaultTitle') },
    other: { 'content-language': locale }, // For Bing/AI engine language detection
  };
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;

  if (!routing.locales.includes(locale as Locale)) {
    notFound();
  }

  const messages = await getMessages();
  const isRtl = rtlLocales.includes(locale as Locale);

  return (
    <html
      lang={locale}
      dir={isRtl ? 'rtl' : 'ltr'}
      className={`${notoSans.variable} ${notoArabic.variable}`}
    >
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

ترجمہ کلیدی تنظیم

en.json ڈھانچے کو نیسٹڈ آبجیکٹ کا استعمال کرنا چاہیے، نہ کہ فلیٹ ڈاٹ سے الگ کردہ چابیاں:

// WRONG — flat keys break next-intl namespace resolution
{
  "nav.home": "Home",
  "nav.blog": "Blog",
  "admin.common.save": "Save"
}

// CORRECT — nested objects
{
  "nav": {
    "home": "Home",
    "blog": "Blog"
  },
  "admin": {
    "common": {
      "save": "Save",
      "cancel": "Cancel",
      "delete": "Delete",
      "edit": "Edit",
      "status": "Status"
    },
    "contacts": {
      "title": "Contacts",
      "addContact": "Add Contact",
      "searchPlaceholder": "Search contacts..."
    }
  }
}

سرور کے اجزاء

// Server component: use getTranslations (async)
import { getTranslations } from 'next-intl/server';

export default async function ServicesPage({ params }: Props) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'services.odoo' });

  return (
    <main>
      <h1>{t('hero.title')}</h1>
      <p>{t('hero.description')}</p>
    </main>
  );
}

// generateMetadata must also fetch translations for locale-aware metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'services.odoo' });

  return {
    title: t('meta.title'),
    description: t('meta.description'),
    alternates: {
      canonical: `https://ecosire.com${locale === 'en' ? '' : `/${locale}`}/services/odoo`,
      languages: {
        'x-default': 'https://ecosire.com/services/odoo',
        en: 'https://ecosire.com/services/odoo',
        zh: 'https://ecosire.com/zh/services/odoo',
        ar: 'https://ecosire.com/ar/services/odoo',
        // ... all 11 locales
      },
    },
  };
}

کلائنٹ کے اجزاء

// Client component: use useTranslations (synchronous)
'use client';

import { useTranslations } from 'next-intl';

// Admin pages use two hooks: module-specific + shared admin.common
export default function ContactsPage() {
  const t  = useTranslations('admin.contacts'); // Module-specific
  const tc = useTranslations('admin.common');   // Shared terms

  // IMPORTANT: stateConfig must be inside component (after hooks init)
  const stateConfig = {
    active:   { label: tc('statusActive'),   className: 'bg-green-100 text-green-800' },
    inactive: { label: tc('statusInactive'), className: 'bg-gray-100 text-gray-700'  },
  };

  return (
    <div>
      <h1>{t('title')}</h1>
      <button>{tc('addNew')}</button>
      {/* ... */}
    </div>
  );
}

زبان بدلنے والا جزو

// components/language-switcher.tsx
'use client';

import { useLocale } from 'next-intl';
import { usePathname, useRouter } from '@/i18n/navigation'; // NOT next/navigation
import { locales, localeNames } from '@/i18n/routing';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Globe } from 'lucide-react';

export function LanguageSwitcher() {
  const locale = useLocale();
  const pathname = usePathname();
  const router = useRouter();

  const handleLocaleChange = (newLocale: string) => {
    router.replace(pathname, { locale: newLocale });
  };

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <button aria-label="Change language">
          <Globe className="h-4 w-4" />
          <span>{localeNames[locale as keyof typeof localeNames]}</span>
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        {locales.map((loc) => (
          <DropdownMenuItem
            key={loc}
            onClick={() => handleLocaleChange(loc)}
            className={loc === locale ? 'font-bold' : ''}
          >
            {localeNames[loc]}
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

ترجمہ ورک فلو

نئی چابیاں شامل کرنا

  1. پہلے en.json میں شامل کریں (سچ کا ماخذ)
  2. ترجمہ اسکرپٹ چلائیں:
node apps/web/scripts/translate-missing.mjs

اسکرپٹ en.json میں موجود کلیدوں کو تلاش کرتا ہے لیکن دوسری لوکیل فائلوں سے غائب ہے اور Google Translate API کو کال کرتا ہے:

// scripts/translate-missing.mjs (simplified)
import { Translate } from '@google-cloud/translate/build/src/v2/index.js';
import { readFileSync, writeFileSync } from 'fs';

const translate = new Translate({ key: process.env.GOOGLE_TRANSLATE_API_KEY });

async function translateMissingKeys(sourceLocale, targetLocale, sourceMessages, targetMessages) {
  const missing = findMissingKeys(sourceMessages, targetMessages);

  for (const [keyPath, value] of missing) {
    const [translated] = await translate.translate(value, targetLocale);
    setNestedKey(targetMessages, keyPath, translated);
  }

  return targetMessages;
}

ترجمہ کوریج کی توثیق کرنا

# Check for missing keys across all locales
node apps/web/scripts/validate-blog-i18n.mjs

# Fix flat keys accidentally added
node apps/web/scripts/fix-en-flat-keys.mjs

بلاگ کے مواد کا ترجمہ

MDX مواد کی فائلوں کے لیے، فرنٹ میٹر ٹائٹلز انگریزی میں رہتے ہیں (عنوان i18n JSON میں ہیں)، لیکن باڈی مواد کا ترجمہ فی زبان میں کیا جاتا ہے:

src/content/blog/
  en/        # English originals (or root directory)
    odoo-erp-guide.mdx
  zh/
    odoo-erp-guide.mdx  # Translated body
  ar/
    odoo-erp-guide.mdx
  # ... 10 locale directories
// src/lib/blog.ts — locale-aware loader
export async function getPost(slug: string, locale: string) {
  // Try locale-specific file first
  const localePath = path.join(contentDir, locale, `${slug}.mdx`);
  const englishPath = path.join(contentDir, `${slug}.mdx`);

  const filePath = fs.existsSync(localePath) ? localePath : englishPath;

  const raw = fs.readFileSync(filePath, 'utf8');
  const { data: frontmatter, content } = matter(raw);
  return { frontmatter, content, locale };
}

اکثر پوچھے گئے سوالات

always کی بجائے localePrefix: 'as-needed' کیوں استعمال کریں؟

as-needed کے ساتھ، انگریزی یو آر ایل کا کوئی سابقہ ​​(/blog/post) نہیں ہے، جب کہ دیگر لوکیلز کو ایک سابقہ ​​(/zh/blog/post) ملتا ہے۔ یہ معیاری کنونشن ہے — انگریزی پہلے سے طے شدہ ہے اور اس میں صاف URLs ہیں۔ always کے ساتھ، انگریزی کو /en/blog/post بھی ملتا ہے، جو غیر ضروری ری ڈائریکٹ بناتا ہے اور اگر آپ نقل مکانی کر رہے ہیں تو آپ کی موجودہ انگریزی لنک ایکویٹی کو تقسیم کر دیتا ہے۔

کیوں en.json کیز کو نیسٹڈ آبجیکٹ ہونا چاہیے نہ کہ فلیٹ ڈاٹ سے الگ کردہ تاروں کی؟

نیکسٹ-انٹل کی نیم اسپیس ریزولیوشن JSON درخت کو عبور کرنے کے لیے نام کی جگہ کی تار کو نقطوں کے ذریعے تقسیم کرتی ہے۔ اگر آپ کی چابیاں "admin.contacts.title" جیسی فلیٹ ڈاٹ سے الگ کردہ تاریں ہیں، تو next-intl messages.admin.contacts.title کو نیسٹڈ پاتھ کے طور پر تلاش کرتا ہے لیکن پتہ چلتا ہے کہ کلید لفظی طور پر روٹ لیول پر سٹرنگ "admin.contacts.title" ہے، جس کی وجہ سے تلاش میں ناکامی ہوتی ہے۔ ہمیشہ اصلی گھریلو اشیاء کا استعمال کریں۔

میں زبانوں میں تکثیریت کو کیسے ہینڈل کروں؟

Next-intl تکثیریت کے لیے ICU پیغام کی شکل استعمال کرتا ہے۔ انگریزی میں دو جمع شکلیں ہیں (ایک/دوسری)، عربی میں چھ (صفر، ایک، دو، چند، بہت سے، دیگر) ہیں۔ اپنی ترجمے کی قدروں میں {count, plural, one {# item} other {# items}} ICU نحو کا استعمال کریں اور اگلا-intl خود بخود ہر مقامی کے لیے درست جمع اصول لاگو کرتا ہے۔

کیا میں پیجز راؤٹر کے ساتھ Next-intl استعمال کرسکتا ہوں؟

next-intl v4 بنیادی طور پر ایپ راؤٹر کے لیے ڈیزائن کیا گیا ہے۔ Pages Router کے لیے، next-intl v2 یا v3 استعمال کریں۔ پیٹرن نمایاں طور پر مختلف ہیں: صفحات کا راؤٹر getStaticProps + NextIntlProvider استعمال کرتا ہے، جبکہ App Router ترتیب میں getRequestConfig + NextIntlClientProvider استعمال کرتا ہے۔ اگر آپ پیجز راؤٹر پر ہیں تو i18n کو اسکیل پر لاگو کرنے سے پہلے ایپ راؤٹر پر منتقل ہونے پر غور کریں۔

میں عربی اور اردو کے لیے RTL سپورٹ کیسے ترتیب دوں؟

dir="rtl" کو <html> عنصر پر سیٹ کریں جب لوکیل ar یا ur ہو۔ سمت کے مخصوص انداز کے لیے Tailwind کا rtl: متغیر سابقہ ​​استعمال کریں (RTL میں rtl:mr-4 بن جاتا ہے ml-4)۔ ایک ہم آہنگ فونٹ لوڈ کریں — نوٹو سنز عربی عربی اور اردو دونوں رسم الخط کا احاطہ کرتا ہے۔ ہارڈ کوڈ شدہ left/right CSS اقدار سے بچیں؛ استعمال کریں start/end منطقی خصوصیات (ms-4, me-4 Tailwind v4 میں)۔

بڑی مقدار میں مواد کا فوری ترجمہ کرنے کا بہترین طریقہ کیا ہے؟

تمام کلیدوں اور مواد کے ابتدائی مشینی ترجمہ کے لیے Google Translate API کا استعمال کریں، پھر مقامی بولنے والوں سے اعلیٰ مرئیت والے مواد (ہوم ​​پیج، پروڈکٹ کی تفصیل، CTA کاپی) کا جائزہ لیں۔ بلاگ کے مواد کے لیے، مشینی ترجمہ ایک نقطہ آغاز کے طور پر قابل قبول ہے لیکن تکنیکی درستگی کے لیے اس کا جائزہ لیا جانا چاہیے۔ ٹریفک کے لحاظ سے کم از کم سرفہرست 3-5 مقامات کے مقامی اسپیکر کے جائزے کے لیے بجٹ۔


اگلے اقدامات

صحیح طریقے سے کی گئی بین الاقوامی کاری ایک مسابقتی کھائی ہے — آپ کا مواد 11 گنا زیادہ ممکنہ صارفین تک پہنچتا ہے، آپ کے SEO hreflang سگنلز تقسیم لنک ایکویٹی کے بجائے مضبوط ہوتے ہیں، اور صارفین اپنی مادری زبان میں نمایاں طور پر زیادہ شرحوں پر تبدیل ہوتے ہیں۔

ECOSIRE نے 5,577 مواد فائلوں، 12,543 ترجمے کی کلیدوں، اور RTL لے آؤٹ سپورٹ کا احاطہ کرنے والے ایک مکمل 11-مقامی نفاذ کو بنایا اور برقرار رکھا ہے۔ اگر آپ کو اپنی Next.js ایپلیکیشن کے لیے انٹرنیشنلائزیشن کی ضرورت ہے، تو ہماری فرنٹ اینڈ انجینئرنگ سروسز یہ جاننے کے لیے کہ ہم اسے آپ کے کوڈ بیس کے لیے کیسے نافذ کرسکتے ہیں۔

E

تحریر

ECOSIRE Research and Development Team

ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔

Chat on WhatsApp