Commits

Secciones que hablan todos los idiomas, campos a medida y el parser que detenía las traducciones

La noche de secciones con nombre y slug por idioma en todas las webs, campos personalizados al estilo ACF, el chat que dejaba citas en 404 y el parser JSON que hacía fallar todas las traducciones de artículos con código.

Retomé a medias una sesión que se había quedado sin créditos: la capa de datos de las traducciones de sección ya estaba esbozada (modelo JSONB, esquemas, migración) y faltaba todo su uso real. La noche acabó con secciones que hablan todos los idiomas en todas las webs, campos personalizados al estilo WordPress y, ya por la tarde, el flujo de traducciones automáticas reparado.

Secciones con nombre y slug por idioma

Hasta ahora cada sección mostraba siempre el nombre y el slug del idioma en el que se había creado, tuviera la web los idiomas que tuviera. Ahora cada sección guarda traducciones {idioma: {name, slug}} editables desde el panel; la API valida idioma, patrones y colisiones entre todos los slugs de todas las secciones, y la web pública sirve el slug del idioma solicitado con 301 desde el original, filtra los artículos por idioma, declara hreflang y publica un sitemap con una URL por idioma y sus alternativas. Lo apliqué en la plantilla, en BeCoach y en la web de presentación, y después lo llevé a escandell.cat con su estilo propio, que ya consumía las secciones del panel pero sin nombre ni slug por idioma. La release 0.1.45 del núcleo salió con todo esto y las builds de becoach y web-base-astro quedaron en verde; en escandell.cat basta con push, build e imagen porque ya filtraba por idioma en el cliente.

Campos personalizados al estilo ACF

La petición era clara: el patrón de ACF de WordPress. Cada sección define sus campos (portfolio → cliente, año, galería...) y los artículos de la sección los rellenan, para que un diseño a medida pueda extraerlos de la API pública. Acordamos el alcance antes de tocar nada: valores en los artículos, conjunto completo de tipos —texto, texto largo, número, fecha, URL, imagen y select, al que después añadí lista de textos y sí/no— y por ahora solo exponerlo en la API, sin render en la plantilla. De los detalles técnicos me quedo con un traspiés de Python: SectionService define un método list, de manera que anotar un método posterior con list[dict] resolvía al método y hacía explotar la importación. Resuelto con from __future__ import annotations, que el proyecto ya usa.

El panel vuelve a casa y el chat deja de dar 404

Dos fricciones rápidas de uso: al editar un artículo de sección, todos los botones de volver llevaban siempre a Artículos, el listado del blog. Ahora cada sección vuelve a su propio listado, y con el blog general apagado la entrada desaparece del menú y del buscador. Más grave fue el chat público: las citas acababan en 404 porque la API construía siempre /blog/{slug}/ — ruta que en escandell.cat no existe — y además mezclaba artículos de otros idiomas en las respuestas y contestaba en castellano por defecto. Ahora el endpoint acepta language y section, filtra el contexto a artículos publicados del idioma solicitado y construye las citas con el slug traducido de cada idioma, sin prefijo en el idioma principal.

Una incidencia que queda abierta

Diagnosticando el chat, el de web-base-astro.escandell.cat responde 504: su uso «chat» apunta a Groq y la cuota está agotada, con los reintentos sobrepasando el timeout del gateway. No es código nuevo, es configuración: hay que recargar cuota o cambiar el proveedor en Ajustes → IA del panel. Pendiente de hacer.

El parser JSON que detenía las traducciones

Por la tarde llegó el reporte de traducciones fallando sistemáticamente, sobre todo en inglés. La causa raíz no era la del informe: el parser hacía split("```") para quitar el cierre del bloque de código de la respuesta, pero si el artículo contenía bloques de código, el JSON correcto del modelo llevaba cierres dentro del valor y el parser lo cortaba siempre → 502. Todo artículo técnico era intraducible, aunque el modelo respondiera perfecto. Reparé el parser para que solo quite el cierre envolvente, reescribí el prompt de idioma (que llegaba literal, sin interpolar), añadí reintentos con response_format json, detección de traducciones truncadas y vacías, y en el planificador un enfriamiento por artículo e idioma con espera creciente, en lugar de quemar el presupuesto de cada ciclo con los mismos artículos caídos para siempre.