Internationalization Modern Implementation
1. React-intl i18next Localization
| Library | Features | Complexity | Use Case |
|---|---|---|---|
| react-intl (Format.js) | ICU message format, React components, hooks | Medium | React apps, message formatting, pluralization, date/number formatting |
| i18next + react-i18next | Plugin ecosystem, namespace support, lazy loading | Low-Medium | Most popular, flexible, works with any framework |
| next-intl | Next.js optimized, server components support | Low | Next.js apps, App Router, Server Components |
| LinguiJS | Compile-time optimization, extraction tool | Medium | Performance-critical apps, small bundle size |
| Polyglot.js | Minimalist, lightweight | Low | Simple projects, small bundle, basic interpolation |
Example: Internationalization with react-intl and i18next
// react-intl Setup
import { IntlProvider, FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
const messages = {
en: {
'app.greeting': 'Hello, {name}!',
'app.items': '{count, plural, =0 {No items} one {# item} other {# items}}',
'app.welcome': 'Welcome to our app'
},
es: {
'app.greeting': '¡Hola, {name}!',
'app.items': '{count, plural, =0 {Sin artículos} one {# artículo} other {# artículos}}',
'app.welcome': 'Bienvenido a nuestra aplicación'
}
};
function App() {
const [locale, setLocale] = useState('en');
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<HomePage />
</IntlProvider>
);
}
// Using FormattedMessage
function HomePage() {
return (
<div>
<FormattedMessage
id="app.greeting"
values={{ name: 'John' }}
/>
<FormattedMessage
id="app.items"
values={{ count: 5 }}
/>
<FormattedNumber value={1234.56} style="currency" currency="USD" />
<FormattedDate value={new Date()} year="numeric" month="long" day="2-digit" />
</div>
);
}
// Using useIntl hook
import { useIntl } from 'react-intl';
function MyComponent() {
const intl = useIntl();
const greeting = intl.formatMessage(
{ id: 'app.greeting' },
{ name: 'Jane' }
);
const placeholder = intl.formatMessage({ id: 'input.placeholder' });
return (
<div>
<h1>{greeting}</h1>
<input placeholder={placeholder} />
</div>
);
}
// i18next Setup
import i18n from 'i18next';
import { initReactI18next, useTranslation } from 'react-i18next';
i18n
.use(initReactI18next)
.init({
resources: {
en: {
translation: {
welcome: 'Welcome',
greeting: 'Hello, {{name}}!',
items: '{{count}} items',
items_plural: '{{count}} items'
}
},
es: {
translation: {
welcome: 'Bienvenido',
greeting: '¡Hola, {{name}}!',
items: '{{count}} artículo',
items_plural: '{{count}} artículos'
}
}
},
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false
}
});
// Using useTranslation hook
function MyComponent() {
const { t, i18n } = useTranslation();
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('greeting', { name: 'John' })}</p>
<p>{t('items', { count: 5 })}</p>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('es')}>Español</button>
</div>
);
}
// i18next with namespaces
i18n.init({
resources: {
en: {
common: { save: 'Save', cancel: 'Cancel' },
dashboard: { title: 'Dashboard', stats: 'Statistics' }
}
},
ns: ['common', 'dashboard'],
defaultNS: 'common'
});
function Dashboard() {
const { t } = useTranslation(['dashboard', 'common']);
return (
<div>
<h1>{t('dashboard:title')}</h1>
<button>{t('common:save')}</button>
</div>
);
}
// next-intl (Next.js)
// messages/en.json
{
"Index": {
"title": "Hello world!",
"description": "This is a description"
}
}
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
export default async function LocaleLayout({ children, params: { locale } }) {
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
export default function IndexPage() {
const t = useTranslations('Index');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
Best Choice: Use i18next for most projects (flexibility +
ecosystem). Use next-intl for Next.js App Router with Server Components.
2. Dynamic Locale Loading Lazy i18n
| Strategy | Implementation | Benefit | Use Case |
|---|---|---|---|
| Code Splitting per Locale | Dynamic import for translation files | Reduce initial bundle, load only active locale | Large apps with many languages |
| Namespace-based Lazy Loading | Load translation namespaces on-demand | Progressive loading, better performance | Apps with distinct feature sections |
| CDN-hosted Translations | Fetch translations from CDN | Update translations without deployment | Frequent content updates, multi-tenant apps |
| Backend Translation API | Fetch from translation service (Lokalise, Crowdin) | Real-time updates, centralized management | Large teams, continuous localization |
Example: Dynamic Locale Loading
// i18next with lazy loading
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
i18n
.use(Backend)
.use(initReactI18next)
.init({
lng: 'en',
fallbackLng: 'en',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
},
ns: ['common', 'dashboard'],
defaultNS: 'common'
});
// Translations loaded dynamically
// /locales/en/common.json
// /locales/en/dashboard.json
// /locales/es/common.json
// React component with dynamic import
import { Suspense } from 'react';
function App() {
const [locale, setLocale] = useState('en');
const changeLanguage = async (lng) => {
await i18n.changeLanguage(lng);
setLocale(lng);
};
return (
<Suspense fallback={<div>Loading translations...</div>}>
<MyComponent />
</Suspense>
);
}
// Next.js dynamic locale import
// app/[locale]/layout.tsx
export async function generateStaticParams() {
return [{ locale: 'en' }, { locale: 'es' }, { locale: 'fr' }];
}
export default async function LocaleLayout({ children, params: { locale } }) {
// Dynamic import of messages
const messages = (await import(`../../messages/${locale}.json`)).default;
return (
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
);
}
// Custom hook for dynamic locale loading
function useDynamicLocale(locale) {
const [messages, setMessages] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
import(`../locales/${locale}.json`)
.then((module) => {
setMessages(module.default);
setLoading(false);
})
.catch((error) => {
console.error('Failed to load locale:', error);
setLoading(false);
});
}, [locale]);
return { messages, loading };
}
// Usage
function App() {
const [locale, setLocale] = useState('en');
const { messages, loading } = useDynamicLocale(locale);
if (loading) return <div>Loading...</div>;
return (
<IntlProvider locale={locale} messages={messages}>
<HomePage />
</IntlProvider>
);
}
// Webpack chunk names for locale files
const loadLocale = (locale) => {
return import(
/* webpackChunkName: "locale-[request]" */
`../locales/${locale}.json`
);
};
// Vite dynamic import
const loadLocaleVite = async (locale) => {
const modules = import.meta.glob('../locales/*.json');
const module = await modules[`../locales/${locale}.json`]();
return module.default;
};
// CDN-hosted translations
async function loadTranslationsFromCDN(locale) {
const response = await fetch(`https://cdn.example.com/i18n/${locale}.json`);
return response.json();
}
i18n.use(Backend).init({
backend: {
loadPath: 'https://cdn.example.com/i18n/{{lng}}/{{ns}}.json',
crossDomain: true
}
});
// Lokalise API integration
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
i18n
.use(Backend)
.use(initReactI18next)
.init({
backend: {
loadPath: 'https://api.lokalise.com/api2/projects/PROJECT_ID/files/download',
customHeaders: {
'X-Api-Token': 'YOUR_API_TOKEN'
}
}
});
// Preload next likely locale
function preloadLocale(locale) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = `/locales/${locale}.json`;
document.head.appendChild(link);
}
// Preload on hover
<button
onMouseEnter={() => preloadLocale('es')}
onClick={() => changeLanguage('es')}
>
Español
</button>
Recommendation: Use dynamic imports for locale files. Reduces
initial bundle by 60-80% for apps with 5+ languages.
3. RTL LTR Layout CSS Logical Properties
| Physical Property | Logical Property | RTL Behavior | Browser Support |
|---|---|---|---|
| margin-left | margin-inline-start |
Becomes right margin in RTL | 95%+ modern browsers |
| margin-right | margin-inline-end |
Becomes left margin in RTL | 95%+ |
| padding-left | padding-inline-start |
Automatic RTL flip | 95%+ |
| border-left | border-inline-start |
Automatic RTL flip | 95%+ |
| text-align: left | text-align: start |
Right-aligned in RTL | Universal |
| float: left | float: inline-start |
Float right in RTL | Modern browsers |
Example: RTL Support with Logical Properties
// Set document direction
<html lang="ar" dir="rtl">
</html>
// React component with direction
function App() {
const [locale, setLocale] = useState('en');
const direction = locale === 'ar' || locale === 'he' ? 'rtl' : 'ltr';
return (
<div dir={direction}>
<Content />
</div>
);
}
// CSS Logical Properties
.card {
/* ❌ Physical properties - need manual RTL handling */
margin-left: 20px;
padding-right: 10px;
border-left: 2px solid blue;
text-align: left;
}
.card {
/* ✅ Logical properties - automatic RTL support */
margin-inline-start: 20px;
padding-inline-end: 10px;
border-inline-start: 2px solid blue;
text-align: start;
}
// Comprehensive logical properties
.container {
/* Inline (horizontal in LTR) */
margin-inline: 20px; /* margin-left + margin-right */
margin-inline-start: 20px; /* margin-left in LTR, margin-right in RTL */
margin-inline-end: 20px; /* margin-right in LTR, margin-left in RTL */
padding-inline-start: 10px;
padding-inline-end: 10px;
/* Block (vertical) */
margin-block: 20px; /* margin-top + margin-bottom */
margin-block-start: 20px; /* margin-top */
margin-block-end: 20px; /* margin-bottom */
/* Borders */
border-inline-start: 1px solid #ccc;
border-inline-end: 1px solid #ccc;
border-block-start: 1px solid #ccc;
border-block-end: 1px solid #ccc;
/* Border radius */
border-start-start-radius: 8px; /* top-left in LTR, top-right in RTL */
border-start-end-radius: 8px; /* top-right in LTR, top-left in RTL */
border-end-start-radius: 8px; /* bottom-left in LTR, bottom-right in RTL */
border-end-end-radius: 8px; /* bottom-right in LTR, bottom-left in RTL */
/* Inset (positioning) */
inset-inline-start: 0; /* left: 0 in LTR, right: 0 in RTL */
inset-inline-end: 0; /* right: 0 in LTR, left: 0 in RTL */
}
// Flexbox with logical properties
.flex-container {
display: flex;
flex-direction: row; /* Automatically reverses in RTL */
justify-content: flex-start; /* Start of inline axis */
gap: 1rem;
}
// RTL-specific styles (when needed)
[dir="rtl"] .special-case {
/* Override for specific RTL behavior */
transform: scaleX(-1); /* Flip horizontally */
}
// Icons that shouldn't flip
.icon-no-flip {
/* Prevent automatic flipping for icons like arrows */
transform: scaleX(var(--icon-flip, 1));
}
[dir="rtl"] .icon-no-flip {
--icon-flip: -1;
}
// Styled-components with RTL
import styled from 'styled-components';
const Card = styled.div`
margin-inline-start: ${props => props.theme.spacing.md};
padding-inline: ${props => props.theme.spacing.sm};
text-align: start;
`;
// Tailwind CSS RTL plugin
// tailwind.config.js
module.exports = {
plugins: [
require('tailwindcss-rtl')
]
};
// Usage
<div className="ms-4 pe-2">
{/* ms-4: margin-inline-start: 1rem */}
{/* pe-2: padding-inline-end: 0.5rem */}
</div>
// PostCSS RTL
// postcss.config.js
module.exports = {
plugins: [
require('postcss-rtlcss')
]
};
// Converts:
.element { margin-left: 10px; }
// To:
[dir="ltr"] .element { margin-left: 10px; }
[dir="rtl"] .element { margin-right: 10px; }
// Material-UI RTL
import { createTheme, ThemeProvider } from '@mui/material/styles';
import rtlPlugin from 'stylis-plugin-rtl';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
const cacheRtl = createCache({
key: 'muirtl',
stylisPlugins: [rtlPlugin]
});
const theme = createTheme({
direction: 'rtl'
});
function RTLApp() {
return (
<CacheProvider value={cacheRtl}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</CacheProvider>
);
}
Best Practice: Always use CSS logical properties in new
projects. Automatic RTL support with zero JavaScript, better semantic meaning.
4. Date-fns Moment.js Timezone Handling
| Library | Bundle Size | Features | Status |
|---|---|---|---|
| date-fns | ~13KB (tree-shakeable) | Modular, immutable, TypeScript, i18n support | RECOMMENDED |
| Moment.js | ~70KB (monolithic) | Comprehensive, mature, large ecosystem | LEGACY |
| Day.js | ~2KB | Moment-compatible API, lightweight | MODERN |
| Luxon | ~20KB | Immutable, timezone-aware, Moment successor | MODERN |
| Intl.DateTimeFormat | 0KB (native) | Built-in browser API, locale-aware | NATIVE |
Example: Date/Time Localization
// date-fns with locales
import { format, formatDistance, formatRelative } from 'date-fns';
import { es, fr, ar } from 'date-fns/locale';
const date = new Date();
// Format with locale
format(date, 'PPPPpppp', { locale: es });
// "miércoles, 18 de diciembre de 2025 a las 14:30:00"
formatDistance(date, new Date(2025, 11, 1), { locale: fr });
// "17 jours"
formatRelative(date, new Date(), { locale: ar });
// "اليوم في 2:30 م"
// Custom format
format(date, "EEEE, MMMM do yyyy 'at' h:mm a", { locale: es });
// React component with date-fns
import { useIntl } from 'react-intl';
import { format } from 'date-fns';
import { enUS, es, fr, ar } from 'date-fns/locale';
const locales = { en: enUS, es, fr, ar };
function DateDisplay({ date }) {
const intl = useIntl();
const locale = locales[intl.locale];
return (
<time dateTime={date.toISOString()}>
{format(date, 'PPP', { locale })}
</time>
);
}
// Intl.DateTimeFormat (Native)
const date = new Date();
// Format with locale
new Intl.DateTimeFormat('en-US').format(date);
// "12/18/2025"
new Intl.DateTimeFormat('es-ES').format(date);
// "18/12/2025"
new Intl.DateTimeFormat('ar-EG').format(date);
// "١٨/١٢/٢٠٢٥"
// Long format
new Intl.DateTimeFormat('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
// "Wednesday, December 18, 2025"
// Time formatting
new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true
}).format(date);
// "2:30 PM"
new Intl.DateTimeFormat('es-ES', {
hour: 'numeric',
minute: 'numeric',
hour12: false
}).format(date);
// "14:30"
// Relative time formatting (Intl.RelativeTimeFormat)
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(0, 'day'); // "today"
rtf.format(1, 'day'); // "tomorrow"
rtf.format(-3, 'month'); // "3 months ago"
const rtfEs = new Intl.RelativeTimeFormat('es', { numeric: 'auto' });
rtfEs.format(-1, 'day'); // "ayer"
rtfEs.format(1, 'day'); // "mañana"
// Timezone handling with date-fns-tz
import { formatInTimeZone, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
const date = new Date('2025-12-18T14:30:00Z');
// Format in specific timezone
formatInTimeZone(date, 'America/New_York', 'yyyy-MM-dd HH:mm:ss zzz');
// "2025-12-18 09:30:00 EST"
formatInTimeZone(date, 'Europe/London', 'yyyy-MM-dd HH:mm:ss zzz');
// "2025-12-18 14:30:00 GMT"
formatInTimeZone(date, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss zzz');
// "2025-12-18 23:30:00 JST"
// Convert to specific timezone
const nyTime = utcToZonedTime(date, 'America/New_York');
// Convert from timezone to UTC
const utcTime = zonedTimeToUtc(nyTime, 'America/New_York');
// Day.js with timezone
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import 'dayjs/locale/es';
import 'dayjs/locale/fr';
dayjs.extend(utc);
dayjs.extend(timezone);
// Set locale
dayjs.locale('es');
// Format with timezone
dayjs().tz('America/New_York').format('YYYY-MM-DD HH:mm:ss z');
// "2025-12-18 09:30:00 EST"
// Luxon with timezone
import { DateTime } from 'luxon';
const dt = DateTime.now().setZone('America/New_York');
dt.toFormat('yyyy-MM-dd HH:mm:ss ZZZZ');
// "2025-12-18 09:30:00 Eastern Standard Time"
// With locale
dt.setLocale('es').toFormat('DDDD');
// "miércoles, 18 de diciembre de 2025"
// React hook for formatted dates
function useFormattedDate(date, formatStr = 'PPP') {
const intl = useIntl();
const locale = locales[intl.locale];
return useMemo(
() => format(date, formatStr, { locale }),
[date, formatStr, locale]
);
}
// Usage
function EventCard({ event }) {
const formattedDate = useFormattedDate(event.date);
return <div>{formattedDate}</div>;
}
Recommendation: Use date-fns for most projects (tree-shakeable,
small). Use Intl API for simple formatting to avoid dependencies.
5. Pluralization ICU Message Format
| Format | Syntax | Use Case | Example |
|---|---|---|---|
| ICU Plural | {count, plural, ...} |
Handle singular/plural forms | "0 items", "1 item", "5 items" |
| ICU Select | {gender, select, ...} |
Conditional text based on value | "He/She/They went to the store" |
| ICU SelectOrdinal | {count, selectordinal, ...} |
Ordinal numbers (1st, 2nd, 3rd) | "1st place", "2nd place", "3rd place" |
| Nested Format | Combination of plural + select | Complex grammatical rules | Gender + plural combinations |
Example: ICU Message Format and Pluralization
// react-intl ICU Plural
const messages = {
en: {
'items.count': '{count, plural, =0 {No items} one {# item} other {# items}}'
},
es: {
'items.count': '{count, plural, =0 {Sin artículos} one {# artículo} other {# artículos}}'
},
ar: {
// Arabic has 6 plural forms!
'items.count': '{count, plural, =0 {لا توجد عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}}'
}
};
// Usage
<FormattedMessage id="items.count" values={{ count: 0 }} /> // "No items"
<FormattedMessage id="items.count" values={{ count: 1 }} /> // "1 item"
<FormattedMessage id="items.count" values={{ count: 5 }} /> // "5 items"
// ICU Select (gender)
const messages = {
'notification': '{gender, select, male {He} female {She} other {They}} sent you a message'
};
<FormattedMessage id="notification" values={{ gender: 'female' }} />
// "She sent you a message"
// ICU SelectOrdinal
const messages = {
'place': 'You finished in {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place'
};
<FormattedMessage id="place" values={{ place: 1 }} /> // "You finished in 1st place"
<FormattedMessage id="place" values={{ place: 2 }} /> // "You finished in 2nd place"
<FormattedMessage id="place" values={{ place: 23 }} /> // "You finished in 23rd place"
// Complex nested pluralization
const messages = {
'complex': '{gender, select, male {He has} female {She has} other {They have}} {count, plural, one {# item} other {# items}}'
};
<FormattedMessage
id="complex"
values={{ gender: 'female', count: 5 }}
/>
// "She has 5 items"
// i18next pluralization
const resources = {
en: {
translation: {
'item': 'item',
'item_plural': 'items',
'key': '{{count}} item',
'key_plural': '{{count}} items'
}
},
es: {
translation: {
'item': 'artículo',
'item_plural': 'artículos',
'key': '{{count}} artículo',
'key_plural': '{{count}} artículos'
}
}
};
// Usage
t('key', { count: 1 }); // "1 item"
t('key', { count: 5 }); // "5 items"
// Custom plural rules (i18next)
i18n.init({
pluralSeparator: '_',
nsSeparator: ':',
keySeparator: '.',
pluralRules: {
ar: (count) => {
if (count === 0) return 'zero';
if (count === 1) return 'one';
if (count === 2) return 'two';
if (count % 100 >= 3 && count % 100 <= 10) return 'few';
if (count % 100 >= 11 && count % 100 <= 99) return 'many';
return 'other';
}
}
});
// Intl.PluralRules (Native)
const pr = new Intl.PluralRules('en-US');
pr.select(0); // "other"
pr.select(1); // "one"
pr.select(2); // "other"
pr.select(5); // "other"
const prAr = new Intl.PluralRules('ar-EG');
prAr.select(0); // "zero"
prAr.select(1); // "one"
prAr.select(2); // "two"
prAr.select(3); // "few"
prAr.select(11); // "many"
prAr.select(100); // "other"
// Helper function for pluralization
function pluralize(count, singular, plural) {
const pr = new Intl.PluralRules('en-US');
const rule = pr.select(count);
return rule === 'one' ? singular : plural;
}
pluralize(1, 'item', 'items'); // "item"
pluralize(5, 'item', 'items'); // "items"
// Advanced: Custom pluralization with object
const pluralForms = {
en: {
item: {
zero: 'no items',
one: '1 item',
other: '{{count}} items'
}
},
ru: {
item: {
one: '{{count}} предмет', // 1, 21, 31, ...
few: '{{count}} предмета', // 2-4, 22-24, ...
many: '{{count}} предметов', // 0, 5-20, 25-30, ...
other: '{{count}} предмета'
}
}
};
function getPluralForm(locale, key, count) {
const pr = new Intl.PluralRules(locale);
const rule = pr.select(count);
const template = pluralForms[locale][key][rule];
return template.replace('{{count}}', count);
}
// Number formatting with plurals
const messages = {
'users.online': '{count, plural, one {# user is} other {# users are}} online'
};
// Format numbers
<FormattedMessage
id="users.online"
values={{ count: 1234 }}
/>
// "1,234 users are online"
// With number formatting
<FormattedMessage
id="users.online"
values={{
count: (
<FormattedNumber value={1234} />
)
}}
/>
Complex Pluralization: Some languages like Arabic have 6 plural
forms, Polish 3 forms. Always use proper i18n libraries, never hardcode plural logic!
6. Locale Detection Browser Language
| Detection Method | Source | Priority | Reliability |
|---|---|---|---|
| URL Parameter | ?lang=es |
1 - Highest | Explicit user choice, shareable links |
| Subdomain | es.example.com |
2 | SEO-friendly, clear intent |
| Path Prefix | /es/page |
3 | SEO-friendly, clear structure |
| Cookie/localStorage | Stored user preference | 4 | Remembers choice across sessions |
| Accept-Language Header | Browser's language setting | 5 | Automatic, but may not match content preference |
| navigator.language | Browser API | 6 - Lowest | Fallback, system language |
Example: Locale Detection Strategies
// Comprehensive locale detection
function detectLocale() {
// 1. Check URL parameter
const urlParams = new URLSearchParams(window.location.search);
const urlLang = urlParams.get('lang');
if (urlLang) return urlLang;
// 2. Check path prefix
const pathMatch = window.location.pathname.match(/^\/([a-z]{2})\//);
if (pathMatch) return pathMatch[1];
// 3. Check cookie
const cookieLang = document.cookie
.split('; ')
.find(row => row.startsWith('locale='))
?.split('=')[1];
if (cookieLang) return cookieLang;
// 4. Check localStorage
const storedLang = localStorage.getItem('locale');
if (storedLang) return storedLang;
// 5. Check browser language
const browserLang = navigator.language || navigator.userLanguage;
const shortLang = browserLang.split('-')[0]; // "en-US" → "en"
// 6. Fallback
return 'en';
}
// React hook for locale detection
function useLocaleDetection(supportedLocales = ['en', 'es', 'fr']) {
const [locale, setLocale] = useState(() => {
const detected = detectLocale();
return supportedLocales.includes(detected) ? detected : 'en';
});
useEffect(() => {
// Save to localStorage
localStorage.setItem('locale', locale);
// Update document language
document.documentElement.lang = locale;
}, [locale]);
return [locale, setLocale];
}
// Next.js locale detection
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if pathname has locale
const pathnameHasLocale = ['en', 'es', 'fr'].some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
// Detect locale
const locale = detectLocaleFromRequest(request);
// Redirect to locale-prefixed URL
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
function detectLocaleFromRequest(request: NextRequest) {
// 1. Check cookie
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (cookieLocale) return cookieLocale;
// 2. Check Accept-Language header
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const locale = acceptLanguage.split(',')[0].split('-')[0];
if (['en', 'es', 'fr'].includes(locale)) return locale;
}
// 3. Fallback
return 'en';
}
// i18next language detection
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(LanguageDetector)
.init({
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage', 'cookie']
},
fallbackLng: 'en'
});
// GeoIP-based locale detection
async function detectLocaleByIP() {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
const countryToLocale = {
'US': 'en',
'GB': 'en',
'ES': 'es',
'MX': 'es',
'FR': 'fr',
'DE': 'de'
};
return countryToLocale[data.country_code] || 'en';
} catch (error) {
return 'en';
}
}
// Language switcher component
function LanguageSwitcher() {
const { i18n } = useTranslation();
const router = useRouter();
const languages = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Español', flag: '🇪🇸' },
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
{ code: 'ar', name: 'العربية', flag: '🇸🇦' }
];
const changeLanguage = (code) => {
i18n.changeLanguage(code);
// Update URL
const newPath = router.pathname.replace(/^\/[a-z]{2}/, `/${code}`);
router.push(newPath);
// Update cookie
document.cookie = `locale=${code}; path=/; max-age=31536000`;
// Update localStorage
localStorage.setItem('locale', code);
// Update HTML lang
document.documentElement.lang = code;
};
return (
<select value={i18n.language} onChange={(e) => changeLanguage(e.target.value)}>
{languages.map(({ code, name, flag }) => (
<option key={code} value={code}>
{flag} {name}
</option>
))}
</select>
);
}
// Accept-Language header parsing
function parseAcceptLanguage(header) {
return header
.split(',')
.map(lang => {
const [code, qValue] = lang.trim().split(';q=');
return {
code: code.split('-')[0],
quality: qValue ? parseFloat(qValue) : 1.0
};
})
.sort((a, b) => b.quality - a.quality)
.map(lang => lang.code);
}
// Example: "en-US,es;q=0.9,fr;q=0.8"
// Returns: ["en", "es", "fr"]
// Automatic redirect based on locale
useEffect(() => {
const detectedLocale = detectLocale();
const currentLocale = i18n.language;
if (detectedLocale !== currentLocale && !localStorage.getItem('locale-confirmed')) {
// Show banner suggesting language
setShowLocaleBanner(true);
}
}, []);
function LocaleBanner({ suggestedLocale, onAccept, onDismiss }) {
return (
<div className="locale-banner">
Would you like to view this site in {getLanguageName(suggestedLocale)}?
<button onClick={onAccept}>Yes</button>
<button onClick={onDismiss}>No</button>
</div>
);
}
Internationalization Summary
- Libraries: Use i18next (most flexible) or next-intl (Next.js optimized)
- Lazy Loading: Dynamic imports for locale files, reduce bundle by 60-80%
- RTL Support: CSS logical properties for automatic RTL layout
- Dates: date-fns with locales or native Intl.DateTimeFormat
- Pluralization: ICU message format for complex plural rules
- Detection: URL parameter → Cookie → Browser language → Fallback
Testing is Critical: Test with native speakers for each
language. Machine translation isn't enough. Consider hiring professional translators for production apps.