Respuesta rápida
next-intl es la mejor librería de internacionalización (i18n) para Next.js App Router. Instalar con npm install next-intl, create a [locale] folder in your app directory, configure middleware for locale detection, and use getTranslations() in Server Components or useTranslations() in Client Components. next-intl is specifically designed for Next.js 13-15+ with native Server Component support, automatic routing, and a ~2KB bundle size.
What is next-intl?
next-intl is a lightweight internationalization library built specifically for Next.js. Unlike general-purpose React i18n libraries, next-intl was designed from the ground up for the App Router and Server Components.
Why next-intl Over Other Libraries?
| Feature | next-intl | react-i18next | react-intl |
|---|---|---|---|
| Built for Next.js | Yes | No (React general) | No (React general) |
| Server Components | Native | Needs wrapper | Needs wrapper |
| Bundle size | ~2KB | ~8KB | ~12KB |
| App Router support | First-class | Partial | Partial |
| TypeScript | Excellent | Good | Good |
| Routing integration | Built-in | Manual | Manual |
Key benefits:
- Zero client-side JS for server-rendered translations - translations stay on the server
- Automatic locale routing - /en/about, /es/about handled automatically
- ICU message format - proper pluralization, gender, and formatting
- Type-safe - full TypeScript support with autocompletion
When you scale beyond JSON files, IntlPull integrates seamlessly with next-intl for translation management, AI translation, and team collaboration.
Installation & Setup
Step 1: Install next-intl
Terminal1npm install next-intl 2# or 3yarn add next-intl 4# or 5pnpm add next-intl
Step 2: Project Structure
Create this folder structure:
/your-nextjs-app
├── /app
│ └── /[locale] # Dynamic locale segment
│ ├── layout.tsx # Locale-aware layout
│ ├── page.tsx # Home page
│ └── /about
│ └── page.tsx # About page
├── /messages # Translation files
│ ├── en.json
│ ├── es.json
│ └── de.json
├── /i18n
│ ├── config.ts # Locale configuration
│ └── request.ts # Server-side config
├── middleware.ts # Locale detection & routing
└── next.config.js
Step 3: Configure Locales
Create i18n/config.ts:
TypeScript1export const locales = ['en', 'es', 'de', 'fr', 'ja'] as const; 2export type Locale = (typeof locales)[number]; 3export const defaultLocale: Locale = 'en'; 4 5// Optional: locale metadata for UI 6export const localeNames: Record<Locale, string> = { 7 en: 'English', 8 es: 'Español', 9 de: 'Deutsch', 10 fr: 'Français', 11 ja: '日本語', 12};
Step 4: Create Request Configuration
Create i18n/request.ts:
TypeScript1import { getRequestConfig } from 'next-intl/server'; 2import { notFound } from 'next/navigation'; 3import { locales } from './config'; 4 5export default getRequestConfig(async ({ locale }) => { 6 // Validate that the incoming locale is supported 7 if (!locales.includes(locale as any)) { 8 notFound(); 9 } 10 11 return { 12 messages: (await import(`../messages/${locale}.json`)).default, 13 }; 14});
Step 5: Configure next.config.js
JavaScript1const createNextIntlPlugin = require('next-intl/plugin'); 2 3const withNextIntl = createNextIntlPlugin('./i18n/request.ts'); 4 5/** @type {import('next').NextConfig} */ 6const nextConfig = { 7 // Your other Next.js config 8}; 9 10module.exports = withNextIntl(nextConfig);
Step 6: Create Middleware
Create middleware.ts in your project root:
TypeScript1import createMiddleware from 'next-intl/middleware'; 2import { locales, defaultLocale } from './i18n/config'; 3 4export default createMiddleware({ 5 locales, 6 defaultLocale, 7 localePrefix: 'always', // or 'as-needed' or 'never' 8}); 9 10export const config = { 11 // Match all pathnames except for 12 // - API routes 13 // - _next (Next.js internals) 14 // - Static files (images, etc.) 15 matcher: ['/((?!api|_next|.*\\..*).*)'], 16};
Step 7: Create Translation Files
Create messages/en.json:
JSON1{ 2 "common": { 3 "welcome": "Welcome to our app", 4 "loading": "Loading...", 5 "error": "Something went wrong" 6 }, 7 "home": { 8 "title": "Home", 9 "description": "This is the home page", 10 "cta": "Get Started" 11 }, 12 "navigation": { 13 "home": "Home", 14 "about": "About", 15 "contact": "Contact" 16 } 17}
Create messages/es.json:
JSON1{ 2 "common": { 3 "welcome": "Bienvenido a nuestra aplicación", 4 "loading": "Cargando...", 5 "error": "Algo salió mal" 6 }, 7 "home": { 8 "title": "Inicio", 9 "description": "Esta es la página de inicio", 10 "cta": "Comenzar" 11 }, 12 "navigation": { 13 "home": "Inicio", 14 "about": "Acerca de", 15 "contact": "Contacto" 16 } 17}
Step 8: Create Root Layout
Create app/[locale]/layout.tsx:
TypeScript1import { NextIntlClientProvider } from 'next-intl'; 2import { getMessages } from 'next-intl/server'; 3import { notFound } from 'next/navigation'; 4import { locales } from '@/i18n/config'; 5 6export function generateStaticParams() { 7 return locales.map((locale) => ({ locale })); 8} 9 10export default async function LocaleLayout({ 11 children, 12 params: { locale }, 13}: { 14 children: React.ReactNode; 15 params: { locale: string }; 16}) { 17 // Validate locale 18 if (!locales.includes(locale as any)) { 19 notFound(); 20 } 21 22 // Get messages for the current locale 23 const messages = await getMessages(); 24 25 return ( 26 <html lang={locale}> 27 <body> 28 <NextIntlClientProvider messages={messages}> 29 {children} 30 </NextIntlClientProvider> 31 </body> 32 </html> 33 ); 34}
Using Translations
Server Components (Recommended)
In Server Components, use getTranslations():
TypeScript1import { getTranslations } from 'next-intl/server'; 2 3export default async function HomePage() { 4 const t = await getTranslations('home'); 5 6 return ( 7 <main> 8 <h1>{t('title')}</h1> 9 <p>{t('description')}</p> 10 <button>{t('cta')}</button> 11 </main> 12 ); 13}
This is the recommended approach because:
- Translations render on the server
- Zero JavaScript shipped to client for these strings
- Better performance and SEO
Client Components
For interactive components, use useTranslations():
TypeScript1'use client'; 2 3import { useTranslations } from 'next-intl'; 4 5export function AddToCartButton() { 6 const t = useTranslations('product'); 7 8 const handleClick = () => { 9 // Client-side logic 10 }; 11 12 return ( 13 <button onClick={handleClick}> 14 {t('addToCart')} 15 </button> 16 ); 17}
Accessing Multiple Namespaces
TypeScript1import { getTranslations } from 'next-intl/server'; 2 3export default async function Page() { 4 const t = await getTranslations('home'); 5 const tCommon = await getTranslations('common'); 6 7 return ( 8 <div> 9 <h1>{t('title')}</h1> 10 <p>{tCommon('welcome')}</p> 11 </div> 12 ); 13}
Variables and Interpolation
Basic Variables
JSON1{ 2 "greeting": "Hello, {name}!", 3 "items": "You have {count} items in your cart" 4}
TypeScriptt('greeting', { name: 'John' }) // "Hello, John!" t('items', { count: 5 }) // "You have 5 items in your cart"
Pluralization (ICU Format)
next-intl uses ICU message format for proper pluralization:
JSON{ "cartItems": "{count, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}" }
TypeScriptt('cartItems', { count: 0 }) // "Your cart is empty" t('cartItems', { count: 1 }) // "1 item in cart" t('cartItems', { count: 5 }) // "5 items in cart"
ICU plural categories by language:
| Language | Categories |
|---|---|
| English | one, other |
| French | one, other (sometimes many) |
| Russian | one, few, many, other |
| Arabic | zero, one, two, few, many, other |
| Japanese | other (no plurals) |
next-intl handles all of this automatically based on the locale.
Select (Gender, Status, etc.)
JSON{ "userStatus": "{status, select, online {User is online} offline {User is offline} away {User is away} other {Unknown status}}" }
TypeScriptt('userStatus', { status: 'online' }) // "User is online"
Rich Text (HTML/Components)
JSON{ "terms": "By signing up, you agree to our <terms>Terms of Service</terms> and <privacy>Privacy Policy</privacy>." }
TypeScript1import { useTranslations } from 'next-intl'; 2import Link from 'next/link'; 3 4function SignupForm() { 5 const t = useTranslations('auth'); 6 7 return ( 8 <p> 9 {t.rich('terms', { 10 terms: (chunks) => <Link href="/terms">{chunks}</Link>, 11 privacy: (chunks) => <Link href="/privacy">{chunks}</Link>, 12 })} 13 </p> 14 ); 15}
Number, Date, and Currency Formatting
Number Formatting
TypeScript1import { useFormatter } from 'next-intl'; 2 3function PriceDisplay({ price }: { price: number }) { 4 const format = useFormatter(); 5 6 return ( 7 <span> 8 {format.number(price, { style: 'currency', currency: 'USD' })} 9 </span> 10 ); 11} 12 13// en-US: "$1,234.56" 14// de-DE: "1.234,56 $" 15// ja-JP: "$1,234.56"
Date Formatting
TypeScript1import { useFormatter } from 'next-intl'; 2 3function DateDisplay({ date }: { date: Date }) { 4 const format = useFormatter(); 5 6 return ( 7 <> 8 <p>{format.dateTime(date, { dateStyle: 'full' })}</p> 9 <p>{format.dateTime(date, { timeStyle: 'short' })}</p> 10 <p>{format.relativeTime(date)}</p> 11 </> 12 ); 13} 14 15// en-US: "Friday, January 17, 2026" 16// de-DE: "Freitag, 17. Januar 2026"
Relative Time
TypeScript1const format = useFormatter(); 2 3format.relativeTime(new Date('2026-01-10')) // "7 days ago" 4format.relativeTime(new Date('2026-01-20')) // "in 3 days"
List Formatting
TypeScript1const format = useFormatter(); 2 3format.list(['Apple', 'Banana', 'Orange'], { type: 'conjunction' }) 4// en: "Apple, Banana, and Orange" 5// de: "Apple, Banana und Orange"
Routing and Navigation
Locale-Aware Links
Use next-intl/link for automatic locale prefixing:
TypeScript1import Link from 'next-intl/link'; 2 3function Navigation() { 4 return ( 5 <nav> 6 <Link href="/">Home</Link> 7 <Link href="/about">About</Link> 8 <Link href="/contact">Contact</Link> 9 </nav> 10 ); 11} 12// Automatically renders as /en/about, /es/about based on current locale
Locale-Aware useRouter
TypeScript1'use client'; 2 3import { useRouter } from 'next-intl/client'; 4 5function SearchForm() { 6 const router = useRouter(); 7 8 const handleSearch = (query: string) => { 9 router.push(`/search?q=${query}`); 10 // Automatically includes locale prefix 11 }; 12 13 return (/* form */); 14}
Language Switcher
TypeScript1'use client'; 2 3import { useLocale } from 'next-intl'; 4import { useRouter, usePathname } from 'next-intl/client'; 5import { locales, localeNames } from '@/i18n/config'; 6 7export function LanguageSwitcher() { 8 const locale = useLocale(); 9 const router = useRouter(); 10 const pathname = usePathname(); 11 12 const handleChange = (newLocale: string) => { 13 router.replace(pathname, { locale: newLocale }); 14 }; 15 16 return ( 17 <select value={locale} onChange={(e) => handleChange(e.target.value)}> 18 {locales.map((loc) => ( 19 <option key={loc} value={loc}> 20 {localeNames[loc]} 21 </option> 22 ))} 23 </select> 24 ); 25}
URL Strategies
Configure in middleware:
TypeScript1// Option 1: Always show locale prefix 2// /en/about, /es/about, /de/about 3localePrefix: 'always' 4 5// Option 2: Hide default locale 6// /about (English), /es/about (Spanish) 7localePrefix: 'as-needed' 8 9// Option 3: Never show prefix (use cookies/headers) 10localePrefix: 'never'
Recommendation: Use 'always' for best SEO and clearest URLs.
TypeScript Integration
Type-Safe Translations
Create global.d.ts:
TypeScript1import en from './messages/en.json'; 2 3type Messages = typeof en; 4 5declare global { 6 interface IntlMessages extends Messages {} 7}
Now you get autocompletion and type checking:
TypeScript1const t = useTranslations('home'); 2 3t('title') // ✅ Valid 4t('nonExistent') // ❌ TypeScript error
Nested Keys
TypeScript// For deeply nested messages const t = useTranslations('settings.account.privacy'); t('title') // settings.account.privacy.title
SEO and Metadata
Locale-Aware Metadata
TypeScript1import { getTranslations } from 'next-intl/server'; 2import { Metadata } from 'next'; 3 4export async function generateMetadata({ 5 params: { locale }, 6}: { 7 params: { locale: string }; 8}): Promise<Metadata> { 9 const t = await getTranslations({ locale, namespace: 'meta' }); 10 11 return { 12 title: t('title'), 13 description: t('description'), 14 }; 15}
Hreflang Tags
TypeScript1import { locales } from '@/i18n/config'; 2 3export async function generateMetadata({ 4 params: { locale }, 5}: { 6 params: { locale: string }; 7}) { 8 const baseUrl = 'https://example.com'; 9 10 return { 11 alternates: { 12 canonical: `${baseUrl}/${locale}`, 13 languages: Object.fromEntries( 14 locales.map((loc) => [loc, `${baseUrl}/${loc}`]) 15 ), 16 }, 17 }; 18}
This generates proper hreflang tags for SEO:
HTML<link rel="alternate" hreflang="en" href="https://example.com/en" /> <link rel="alternate" hreflang="es" href="https://example.com/es" /> <link rel="alternate" hreflang="de" href="https://example.com/de" />
Performance Optimization
Namespace-Based Code Splitting
Only load translations needed for each page:
TypeScript1// In i18n/request.ts 2export default getRequestConfig(async ({ locale }) => { 3 return { 4 messages: { 5 // Load only common namespace by default 6 common: (await import(`../messages/${locale}/common.json`)).default, 7 }, 8 }; 9}); 10 11// In specific pages, load additional namespaces 12import { getMessages } from 'next-intl/server'; 13 14export default async function CheckoutPage() { 15 const messages = await getMessages({ namespace: 'checkout' }); 16 // ... 17}
Static Generation
Ensure all locale pages are statically generated:
TypeScriptexport function generateStaticParams() { return locales.map((locale) => ({ locale })); }
Avoiding Hydration Mismatches
Pass server time to prevent date/time mismatches:
TypeScript1<NextIntlClientProvider 2 messages={messages} 3 timeZone="Europe/Berlin" 4 now={new Date()} 5> 6 {children} 7</NextIntlClientProvider>
Managing Translations at Scale
As your app grows, managing JSON files manually becomes problematic:
- Missing translations are hard to track
- Merge conflicts when multiple developers edit translation files
- No context for translators
- Slow workflow with manual export/import
IntlPull Integration
IntlPull provides seamless next-intl integration:
Terminal1# Initialize IntlPull in your project 2npx @intlpullhq/cli init 3 4# Upload your existing translations 5npx @intlpullhq/cli upload 6 7# Get AI translations for new languages 8npx @intlpullhq/cli translate --languages es,de,fr,ja 9 10# Pull translations back to your project 11npx @intlpullhq/cli download 12 13# Watch mode for real-time sync during development 14npx @intlpullhq/cli watch
Benefits:
- AI-powered translation with GPT-4, Claude, DeepL
- Visual editor with screenshot context
- Team collaboration with review workflows
- Automatic sync with your codebase
- Translation memory for consistency
Common Patterns
Loading States with Suspense
TypeScript1import { Suspense } from 'react'; 2 3export default function Page() { 4 return ( 5 <Suspense fallback={<Skeleton />}> 6 <TranslatedContent /> 7 </Suspense> 8 ); 9}
Error Boundaries
TypeScript1import { useTranslations } from 'next-intl'; 2 3function ErrorBoundary({ error }: { error: Error }) { 4 const t = useTranslations('errors'); 5 6 return ( 7 <div> 8 <h2>{t('title')}</h2> 9 <p>{t('message')}</p> 10 </div> 11 ); 12}
Dynamic Routes with Locales
TypeScript1// app/[locale]/blog/[slug]/page.tsx 2import { getTranslations } from 'next-intl/server'; 3 4export default async function BlogPost({ 5 params: { locale, slug }, 6}: { 7 params: { locale: string; slug: string }; 8}) { 9 const t = await getTranslations('blog'); 10 11 // Fetch localized content 12 const post = await getPost(slug, locale); 13 14 return ( 15 <article> 16 <h1>{post.title}</h1> 17 <p>{t('readTime', { minutes: post.readTime })}</p> 18 </article> 19 ); 20}
RTL Language Support
For Arabic, Hebrew, Persian, and other RTL languages:
TypeScript1import { locales } from '@/i18n/config'; 2 3const rtlLocales = ['ar', 'he', 'fa']; 4 5export default async function LocaleLayout({ 6 children, 7 params: { locale }, 8}: { 9 children: React.ReactNode; 10 params: { locale: string }; 11}) { 12 const dir = rtlLocales.includes(locale) ? 'rtl' : 'ltr'; 13 14 return ( 15 <html lang={locale} dir={dir}> 16 <body>{children}</body> 17 </html> 18 ); 19}
Use CSS logical properties:
CSS1/* Instead of margin-left, use: */ 2margin-inline-start: 1rem; 3 4/* Instead of padding-right, use: */ 5padding-inline-end: 1rem; 6 7/* Instead of text-align: left, use: */ 8text-align: start;
Testing
Unit Testing Components
TypeScript1import { render, screen } from '@testing-library/react'; 2import { NextIntlClientProvider } from 'next-intl'; 3import messages from '../messages/en.json'; 4import { HomePage } from './page'; 5 6function renderWithIntl(ui: React.ReactNode, locale = 'en') { 7 return render( 8 <NextIntlClientProvider locale={locale} messages={messages}> 9 {ui} 10 </NextIntlClientProvider> 11 ); 12} 13 14test('renders home page title', () => { 15 renderWithIntl(<HomePage />); 16 expect(screen.getByText('Home')).toBeInTheDocument(); 17});
Testing Multiple Locales
TypeScript1const locales = ['en', 'es', 'de']; 2 3describe.each(locales)('HomePage in %s', (locale) => { 4 test('renders without crashing', () => { 5 const messages = require(`../messages/${locale}.json`); 6 renderWithIntl(<HomePage />, locale, messages); 7 // Assert based on expected translations 8 }); 9});
Migrating to next-intl
From react-i18next
TypeScript1// Before (react-i18next) 2import { useTranslation } from 'react-i18next'; 3const { t } = useTranslation(); 4t('home.title') 5 6// After (next-intl) 7import { useTranslations } from 'next-intl'; 8const t = useTranslations('home'); 9t('title')
From next-translate
TypeScript1// Before (next-translate) 2import useTranslation from 'next-translate/useTranslation'; 3const { t } = useTranslation('common'); 4 5// After (next-intl) 6import { useTranslations } from 'next-intl'; 7const t = useTranslations('common');
Translation file format is similar—most JSON files work without changes.
next-intl vs Alternatives
| Aspect | next-intl | react-i18next | next-translate |
|---|---|---|---|
| Next.js optimized | Yes | No | Yes |
| Server Components | Native | Wrapper needed | Native |
| Bundle size | ~2KB | ~8KB | ~1.5KB |
| ICU format | Full | Plugin needed | Basic |
| TypeScript | Excellent | Good | Basic |
| Routing | Built-in | Manual | Built-in |
| Active development | Very active | Active | Moderate |
| Documentation | Excellent | Good | Good |
Recommendation: Use next-intl for any new Next.js App Router project. It's the most complete, best-maintained option specifically designed for Next.js.
Troubleshooting
"Missing message" Errors
TypeScript1// In development, show key name; in production, return empty 2<NextIntlClientProvider 3 messages={messages} 4 onError={(error) => { 5 if (process.env.NODE_ENV === 'development') { 6 console.error(error); 7 } 8 }} 9 getMessageFallback={({ key }) => { 10 return process.env.NODE_ENV === 'development' ? `[${key}]` : ''; 11 }} 12>
Hydration Mismatch
Usually caused by date/time differences between server and client:
TypeScript1// Pass server time explicitly 2<NextIntlClientProvider 3 messages={messages} 4 now={new Date()} 5 timeZone="UTC" 6>
Middleware Not Running
Check your matcher pattern:
TypeScript1export const config = { 2 matcher: [ 3 // Match all paths except static files and API 4 '/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)', 5 ], 6};
Frequently Asked Questions
What is next-intl?
next-intl is an internationalization (i18n) library specifically built for Next.js. It provides translation management, locale-based routing, date/number formatting, and full support for Server Components and the App Router. It's the recommended i18n solution for Next.js 13+ projects due to its native integration and small bundle size (~2KB).
Is next-intl better than react-i18next for Next.js?
Yes, next-intl is better than react-i18next for Next.js App Router projects. next-intl was designed specifically for Next.js with native Server Component support, built-in routing, and zero configuration for the App Router. react-i18next is a general React library that requires additional setup for Server Components and doesn't integrate as seamlessly with Next.js routing.
How do I set up next-intl with Next.js App Router?
To set up next-intl: (1) Install with npm install next-intl, (2) Create a [locale] folder in app directory, (3) Create translation JSON files in a messages folder, (4) Configure middleware.ts for locale detection, (5) Create i18n/request.ts for server config, (6) Wrap your app with NextIntlClientProvider. See the complete setup section above for detailed code.
Does next-intl work with Server Components?
Yes, next-intl has native Server Component support. Use getTranslations() from next-intl/server in Server Components—translations render on the server with zero client-side JavaScript. This is a major advantage over react-i18next which requires wrapper components for Server Component support.
How do I add a language switcher with next-intl?
Use useLocale() and useRouter() from next-intl: Get the current locale with useLocale(), get the pathname with usePathname() from next-intl/client, then call router.replace(pathname, { locale: newLocale }) to switch languages. See the Language Switcher section above for complete code.
How do I handle pluralization in next-intl?
next-intl uses ICU message format for pluralization. Define plural messages like: "{count, plural, =0 {No items} one {# item} other {# items}}". Then call t('key', { count: 5 }). next-intl automatically selects the correct plural form based on the locale—handling languages with 2 forms (English) to 6 forms (Arabic) correctly.
Can I use next-intl with TypeScript?
Yes, next-intl has excellent TypeScript support. Create a global.d.ts file that extends IntlMessages with your message types, and you'll get full autocompletion and type checking for translation keys. This catches missing or misspelled keys at compile time rather than runtime.
How do I manage translations at scale with next-intl?
Use a Translation Management System (TMS) like IntlPull when you have 500+ strings or 3+ languages. IntlPull provides: CLI sync (npx @intlpullhq/cli upload/download), AI-powered translation, team collaboration, visual context for translators, and seamless next-intl integration. This eliminates manual JSON file management and merge conflicts.
What's the bundle size of next-intl?
next-intl adds approximately 2KB to your client bundle (gzipped). This is significantly smaller than react-i18next (~8KB) or react-intl (~12KB). Additionally, translations rendered in Server Components add zero bytes to the client bundle since they're rendered server-side.
Does next-intl support RTL languages?
Yes, next-intl fully supports RTL languages like Arabic, Hebrew, and Persian. You need to set the dir="rtl" attribute on your HTML element based on locale and use CSS logical properties (margin-inline-start instead of margin-left). See the RTL Language Support section for implementation details.
How do I test components using next-intl?
Wrap components with NextIntlClientProvider in tests. Create a helper function that wraps your component with the provider, locale, and messages. Use this for both unit tests and integration tests. You can test multiple locales using describe.each() to ensure translations work across all supported languages.
Summary
next-intl is the best choice for Next.js internationalization in 2026. Key takeaways:
- Built for Next.js - Native App Router and Server Component support
- Lightweight - ~2KB bundle, zero JS for server-rendered translations
- Complete - Routing, formatting, pluralization, TypeScript
- Well-maintained - Active development, excellent documentation
Getting started:
npm install next-intl- Create
[locale]folder structure - Configure middleware and i18n/request.ts
- Use
getTranslations()in Server Components - Scale with IntlPull for AI translation and team collaboration
Ready to manage next-intl translations professionally? Start free with IntlPull — AI translation, visual editor, and CLI sync included.
