11.3 בינלאומיות פתרון
פתרון - בינלאומיות¶
פתרון תרגיל 1 - הגדרת i18n בסיסית¶
// 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',
ns: ['common', 'errors'],
interpolation: {
escapeValue: false,
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
detection: {
order: ['cookie', 'localStorage', 'navigator'],
caches: ['cookie'],
cookieOptions: { path: '/', sameSite: 'strict' },
},
});
export default i18n;
// public/locales/he/common.json
{
"app": {
"title": "האפליקציה שלי"
},
"nav": {
"home": "דף הבית",
"products": "מוצרים",
"about": "אודות",
"contact": "צור קשר"
},
"actions": {
"save": "שמור",
"cancel": "ביטול",
"delete": "מחק",
"edit": "ערוך"
}
}
// public/locales/he/errors.json
{
"required": "שדה חובה",
"invalidEmail": "כתובת אימייל לא תקינה",
"passwordTooShort": "סיסמה חייבת להכיל לפחות {{min}} תווים"
}
// public/locales/en/common.json
{
"app": {
"title": "My Application"
},
"nav": {
"home": "Home",
"products": "Products",
"about": "About",
"contact": "Contact"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit"
}
}
// public/locales/en/errors.json
{
"required": "This field is required",
"invalidEmail": "Invalid email address",
"passwordTooShort": "Password must be at least {{min}} characters"
}
// public/locales/ar/common.json
{
"app": {
"title": "تطبيقي"
},
"nav": {
"home": "الصفحة الرئيسية",
"products": "المنتجات",
"about": "حول",
"contact": "اتصل بنا"
},
"actions": {
"save": "حفظ",
"cancel": "إلغاء",
"delete": "حذف",
"edit": "تعديل"
}
}
// public/locales/ar/errors.json
{
"required": "هذا الحقل مطلوب",
"invalidEmail": "عنوان البريد الإلكتروني غير صالح",
"passwordTooShort": "يجب أن تتكون كلمة المرور من {{min}} أحرف على الأقل"
}
פתרון תרגיל 2 - קומפוננטת החלפת שפה עם RTL¶
'use client';
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
const languages = [
{ code: 'he', name: 'עברית', flag: '🇮🇱', dir: 'rtl' as const },
{ code: 'en', name: 'English', flag: '🇺🇸', dir: 'ltr' as const },
{ code: 'ar', name: 'العربية', flag: '🇸🇦', dir: 'rtl' as const },
];
function LanguageSwitcher() {
const { i18n } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const currentLang = languages.find((l) => l.code === i18n.language) || languages[0];
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
function changeLanguage(lang: typeof languages[0]) {
i18n.changeLanguage(lang.code);
document.documentElement.dir = lang.dir;
document.documentElement.lang = lang.code;
localStorage.setItem('preferred-language', lang.code);
setIsOpen(false);
}
// טעינת שפה שמורה בעת אתחול
useEffect(() => {
const saved = localStorage.getItem('preferred-language');
if (saved) {
const lang = languages.find((l) => l.code === saved);
if (lang) {
changeLanguage(lang);
}
}
}, []);
return (
<div ref={menuRef} className="lang-switcher">
<button
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-haspopup="true"
className="lang-button"
>
<span>{currentLang.flag}</span>
<span>{currentLang.name}</span>
</button>
{isOpen && (
<ul role="menu" className="lang-menu">
{languages.map((lang) => (
<li key={lang.code} role="none">
<button
role="menuitem"
onClick={() => changeLanguage(lang)}
aria-current={lang.code === i18n.language ? 'true' : undefined}
lang={lang.code}
className="lang-option"
>
<span>{lang.flag}</span>
<span>{lang.name}</span>
</button>
</li>
))}
</ul>
)}
</div>
);
}
/* CSS עם Logical Properties */
.lang-switcher {
position: relative;
}
.lang-button {
display: flex;
align-items: center;
gap: 8px;
padding-inline: 12px;
padding-block: 8px;
border: 1px solid #ccc;
border-radius: 6px;
background: transparent;
cursor: pointer;
}
.lang-menu {
position: absolute;
inset-inline-end: 0;
inset-block-start: 100%;
margin-block-start: 4px;
padding: 4px;
list-style: none;
background: white;
border: 1px solid #ccc;
border-radius: 6px;
min-inline-size: 150px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.lang-option {
display: flex;
align-items: center;
gap: 8px;
padding-inline: 12px;
padding-block: 8px;
inline-size: 100%;
border: none;
background: transparent;
cursor: pointer;
text-align: start;
border-radius: 4px;
}
.lang-option:hover {
background: #f5f5f5;
}
.lang-option[aria-current="true"] {
background: #e8f0fe;
font-weight: bold;
}
/* Layout שעובד ב-RTL ו-LTR */
.page-layout {
display: flex;
gap: 24px;
}
.sidebar {
inline-size: 250px;
padding-inline-end: 24px;
border-inline-end: 1px solid #eee;
}
.main-content {
flex: 1;
padding-inline-start: 24px;
}
.card {
padding-inline: 16px;
padding-block: 12px;
margin-block-end: 16px;
border-radius: 8px;
text-align: start;
}
.breadcrumb {
display: flex;
gap: 8px;
}
.breadcrumb-separator {
/* מתהפך אוטומטית ב-RTL */
transform: scaleX(1);
}
[dir="rtl"] .breadcrumb-separator {
transform: scaleX(-1);
}
פתרון תרגיל 3 - Hook לעיצוב לוקלי¶
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type DateStyle = 'short' | 'medium' | 'long' | 'full';
function useFormatters() {
const { i18n } = useTranslation();
const locale = i18n.language;
return useMemo(() => {
const formatNumber = (value: number) =>
new Intl.NumberFormat(locale).format(value);
const formatCurrency = (value: number, currency = 'ILS') =>
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);
const formatPercent = (value: number) =>
new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}).format(value);
const dateStyleOptions: Record<DateStyle, Intl.DateTimeFormatOptions> = {
short: { dateStyle: 'short' },
medium: { dateStyle: 'medium' },
long: { dateStyle: 'long' },
full: { dateStyle: 'full' },
};
const formatDate = (date: Date, style: DateStyle = 'medium') =>
new Intl.DateTimeFormat(locale, dateStyleOptions[style]).format(date);
const formatRelativeTime = (date: Date) => {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const now = Date.now();
const diffMs = date.getTime() - now;
const diffSec = Math.round(diffMs / 1000);
const diffMin = Math.round(diffSec / 60);
const diffHour = Math.round(diffMin / 60);
const diffDay = Math.round(diffHour / 24);
const diffMonth = Math.round(diffDay / 30);
const diffYear = Math.round(diffDay / 365);
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
if (Math.abs(diffDay) < 30) return rtf.format(diffDay, 'day');
if (Math.abs(diffMonth) < 12) return rtf.format(diffMonth, 'month');
return rtf.format(diffYear, 'year');
};
const formatList = (items: string[]) =>
new Intl.ListFormat(locale, { style: 'long', type: 'conjunction' }).format(items);
return {
number: formatNumber,
currency: formatCurrency,
percent: formatPercent,
date: formatDate,
relativeTime: formatRelativeTime,
list: formatList,
};
}, [locale]);
}
// קומפוננטת דוגמה
function FormattersDemo() {
const { i18n } = useTranslation();
const fmt = useFormatters();
const now = new Date();
const pastDate = new Date(now.getTime() - 3 * 60 * 60 * 1000); // לפני 3 שעות
return (
<div>
<h2>דוגמאות עיצוב ({i18n.language})</h2>
<h3>מספרים</h3>
<p>{fmt.number(1234567.89)}</p>
<h3>מטבע</h3>
<p>{fmt.currency(199.90)}</p>
<p>{fmt.currency(199.90, 'USD')}</p>
<h3>אחוזים</h3>
<p>{fmt.percent(0.157)}</p>
<h3>תאריכים</h3>
<p>{fmt.date(now, 'short')}</p>
<p>{fmt.date(now, 'long')}</p>
<p>{fmt.date(now, 'full')}</p>
<h3>זמן יחסי</h3>
<p>{fmt.relativeTime(pastDate)}</p>
<h3>רשימה</h3>
<p>{fmt.list(['ריאקט', 'טייפסקריפט', 'Next.js'])}</p>
</div>
);
}
export { useFormatters, FormattersDemo };
פתרון תרגיל 4 - טופס רב-לשוני¶
// public/locales/he/contact.json
{
"title": "צור קשר",
"fields": {
"name": "שם מלא",
"namePlaceholder": "הכנס את שמך",
"email": "אימייל",
"emailPlaceholder": "example@email.com",
"subject": "נושא",
"subjectPlaceholder": "במה נוכל לעזור?",
"message": "הודעה",
"messagePlaceholder": "כתוב את הודעתך כאן..."
},
"validation": {
"required": "שדה חובה",
"minLength": "השדה חייב להכיל לפחות {{min}} תווים",
"maxLength": "השדה לא יכול להכיל יותר מ-{{max}} תווים",
"invalidEmail": "כתובת אימייל לא תקינה"
},
"submit": "שלח הודעה",
"success": "ההודעה נשלחה בהצלחה! ניצור איתך קשר בהקדם."
}
// public/locales/en/contact.json
{
"title": "Contact Us",
"fields": {
"name": "Full Name",
"namePlaceholder": "Enter your name",
"email": "Email",
"emailPlaceholder": "example@email.com",
"subject": "Subject",
"subjectPlaceholder": "How can we help?",
"message": "Message",
"messagePlaceholder": "Write your message here..."
},
"validation": {
"required": "This field is required",
"minLength": "Must be at least {{min}} characters",
"maxLength": "Must not exceed {{max}} characters",
"invalidEmail": "Invalid email address"
},
"submit": "Send Message",
"success": "Message sent successfully! We'll get back to you soon."
}
'use client';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
function ContactForm() {
const { t } = useTranslation('contact');
const [errors, setErrors] = useState<Record<string, string>>({});
const [success, setSuccess] = useState(false);
// סכמת אימות עם תרגומים
function createSchema() {
return z.object({
name: z
.string()
.min(1, t('validation.required'))
.min(2, t('validation.minLength', { min: 2 }))
.max(50, t('validation.maxLength', { max: 50 })),
email: z
.string()
.min(1, t('validation.required'))
.email(t('validation.invalidEmail')),
subject: z
.string()
.min(1, t('validation.required'))
.max(100, t('validation.maxLength', { max: 100 })),
message: z
.string()
.min(1, t('validation.required'))
.min(10, t('validation.minLength', { min: 10 }))
.max(1000, t('validation.maxLength', { max: 1000 })),
});
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData);
const schema = createSchema();
const result = schema.safeParse(data);
if (!result.success) {
const fieldErrors: Record<string, string> = {};
for (const error of result.error.errors) {
const field = error.path[0] as string;
fieldErrors[field] = error.message;
}
setErrors(fieldErrors);
setSuccess(false);
return;
}
setErrors({});
setSuccess(true);
}
if (success) {
return (
<div role="alert" className="success-message">
<p>{t('success')}</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="contact-form" noValidate>
<h2>{t('title')}</h2>
<div className="form-field">
<label htmlFor="name">{t('fields.name')}</label>
<input
id="name"
name="name"
type="text"
placeholder={t('fields.namePlaceholder')}
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && <p id="name-error" className="error">{errors.name}</p>}
</div>
<div className="form-field">
<label htmlFor="email">{t('fields.email')}</label>
<input
id="email"
name="email"
type="email"
placeholder={t('fields.emailPlaceholder')}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <p id="email-error" className="error">{errors.email}</p>}
</div>
<div className="form-field">
<label htmlFor="subject">{t('fields.subject')}</label>
<input
id="subject"
name="subject"
type="text"
placeholder={t('fields.subjectPlaceholder')}
aria-invalid={!!errors.subject}
aria-describedby={errors.subject ? 'subject-error' : undefined}
/>
{errors.subject && <p id="subject-error" className="error">{errors.subject}</p>}
</div>
<div className="form-field">
<label htmlFor="message">{t('fields.message')}</label>
<textarea
id="message"
name="message"
rows={5}
placeholder={t('fields.messagePlaceholder')}
aria-invalid={!!errors.message}
aria-describedby={errors.message ? 'message-error' : undefined}
/>
{errors.message && <p id="message-error" className="error">{errors.message}</p>}
</div>
<button type="submit">{t('submit')}</button>
</form>
);
}
export default ContactForm;
.contact-form {
max-inline-size: 500px;
margin: 0 auto;
}
.form-field {
margin-block-end: 16px;
}
.form-field label {
display: block;
margin-block-end: 4px;
font-weight: bold;
text-align: start;
}
.form-field input,
.form-field textarea {
inline-size: 100%;
padding-inline: 12px;
padding-block: 8px;
border: 1px solid #ccc;
border-radius: 4px;
text-align: start;
}
.form-field .error {
color: #c31432;
margin-block-start: 4px;
text-align: start;
}
פתרון תרגיל 5 - SEO רב-לשוני ב-Next.js¶
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const locales = ['he', 'en', 'ar'];
const defaultLocale = 'he';
function getPreferredLocale(request: NextRequest): string {
const cookieLocale = request.cookies.get('locale')?.value;
if (cookieLocale && locales.includes(cookieLocale)) return cookieLocale;
const acceptLang = request.headers.get('accept-language') || '';
for (const locale of locales) {
if (acceptLang.toLowerCase().includes(locale)) return locale;
}
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// דילוג על קבצים סטטיים
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
pathname.includes('.') // קבצים סטטיים
) {
return;
}
// בדיקה אם יש locale ב-URL
const hasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!hasLocale) {
const locale = getPreferredLocale(request);
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)'],
};
// app/[locale]/layout.tsx
import { type Metadata } from 'next';
const localeNames: Record<string, string> = {
he: 'he-IL',
en: 'en-US',
ar: 'ar-SA',
};
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = params;
const baseUrl = 'https://myapp.com';
const titles: Record<string, string> = {
he: 'האפליקציה שלי - דף הבית',
en: 'My Application - Home',
ar: 'تطبيقي - الصفحة الرئيسية',
};
const descriptions: Record<string, string> = {
he: 'אפליקציה מקצועית עם תמיכה רב-לשונית',
en: 'A professional application with multi-language support',
ar: 'تطبيق احترافي مع دعم متعدد اللغات',
};
return {
title: titles[locale],
description: descriptions[locale],
alternates: {
canonical: `${baseUrl}/${locale}`,
languages: Object.fromEntries(
Object.entries(localeNames).map(([code, hreflang]) => [
hreflang,
`${baseUrl}/${code}`,
])
),
},
};
}
export default function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const dir = ['he', 'ar'].includes(params.locale) ? 'rtl' : 'ltr';
return (
<html lang={params.locale} dir={dir}>
<body>{children}</body>
</html>
);
}
export function generateStaticParams() {
return [{ locale: 'he' }, { locale: 'en' }, { locale: 'ar' }];
}
// 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(),
changeFrequency: 'weekly' as const,
priority: page === '' ? 1.0 : 0.8,
alternates: {
languages: Object.fromEntries(
locales.map((l) => [l, `${baseUrl}/${l}${page}`])
),
},
}))
);
}
פתרון תרגיל 6 - דף מוצר רב-לשוני מלא¶
// public/locales/he/product.json
{
"inStock": "במלאי",
"outOfStock": "אזל מהמלאי",
"stockCount_one": "פריט אחד במלאי",
"stockCount_other": "{{count}} פריטים במלאי",
"addToCart": "הוסף לעגלה",
"buyNow": "קנה עכשיו",
"reviews": "ביקורות",
"reviewCount_one": "ביקורת אחת",
"reviewCount_other": "{{count}} ביקורות",
"specs": "מפרט טכני",
"lastUpdated": "עודכן לאחרונה: {{date}}",
"description": "תיאור"
}
// public/locales/en/product.json
{
"inStock": "In Stock",
"outOfStock": "Out of Stock",
"stockCount_one": "{{count}} item in stock",
"stockCount_other": "{{count}} items in stock",
"addToCart": "Add to Cart",
"buyNow": "Buy Now",
"reviews": "Reviews",
"reviewCount_one": "{{count}} review",
"reviewCount_other": "{{count}} reviews",
"specs": "Technical Specifications",
"lastUpdated": "Last updated: {{date}}",
"description": "Description"
}
'use client';
import { useTranslation } from 'react-i18next';
import { useFormatters } from './useFormatters';
interface Review {
id: string;
author: string;
rating: number;
text: string;
date: Date;
}
interface Product {
id: string;
name: Record<string, string>;
description: Record<string, string>;
price: number;
currency: Record<string, string>;
stock: number;
image: string;
specs: Record<string, string>;
reviews: Review[];
lastUpdated: Date;
}
const currencyMap: Record<string, string> = {
he: 'ILS',
en: 'USD',
ar: 'SAR',
};
function ProductPage({ product }: { product: Product }) {
const { t, i18n } = useTranslation('product');
const fmt = useFormatters();
const locale = i18n.language;
const currency = currencyMap[locale] || 'ILS';
return (
<div className="product-page">
<div className="product-layout">
{/* תמונה */}
<div className="product-image">
<img src={product.image} alt={product.name[locale]} />
</div>
{/* פרטים */}
<div className="product-details">
<h1>{product.name[locale]}</h1>
{/* מחיר */}
<p className="price">{fmt.currency(product.price, currency)}</p>
{/* מלאי */}
<p className={product.stock > 0 ? 'in-stock' : 'out-of-stock'}>
{product.stock > 0
? t('stockCount', { count: product.stock })
: t('outOfStock')}
</p>
{/* כפתורי פעולה */}
<div className="actions">
<button disabled={product.stock === 0}>
{t('addToCart')}
</button>
<button disabled={product.stock === 0}>
{t('buyNow')}
</button>
</div>
{/* עדכון אחרון */}
<p className="last-updated">
{t('lastUpdated', {
date: fmt.date(product.lastUpdated, 'long'),
})}
</p>
</div>
</div>
{/* תיאור */}
<section>
<h2>{t('description')}</h2>
<p>{product.description[locale]}</p>
</section>
{/* מפרט */}
<section>
<h2>{t('specs')}</h2>
<table>
<tbody>
{Object.entries(product.specs).map(([key, value]) => (
<tr key={key}>
<th>{key}</th>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</section>
{/* ביקורות */}
<section>
<h2>
{t('reviews')} ({t('reviewCount', { count: product.reviews.length })})
</h2>
{product.reviews.map((review) => (
<div key={review.id} className="review">
<div className="review-header">
<strong>{review.author}</strong>
<span>{'*'.repeat(review.rating)}{'*'.repeat(5 - review.rating)}</span>
<span>{fmt.relativeTime(review.date)}</span>
</div>
<p>{review.text}</p>
</div>
))}
</section>
</div>
);
}
export default ProductPage;
.product-page {
max-inline-size: 1000px;
margin: 0 auto;
padding-inline: 16px;
}
.product-layout {
display: flex;
gap: 32px;
margin-block-end: 32px;
}
.product-image {
flex: 1;
}
.product-image img {
inline-size: 100%;
border-radius: 8px;
}
.product-details {
flex: 1;
}
.price {
font-size: 2rem;
font-weight: bold;
margin-block: 8px;
}
.in-stock {
color: #0d7c3e;
}
.out-of-stock {
color: #c31432;
}
.actions {
display: flex;
gap: 12px;
margin-block: 16px;
}
.actions button {
padding-inline: 24px;
padding-block: 12px;
border-radius: 8px;
cursor: pointer;
}
.review {
padding-block: 16px;
border-block-end: 1px solid #eee;
}
.review-header {
display: flex;
gap: 12px;
align-items: center;
margin-block-end: 8px;
}
תשובות לשאלות¶
1. ההבדל בין i18n ל-l10n:
i18n (internationalization) הוא תהליך הכנת הקוד לתמיכה בשפות - יצירת תשתית שמאפשרת תרגום, עיצוב מספרים ותאריכים, ותמיכה ב-RTL. דוגמה: שימוש ב-t('greeting') במקום טקסט קבוע. l10n (localization) הוא התרגום וההתאמה בפועל לשפה ותרבות ספציפית. דוגמה: כתיבת קובץ he.json עם "greeting": "שלום" ו-en.json עם "greeting": "Hello".
2. CSS Logical Properties:
CSS Logical Properties כמו margin-inline-start מתאימים את עצמם אוטומטית לכיוון הדף. כשמשתמשים ב-margin-left, הערך נשאר תמיד בצד שמאל גם ב-RTL, מה שיוצר layout שבור. למשל, אייקון שצריך להיות לפני הטקסט ייראה אחריו ב-RTL. עם Logical Properties, inline-start הופך אוטומטית לימין ב-RTL ולשמאל ב-LTR.
3. Namespaces ב-i18next:
namespace הוא קבוצה לוגית של תרגומים. במקום קובץ אחד ענק, מחלקים את התרגומים לקבצים לפי תחום: common.json לדברים כלליים, auth.json לאימות, dashboard.json ללוח בקרה. היתרונות: טעינה עצלנית (טוענים רק את מה שצריך), קל יותר לתחזק ולתרגם, מונע התנגשויות בשמות מפתחות.
4. Intl API:
ה-Intl API הוא חלק מובנה ב-JavaScript שמספק עיצוב מותאם תרבותית. הוא יודע את הכללים של כל שפה - סדר תאריך (יום/חודש/שנה מול חודש/יום/שנה), מפריד אלפים (פסיק מול נקודה), סמל מטבע ומיקומו, וכללי ריבוי. היתרון: לא צריך לכתוב לוגיקת עיצוב ידנית, הכללים תמיד מעודכנים, והתוצאה מדויקת עבור כל locale.
5. תגית hreflang:
תגית hreflang מודיעה למנועי חיפוש שקיימות גרסאות של הדף בשפות אחרות. היא עוזרת ל-Google להציג את הגרסה הנכונה למשתמשים לפי שפתם ומיקומם. ללא hreflang, מנוע החיפוש עלול להציג את הגרסה הלא נכונה, או אפילו לראות את הגרסאות השונות כתוכן כפול (duplicate content) ולפגוע בדירוג.