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 Mart 20269 dk okuma1.9k Kelime|

Next.js'de Uluslararasılaştırma: 11-Yerel Ayar Uygulaması

Küresel bir kitleye ulaşmak, dizelerinizi Google Çeviri'de çalıştırmaktan daha fazlasını gerektirir. Üretimin uluslararası hale getirilmesi uygulaması, yönlendirme stratejisini, RTL (sağdan sola) düzen desteğini, sunucu tarafı yerel ayar çözümlemesini, çeviri anahtarı organizasyonunu, SEO hreflang sinyallerini ve sürdürülebilir bir çeviri iş akışını içerir. Bunlardan herhangi birini yanlış anladığınızda, ya Arapça/Urduca kullanıcıları için bozuk düzenlere, arama sıralamalarınızı bölen eksik hreflang etiketlerine ya da içerik büyümesinin ağırlığı altında çöken bir çeviri hattına sahip olursunuz.

Bu kılavuz, next-intl v4.8 üzerine kurulu ECOSIRE.COM'da kullanılan 11 yerel ayarın tamamını (İngilizce artı Çince, İspanyolca, Arapça, Portekizce, Fransızca, Almanca, Japonca, Türkçe, Hintçe ve Urduca) belgelemektedir. Buradaki her model, 5.577 MDX içerik dosyası ve 12.543 çeviri anahtarında üretim testinden geçirilmiştir.

Önemli Çıkarımlar

  • localePrefix: 'as-needed' kullanın — İngilizcede önek yoktur, diğer yerel ayarlarda /zh/, /ar/ vb. bulunur.
  • next-intl v4, routing.ts + navigation.ts modelini kullanır — asla yerel ayarlara duyarlı bileşenlerde next/navigation öğesinden doğrudan içe aktarmayın
  • Arapça (ar) ve Urduca (ur), <html> üzerinde dir="rtl" ve RTL uyumlu bir yazı tipi (Noto Sans Arabic) gerektirir
  • en.json anahtarları iç içe geçmiş nesneler OLMALIDIR — düz noktayla ayrılmış anahtarlar ad alanı çözümlemesini bozar
  • Her sayfada hreflang için alternates.languages ile generateMetadata() (hiçbir zaman statik export const metadata değil)
  • Sunucu bileşenleri getTranslations('namespace') kullanır, istemci bileşenleri useTranslations('namespace') kullanır
  • Yönetici sayfaları iki çeviri kancası kullanır: modüle özgü anahtarlar için t, paylaşılan admin.common anahtarları için tc
  • Google Translate API ile yeni anahtarların çevirisini otomatikleştirin; Manuel çeviride geliştirmeyi asla engellemeyin

Proje Kurulumu

pnpm add next-intl@4

Her uygulama için dizin yapısı:

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

Yönlendirme Yapılandırması

// 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);

Ara yazılım / Proxy

next-intl v4, yerel ayar tespiti ve yönlendirme için bir ara yazılım işlevi kullanır:

// 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).*)',
  ],
};

Sunucu Tarafı Mesaj Yükleniyor

// 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 ve Yazı Tipi Desteğiyle Yerel Düzen

// 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>
  );
}

Çeviri Anahtar Organizasyonu

en.json yapısı, düz noktayla ayrılmış anahtarlar yerine iç içe geçmiş nesneler kullanmalıdır:

// 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..."
    }
  }
}

Sunucu Bileşenleri

// 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
      },
    },
  };
}

İstemci Bileşenleri

// 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>
  );
}

Dil Değiştirici Bileşeni

// 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>
  );
}

Çeviri İş Akışı

Yeni Anahtarlar Ekleme

  1. Önce en.json'a ekleyin (gerçeğin kaynağı)
  2. Çeviri komut dosyasını çalıştırın:
node apps/web/scripts/translate-missing.mjs

Komut dosyası, en.json dosyasında bulunan ancak diğer yerel ayar dosyalarında eksik olan anahtarları bulur ve Google Çeviri API'sini çağırır:

// 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;
}

Çeviri Kapsamının Doğrulanması

# 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

Blog İçeriği Çevirisi

MDX içerik dosyaları için ön madde başlıkları İngilizce kalır (başlıklar i18n JSON'dadır), ancak gövde içeriği yerel ayara göre çevrilir:

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 };
}

Sıkça Sorulan Sorular

Neden always yerine localePrefix: 'as-needed' kullanmalısınız?

