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.tsmodelini kullanır — asla yerel ayarlara duyarlı bileşenlerdenext/navigationöğesinden doğrudan içe aktarmayın- Arapça (
ar) ve Urduca (ur),<html>üzerindedir="rtl"ve RTL uyumlu bir yazı tipi (Noto Sans Arabic) gerektiriren.jsonanahtarları iç içe geçmiş nesneler OLMALIDIR — düz noktayla ayrılmış anahtarlar ad alanı çözümlemesini bozar- Her sayfada hreflang için
alternates.languagesilegenerateMetadata()(hiçbir zaman statikexport const metadatadeğil)- Sunucu bileşenleri
getTranslations('namespace')kullanır, istemci bileşenleriuseTranslations('namespace')kullanır- Yönetici sayfaları iki çeviri kancası kullanır: modüle özgü anahtarlar için
t, paylaşılanadmin.commonanahtarları içintc- 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
- Önce
en.json'a ekleyin (gerçeğin kaynağı) - Ç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.
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.
İlgili Makaleler
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.
E-Ticaret'i Uluslararası Pazarlara Genişletmek: Eksiksiz Bir Strateji Kılavuzu
Pazar araştırması, yerelleştirme, lojistik, ödemeler, yasal uyumluluk ve pazarlama stratejisini kapsayan uluslararası e-ticaretin genişletilmesine yönelik eksiksiz kılavuz.
Uluslararası Genişleme için ERP: 2026'da Çok Ülkeli Operasyonları Yönetmek
Çoklu para birimi muhasebesi, vergi uyumluluğu, yerelleştirme, tedarik zinciri yönetimi ve Odoo çoklu şirket yapılandırması dahil olmak üzere uluslararası genişlemeyi yönetmek için ERP sistemleri nasıl kullanılır?