Construyendo un escaneo de posts consciente de idioma para blog-manager
Tengo una app Rails llamada blog-manager que escanea mis dos sitios de Astro en busca de posts nuevos, los importa, y los sindica a Medium y Dev.to. Ha sido plana todo este tiempo: cada post vive directamente en src/content/blog/<slug>.md, un archivo por post, sin subdirectorios.
Eso está por cambiar. Quiero traducciones al español de mis posts, y el plan que salió de un spike hace unos días se decidió por un enfoque del lado de git: canónico en inglés en src/content/blog/en/<slug>.md, hermano en español en es/<slug>.md, escritos en el mismo commit para que cualquier divergencia aparezca en los diffs en vez de esconderse en alguna base de datos. blog-manager y la sindicación se quedan en inglés para siempre, solo necesitan saber dónde buscar una vez que los posts se muevan a en/.
Este issue era el trabajo previo necesario: enseñarle al scanner a encontrar posts en el layout nuevo, enseñarles a las skills de publicación a escribir ahí, y construir una verificación de CI que detecte una traducción faltante. Nada de la migración real del sitio pasa acá, eso es un trabajo aparte y más grande. Esto solo se asegura de que blog-manager no se rompa cuando eso ocurra.
El cambio en el scanner
El núcleo del scanner de blog-manager es una llamada a la GitHub Contents API: listar los archivos en src/content/blog/, comparar contra lo que hay en la base de datos, hacer upsert de lo que cambió, y soft-delete de lo que desapareció. Simple, y funcionó bien durante un año.
Mi primer intento de conciencia de idioma fue: sondear primero src/content/blog/en/. Si existe y tiene archivos markdown, escanear eso en vez del directorio plano. Si no, volver al plano, sin cambios. Dos modos de falla que ya había pensado: un directorio en/ faltante lanza un 404 de GitHub, que tuve que atrapar en vez de dejarlo propagarse (el handler discard_on de mi job trata un 404 no atrapado como un blog roto y lo marca como failed), y un directorio en/ que existe pero todavía está vacío necesitaba el mismo fallback, o una migración a medio terminar se vería como si se hubieran borrado todos los posts.
Escribí pruebas para ambos casos. Todo pasó. Abrí el PR.
Lo que atrapó la revisión de código
Corro una revisión de múltiples ángulos antes de mergear cualquier cosa a este repo, ocho pasadas independientes sobre el diff buscando bugs de corrección, código redundante, y violaciones de convención. Cuatro de ellas, trabajando de forma independiente, convergieron en el mismo hallazgo: mi lógica de “preferir en/, volver al plano” asumía que la migración era todo o nada. En el momento en que en/ tuviera aunque sea un archivo, dejaba de mirar el directorio plano por completo.
Eso está mal para el plan real. Mi archivo de 166 posts no se va a mover a en/ de una sola vez, son demasiados archivos para traducir y prefiero hacerlo gradualmente, quizás empezando solo con posts nuevos y rellenando los viejos después, si es que me tomo la molestia. Lo que significa que por un tiempo, en/ va a tener algunos posts y el directorio plano va a tener otros, al mismo tiempo, a propósito. Mi scanner habría hecho soft-delete en silencio de cada post que siguiera en el directorio plano la primera vez que moviera aunque fuera un solo post a en/. No era pérdida de datos permanente, ya que los posts se autorrestauran si su archivo reaparece, pero habrían desaparecido de mi dashboard y habrían quedado fuera de la sindicación hasta terminar una migración que podría tomarme meses.
El arreglo fue dejar de tratar un en/ no vacío como totalmente autoritativo y en cambio unirlo con el listado plano, en/ gana si un slug aparece en ambos, pero nada de lo ya migrado al plano se pierde solo porque empecé a mover archivos. Es un cambio de código pequeño. Encontrarlo no fue pequeño, hicieron falta cuatro ángulos de revisión distintos aterrizando de forma independiente en el mismo bug antes de confiar lo suficiente para reescribir la lógica central y las pruebas alrededor de ella.
Dos de esas cuatro pasadas de revisión también atraparon algo que se me había pasado por un motivo muy distinto: había escrito comentarios de código nuevos con rayas largas, lo cual viola una regla de formato que tengo para literalmente todo lo que escribo, incluido el código. Atrapado por la misma pasada automatizada, en la misma revisión, justo al lado del bug de corrección real. Distinta gravedad, misma lección: una pasada de revisión no sabe qué hallazgo importa más hasta que se revisa.
Un bug que no escribí
Mientras conectaba la verificación de CI a la configuración de pre-commit de mi blog principal, había una sesión de agente completamente aparte activa en el mismo directorio de trabajo, publicando una función de feed RSS. Ninguna de las dos usaba un git worktree, la convención de ese proyecto es “trabajar directamente en main, sin ramas paralelas,” lo cual es seguro siempre y cuando sea realmente cierto que solo una cosa toca el repo a la vez.
Esta vez no lo fue. Había agregado una línea a package.json, un script de npm que apuntaba a mi nuevo script de verificación de traducción, y la dejé sin commitear por unos minutos mientras probaba cosas. La otra sesión commiteó sus propios cambios con un git add simple y arrastró mi línea junto con ellos. Nada se rompió, la línea es correcta y funciona, pero ahora git log -- package.json se la atribuye a un commit sobre un feed RSS que no tiene nada que ver con traducciones. No reescribí el commit ya mergeado, eso se sintió como más riesgo del que justificaba una línea mal atribuida. Lo anoté en cambio, para atraparlo la próxima vez antes de que pase en vez de después.
Lo que sigue
El scanner y las skills de publicación están terminados y mergeados. La migración real, mover mi archivo a en/, levantar el enrutamiento de i18n de Astro, y publicar traducciones reales al español, es trabajo aparte, empezando con mi sitio más chico como piloto antes de tocar el más grande.
Lecturas relacionadas
Propagación de campos compartidos hacia las traducciones al español
Un 'sincronizar esto al español' de un solo clic que reutiliza el código de diffing existente, se salta la sha de confirmación a propósito, y se niega a escribir commits que no cambian nada.
Modo de traducción para blog-manager
Un editor de vista dividida donde las traducciones comparten metadata pero son dueñas de su propia prosa, un modelo de obsolescencia de dos shas, y un bug de formato que copié de mi propio código anterior.
El bug de normalización que solo aparece con etiquetas hechas de nada
Un normalizador basado en strip se topa con una etiqueta de puro signo de puntuación: string vacío como clave de hash, sustitución de etiqueta equivocada, y un autocompletado que hace match con todo. Tres síntomas, una sola causa raíz.