11.3 בינלאומיות הרצאה
בינלאומיות - i18n¶
בינלאומיות (Internationalization - i18n) היא התהליך של הכנת אפליקציה לתמיכה בשפות ותרבויות שונות. לוקליזציה (Localization - l10n) היא ההתאמה בפועל לשפה ותרבות ספציפית. בשיעור זה נלמד כיצד לבנות אפליקציות רב-לשוניות, כולל תמיכה ב-RTL שחשובה במיוחד לעברית.
מושגי יסוד¶
- i18n (internationalization - 18 אותיות בין i ל-n) - הארכיטקטורה שמאפשרת תמיכה בשפות שונות
- l10n (localization - 10 אותיות בין l ל-n) - התרגום וההתאמה בפועל לשפה ספציפית
- locale - מזהה שפה ואזור, למשל
he-IL(עברית - ישראל),en-US(אנגלית - ארה"ב)
הגדרת react-i18next¶
ספריית react-i18next היא הפתרון הנפוץ ביותר לניהול תרגומים באפליקציות ריאקט.
הגדרת i18next¶
// lib/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
i18n
.use(HttpBackend) // טעינת תרגומים מקבצים
.use(LanguageDetector) // זיהוי שפת המשתמש אוטומטית
.use(initReactI18next) // חיבור לריאקט
.init({
fallbackLng: 'he', // שפת ברירת מחדל
supportedLngs: ['he', 'en', 'ar'],
defaultNS: 'common', // namespace ברירת מחדל
ns: ['common', 'auth', 'dashboard'],
interpolation: {
escapeValue: false, // ריאקט כבר עושה escape
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
detection: {
order: ['cookie', 'localStorage', 'navigator', 'htmlTag'],
caches: ['cookie', 'localStorage'],
},
});
export default i18n;
קבצי תרגום¶
// public/locales/he/common.json
{
"app": {
"title": "האפליקציה שלי",
"description": "אפליקציית דוגמה עם תמיכה רב-לשונית"
},
"nav": {
"home": "דף הבית",
"about": "אודות",
"contact": "יצירת קשר",
"settings": "הגדרות"
},
"actions": {
"save": "שמור",
"cancel": "ביטול",
"delete": "מחק",
"edit": "ערוך",
"confirm": "אישור"
},
"messages": {
"welcome": "שלום, {{name}}!",
"itemCount": "{{count}} פריט",
"itemCount_other": "{{count}} פריטים",
"lastUpdated": "עודכן לאחרונה: {{date}}"
}
}
// public/locales/en/common.json
{
"app": {
"title": "My Application",
"description": "A sample application with multi-language support"
},
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact",
"settings": "Settings"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"confirm": "Confirm"
},
"messages": {
"welcome": "Hello, {{name}}!",
"itemCount_one": "{{count}} item",
"itemCount_other": "{{count}} items",
"lastUpdated": "Last updated: {{date}}"
}
}
שימוש בתרגומים¶
// namespaces נפרדים
// public/locales/he/auth.json
{
"login": {
"title": "התחברות",
"email": "אימייל",
"password": "סיסמה",
"submit": "התחבר",
"forgotPassword": "שכחת סיסמה?",
"noAccount": "אין לך חשבון?",
"register": "הרשמה"
}
}
'use client';
import { useTranslation } from 'react-i18next';
function LoginPage() {
// שימוש ב-namespace ספציפי
const { t } = useTranslation('auth');
return (
<form>
<h1>{t('login.title')}</h1>
<label htmlFor="email">{t('login.email')}</label>
<input id="email" type="email" />
<label htmlFor="password">{t('login.password')}</label>
<input id="password" type="password" />
<button type="submit">{t('login.submit')}</button>
<a href="/forgot-password">{t('login.forgotPassword')}</a>
<p>
{t('login.noAccount')} <a href="/register">{t('login.register')}</a>
</p>
</form>
);
}
// שימוש עם אינטרפולציה וריבוי
function Dashboard({ user, items }: { user: { name: string }; items: unknown[] }) {
const { t } = useTranslation();
return (
<div>
{/* אינטרפולציה - הצבת ערכים */}
<h1>{t('messages.welcome', { name: user.name })}</h1>
{/* ריבוי - singular/plural */}
<p>{t('messages.itemCount', { count: items.length })}</p>
{/* עיצוב תאריכים */}
<p>{t('messages.lastUpdated', { date: new Date().toLocaleDateString() })}</p>
</div>
);
}
החלפת שפה - Language Switching¶
'use client';
import { useTranslation } from 'react-i18next';
const languages = [
{ code: 'he', name: 'עברית', dir: 'rtl' },
{ code: 'en', name: 'English', dir: 'ltr' },
{ code: 'ar', name: 'العربية', dir: 'rtl' },
];
function LanguageSwitcher() {
const { i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
// עדכון כיוון הדף
const lang = languages.find((l) => l.code === lng);
if (lang) {
document.documentElement.dir = lang.dir;
document.documentElement.lang = lng;
}
};
return (
<div role="group" aria-label="בחירת שפה">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => changeLanguage(lang.code)}
aria-pressed={i18n.language === lang.code}
lang={lang.code}
>
{lang.name}
</button>
))}
</div>
);
}
תמיכה ב-RTL¶
תמיכה ב-RTL (Right-to-Left) חשובה במיוחד עבור עברית וערבית. הנה הכלים והטכניקות:
שימוש ב-CSS Logical Properties¶
/* במקום left/right - השתמשו ב-inline-start/inline-end */
/* לא מומלץ - שובר ב-RTL */
.sidebar {
margin-left: 20px;
padding-right: 16px;
text-align: left;
border-left: 2px solid blue;
}
/* מומלץ - עובד גם ב-LTR וגם ב-RTL */
.sidebar {
margin-inline-start: 20px;
padding-inline-end: 16px;
text-align: start;
border-inline-start: 2px solid blue;
}
/* מיפוי מלא */
/* margin-left -> margin-inline-start */
/* margin-right -> margin-inline-end */
/* padding-left -> padding-inline-start */
/* padding-right -> padding-inline-end */
/* left -> inset-inline-start */
/* right -> inset-inline-end */
/* text-align: left -> text-align: start */
/* text-align: right -> text-align: end */
/* border-left -> border-inline-start */
/* float: left -> float: inline-start */
הגדרת כיוון ב-Layout¶
// app/layout.tsx
import { cookies } from 'next/headers';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies();
const locale = cookieStore.get('i18next')?.value || 'he';
const dir = ['he', 'ar'].includes(locale) ? 'rtl' : 'ltr';
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}
Flexbox ו-RTL¶
/* Flexbox מתהפך אוטומטית ב-RTL! */
.container {
display: flex;
gap: 16px;
}
/* ב-LTR: [item1] [item2] [item3] -> */
/* ב-RTL: <- [item3] [item2] [item1] */
/* אם רוצים למנוע היפוך */
.container-no-flip {
display: flex;
direction: ltr; /* תמיד LTR */
}
אייקונים ב-RTL¶
// חלק מהאייקונים צריכים להתהפך ב-RTL
function DirectionalIcon({ icon }: { icon: 'arrow' | 'check' }) {
const { i18n } = useTranslation();
const isRTL = i18n.dir() === 'rtl';
// חץ צריך להתהפך, סימן V לא
const shouldFlip = icon === 'arrow' && isRTL;
return (
<span style={{ transform: shouldFlip ? 'scaleX(-1)' : 'none' }}>
{icon === 'arrow' ? '>' : 'V'}
</span>
);
}
עיצוב תאריכים, מספרים ומטבעות - Intl API¶
ה-Intl API של JavaScript מספק כלים מובנים לעיצוב לוקלי:
עיצוב מספרים¶
// עיצוב מספרים
const number = 1234567.89;
new Intl.NumberFormat('he-IL').format(number);
// "1,234,567.89"
new Intl.NumberFormat('en-US').format(number);
// "1,234,567.89"
new Intl.NumberFormat('de-DE').format(number);
// "1.234.567,89"
// עיצוב אחוזים
new Intl.NumberFormat('he-IL', {
style: 'percent',
minimumFractionDigits: 1,
}).format(0.256);
// "25.6%"
עיצוב מטבעות¶
// מטבעות
new Intl.NumberFormat('he-IL', {
style: 'currency',
currency: 'ILS',
}).format(99.90);
// "99.90 ₪"
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(99.90);
// "$99.90"
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY',
}).format(1500);
// "¥1,500"
עיצוב תאריכים¶
const date = new Date('2024-03-15T14:30:00');
// עיצוב בסיסי
new Intl.DateTimeFormat('he-IL').format(date);
// "15.3.2024"
new Intl.DateTimeFormat('en-US').format(date);
// "3/15/2024"
// עיצוב מפורט
new Intl.DateTimeFormat('he-IL', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
// "יום שישי, 15 במרץ 2024"
// זמן יחסי
const rtf = new Intl.RelativeTimeFormat('he', { numeric: 'auto' });
rtf.format(-1, 'day'); // "אתמול"
rtf.format(2, 'hour'); // "בעוד שעתיים"
rtf.format(-3, 'month'); // "לפני 3 חודשים"
Hook שימושי לעיצוב¶
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
function useFormatters() {
const { i18n } = useTranslation();
const locale = i18n.language;
return useMemo(() => ({
number: (value: number) =>
new Intl.NumberFormat(locale).format(value),
currency: (value: number, currency = 'ILS') =>
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value),
percent: (value: number) =>
new Intl.NumberFormat(locale, { style: 'percent' }).format(value),
date: (value: Date, options?: Intl.DateTimeFormatOptions) =>
new Intl.DateTimeFormat(locale, options).format(value),
relativeTime: (value: number, unit: Intl.RelativeTimeFormatUnit) =>
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(value, unit),
}), [locale]);
}
// שימוש
function ProductCard({ price, lastUpdated }: { price: number; lastUpdated: Date }) {
const fmt = useFormatters();
return (
<div>
<p>{fmt.currency(price)}</p>
<p>{fmt.date(lastUpdated, { dateStyle: 'long' })}</p>
</div>
);
}
כללי ריבוי - Pluralization Rules¶
שפות שונות מטפלות בריבוי בצורות שונות. i18next תומך בכל הכללים:
// עברית - 2 צורות (יחיד וריבוי)
{
"items_one": "פריט אחד",
"items_other": "{{count}} פריטים"
}
// אנגלית - 2 צורות
{
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}
// ערבית - 6 צורות!
{
"items_zero": "لا عناصر",
"items_one": "عنصر واحد",
"items_two": "عنصران",
"items_few": "{{count}} عناصر",
"items_many": "{{count}} عنصراً",
"items_other": "{{count}} عنصر"
}
// שימוש - i18next בוחר את הצורה הנכונה אוטומטית
function ItemCounter({ count }: { count: number }) {
const { t } = useTranslation();
return <p>{t('items', { count })}</p>;
// count=0: "0 פריטים"
// count=1: "פריט אחד"
// count=5: "5 פריטים"
}
SEO לאתרים רב-לשוניים¶
תגיות hreflang¶
// app/layout.tsx - הוספת hreflang ב-Next.js
import { type Metadata } from 'next';
export const metadata: Metadata = {
alternates: {
canonical: 'https://myapp.com',
languages: {
'he-IL': 'https://myapp.com/he',
'en-US': 'https://myapp.com/en',
'ar-SA': 'https://myapp.com/ar',
'x-default': 'https://myapp.com',
},
},
};
<!-- הפלט ב-HTML -->
<link rel="alternate" hreflang="he-IL" href="https://myapp.com/he" />
<link rel="alternate" hreflang="en-US" href="https://myapp.com/en" />
<link rel="alternate" hreflang="ar-SA" href="https://myapp.com/ar" />
<link rel="alternate" hreflang="x-default" href="https://myapp.com" />
ניתוב לפי שפה ב-Next.js¶
// middleware.ts - ניתוב אוטומטי לפי שפה
import { NextRequest, NextResponse } from 'next/server';
const locales = ['he', 'en', 'ar'];
const defaultLocale = 'he';
function getLocale(request: NextRequest): string {
// בדיקת עוגייה
const cookieLocale = request.cookies.get('locale')?.value;
if (cookieLocale && locales.includes(cookieLocale)) {
return cookieLocale;
}
// בדיקת Accept-Language header
const acceptLanguage = request.headers.get('accept-language') || '';
for (const locale of locales) {
if (acceptLanguage.includes(locale)) {
return locale;
}
}
return defaultLocale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// בדיקה אם כבר יש locale ב-URL
const hasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return;
// ניתוב לשפה המתאימה
const locale = getLocale(request);
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
);
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Sitemap רב-לשוני¶
// app/sitemap.ts
import { type MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://myapp.com';
const locales = ['he', 'en', 'ar'];
const pages = ['', '/about', '/contact', '/products'];
return pages.flatMap((page) =>
locales.map((locale) => ({
url: `${baseUrl}/${locale}${page}`,
lastModified: new Date(),
alternates: {
languages: Object.fromEntries(
locales.map((l) => [l, `${baseUrl}/${l}${page}`])
),
},
}))
);
}
בינלאומיות ב-Next.js עם next-intl¶
חלופה פופולרית ל-react-i18next ב-Next.js:
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default,
}));
// שימוש בקומפוננטה (Server Component)
import { useTranslations } from 'next-intl';
function HomePage() {
const t = useTranslations('home');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
סיכום¶
בשיעור זה למדנו על:
- i18n ו-l10n - ההבדל בין הכנת התשתית לבין התרגום בפועל
- react-i18next - הגדרה, קבצי תרגום, namespaces ושימוש
- החלפת שפה - מנגנון בחירת שפה ועדכון הממשק
- תמיכה ב-RTL - CSS Logical Properties, הגדרת כיוון, היפוך אלמנטים
- Intl API - עיצוב מספרים, מטבעות, תאריכים וזמן יחסי
- כללי ריבוי - טיפול בריבוי שונה בכל שפה
- SEO רב-לשוני - hreflang, ניתוב לפי שפה, Sitemap
תמיכה רב-לשונית היא השקעה שמשתלמת - היא מאפשרת להגיע לקהל רחב יותר ומשפרת את חוויית המשתמש.