Next.jsReactTypeScripti18n

Arquitectura multilingüe con Next.js App Router + Static Export

Sloth255
Sloth255
·2 min read·434 words

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:

src/locales/en.json
{
  "title": "title"
}
src/locales/ja.json
{
  "title": "タイトル"
}

3. Gestión de estado con la API Context de React

src/i18n/context.tsx
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

src/app/layout.tsx
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>
  );
}
src/app/[locale]/layout.tsx
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):

src/app/page.tsx
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:

src/components/ClientLocaleRedirect.tsx
'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

  1. Seguridad de tipos: TypeScript + Context API permite verificar la existencia de claves de traducción
  2. Dependencias claras: Los componentes que usan useTranslations deben colocarse dentro del Provider, haciendo el diseño explícito
  3. Rendimiento: Entrega rápida de páginas mediante generación estática
  4. SEO: Cada página de idioma tiene una URL independiente, optimizada para motores de búsqueda
  5. 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.