# Laravel multilingüe: rutas traducidas bien hechas

> Dale a cada locale una URL realmente traducida (/pricing, /nl/tarieven, /es/precios) en vez de prefijar slugs en inglés. Resuelve los slugs desde archivos de traducción al registrar las rutas, emite un conjunto hreflang recíproco completo incluyendo x-default, mantén los canonicals dentro del locale, y no redirijas nunca a un crawler por Accept-Language. Esas cuatro reglas cubren la mayor parte de lo que falla.

La mayoría de las aplicaciones Laravel multilingües se construyen igual: una tabla de rutas en inglés con un prefijo de locale atornillado delante. Funciona, y deja a las versiones que no son inglés sin poder posicionar por nada que alguien busque de verdad.

## Por qué el slug tiene que estar traducido

Un prospecto neerlandés que busca precios busca *tarieven*. La URL es una señal de relevancia real, aunque modesta, y más importante: es lo que aparece en los resultados, se comparte en mensajes y lo leen los asistentes. `/nl/pricing` anuncia la traducción de un sitio inglés. `/nl/tarieven` anuncia una página en neerlandés.

Lo mismo vale para toda la ruta. Este sitio corre `/pricing`, `/nl/tarieven` y `/es/precios`; `/ai-development`, `/nl/ai-development` y `/es/desarrollo-ia`. El mercado neerlandés usa el término inglés para esa, así que traducirla sería peor.

Ese último detalle es el punto de verdad: la elección de slug es una decisión de keyword por idioma, no una tarea de traducción. No se puede automatizar, y es la razón de que los slugs vivan en archivos de traducción, donde una persona puede fijarlos uno a uno.

## Cómo construirlo

Pon los slugs en un archivo de traducción por locale, una entrada por página:

```php
// lang/nl/routes.php
return [
    'pricing' => 'tarieven',
    'about' => 'over',
    'ai-development' => 'ai-development',
];
```

Luego registra las rutas una sola vez, resolviendo cada URI a través del traductor, dentro del mecanismo de rutas localizadas que uses. Cada locale obtiene su propia ruta con su propio path y un nombre prefijado por locale, de modo que `route('pricing')` da la URL del locale actual y los enlaces son correctos en todas partes sin un solo condicional en una plantilla.

Dos reglas hacen que esto aguante con el tiempo. No renombres nunca un slug que ya posiciona. No hay ventaja, y heredas una redirección que hay que mantener para siempre. Y guarda la tabla de redirecciones de URLs antiguas como datos en un solo array, no como una pila de closures sueltos, para que un test pueda afirmarla entera.

## Los errores de hreflang que importan

Aquí es donde más se rompen los sitios multilingües, y los fallos son silenciosos.

**El conjunto debe ser recíproco y referenciarse a sí mismo.** La página de cada locale lista todos los locales, incluido el suyo. Si la página en neerlandés apunta a inglés y español pero la inglesa no apunta de vuelta al neerlandés, los buscadores descartan la relación.

**`x-default` no es opcional.** Le dice al motor qué servir a alguien a quien no le encaja ninguno de tus locales. Apúntalo al idioma de mayor alcance, normalmente el inglés.

**Los canonicals se quedan dentro del locale.** El canonical de la página en neerlandés es la URL en neerlandés. Un canonical que apunta desde la página neerlandesa a la inglesa es una instrucción de sacar la página neerlandesa del índice, y es un accidente notablemente frecuente.

**Lista solo los locales que existen.** Si un artículo de blog tiene versión inglesa y no neerlandesa, no emitas un alternate en neerlandés. Por eso las traducciones necesitan un identificador compartido (el nombre de archivo, o una clave) independiente del slug traducido, para que el sistema sepa si una traducción existe en vez de asumirlo.

## No redirijas nunca a un crawler por Accept-Language

Redirigir automáticamente a los visitantes de primera visita al idioma de su navegador es bueno para las personas y desastroso si también aplica a los bots. Un crawler que llega con una IP europea y sin preferencia de idioma acaba rebotado a un locale, y los demás quedan sin rastrear y sin indexar.

Tres reglas lo mantienen seguro: exime por completo a los user agents de crawlers conocidos; redirige solo una primera visita, condicionada a una cookie, para que quien eligió un idioma lo conserve; y usa siempre una redirección temporal, nunca una permanente, porque el destino depende del visitante.

## Testear lo que no se ve

Dos tests pillan la mayor parte.

Afirma que cada ruta se resuelve en cada locale, devuelve 200 y renderiza exactamente un `h1`. Impúlalo desde la config de locales para que añadir un idioma extienda la cobertura sola. Si añadir un locale exige un cambio de código en cualquier sitio que no sea una entrada de config y un directorio de idioma, la arquitectura está mal y este test es lo que te lo dice.

Luego afirma los conjuntos hreflang: para cada página en cada locale, el conjunto contiene cada locale, se contiene a sí mismo, incluye `x-default`, y cada URL del conjunto se resuelve. La reciprocidad es exactamente el tipo de propiedad tediosa de comprobar a mano y trivial de comprobar en un bucle.

## La versión corta

Slugs traducidos desde archivos de traducción. Un solo registro de rutas para todos los locales. Hreflang recíproco y autorreferenciado con `x-default`. Canonicals dentro del locale. Bots exentos de las redirecciones de idioma. Tests que iteran la lista de locales en vez de dejarla fija.

Haz eso y añadir un cuarto idioma es un directorio y una entrada de config, que es la única definición razonable de terminado aquí.

---

Publicado por Vuewer. Funciones de IA y aplicaciones web, llevadas a producción.
Versión canónica: https://vuewer.com/es/blog/laravel-multilingue-rutas-traducidas