Detectando el idioma preferido del usuario

Redirigiendo a los nuevos visitantes a su idioma más probable

Problema

Cuando un usuario visita por primera vez la raíz de una aplicación (por ejemplo, /), se muestra un idioma predeterminado, como el inglés. Esto crea fricción inmediata para los usuarios que hablan otros idiomas, obligándolos a encontrar manualmente un selector de idioma, aunque su navegador ya comunica su preferencia.

Solución

Utilizar middleware para interceptar las solicitudes a la ruta raíz (/). Verificar el encabezado HTTP Accept-Language del usuario para encontrar su idioma preferido. Si ese idioma es compatible con la aplicación, redirigir al usuario a la raíz de ese idioma (por ejemplo, /fr). Si no, redirigirlos a un idioma predeterminado (por ejemplo, /en).

Pasos

1. Instalar un analizador de idiomas

El encabezado Accept-Language puede ser complejo (por ejemplo, fr-CH, fr;q=0.9, en;q=0.8). Una pequeña biblioteca ayuda a analizar este encabezado y encontrar la mejor coincidencia de nuestra lista de idiomas compatibles.

Ejecuta este comando en tu terminal:

npm install accept-language-parser

2. Definir tus idiomas y el predeterminado

Crea un archivo de configuración central para almacenar tu lista de idiomas compatibles y definir un valor predeterminado. Este valor predeterminado se utilizará si las preferencias del navegador del usuario no coinciden con ningún idioma que admitas.

// i18n.config.ts

export const locales = ['en', 'es', 'fr'];
export const defaultLocale = 'en';

3. Crear el middleware

Crea un archivo middleware.ts en la raíz de tu proyecto. Este archivo se ejecutará en las solicitudes entrantes, permitiéndote verificar la ruta y los encabezados.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import parser from 'accept-language-parser';
import { locales, defaultLocale } from './i18n.config';

// Función auxiliar para encontrar la mejor coincidencia de idioma
function getBestLocale(acceptLangHeader: string | null) {
  if (!acceptLangHeader) {
    return defaultLocale;
  }

  // Utiliza el analizador para encontrar el mejor idioma compatible
  const bestMatch = parser.pick(locales, acceptLangHeader, {
    loose: true,
  });

  return bestMatch || defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Verifica si la solicitud es para la ruta raíz
  if (pathname === '/') {
    // Obtiene el idioma preferido del usuario
    const acceptLang = request.headers.get('Accept-Language');
    const bestLocale = getBestLocale(acceptLang);

    // Redirige a la ruta del idioma mejor coincidente
    request.nextUrl.pathname = `/${bestLocale}`;
    return NextResponse.redirect(request.nextUrl);
  }

  // 2. Para todas las demás rutas, continúa normalmente
  return NextResponse.next();
}

export const config = {
  matcher: [
    // Omite todas las rutas que comienzan con:
    // - api (rutas API)
    // - _next/static (archivos estáticos)
    // - _next/image (archivos de optimización de imágenes)
    // - favicon.ico (archivo favicon)
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

Este código solo ejecuta lógica para la ruta raíz (/). Si un usuario visita /, verifica su encabezado Accept-Language, encuentra la mejor coincidencia (por ejemplo, es), y los redirige a /es. Todas las demás solicitudes, como /en/about, son ignoradas por esta lógica y pasan a través.