Contexto y requisitos
Queremos implementar soporte multilingüe bajo las siguientes restricciones:
- Next.js 15 App Router con TypeScript
- Static Export (
output: 'export') - Despliegue mediante S3 + CloudFront - Sin Server-Side Rendering - No se pueden usar APIs dinámicas ni middleware
- SEO requerido - Debe proporcionar contenido en el idioma apropiado a los motores de búsqueda
Descripción de la arquitectura
1. Diseño del routing
/ # Página raíz (predeterminado: en)
/en/ # Página en inglés
/ja/ # Página en japonés
Usa el Segmento Dinámico [locale] para pre-renderizar páginas para cada idioma durante la generación estática.
2. Gestión de datos de traducción
Los datos de traducción se gestionan en formato JSON:
{
"title": "title"
}
{
"title": "タイトル"
}
3. Gestión de estado con la API Context de React
interface I18nContextType {
messages: Record<string, any>;
locale: string;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
export function I18nProvider({
children,
messages,
locale
}: I18nProviderProps) {
return (
<I18nContext.Provider value={{ messages, locale }}>
{children}
</I18nContext.Provider>
);
}
export function useTranslations() {
const context = useContext(I18nContext);
if (!context) {
throw new Error('useTranslations must be used within I18nProvider');
}
const t = (key: string): string => {
// Soporte para claves anidadas
const keys = key.split('.');
let value: any = context.messages;
for (const k of keys) {
value = value?.[k];
}
return typeof value === 'string' ? value : key;
};
return {
t,
locale: context.locale,
messages: context.messages,
isLoaded: true
};
}
4. Provisión de Context en la jerarquía de layouts
export default async function RootLayout({ children }: Props) {
const locale = defaultLocale;
const messages = await getMessages(locale);
return (
<html lang={locale}>
<body>
<I18nProvider messages={messages} locale={locale}>
<LanguageSwitcherClient />
{children}
</I18nProvider>
</body>
</html>
);
}
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
const messages = await getMessages(locale);
return (
<div lang={locale}>
<I18nProvider messages={messages} locale={locale}>
{children}
</I18nProvider>
</div>
);
}
Idioma predeterminado y redirección del lado del cliente
Del lado del servidor (durante la generación estática)
La página raíz (/) siempre se genera en inglés (en):
export default async function RootPage() {
const locale = defaultLocale; // 'en'
const messages = await getMessages(locale);
return (
<I18nProvider messages={messages} locale={locale}>
<ClientLocaleRedirect />
<PageContent />
</I18nProvider>
);
}
Justificación:
- Estrategia SEO: Proporcionar contenido estable a los rastreadores de motores de búsqueda
- Restricciones de exportación estática: No se puede determinar el idioma del usuario en el lado del servidor
Del lado del cliente (Navegador)
En el acceso inicial, redirige automáticamente después de detectar la configuración del idioma del navegador:
'use client';
export default function ClientLocaleRedirect() {
useEffect(() => {
// Verificar si la redirección ya se realizó
if (typeof window === 'undefined' ||
sessionStorage.getItem('localeRedirectDone')) {
return;
}
const supportedLocales = ['en', 'ja'];
const pathname = window.location.pathname;
// Redirigir solo para la ruta raíz
if (pathname === '/') {
// Obtener la configuración de idioma del navegador
const browserLocales = navigator.languages || [navigator.language];
// Encontrar idioma soportado
const preferredLocale = browserLocales
.map(lang => lang.split('-')[0]) // 'ja-JP' -> 'ja'
.find(lang => supportedLocales.includes(lang));
// Redirigir si es diferente al predeterminado
if (preferredLocale && preferredLocale !== defaultLocale) {
sessionStorage.setItem('localeRedirectDone', 'true');
window.location.replace(`/${preferredLocale}`);
}
}
}, []);
return null;
}
Escenarios de comportamiento
| Situación | Durante SSG | Comportamiento del cliente |
|---|---|---|
| Rastreador de motor de búsqueda | Generar / en inglés |
Sin redirección |
| Usuario con navegador en japonés | Generar / en inglés |
Redirigir a /ja |
| Usuario con navegador en inglés | Generar / en inglés |
Sin redirección (mostrar tal cual) |
| Usuario con idioma no soportado | Generar / en inglés |
Sin redirección (mostrar en inglés) |
Uso en componentes
'use client';
function NetworkInfoClient() {
const { t } = useTranslations();
return (
<div>
<h2>{t('title')}</h2>
</div>
);
}
Ventajas técnicas
- Seguridad de tipos: TypeScript + Context API permite verificar la existencia de claves de traducción
- Dependencias claras: Los componentes que usan
useTranslationsdeben colocarse dentro del Provider, haciendo el diseño explícito - Rendimiento: Entrega rápida de páginas mediante generación estática
- SEO: Cada página de idioma tiene una URL independiente, optimizada para motores de búsqueda
- UX: La redirección automática basada en la configuración del idioma del navegador reduce el esfuerzo del usuario
Resumen
Hemos implementado soporte multilingüe usando la API Context de React en un entorno de exportación estática de Next.js App Router. Complementamos las restricciones del lado del servidor con lógica del lado del cliente, logrando una buena UX.
Cambiar el idioma predeterminado solo requiere editar defaultLocale en config.ts, garantizando la extensibilidad.
