¿Alguna vez pateaste el i18n para “después” y terminó quedando para nunca? A mí me pasó mil veces. Por eso armé este video corto donde, en 10 minutos, dejo lista una app de Next.js con internacionalización usando next-intl. Es simple, escalable y no tenés que pelearte con rutas raras ni mil providers.
Lo clave del video
¿Por qué next-intl?
- Funciona perfecto con el App Router de Next.js.
- Soporta server y client components.
- Te da helpers para fechas, números y pluralización sin que tengas que reinventar la rueda.
- Middleware listo para redirigir al locale correcto.
Estructura mínima que uso
- Rutas con prefijo de idioma:
/[locale]/... - Un middleware para detectar/forzar locale.
- Un
NextIntlClientProvideren el layout. - Archivos de mensajes por idioma.
1) Middleware para locales
Define qué idiomas soportás y el default. Listo, next-intl se encarga del resto.
// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['es', 'en'],
defaultLocale: 'es'
});
export const config = {
// Evita interferir con assets internos y archivos estáticos
matcher: ['/((?!_next|.*\\..*).*)']
};
2) Provider en el layout de cada locale
Cargamos los mensajes y envolvemos la app para que todo tenga acceso a las traducciones.
// app/[locale]/layout.tsx
import {NextIntlClientProvider} from 'next-intl';
import {notFound} from 'next/navigation';
export function generateStaticParams() {
return [{locale: 'es'}, {locale: 'en'}];
}
export default async function LocaleLayout({
children,
params: {locale}
}: {
children: React.ReactNode;
params: {locale: 'es' | 'en'};
}) {
let messages;
try {
messages = (await import(`../../messages/${locale}.json`)).default;
} catch (error) {
notFound();
}
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
3) Mensajes por idioma
Definí tus textos por namespace. Ejemplo:
// messages/es.json
{
"Home": {
"title": "Bienvenido",
"cta": "Ver vuelos"
}
}
// messages/en.json
{
"Home": {
"title": "Welcome",
"cta": "Browse flights"
}
}
4) Usar traducciones en Server o Client
- Server (recomendado cuando podés):
getTranslations. - Client:
useTranslations.
// app/[locale]/page.tsx (Server Component)
import {getTranslations} from 'next-intl/server';
export default async function Page({params: {locale}}: {params: {locale: 'es' | 'en'}}) {
const t = await getTranslations({locale, namespace: 'Home'});
return (
<>
<h1>{t('title')}</h1>
<p>{t('cta')}</p>
</>
);
}
// components/CTA.tsx (Client Component)
'use client';
import {useTranslations} from 'next-intl';
export default function CTA() {
const t = useTranslations('Home');
return <button>{t('cta')}</button>;
}
5) Cambiador de idioma (sin romper la URL actual)
Usando el Link de next-intl y el pathname del App Router.
// components/LocaleSwitcher.tsx
'use client';
import {useLocale} from 'next-intl';
import {usePathname} from 'next/navigation';
import Link from 'next-intl/link';
export default function LocaleSwitcher() {
const locale = useLocale();
const pathname = usePathname();
return (
<nav>
<Link href={pathname} locale="es" aria-current={locale === 'es' ? 'page' : undefined}>
ES
</Link>
{' · '}
<Link href={pathname} locale="en" aria-current={locale === 'en' ? 'page' : undefined}>
EN
</Link>
</nav>
);
}
Bonus: formateo de fechas y números
Queda muy pro y respeta el locale activo.
// components/Price.tsx
'use client';
import {useFormatter} from 'next-intl';
export default function Price({value}: {value: number}) {
const f = useFormatter();
return <span>{f.number(value, {style: 'currency', currency: 'USD'})}</span>;
}
Mirá el video completo
Recursos y repo
- Librería: https://next-intl.dev/
- Repo del ejemplo (¡dejale una estrellita!): https://github.com/martin2844/flights-next-intl-example
- Desplegá tu app con Coolify en 5 minutos: https://youtu.be/DAaXdNrcTV0
- Créditos gratis en Hetzner para tu VPS: https://hetzner.cloud/?ref=Sswaf20wbckq
Cierre
Si venías esquivando el i18n, con next-intl ya no hay excusas. Queda prolijo, mantenible y te permite escalar a varios idiomas sin dolor. Cualquier duda me la dejás en comentarios, y si te sirvió, compartilo con ese/a amigo/a que siempre dice “lo hago después”. Nos vemos en el próximo, con más código y mate.
![]()

