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>
  );
}

Library Comparison

Library Bundle Size Stars
i18next ~10KB ⭐ Most Popular
react-intl ~14KB ⭐ Facebook/Format.js
next-intl ~6KB ⭐ Next.js optimized
LinguiJS ~3KB ⭐ Smallest

Message File Formats

  • JSON: Most common, easy to parse
  • YAML: More readable, comments
  • PO (Gettext): Translation industry standard
  • XLIFF: XML-based, professional tools
  • ICU Format: Advanced interpolation
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>

Lazy Loading Benefits

  • ✅ Smaller initial bundle (50-70% reduction)
  • ✅ Faster page load times
  • ✅ Load only needed languages
  • ✅ Update translations without rebuild
  • ✅ Better cache utilization

Loading Strategies

Strategy Load Time
Bundle all locales Initial
Dynamic import On language change
Prefetch On hover/idle
CDN fetch Runtime
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>
  );
}

Logical Properties Map

Physical Logical
margin-left margin-inline-start
margin-right margin-inline-end
margin-top margin-block-start
margin-bottom margin-block-end
left inset-inline-start
right inset-inline-end

RTL Languages

  • Arabic (ar): العربية
  • Hebrew (he): עברית
  • Persian (fa): فارسی
  • Urdu (ur): اردو
  • Yiddish (yi): ייִדיש
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>;
}

Library Comparison

Library Size Tree-shake
date-fns 13KB ✅ Yes
Day.js 2KB ⚠️ Plugins
Luxon 20KB ❌ No
Moment.js 70KB ❌ No
Intl API 0KB N/A

Common Date Formats

  • US: MM/DD/YYYY (12/18/2025)
  • Europe: DD/MM/YYYY (18/12/2025)
  • ISO: YYYY-MM-DD (2025-12-18)
  • Japan: YYYY年MM月DD日
  • Arabic: DD/MM/YYYY (٢٠٢٥/١٢/١٨)
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} />
    )
  }} 
/>

Plural Categories

Language Plural Forms
English 2 (one, other)
French 2 (one, other)
Russian 3 (one, few, many)
Arabic 6 (zero, one, two, few, many, other)
Polish 3 (one, few, many)

ICU Format Types

  • plural: Count-based (0, 1, many)
  • select: Enum values (male, female)
  • selectordinal: Ordinals (1st, 2nd)
  • number: Number formatting
  • date: Date formatting
  • time: Time formatting
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>
  );
}

Detection Priority

  1. URL parameter (?lang=es)
  2. Subdomain (es.example.com)
  3. Path prefix (/es/page)
  4. Cookie (persistent)
  5. localStorage (persistent)
  6. Accept-Language header
  7. navigator.language
  8. Fallback (en)

Best Practices

  • ✅ Respect explicit user choice
  • ✅ Store preference persistently
  • ✅ Show language switcher prominently
  • ✅ Use path prefixes for SEO
  • ✅ Fallback to English or default
  • ❌ Don't auto-redirect without confirmation
  • ❌ Don't rely solely on IP location

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.