Back to Blog
i18nLocalizationReactNext.js

Internationalization and Localization in Web Apps

Build multilingual web applications. Learn i18n patterns, translation management, and locale handling.

B
Bootspring Team
Engineering
February 27, 2026
3 min read

Build applications that work seamlessly across languages and regions.

Setting Up i18n in Next.js#

1// next.config.js 2module.exports = { 3 i18n: { 4 locales: ['en', 'es', 'fr', 'de'], 5 defaultLocale: 'en', 6 }, 7};

Using next-intl#

1// messages/en.json 2{ 3 "home": { 4 "title": "Welcome to our app", 5 "description": "Build something amazing" 6 }, 7 "common": { 8 "submit": "Submit", 9 "cancel": "Cancel" 10 } 11} 12 13// messages/es.json 14{ 15 "home": { 16 "title": "Bienvenido a nuestra app", 17 "description": "Construye algo increíble" 18 }, 19 "common": { 20 "submit": "Enviar", 21 "cancel": "Cancelar" 22 } 23}
1// app/[locale]/page.tsx 2import { useTranslations } from 'next-intl'; 3 4export default function Home() { 5 const t = useTranslations('home'); 6 7 return ( 8 <div> 9 <h1>{t('title')}</h1> 10 <p>{t('description')}</p> 11 </div> 12 ); 13}

Interpolation and Plurals#

1// messages/en.json 2{ 3 "greeting": "Hello, {name}!", 4 "items": { 5 "zero": "No items", 6 "one": "One item", 7 "other": "{count} items" 8 } 9} 10 11// Usage 12t('greeting', { name: 'John' }); // "Hello, John!" 13t('items', { count: 5 }); // "5 items"

Formatting Numbers and Dates#

1import { useFormatter } from 'next-intl'; 2 3function PriceDisplay({ amount }: { amount: number }) { 4 const format = useFormatter(); 5 6 return ( 7 <span> 8 {format.number(amount, { style: 'currency', currency: 'USD' })} 9 </span> 10 ); 11} 12 13function DateDisplay({ date }: { date: Date }) { 14 const format = useFormatter(); 15 16 return ( 17 <span> 18 {format.dateTime(date, { dateStyle: 'full' })} 19 </span> 20 ); 21}

Language Switcher#

1'use client'; 2 3import { useRouter, usePathname } from 'next/navigation'; 4import { useLocale } from 'next-intl'; 5 6const locales = [ 7 { code: 'en', name: 'English' }, 8 { code: 'es', name: 'Español' }, 9 { code: 'fr', name: 'Français' }, 10]; 11 12export function LanguageSwitcher() { 13 const router = useRouter(); 14 const pathname = usePathname(); 15 const currentLocale = useLocale(); 16 17 const switchLocale = (newLocale: string) => { 18 const newPath = pathname.replace(`/${currentLocale}`, `/${newLocale}`); 19 router.push(newPath); 20 }; 21 22 return ( 23 <select 24 value={currentLocale} 25 onChange={(e) => switchLocale(e.target.value)} 26 > 27 {locales.map(locale => ( 28 <option key={locale.code} value={locale.code}> 29 {locale.name} 30 </option> 31 ))} 32 </select> 33 ); 34}

RTL Support#

1// Handle right-to-left languages 2const rtlLocales = ['ar', 'he', 'fa']; 3 4function Layout({ locale, children }) { 5 const dir = rtlLocales.includes(locale) ? 'rtl' : 'ltr'; 6 7 return ( 8 <html lang={locale} dir={dir}> 9 <body>{children}</body> 10 </html> 11 ); 12}
1/* RTL-aware styles */ 2.sidebar { 3 margin-inline-start: 1rem; /* Works for both LTR and RTL */ 4} 5 6[dir="rtl"] .icon-arrow { 7 transform: scaleX(-1); 8}

Server-Side Translation#

1import { getTranslations } from 'next-intl/server'; 2 3export async function generateMetadata({ params: { locale } }) { 4 const t = await getTranslations({ locale, namespace: 'meta' }); 5 6 return { 7 title: t('title'), 8 description: t('description'), 9 }; 10}

Best Practices#

  1. Extract all strings: Never hardcode text in components
  2. Use namespaces: Organize translations by feature/page
  3. Handle plurals: Different languages have different plural rules
  4. Test RTL: Verify layout works for right-to-left languages
  5. Lazy load: Only load translations for current locale

Keep translations close to code, use context for ambiguous terms, and always test with native speakers.

Share this article

Help spread the word about Bootspring