Traducir páginas personalizadas con marcadores
Qué es esto
Cómo hacer que una página personalizada se traduzca sola. Las páginas personalizadas —subidas con upload-custom-page, tipo de página custom, plantilla custom-page— envían tu marcado tal como fue escrito, por lo que la plataforma nunca adivina qué texto es legible para humanos. Lo declaras con marcadores de traducción: elementos <T key="…"> en cuerpos Astro/JSX, atributos data-t-key="…" en cuerpos de HTML plano. Las cadenas marcadas se traducen automáticamente a cada idioma configurado para el módulo de páginas personalizadas; todo lo demás se envía sin cambios. Crea la página una vez, en un idioma, y publícala una vez: la plataforma genera una URL por idioma.
Cómo funciona
La traducción ocurre en tres fases:
-
Extracción, al subir.
upload-custom-pageanalizabody_sourcey recopila cada marcador en un mapa plano declave → texto predeterminado, almacenado en la página comotranslatable_strings. El cuerpo se almacena tal como lo escribiste. La extracción es estricta: cualquier marcador mal formado (clave no válida, falta de valor predeterminado, marcadores anidados) rechaza toda la carga con una lista de errores numerada; no se omite nada silenciosamente. -
Distribución, al publicar. Durante
publish→publish-confirm, la plataforma traduce automáticamente los valores detranslatable_strings—además detitleydescription— a cada idioma enmodules.custom_pages.locales, menos el idioma original de la página. Cada idioma de destino se convierte en una variante de página agrupada bajo tucanonical_slug. Los títulos y descripciones traducidos se recortan a 60 y 160 caracteres si la traducción es larga; por lo tanto, cuando el proyecto tenga idiomas adicionales, apunta a ≤50/≤140 en el original para dar margen a las traducciones. Añadir un idioma al módulo más tarde distribuye cada página personalizada activa al nuevo idioma como borrador, listo para publicar. Sin idiomas configurados, la publicación se realiza sin distribución. -
Sustitución, al compilar. La página de cada idioma se renderiza insertando la cadena traducida en la ubicación de cada marcador. En cuerpos Astro/JSX, el elemento
<T …>…</T>completo se reemplaza por el texto traducido; el contenedor se elimina, así que mantén las clases y atributos en un elemento padre. En cuerpos HTML, el atributodata-t-keyse elimina y el texto del elemento se reemplaza; el elemento sobrevive, así que coloca el atributo directamente en el elemento con estilo. Una clave que falte en el mapa de una variante recurre al valor predeterminado original; las páginas nunca se renderizan con huecos.
La selección del analizador es automática y es un escaneo de texto plano, no consciente del análisis: un cuerpo que comienza con frontmatter ---, o que contiene la secuencia de caracteres <T seguida de espacio en blanco, / o > en cualquier lugar (incluso dentro de una cadena de JavaScript o un comentario HTML), se analiza como Astro/JSX; cualquier otra cosa se analiza como HTML y debe usar data-t-key. En cuerpos Astro/JSX, cada byte fuera de las ubicaciones de los marcadores se envía exactamente como se escribió (un cuerpo con cero marcadores pasa sin cambios). Los cuerpos de HTML plano se vuelven a serializar a través de un analizador HTML compatible con estándares durante la sustitución: el significado de tu marcado se conserva, pero los detalles de formato como las comillas de los atributos pueden normalizarse.
La regla de oro: nunca traduzcas manualmente una página personalizada y nunca crees copias por idioma. Una página fuente con marcadores es todo el flujo de trabajo. Las copias hechas a mano (landing más una landing-es traducida manualmente) producen dos páginas no relacionadas: sin canonical_slug compartido, sin enlaces hreflang, sin selector de idioma y una segunda página que se desvía silenciosamente cada vez que editas la primera.
Cuándo usarlo
- Estás enviando una página de aterrizaje, de producto o de marketing en un proyecto con más de un idioma y quieres que cada idioma se genere al publicar.
- Estás convirtiendo una exportación de Lovable, v0 o Bolt y necesitas saber qué cadenas envolver antes de subir.
- Estás añadiendo un idioma a un proyecto y quieres que las páginas personalizadas existentes aparezcan en él.
- Una página localizada muestra frases en el idioma original y necesitas encontrar las cadenas no marcadas.
- Una página necesita una imagen diferente por idioma (capturas de pantalla localizadas, carteles con texto).
Referencia de marcadores
Formato Astro/JSX: <T>
Dos formas equivalentes de declarar el texto predeterminado (idioma original):
<h1><T key="hero.title" default="Your storefront, in every language" /></h1>
<p><T key="hero.subtitle">One upload. Every locale. No hand-translation.</T></p>keyes obligatorio y debe ser un literal de cadena estático.<T key={expr}>es rechazado.- El valor predeterminado proviene del atributo
default(solo literal de cadena estático), o del texto interno estático.<T key="x" default="…" />funciona; el cierre automático sindefaultes rechazado por no tener valor predeterminado. - El contenido interno debe ser texto plano: las expresiones (
{…}), elementos anidados y fragmentos dentro de un marcador son rechazados. <T>dentro de otro<T>es rechazado.- En la ruta JSX, un
<Tdentro de un literal de cadena de JavaScript se analiza solo como una cadena; nunca se extrae como marcador. Pero la selección del analizador ocurre antes del análisis, como un escaneo de texto plano: la secuencia<Tseguida de espacio en blanco,/o>en cualquier lugar del cuerpo —incluyendo dentro de una cadena de script— dirige todo el cuerpo al analizador JSX. En un cuerpo HTML, mantén esa secuencia fuera de los scripts (divídela como'<' + 'T 'si realmente la necesitas), o el cuerpo cambiará al analizador JSX y fallará.
Formato HTML: data-t-key
<h1 data-t-key="hero.title">Your storefront, in every language</h1><a class="cta" href="/signup/" data-t-key="hero.cta">Start free</a>- El contenido de texto del elemento es el valor predeterminado; debe ser no vacío.
- El elemento marcado debe envolver solo texto plano; los elementos anidados dentro de él son rechazados.
- Un elemento
data-t-keydentro de otro elementodata-t-keyes rechazado. - Los comentarios HTML no pueden contener marcadores.
Reglas de nomenclatura de claves
Las claves deben coincidir con ^[a-z][a-z0-9_.]*[a-z0-9]:
- comenzar con una letra minúscula, terminar con una letra minúscula o dígito;
- caracteres interiores: letras minúsculas, dígitos, guiones bajos, puntos;
- mínimo dos caracteres: las claves de un solo carácter son rechazadas.
Usa puntos y guiones bajos para crear espacios de nombres: hero.title, pricing.tier_pro.cta. La misma clave puede aparecer varias veces solo con un valor predeterminado idéntico; las cadenas repetidas (un botón “Reservar demo” usado tres veces) se desduplican en una sola entrada y obtienen una traducción consistente. La misma clave con dos valores predeterminados diferentes es rechazada.
El texto no marcado se envía literalmente
Este es el error de traducción número uno. La sustitución reescribe las ubicaciones de los marcadores y nada más: cualquier copia visible que no hayas marcado se traslada a cada idioma exactamente como se escribió. No se activa ninguna advertencia; el síntoma es una página en español con frases en inglés. Antes de subir, lee el cuerpo una vez haciendo una sola pregunta por cadena: “¿debería un lector francés ver esto en francés?” Si es así, márcalo.
Los valores independientes siguen la misma prueba: marca los valores que contienen palabras —“Hasta 60 minutos”, “Lun–Vie”— o se leerán como texto en el idioma original en cada página localizada. Deja los valores puramente numéricos y de unidades —“1.7 L”, “8:00–18:00”— sin marcar: se envían literalmente, lo cual es correcto para la notación universal. Los números dentro de las frases marcadas se conservan mediante la traducción.
Localización de texto controlado por scripts
Los marcadores no pueden vivir dentro de <script>; ningún analizador extrae contenido de scripts. Mantén cada cadena traducible en un elemento DOM marcado y deja que JavaScript controle la visibilidad en lugar de producir prosa: renderiza cada estado de mensaje como su propio elemento marcado y alterna atributos hidden, y haz que los scripts inyecten números puros en ranuras no marcadas (<span data-result></span>) junto al texto de etiqueta marcado. Nunca construyas frases en JS; los fragmentos concatenados se traducen como fragmentos y el orden de las palabras no sobrevive entre idiomas.
Nombres accesibles y texto de atributos
El texto de los atributos —alt, aria-label, placeholder— no puede llevar marcadores y se envía sin traducir a todos los idiomas, así que prefiere texto marcado visible para las etiquetas. Donde un elemento realmente necesite un nombre accesible, usa aria-labelledby apuntando a un elemento marcado, para que el nombre se traduzca con el resto de la copia; marca el arte puramente decorativo como aria-hidden="true" para que no necesite nombre. Mantén el texto de atributo inevitable corto y neutral respecto al idioma.
Idioma original: el argumento language
languagedeclara el idioma del cuerpo y los valores predeterminados de los marcadores. Es opcional y toma por defecto el idioma principal del proyecto.- Debe ser uno de los idiomas configurados del proyecto.
- Al volver a subir una página existente,
languagese ignora: la página mantiene su idioma original.
URLs, canonical_slug y hreflang
canonical_slugtoma por defecto elslugy es la identidad duradera que agrupa todas las variantes de idioma de una página. Configúralo una vez; sobrevive a republicaciones y cambios de slug.- Patrón de URL: el idioma original/predeterminado se sirve en
/{canonical_slug}/; todos los demás idiomas en/{locale}/{canonical_slug}/. El slugindexapunta a la raíz del idioma (/y/{locale}/). - Nunca escribas un prefijo de idioma en el slug; el enrutamiento lo añade. El slug almacenado de cada variante de idioma es igual al
canonical_slug. - Los alternativos hreflang (incluyendo
x-default, que apunta a la URL del idioma predeterminado) se generan tan pronto como dos o más variantes de idioma activas comparten uncanonical_slug. Nada de esto necesita configuración.
Medios localizados: data-t-media y media_variants
Los marcadores de texto traducen cadenas; los marcadores de medios intercambian activos por idioma:
<img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/hero-en.jpg" data-t-media="hero.image" width="1200" height="800" alt="Dashboard with English labels" loading="lazy">"media_variants": { "hero.image": { "es": "https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/hero-es.jpg" }}data-t-media="<key>"en un elemento<img>,<video>o<source>declara una ranura de medios localizable. Elsrcen línea es el valor predeterminado para cada idioma sin una anulación; no se extrae texto para los marcadores de medios.media_variantses{ "<key>": { "<locale>": "<URL absoluta>" } }. Cada clave debe coincidir con un marcadordata-t-mediaen el cuerpo, y cada URL debe ser absolutahttp(s).- Las URLs de medios se escriben, nunca se traducen automáticamente; llegan a cada idioma exactamente como las escribiste.
- Usa
upload-mediapara alojar imágenes; devuelve URLs de Cloudinary conf_auto,q_autoya aplicados. - El atributo
data-t-mediase elimina de la página renderizada.
Lo que nunca se traduce
- El marcado del cuerpo en sí; cada variante de idioma lleva el cuerpo idéntico; solo los valores de los marcadores difieren al renderizar.
slugycanonical_slug.- URLs de
media_variants. schema_org_type,og_image,speakable_selector,published_date,updated_date.mcp_resourcepermanece solo en la página original; las variantes de idioma lo eliminan.- La etiqueta de la barra lateral se elimina en las variantes, por lo que los menús localizados recurren al título traducido.
Traducido: title, description (recortado a 60/160 con puntos suspensivos si es necesario) y los valores de translatable_strings. Si una traducción automática necesita corrección, modifica esa cadena por idioma en la pestaña Traducciones del Modo Edición; borrar un valor recurre al valor predeterminado original.
Ejemplo
Un proyecto con modules.custom_pages.locales = ["en", "es", "de"] y idioma predeterminado en. El cuerpo:
<section class="hero"> <h1 data-t-key="hero.title">Coffee subscriptions, delivered weekly</h1> <p data-t-key="hero.subtitle">Freshly roasted beans from small farms, at your door every Monday.</p> <a class="cta" href="/signup/" data-t-key="hero.cta">Start your subscription</a> <img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/hero-en.jpg" data-t-media="hero.image" width="1200" height="800" alt="A bag of freshly roasted coffee beans" data-hero></section>La subida:
{ "project_id": "8fK2hQbV3yTGr1Lw9XcD", "project_name": "Brew & Co", "slug": "subscriptions", "title": "Weekly coffee subscriptions", "description": "Freshly roasted specialty coffee delivered to your door every week. Pause or cancel anytime.", "body_source": "<section class=\"hero\">…</section>", "media_variants": { "hero.image": { "es": "https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/hero-es.jpg" } }}La respuesta informa "markers_extracted_count": 3 (las tres claves de texto) y una página borrador. Después de publish → publish-confirm:
- los tres valores de cadena, más el título y la descripción, se traducen automáticamente a
esyde; - el sitio sirve
/subscriptions/(inglés),/es/subscriptions/y/de/subscriptions/, enlazados mediante hreflang conx-defaulten la URL en inglés; - la página en español muestra la anulación
hero-es.jpg; el alemán recurre alhero-en.jpgen línea; - el marcado del cuerpo de los tres es idéntico; solo difieren las cadenas marcadas y la ranura de medios.
Errores comunes
Frases no traducidas en una página localizada. No es un mensaje de error, es el síntoma de una copia no marcada. La cadena se envía literalmente en cada idioma. Envuélvela en un marcador, vuelve a subir, vuelve a publicar.
Marker extraction failed (1 error(s)): … — la subida fue rechazada; cada línea numerada lleva el desplazamiento, línea, columna, clave y motivo. Corrige cada marcador listado y vuelve a subir. Los motivos más comunes siguen.
Invalid marker key 'a'. Keys must match /^[a-z][a-z0-9_.]*[a-z0-9]$/ (lowercase, digits, underscore, dot).Las claves necesitan al menos dos caracteres, un primer carácter en minúscula y un último carácter alfanumérico.
Duplicate marker key 'hero.cta' with conflicting default. Existing default: 'Book a demo'. New default: 'Get started'. Use one default per key.Reutiliza una clave solo para texto idéntico; de lo contrario, dale a la segunda cadena su propia clave.
language 'fr' is not in project.locales [en, es]. Add it to modules.custom_pages.locales first, or omit `language` to use the default_locale ('en').Configura el idioma en el módulo custom-pages (PUT /modules/custom_pages con locales), luego vuelve a subir. Añadir el idioma también distribuye las páginas personalizadas activas existentes como borradores.
media_variants key 'hero.image' has no matching data-t-media marker in the page body. Add data-t-media="hero.image" to the <img|video|source> element, or remove the override.Cada clave de media_variants debe coincidir con un atributo data-t-media en el cuerpo.
Copias de página traducidas a mano. Síntoma: /landing/ y una /landing-es/ hecha a mano viven ambas, sin hreflang entre ellas, sin selector de idioma y con desviaciones en cada edición. Elimina la copia, configura el idioma en el módulo y deja que la publicación genere la variante.