as-needed ile, İngilizce URL'lerin öneki yoktur (/blog/post), diğer yerel ayarlar ise bir önek alır (/zh/blog/post). Bu standart kuraldır; İngilizce varsayılandır ve temiz URL'lere sahiptir. always ile İngilizce aynı zamanda /en/blog/post alır, bu da gereksiz yönlendirmeler oluşturur ve geçiş yapıyorsanız mevcut İngilizce bağlantı eşitliğinizi böler.

En.json anahtarları neden düz, noktayla ayrılmış dizeler değil de iç içe geçmiş nesneler olmalıdır?

next-intl'in ad alanı çözünürlüğü, JSON ağacını geçmek için ad alanı dizesini noktalara böler. Anahtarlarınız "admin.contacts.title" gibi düz, noktayla ayrılmış dizelerse, next-intl, iç içe geçmiş bir yol olarak messages.admin.contacts.title'yi arar, ancak anahtarın kelimenin tam anlamıyla kök düzeyinde "admin.contacts.title" dizesi olduğunu bulur ve bu da arama hatasına neden olur. Her zaman gerçek iç içe geçmiş nesneleri kullanın.

Diller arasında çoğullamayı nasıl halledebilirim?

next-intl çoğullaştırma için ICU mesaj formatını kullanır. İngilizcede iki çoğul (bir/diğer), Arapçada altı (sıfır, bir, iki, birkaç, çok, diğer) çoğul hali vardır. Çeviri değerlerinizde {count, plural, one {# item} other {# items}} ICU sözdizimini kullanın; next-intl her yerel ayar için doğru çoğul kuralları otomatik olarak uygular.

Next-intl'yi Sayfa Yönlendiricisi ile kullanabilir miyim?

next-intl v4 öncelikle Uygulama Yönlendiricisi için tasarlanmıştır. Pages Router için next-intl v2 veya v3'ü kullanın. Desenler önemli ölçüde farklılık gösterir: Sayfa Yönlendiricisi düzende getStaticProps + NextIntlProvider kullanır, Uygulama Yönlendiricisi ise getRequestConfig + NextIntlClientProvider kullanır. Sayfa Yönlendiricisini kullanıyorsanız i18n'yi geniş ölçekte uygulamadan önce Uygulama Yönlendiricisine geçmeyi düşünün.

Arapça ve Urduca için RTL desteğini nasıl ayarlarım?

Yerel ayar ar veya ur olduğunda <html> öğesinde dir="rtl" değerini ayarlayın. Yöne özgü stiller için Tailwind'in rtl: varyant önekini kullanın (rtl:mr-4, RTL'de ml-4 olur). Uyumlu bir yazı tipi yükleyin — Noto Sans Arabic hem Arapça hem de Urduca alfabeleri kapsar. Sabit kodlanmış left/right CSS değerlerinden kaçının; start/end mantıksal özelliklerini kullanın (Tailwind v4'te ms-4, me-4).

Büyük miktarda içeriği hızlı bir şekilde çevirmek için en iyi yaklaşım nedir?

Tüm anahtarların ve içeriğin ilk makine çevirisi için Google Translate API'yi kullanın, ardından anadili İngilizce olan kişilerin yüksek görünürlükteki içeriği (ana sayfa, ürün açıklamaları, CTA kopyası) incelemesini sağlayın. Blog içeriği için makine çevirisi başlangıç ​​noktası olarak kabul edilebilir ancak teknik doğruluk açısından incelenmelidir. Trafiğe göre en az ilk 3-5 yerel ayarın ana dilini konuşan kişilerin incelemesine yönelik bütçe.


Sonraki Adımlar

Doğru yapıldığında uluslararasılaştırma rekabet açısından zorlu bir süreçtir; içeriğiniz 11 kat daha fazla potansiyel müşteriye ulaşır, SEO hreflang sinyalleriniz bölünmüş bağlantı eşitliği yerine birleşir ve kendi ana dillerindeki kullanıcılar önemli ölçüde daha yüksek oranlarda dönüşüm sağlar.

ECOSIRE, 5.577 içerik dosyasını, 12.543 çeviri anahtarını ve RTL düzen desteğini kapsayan 11 yerel ayarlı tam bir uygulama oluşturmuş ve sürdürmektedir. Next.js uygulamanız için uluslararasılaştırmaya ihtiyacınız varsa bunu kod tabanınıza nasıl uygulayabileceğimizi öğrenmek için ön uç mühendislik hizmetlerimizi keşfedin.

E

Yazan

ECOSIRE Research and Development Team

ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.

WhatsApp'ta Sohbet Et