Ahora con creación de páginas por IA vía el servidor MCP

i18n

Cada campo es multilingüe por defecto. Cómo los locales dan forma a las URL, a cuál se resuelve una petición y qué pasa cuando falta una traducción.

En cmssy no modelas traducciones. Cada campo ya es multilingüe: el contenido se guarda indexado por código de idioma, así que una página lleva todos sus idiomas y una instancia de bloque todos los suyos.

Eso elimina el problema habitual de duplicación -no hay "página en inglés" y "página en alemán" que mantener sincronizadas- y lo sustituye por dos preguntas más pequeñas. ¿A qué locale se resuelve una petición, y qué se muestra cuando un campo aún no está traducido?

El conjunto de locales

Los locales vienen de la configuración del sitio, no de tu código:

const { defaultLocale, locales } = await resolveSiteLocales();

Añadir un idioma es un ajuste del workspace. Tu app lo recoge en la siguiente petición, lo que significa que un locale nuevo no necesita despliegue, solo traducciones.

URL: el locale por defecto no lleva prefijo

Esta es la regla de la que se deriva todo lo demás:

localizedPath("/pricing", "es", "es"); // "/pricing"    por defecto: desnuda
localizedPath("/pricing", "de", "es"); // "/de/pricing" el resto: con prefijo
localizedPath("/", "de", "es");        // "/de"

Mantener el locale por defecto sin prefijo significa que tus URL canónicas no cambian cuando añades un segundo idioma: enlaces existentes, comparticiones y posiciones sobreviven.

Resolver una petición

La dirección inversa lee el primer segmento de la ruta, y es estricta a propósito:

splitLocaleFromPath(["de", "pricing"], { defaultLocale: "es", locales: ["es", "de"] });
// -> { locale: "de", path: ["pricing"] }

splitLocaleFromPath(["pricing"], { defaultLocale: "es", locales: ["es", "de"] });
// -> { locale: "es", path: ["pricing"] }

Un primer segmento cuenta como locale solo si está en locales y no es el por defecto. Así que /es/pricing no es la página de precios en español: es una página cuyo slug empieza por casualidad con es. Una URL canónica por página e idioma, sin duplicados.

La ruta catch-all hace esto antes de buscar un slug:

const { path: strippedPath } = splitLocaleFromPath(path, await resolveSiteLocales());
const slug = "/" + (strippedPath ?? []).join("/");

Traducciones ausentes

Un campo cae a un respaldo en vez de desaparecer:

pickLocalized(value, locale, defaultLocale);
// value[locale] ?? value[defaultLocale] ?? primer valor disponible

Tres pasos: el idioma pedido, luego el por defecto, luego lo que exista. El último importa: un campo traducido solo al francés se sigue renderizando en una página alemana en vez de dejar un hueco.

La consecuencia para quien escribe bloques: una página parcialmente traducida es un estado normal, no un error. Escribe componentes que rendericen con sensatez cuando un campo vuelva en el idioma equivocado, y prueba con un locale que no hayas traducido: es lo primero con lo que se toparán tus visitantes en un idioma nuevo.

Middleware

export const proxy = createCmssyProxy(cmssy, {
  // Solo si tus URL llevan el idioma Y tus rutas son caminos estáticos
  // en vez de una catch-all.
  stripLocalePrefix: true,
});

El SDK lo dice explícitamente: déjalo apagado para una app catch-all que lee el idioma de la ruta por sí misma. Actívalo igualmente y la ruta recibe un camino sin prefijo, así que splitLocaleFromPath no encuentra nada que recortar y cae al locale por defecto: todos los idiomas se renderizan como el predeterminado.

El middleware sigue resolviendo el idioma y pasándolo en una cabecera, así que una app puede leerlo de ahí. Pero si tu ruta deriva el locale de la ruta, como suele hacer una catch-all, esas dos fuentes le están diciendo ahora cosas distintas.

Declara el idioma en el HTML

Fija <html lang> desde el locale resuelto. Es lo que leen los lectores de pantalla y las herramientas de traducción, y es un contrato: por eso la prueba de humo del editor se apoya en él en vez de buscar una palabra en tu copia. La copia es contenido; un editor puede reescribirla en cualquier momento, y entonces la prueba miente.

Siguientes pasos

  • SEO: alternativas hreflang construidas desde el mismo conjunto de locales.
  • Pruebas: la afirmación sobre <html lang>.
  • Modelos de datos: los registros también son multilingües.