# Rutas traducidas en Next.js 16 sin librerías

> Cómo tener /servicios en español y /services en inglés en Next.js 16 con un mapa de rutas y un proxy.ts, sin añadir librerías de internacionalización.

URL: https://codegea.com/rutas-traducidas-en-next-js-16-sin-librerias
Web · 2026-10-18 · Derian Pinto

Cuando un sitio tiene dos idiomas, lo habitual es traducir los textos y dejar las URLs en un solo idioma: `/es/services` y `/en/services`. Para SEO es mejor que la URL también esté traducida: `/servicios` en español y `/en/services` en inglés. Next.js no lo hace solo, y no hace falta una librería para conseguirlo. Así lo resolvimos en este sitio.

## La idea

Las carpetas de `app/` tienen **un nombre interno fijo**, en inglés: `app/[locale]/services`, `app/[locale]/contact`. Lo que ve el visitante es otra cosa. Un archivo `proxy.ts` traduce la URL pública a la carpeta interna antes de que Next.js renderice nada.

- El español es la raíz, sin prefijo: `/servicios`, `/contacto`.
- El inglés va bajo `/en`: `/en/services`, `/en/contact`.
- La carpeta es la misma en ambos casos.

Desde Next.js 16 este archivo se llama `proxy.ts` (antes `middleware.ts`; la convención antigua está marcada como obsoleta en la documentación).

## Paso 1: un único mapa de rutas

Toda la información vive en un solo archivo, `i18n/routes.ts`:

```ts
export const SEGMENTS = {
  services: { internal: 'services', es: 'servicios', en: 'services' },
  contact: { internal: 'contact', es: 'contacto', en: 'contact' },
  privacy: {
    internal: 'privacy',
    es: 'politica-de-privacidad',
    en: 'privacy-policy',
  },
  // …
} as const;
```

Cada entrada dice cómo se llama la carpeta interna y cómo se escribe en cada idioma. Ningún componente escribe una URL a mano: todos usan funciones como `segmentPath('en', 'contact')`, que devuelve `/en/contact`. Si un día cambias `contacto` por `escribenos`, lo cambias en un sitio.

## Paso 2: construir las URLs con una función

```ts
const prefix = (locale: Locale) =>
  locale === DEFAULT_LOCALE ? '' : `/${locale}`;

export const segmentPath = (locale: Locale, key: SegmentKey) =>
  `${prefix(locale)}/${SEGMENTS[key][locale]}`;
```

El idioma por defecto no lleva prefijo; los demás sí.

## Paso 3: el proxy traduce

`proxy.ts` mira la URL, detecta el idioma y reescribe hacia la carpeta interna:

```ts
export function proxy(req: NextRequest) {
  const { pathname, search } = req.nextUrl;
  const { locale, explicit, rest } = splitLocale(pathname);
  const prefix = locale === DEFAULT_LOCALE ? '' : `/${locale}`;

  // /es/... se redirige a la URL sin prefijo: una sola URL por página
  if (explicit && locale === DEFAULT_LOCALE) {
    return NextResponse.redirect(new URL(join(...rest) + search, req.url), 308);
  }

  let internal = rest;
  const match = rest[0] ? matchSegment(locale, rest[0]) : null;
  if (match) {
    // pidieron /services en español: redirige a la forma canónica /servicios
    if (match.canonical) {
      return NextResponse.redirect(
        new URL(prefix + join(match.canonical, ...rest.slice(1)) + search, req.url),
        308,
      );
    }
    internal = [match.internal, ...rest.slice(1)];
  }

  return NextResponse.rewrite(new URL(join(locale, ...internal) + search, req.url));
}
```

Hace tres cosas:

1. **Reescribe** `/servicios` a `/es/services`. El visitante sigue viendo `/servicios`.
2. **Redirige** (308) a la forma correcta cuando alguien entra con el segmento de otro idioma, por ejemplo `/services` en español. Así cada contenido tiene una sola URL.
3. **Quita el prefijo** `/es` del idioma por defecto con otra redirección.

Y el `matcher` deja fuera lo que no debe pasar por aquí:

```ts
export const config = {
  matcher: ['/((?!api/|admin(?:/|$)|_next/).*)'],
};
```

## Paso 4: dile a Google que son la misma página

Con URLs distintas por idioma, necesitas `hreflang`. Cada página debe saber cuál es su equivalente en el otro idioma. Nosotros pasamos las URLs alternas a un único componente de plantilla, que genera el selector ES | EN y las etiquetas `hreflang`. Un artículo sin traducción simplemente no se anuncia en el otro idioma.

## Lo que gana y lo que cuesta

**Gana:** URLs legibles en cada idioma, ninguna dependencia, y todo el comportamiento en un archivo que puedes leer de principio a fin.

**Cuesta:** tienes que mantener tú el mapa y las redirecciones. Si el sitio crece a muchos idiomas, o necesitas formatear fechas, plurales o monedas por idioma, una librería de i18n sigue siendo una buena idea. Para dos idiomas y un sitio de contenido, el mapa se queda corto antes de ser un problema.

## Dos advertencias

- **Prueba las redirecciones con ****`curl -I`****.** Un bucle de redirecciones entre `/es/x` y `/x` se ve rápido, pero solo si lo pruebas.
- **No pongas en el ****`matcher`**** rutas que no necesitan traducción**, como `/api` o el panel de administración. En nuestro caso, el admin de Payload queda fuera.

Si tu sitio es en Next.js y quieres ver cómo cambió el framework, lee [qué cambia en Next.js 16 para sitios corporativos](/nextjs-16-que-cambia-para-sitios-corporativos).
