التدويل في Next.js: 11-التنفيذ المحلي
يتطلب بناء جمهور عالمي أكثر من مجرد تشغيل سلاسلك من خلال خدمة الترجمة من Google. يتضمن تنفيذ تدويل الإنتاج استراتيجية التوجيه، ودعم تخطيط RTL (من اليمين إلى اليسار)، ودقة اللغة من جانب الخادم، وتنظيم مفتاح الترجمة، وإشارات SEO hreflang، وسير عمل ترجمة مستدام. إذا أخطأت في أي من هذه الأخطاء، فإما أن يكون لديك تخطيطات معطلة للمستخدمين باللغة العربية/الأردية، أو تفتقد علامات hreflang التي تؤدي إلى تقسيم تصنيفات البحث الخاصة بك، أو أن مسار الترجمة ينهار تحت وطأة نمو المحتوى.
يوثق هذا الدليل الإعداد الكامل المكون من 11 لغة المستخدمة في ECOSIRE.COM - الإنجليزية بالإضافة إلى الصينية والإسبانية والعربية والبرتغالية والفرنسية والألمانية واليابانية والتركية والهندية والأردية - المبنية على الإصدار التالي من intl v4.8. تم اختبار كل نمط هنا إنتاجيًا عبر 5,577 ملف محتوى MDX و12,543 مفتاح ترجمة.
الوجبات الرئيسية
- استخدم
localePrefix: 'as-needed'- لا تحتوي اللغة الإنجليزية على بادئة، بينما تحصل اللغات الأخرى على/zh/،/ar/، وما إلى ذلك.- يستخدم الإصدار التالي من intl v4 نمط
routing.ts+navigation.ts- لا يتم الاستيراد مطلقًا منnext/navigationمباشرة في المكونات المدركة للغة- العربية (
ar) والأردية (ur) تتطلبdir="rtl"على<html>وخطًا متوافقًا مع RTL (Noto Sans Arab)- يجب أن تكون مفاتيح
en.jsonكائنات متداخلة - المفاتيح المسطحة المفصولة بالنقاط تكسر دقة مساحة الاسمgenerateMetadata()(ليس ثابتًا أبدًاexport const metadata) معalternates.languagesلـ hreflang في كل صفحة- تستخدم مكونات الخادم
getTranslations('namespace')، وتستخدم مكونات العميلuseTranslations('namespace')- تستخدم صفحات الإدارة خطافين للترجمة:
tللمفاتيح الخاصة بالوحدة النمطية،tcلمفاتيحadmin.commonالمشتركة- الترجمة الآلية للمفاتيح الجديدة باستخدام Google Translate 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>
);
}
سير عمل الترجمة
إضافة مفاتيح جديدة
- أضف إلى
en.jsonأولاً (مصدر الحقيقة) - قم بتشغيل البرنامج النصي للترجمة:
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 };
}
الأسئلة المتداولة
لماذا تستخدم localePrefix: 'as-needed' بدلاً من always؟
مع as-needed، لا تحتوي عناوين URL الإنجليزية على بادئة (/blog/post)، بينما تحصل اللغات الأخرى على بادئة (/zh/blog/post). هذا هو الاصطلاح القياسي — اللغة الإنجليزية هي اللغة الافتراضية ولها عناوين URL نظيفة. مع always، تحصل اللغة الإنجليزية أيضًا على /en/blog/post، مما يؤدي إلى إنشاء عمليات إعادة توجيه غير ضرورية وتقسيم حقوق الارتباط الحالية باللغة الإنجليزية إذا كنت تقوم بالترحيل.
لماذا يجب أن تكون مفاتيح en.json كائنات متداخلة وليست سلاسل مسطحة مفصولة بنقاط؟
يقوم حل مساحة الاسم الخاص بـ next-intl بتقسيم سلسلة مساحة الاسم بالنقاط لاجتياز شجرة JSON. إذا كانت مفاتيحك عبارة عن سلاسل مسطحة مفصولة بنقاط مثل "admin.contacts.title"، فإن next-intl يبحث عن messages.admin.contacts.title كمسار متداخل ولكنه يجد أن المفتاح هو السلسلة "admin.contacts.title" حرفيًا على مستوى الجذر، مما يتسبب في فشل البحث. استخدم دائمًا الكائنات المتداخلة الفعلية.
كيف أتعامل مع صيغة الجمع عبر اللغات؟
يستخدم next-intl تنسيق رسالة ICU للجمع. اللغة الإنجليزية لها صيغتي جمع (واحد/آخر)، والعربية لها ستة (صفر، واحد، اثنان، قليل، كثير، آخر). استخدم بناء جملة {count, plural, one {# item} other {# items}} ICU في قيم الترجمة الخاصة بك وسيطبق next-intl قواعد الجمع الصحيحة لكل لغة تلقائيًا.
هل يمكنني استخدام next-intl مع جهاز توجيه الصفحات؟
تم تصميم next-intl v4 بشكل أساسي لجهاز توجيه التطبيقات. بالنسبة لجهاز توجيه الصفحات، استخدم next-intl v2 أو v3. تختلف الأنماط بشكل كبير: يستخدم جهاز توجيه الصفحات getStaticProps + NextIntlProvider، بينما يستخدم جهاز توجيه التطبيقات getRequestConfig + NextIntlClientProvider في التخطيط. إذا كنت تستخدم جهاز توجيه الصفحات، ففكر في الانتقال إلى جهاز توجيه التطبيقات قبل تنفيذ i18n على نطاق واسع.
كيف أقوم بإعداد دعم RTL للغتين العربية والأردية؟
قم بتعيين dir="rtl" على العنصر <html> عندما تكون الإعدادات المحلية هي ar أو ur. استخدم البادئة المتغيرة rtl: الخاصة بـ Tailwind للأنماط الخاصة بالاتجاه (rtl:mr-4 تصبح ml-4 في RTL). قم بتحميل خط متوافق - يغطي Noto Sans Arab النصوص العربية والأردية. تجنب قيم left/right المضمنة في CSS؛ استخدم start/end الخصائص المنطقية (ms-4، me-4 في Tailwind v4).
ما هو الأسلوب الأفضل لترجمة كميات كبيرة من المحتوى بسرعة؟
استخدم Google Translate API للترجمة الآلية الأولية لجميع المفاتيح والمحتوى، ثم اطلب من المتحدثين الأصليين مراجعة المحتوى عالي الوضوح (الصفحة الرئيسية، أوصاف المنتج، نسخة CTA). بالنسبة لمحتوى المدونة، تعتبر الترجمة الآلية مقبولة كنقطة بداية ولكن يجب مراجعتها للتأكد من دقتها الفنية. ميزانية لمراجعة المتحدث الأصلي لأفضل 3-5 لغات على الأقل حسب حركة المرور.
الخطوات التالية
إن التدويل الذي يتم بشكل صحيح هو بمثابة خندق تنافسي - حيث يصل المحتوى الخاص بك إلى عدد أكبر من العملاء المحتملين بمقدار 11 مرة، وتدمج إشارات SEO hreflang الخاصة بك بدلاً من تقسيم حقوق الارتباط، ويتحول المستخدمون بلغتهم الأم بمعدلات أعلى بكثير.
قامت ECOSIRE ببناء وصيانة تطبيق كامل مكون من 11 لغة يغطي 5577 ملف محتوى و12543 مفتاح ترجمة ودعم تخطيط RTL. إذا كنت بحاجة إلى تدويل تطبيق Next.js الخاص بك، استكشف خدماتنا الهندسية للواجهة الأمامية لمعرفة كيف يمكننا تنفيذ ذلك لقاعدة التعليمات البرمجية الخاصة بك.
بقلم
ECOSIRE Research and Development Team
بناء منتجات رقمية بمستوى المؤسسات في ECOSIRE. مشاركة رؤى حول تكاملات Odoo وأتمتة التجارة الإلكترونية وحلول الأعمال المدعومة بالذكاء الاصطناعي.
مقالات ذات صلة
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.
توسيع التجارة الإلكترونية إلى الأسواق الدولية: دليل استراتيجي كامل
دليل كامل لتوسيع التجارة الإلكترونية الدولية يغطي أبحاث السوق، والتوطين، والخدمات اللوجستية، والمدفوعات، والامتثال القانوني، واستراتيجية التسويق.
تخطيط موارد المؤسسات للتوسع الدولي: إدارة العمليات المتعددة البلدان في عام 2026
كيفية استخدام أنظمة تخطيط موارد المؤسسات (ERP) لإدارة التوسع الدولي، بما في ذلك المحاسبة متعددة العملات، والامتثال الضريبي، والتوطين، وإدارة سلسلة التوريد، وتكوين Odoo متعدد الشركات.