التدويل (i18n) في Next.js
فهم التدويل في Next.js
التدويل (i18n) هو عملية إعداد تطبيقك لدعم لغات ومناطق متعددة. توفر Next.js ميزات قوية مدمجة لإنشاء تطبيقات متعددة اللغات مع توجيه اللغة والكشف التلقائي عن اللغة والترجمات السلسة.
توجيه i18n في Next.js
تدعم Next.js استراتيجيتين للتوجيه للتدويل:
- توجيه المسار الفرعي: /en/about، /ar/about
- توجيه النطاق: example.com، example.ar
إعداد i18n مع App Router
أولاً، قم بإنشاء بنية مجلدات للغاتك:
app/
├── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/
│ │ └── page.tsx
│ └── products/
│ └── page.tsx
└── middleware.ts
إنشاء تكوين i18n
قم بإنشاء ملف تكوين للغات المدعومة:
// i18n.config.ts
export const i18n = {
defaultLocale: 'en',
locales: ['en', 'ar', 'fr', 'es'],
} as const;
export type Locale = (typeof i18n)['locales'][number];
// أسماء اللغات للعرض
export const localeNames: Record<Locale, string> = {
en: 'English',
ar: 'العربية',
fr: 'Français',
es: 'Español',
};
// اللغات RTL
export const rtlLocales: Locale[] = ['ar'];
Middleware للكشف عن اللغة
قم بإنشاء middleware للتعامل مع الكشف عن اللغة وإعادة التوجيه:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { i18n } from './i18n.config';
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// تحقق من وجود اللغة في المسار
const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
// إعادة التوجيه إذا لم تكن هناك لغة
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
);
}
}
function getLocale(request: NextRequest): string {
// 1. تحقق من cookie
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (cookieLocale && i18n.locales.includes(cookieLocale as any)) {
return cookieLocale;
}
// 2. تحقق من رأس Accept-Language
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const preferredLocale = acceptLanguage
.split(',')
.map((lang) => lang.split(';')[0].trim().split('-')[0])
.find((lang) => i18n.locales.includes(lang as any));
if (preferredLocale) {
return preferredLocale;
}
}
// 3. اللغة الافتراضية
return i18n.defaultLocale;
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
التخطيط الجذري مع اللغة
قم بإنشاء تخطيط ديناميكي يتعامل مع الإعدادات الخاصة باللغة:
// app/[locale]/layout.tsx
import { Inter } from 'next/font/google';
import { Locale, i18n, rtlLocales } from '@/i18n.config';
import { notFound } from 'next/navigation';
const inter = Inter({ subsets: ['latin'] });
export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ locale }));
}
export default function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: Locale };
}) {
// التحقق من صحة اللغة
if (!i18n.locales.includes(locale)) {
notFound();
}
const isRTL = rtlLocales.includes(locale);
return (
<html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>
<body className={inter.className}>
{children}
</body>
</html>
);
}
إنشاء قواميس الترجمة
نظم ترجماتك في ملفات منفصلة:
// dictionaries/en.json
{
"navigation": {
"home": "Home",
"about": "About",
"products": "Products",
"contact": "Contact"
},
"home": {
"title": "Welcome to Our Website",
"description": "Discover amazing products and services",
"cta": "Get Started"
},
"products": {
"title": "Our Products",
"featured": "Featured Products",
"viewDetails": "View Details",
"addToCart": "Add to Cart"
},
"common": {
"loading": "Loading...",
"error": "Something went wrong",
"retry": "Try Again",
"learnMore": "Learn More"
}
}
// dictionaries/ar.json
{
"navigation": {
"home": "الرئيسية",
"about": "من نحن",
"products": "المنتجات",
"contact": "اتصل بنا"
},
"home": {
"title": "مرحباً بك في موقعنا",
"description": "اكتشف منتجات وخدمات مذهلة",
"cta": "ابدأ الآن"
},
"products": {
"title": "منتجاتنا",
"featured": "المنتجات المميزة",
"viewDetails": "عرض التفاصيل",
"addToCart": "أضف إلى السلة"
},
"common": {
"loading": "جاري التحميل...",
"error": "حدث خطأ ما",
"retry": "حاول مرة أخرى",
"learnMore": "اعرف المزيد"
}
}
دالة تحميل القاموس
قم بإنشاء دالة لتحميل الترجمات:
// lib/dictionaries.ts
import 'server-only';
import type { Locale } from '@/i18n.config';
const dictionaries = {
en: () => import('@/dictionaries/en.json').then((module) => module.default),
ar: () => import('@/dictionaries/ar.json').then((module) => module.default),
fr: () => import('@/dictionaries/fr.json').then((module) => module.default),
es: () => import('@/dictionaries/es.json').then((module) => module.default),
};
export const getDictionary = async (locale: Locale) => {
return dictionaries[locale]();
};
استخدام الترجمات في مكونات الخادم
الوصول إلى الترجمات في مكونات الخادم الخاصة بك:
// app/[locale]/page.tsx
import { getDictionary } from '@/lib/dictionaries';
import { Locale } from '@/i18n.config';
export default async function HomePage({ params }: { params: { locale: Locale } }) {
const dict = await getDictionary(params.locale);
return (
<main>
<h1>{dict.home.title}</h1>
<p>{dict.home.description}</p>
<button>{dict.home.cta}</button>
</main>
);
}
مكون مبدل اللغة
قم بإنشاء مكون للتبديل بين اللغات:
// components/LanguageSwitcher.tsx
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { i18n, localeNames, type Locale } from '@/i18n.config';
import { useState } from 'react';
export default function LanguageSwitcher({ currentLocale }: { currentLocale: Locale }) {
const pathname = usePathname();
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const switchLocale = (locale: Locale) => {
// إزالة اللغة الحالية من المسار
const segments = pathname.split('/');
segments[1] = locale;
const newPath = segments.join('/');
// تعيين cookie لتفضيل اللغة
document.cookie = `NEXT_LOCALE=${locale}; path=/; max-age=31536000`;
router.push(newPath);
setIsOpen(false);
};
return (
<div className="language-switcher">
<button onClick={() => setIsOpen(!isOpen)}>
{localeNames[currentLocale]}
</button>
{isOpen && (
<ul>
{i18n.locales.map((locale) => (
<li key={locale}>
<button
onClick={() => switchLocale(locale)}
className={locale === currentLocale ? 'active' : ''}
>
{localeNames[locale]}
</button>
</li>
))}
</ul>
)}
</div>
);
}
دعم RTL
أضف CSS لدعم اللغات من اليمين إلى اليسار:
/* styles/rtl.css */
/* تعديلات RTL العامة */
html[dir='rtl'] {
text-align: right;
}
html[dir='rtl'] .container {
direction: rtl;
}
/* التنقل */
html[dir='rtl'] .nav-list {
flex-direction: row-reverse;
}
/* محاذاة النص */
html[dir='rtl'] p,
html[dir='rtl'] h1,
html[dir='rtl'] h2,
html[dir='rtl'] h3 {
text-align: right;
}
/* الهوامش والحشو */
html[dir='rtl'] .ml-4 {
margin-left: 0;
margin-right: 1rem;
}
html[dir='rtl'] .mr-4 {
margin-right: 0;
margin-left: 1rem;
}
/* الخصائص المنطقية (نهج أفضل) */
.spacing {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
}
الروابط الواعية باللغة
قم بإنشاء غلاف لمكون Link في Next.js يحافظ على اللغة:
// components/LocaleLink.tsx
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { Locale } from '@/i18n.config';
interface LocaleLinkProps {
href: string;
children: React.ReactNode;
className?: string;
}
export default function LocaleLink({ href, children, className }: LocaleLinkProps) {
const params = useParams();
const locale = params?.locale as Locale;
// إضافة اللغة إلى href إذا لم تكن موجودة
const localizedHref = href.startsWith('/')
? `/${locale}${href}`
: `/${locale}/${href}`;
return (
<Link href={localizedHref} className={className}>
{children}
</Link>
);
}
ترجمة المحتوى الديناميكي
التعامل مع الترجمات للمحتوى من APIs أو قواعد البيانات:
// lib/translations.ts
import { Locale } from '@/i18n.config';
interface TranslatableContent {
title: {
en: string;
ar: string;
fr: string;
es: string;
};
description: {
en: string;
ar: string;
fr: string;
es: string;
};
}
export function getTranslatedContent<T extends TranslatableContent>(
content: T,
locale: Locale
): { title: string; description: string } {
return {
title: content.title[locale],
description: content.description[locale],
};
}
// الاستخدام في المكون
const product = {
id: 1,
title: {
en: 'Laptop',
ar: 'حاسوب محمول',
fr: 'Ordinateur portable',
es: 'Portátil',
},
description: {
en: 'High-performance laptop',
ar: 'حاسوب محمول عالي الأداء',
fr: 'Ordinateur portable haute performance',
es: 'Portátil de alto rendimiento',
},
};
const translated = getTranslatedContent(product, 'ar');
// { title: 'حاسوب محمول', description: 'حاسوب محمول عالي الأداء' }
تنسيق الأرقام والتواريخ
استخدم Intl API لتنسيق واعي باللغة:
// lib/formatting.ts
import { Locale } from '@/i18n.config';
export function formatNumber(value: number, locale: Locale): string {
return new Intl.NumberFormat(locale).format(value);
}
export function formatCurrency(value: number, locale: Locale, currency = 'USD'): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(value);
}
export function formatDate(date: Date, locale: Locale): string {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
export function formatRelativeTime(date: Date, locale: Locale): string {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const diffInDays = Math.floor((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return rtf.format(diffInDays, 'day');
}
// الاستخدام
formatNumber(1234567.89, 'ar'); // "١٬٢٣٤٬٥٦٧٫٨٩"
formatCurrency(99.99, 'en', 'USD'); // "$99.99"
formatDate(new Date(), 'ar'); // "١٤ فبراير ٢٠٢٦"
formatRelativeTime(new Date(Date.now() - 86400000), 'en'); // "yesterday"
تحسين محركات البحث مع hreflang
أضف روابط اللغات البديلة لتحسين محركات البحث:
// app/[locale]/layout.tsx
import { Metadata } from 'next';
import { i18n, Locale } from '@/i18n.config';
export async function generateMetadata({
params,
}: {
params: { locale: Locale };
}): Promise<Metadata> {
const baseUrl = 'https://example.com';
return {
alternates: {
canonical: `${baseUrl}/${params.locale}`,
languages: Object.fromEntries(
i18n.locales.map((locale) => [locale, `${baseUrl}/${locale}`])
),
},
};
}
Hook مساعد الترجمة
قم بإنشاء hook من جانب العميل للترجمات:
// hooks/useTranslations.ts
'use client';
import { useParams } from 'next/navigation';
import { Locale } from '@/i18n.config';
import { createContext, useContext, ReactNode } from 'react';
type Dictionary = Record<string, any>;
const TranslationsContext = createContext<Dictionary | null>(null);
export function TranslationsProvider({
children,
dictionary,
}: {
children: ReactNode;
dictionary: Dictionary;
}) {
return (
<TranslationsContext.Provider value={dictionary}>
{children}
</TranslationsContext.Provider>
);
}
export function useTranslations(namespace?: string) {
const dictionary = useContext(TranslationsContext);
const params = useParams();
const locale = params?.locale as Locale;
if (!dictionary) {
throw new Error('useTranslations must be used within TranslationsProvider');
}
const t = (key: string, variables?: Record<string, string>): string => {
const keys = namespace ? `${namespace}.${key}`.split('.') : key.split('.');
let value: any = dictionary;
for (const k of keys) {
value = value?.[k];
}
if (typeof value !== 'string') {
console.warn(`Translation missing for key: ${key}`);
return key;
}
// استبدال المتغيرات
if (variables) {
return Object.entries(variables).reduce(
(str, [varKey, varValue]) => str.replace(`{{${varKey}}}`, varValue),
value
);
}
return value;
};
return { t, locale };
}
// الاستخدام في مكون العميل
function MyComponent() {
const { t } = useTranslations('products');
return <h1>{t('title')}</h1>;
}
تمرين تطبيقي
المهمة: بناء صفحة منتج متعددة اللغات للتجارة الإلكترونية:
- دعم اللغات الإنجليزية والعربية والإسبانية
- تنفيذ دعم RTL مناسب للعربية
- إنشاء مبدل لغة في التنقل
- تنسيق الأسعار وفقاً للغة (USD للإنجليزية، SAR للعربية، EUR للإسبانية)
- تنسيق تواريخ المنتجات بتنسيق خاص باللغة
- إضافة علامات hreflang لتحسين محركات البحث
- ترجمة أسماء وأوصاف المنتجات من بيانات API
إضافي: تنفيذ نظام احتياطي للترجمة يعرض الإنجليزية إذا كانت الترجمة مفقودة للغة الحالية.
الملخص
النقاط الرئيسية حول التدويل في Next.js:
- استخدم middleware للكشف التلقائي عن اللغة والتوجيه
- نظم الترجمات في ملفات قاموس JSON لكل لغة
- دعم لغات RTL مع تعديلات dir وCSS المناسبة
- استخدم Intl API لتنسيق الأرقام والعملات والتواريخ
- تنفيذ روابط وتنقل واعية باللغة
- إضافة علامات hreflang لتحسين محركات البحث متعددة اللغات
- فصل أساليب الترجمة من جانب الخادم (getDictionary) ومن جانب العميل (useTranslations)