Introducción
Next.js 14+ con el App Router aporta nuevas y potentes capacidades para la internacionalización. Esta guía le muestra cómo configurar una aplicación Next.js totalmente internacionalizada con enrutamiento, SEO y gestión de traducción adecuados.
Prerrequisitos
- Next.js 14 o posterior
- Conocimientos básicos de App Router
- Node.js 18+
Paso 1: Instalar next-intl
Terminalnpm install next-intl
Paso 2: Estructura del proyecto
/app
/[locale]
/layout.tsx
/page.tsx
/about
/page.tsx
/messages
/en.json
/es.json
/fr.json
/i18n.ts
/middleware.ts
/next.config.js
Paso 3: Configurar next-intl
Cree i18n.ts en la raíz de su proyecto:
TypeScript1import { getRequestConfig } from 'next-intl/server'; 2 3export default getRequestConfig(async ({ locale }) => ({ 4 messages: (await import(`./messages/${locale}.json`)).default 5}));
Paso 4: Crear middleware
TypeScript1// middleware.ts 2import createMiddleware from 'next-intl/middleware'; 3 4export default createMiddleware({ 5 locales: ['en', 'es', 'fr', 'de', 'ja'], 6 defaultLocale: 'en', 7 localePrefix: 'as-needed' // or 'always' for /en/about style 8}); 9 10export const config = { 11 matcher: ['/', '/(de|en|es|fr|ja)/:path*'] 12};
Paso 5: Actualizar next.config.js
JavaScript1const withNextIntl = require('next-intl/plugin')(); 2 3module.exports = withNextIntl({ 4 // Your existing Next.js config 5});
Paso 6: Crear Layout con Locale
TSX1// app/[locale]/layout.tsx 2import { NextIntlClientProvider } from 'next-intl'; 3import { getMessages } from 'next-intl/server'; 4 5export default async function LocaleLayout({ 6 children, 7 params: { locale } 8}: { 9 children: React.ReactNode; 10 params: { locale: string }; 11}) { 12 const messages = await getMessages(); 13 14 return ( 15 <html lang={locale}> 16 <body> 17 <NextIntlClientProvider messages={messages}> 18 {children} 19 </NextIntlClientProvider> 20 </body> 21 </html> 22 ); 23}
Paso 7: Crear archivos de traducción
JSON1// messages/en.json 2{ 3 "home": { 4 "title": "Welcome to our app", 5 "description": "The best solution for your needs", 6 "cta": "Get Started" 7 }, 8 "navigation": { 9 "home": "Home", 10 "about": "About", 11 "contact": "Contact" 12 } 13}
JSON1// messages/es.json 2{ 3 "home": { 4 "title": "Bienvenido a nuestra aplicación", 5 "description": "La mejor solución para tus necesidades", 6 "cta": "Comenzar" 7 }, 8 "navigation": { 9 "home": "Inicio", 10 "about": "Acerca de", 11 "contact": "Contacto" 12 } 13}
Paso 8: Utilizar traducciones en las páginas
Componentes del servidor
TSX1// app/[locale]/page.tsx 2import { getTranslations } from 'next-intl/server'; 3 4export default async function HomePage() { 5 const t = await getTranslations('home'); 6 7 return ( 8 <main> 9 <h1>{t('title')}</h1> 10 <p>{t('description')}</p> 11 <button>{t('cta')}</button> 12 </main> 13 ); 14}
Componentes Cliente
TSX1'use client'; 2 3import { useTranslations } from 'next-intl'; 4 5export function LanguageSwitcher() { 6 const t = useTranslations('navigation'); 7 // ... language switching logic 8}
Paso 9: Añadir Cambiador de Idioma
TSX1'use client'; 2 3import { useLocale } from 'next-intl'; 4import { useRouter, usePathname } from 'next/navigation'; 5 6const locales = ['en', 'es', 'fr', 'de', 'ja']; 7 8export function LanguageSwitcher() { 9 const locale = useLocale(); 10 const router = useRouter(); 11 const pathname = usePathname(); 12 13 const switchLocale = (newLocale: string) => { 14 const newPath = pathname.replace(`/${locale}`, `/${newLocale}`); 15 router.push(newPath); 16 }; 17 18 return ( 19 <select value={locale} onChange={(e) => switchLocale(e.target.value)}> 20 {locales.map((loc) => ( 21 <option key={loc} value={loc}> 22 {loc.toUpperCase()} 23 </option> 24 ))} 25 </select> 26 ); 27}
Step 10: SEO Optimization
Generate Static Params
TSX1// app/[locale]/page.tsx 2export function generateStaticParams() { 3 return [ 4 { locale: 'en' }, 5 { locale: 'es' }, 6 { locale: 'fr' }, 7 { locale: 'de' }, 8 { locale: 'ja' }, 9 ]; 10}
Metadata with Translations
TSX1import { getTranslations } from 'next-intl/server'; 2 3export async function generateMetadata({ params: { locale } }) { 4 const t = await getTranslations({ locale, namespace: 'home' }); 5 6 return { 7 title: t('meta.title'), 8 description: t('meta.description'), 9 alternates: { 10 canonical: `https://example.com/${locale}`, 11 languages: { 12 'en': 'https://example.com/en', 13 'es': 'https://example.com/es', 14 'fr': 'https://example.com/fr', 15 }, 16 }, 17 }; 18}
Hreflang Tags
TSX1// app/[locale]/layout.tsx 2export default function Layout({ children, params: { locale } }) { 3 return ( 4 <html lang={locale}> 5 <head> 6 <link rel="alternate" hrefLang="en" href="https://example.com/en" /> 7 <link rel="alternate" hrefLang="es" href="https://example.com/es" /> 8 <link rel="alternate" hrefLang="x-default" href="https://example.com" /> 9 </head> 10 <body>{children}</body> 11 </html> 12 ); 13}
Advanced: Pluralization and Formatting
Plurals
JSON1{ 2 "cart": { 3 "items": "{count, plural, =0 {No items} =1 {1 item} other {# items}}" 4 } 5}
TSXt('cart.items', { count: 5 }) // "5 items"
Rich Text
JSON{ "welcome": "Hello, <bold>{name}</bold>!" }
TSX1t.rich('welcome', { 2 name: 'John', 3 bold: (chunks) => <strong>{chunks}</strong> 4})
Dates and Numbers
TSX1import { useFormatter } from 'next-intl'; 2 3function PriceDisplay({ amount, date }) { 4 const format = useFormatter(); 5 6 return ( 7 <div> 8 <p>{format.number(amount, { style: 'currency', currency: 'USD' })}</p> 9 <p>{format.dateTime(date, { dateStyle: 'full' })}</p> 10 </div> 11 ); 12}
Managing Translations at Scale
Manually managing JSON files gets painful fast. Here's how IntlPull helps:
Bidirectional Sync
Terminal# Watch for changes and sync automatically npx @intlpullhq/cli sync --watch
This keeps your local files in sync with IntlPull in real-time.
Upload & Download
Terminal1# Pull latest from IntlPull 2npx @intlpullhq/cli download --output ./messages 3 4# Push new strings 5npx @intlpullhq/cli upload --source ./messages/en.json
AI Translation
When you add a new string, IntlPull automatically translates it to all your configured languages using context-aware AI.
Common Issues and Solutions
Issue: Hydration Mismatch
Solution: Ensure your middleware and locale detection are consistent.
TypeScript1// middleware.ts 2export default createMiddleware({ 3 localePrefix: 'always', // Prevents mismatches 4});
Issue: Flash of Wrong Language
Solution: Use cookies for persistence:
TypeScript1export default createMiddleware({ 2 localeDetection: true, 3 // Stores preference in cookie 4});
Issue: Missing Translations in Production
Solution: Set up fallback handling:
TypeScript1getRequestConfig(async ({ locale }) => ({ 2 messages: { 3 ...(await import(`./messages/en.json`)).default, // Fallback 4 ...(await import(`./messages/${locale}.json`)).default, 5 }, 6}));
Performance Tips
- Use static generation where possible
- Code-split large translation namespaces
- Cache translations with ISR
- Lazy load non-critical translations
Conclusion
Next.js with next-intl provides a powerful, type-safe way to build internationalized applications. Combined with a translation management system like IntlPull, you can ship localized features faster than ever.
Ready to simplify your Next.js i18n? Try IntlPull free and automate your translation workflow.
