feat: herramientas de detección y traducción incremental - #206
Open
oidacra wants to merge 28 commits into
Open
Conversation
El tooling de este repo deriva de angular/angular-ja, que desde entonces resolvió varios problemas que aquí seguían abiertos. Este commit toma lo aplicable y agrega la detección de traducciones desactualizadas, que angular-ja no cubre. update-origin - Falla si un objetivo de copia no coincide con ningún archivo, en vez de omitirlo en silencio. Es la causa raíz del desfase que #192 corrigió a mano: las rutas cambiaron upstream y nadie se enteró. - Cada objetivo se glob-ea por separado para poder validarlo individualmente. - Excluye readmes de apps de ejemplo y páginas índice sin prosa, siguiendo el criterio de angular-ja. No se excluye kitchen-sink.md, que aquí sí está traducido. - Elimina las copias en inglés de los archivos ahora excluidos: el build superpone adev-es sobre origin, así que dejarlas congelaría esas páginas en una versión vieja en vez de dejar pasar la de origin. check-translations (nuevo) - Detecta traducciones desactualizadas comparando el .en.md del commit donde se tradujo por última vez contra el actual. No necesita metadata: el historial de git ya lo contiene. - Detecta archivos sin traducir (sin .en.md), equivalente al list-untranslated de angular-ja. - Separa cambios de prosa del ruido de formato para que el reporte sea accionable. - Acepta --ref para auditar una rama o un PR antes de mergearlo. lint-glossary (nuevo) - Verifica consistencia terminológica con reglas en glosario.yml, mismo formato expected/pattern que el prh.yml de angular-ja. - Ignora bloques de código, código en línea, enlaces y anchors {#id}. - Solo términos inequívocos: una regla con falsos positivos hace que el linter se ignore.
La detección solo servía si alguien se acordaba de correrla y leer la terminal. Este commit la convierte en trabajo visible y reclamable, para poder repartirlo entre varias personas. El modelo es híbrido, según la naturaleza de cada problema: - Sin traducir → un único issue de tracking con checkboxes agrupados por sección. Son muchos y de baja rotación; un issue por cada uno sería ruido. Cada entrada trae un link que pre-rellena un issue de declaración, así solo se abre uno cuando alguien de verdad va a tomar el archivo. - Desactualizados → un issue individual por archivo, con el diff del original incluido. Son pocos y cada uno es una unidad de trabajo concreta, asignable y cerrable. El workflow corre en push a main y también en eventos de issues, para que el tracking refleje los reclamos sin esperar al siguiente push. Detalles que importan: - checkout usa fetch-depth: 0 porque la detección compara el .en.md de commits anteriores. Con el default (depth 1) no encontraría nada. - checkout NO trae el submódulo: el .en.md ya guarda el original, así que no hace falta clonar angular/angular en CI. - El script es idempotente y los diffs se recortan a 12k caracteres, por debajo del límite de GitHub para cuerpos de issue. Verificado con un dry-run sobre los datos reales del repo, simulando el ciclo completo: creación, reejecución sin cambios, declaración de un contribuidor, traducción completada y cierre automático.
Los prefijos de alerta son claves del tokenizer de adev, no prosa. adev/shared-docs/pipeline/shared/marked/extensions/docs-alert.mts declara un enum con las claves en INGLES y construye el matcher desde Object.keys(AlertSeverityLevel). Verificado contra angular/angular al SHA que tenemos fijado (47a7396) y probado con el regex reconstruido: NOTE: hola -> matchea, renderiza la caja NOTA: hola -> NO matchea IMPORTANTE: hola -> NO matchea ÚTIL: hola -> NO matchea Nuestro corpus tiene 423 avisos con el prefijo traducido, que hoy se renderizan como párrafo plano en vez de caja de color: ÚTIL 137, NOTA 109, IMPORTANTE 81, CONSEJO 74, RESUMEN 16, CRÍTICO 5, PREGUNTA 1. La causa es nuestro propio skill, que instruía traducirlos. angular-ja, con 10 años en esto, mantiene la clave en inglés y traduce solo el cuerpo: HELPFUL: これは、一般的なランタイムエラー... Cambios: - El skill ahora manda dejar el prefijo en inglés, con la evidencia y el ejemplo de angular-ja. - 7 reglas nuevas en glosario.yml que detectan los prefijos traducidos. El linter marca 410; los 13 restantes viven dentro de bloques de código, donde el enmascarado los ignora correctamente. Además, el Paso 1 del skill era destructivo. Hacía `cp archivo.md archivo.en.md` sin condición: aplicado a un archivo YA traducido pero desactualizado, escribe español sobre el .en.md y destruye el único registro de qué inglés se tradujo, dejando ese archivo indetectable para check-translations para siempre. Ahora exige comprobar primero si el .en.md existe. Esto NO arregla los 423 callouts existentes: evita que sigan apareciendo. El arreglo del corpus va después de que mergee #192, para no colisionar con sus 685 archivos. También se versiona .claude/skills/ (antes solo vivía en un portátil) y se ignora settings.local.json, que tiene rutas absolutas personales.
Convierte "este archivo está desactualizado" en "aplica estos N cambios concretos", sin retraducir el archivo. Probado end-to-end sobre los dos archivos que hoy están desactualizados de verdad. El principio es leer mucho y escribir poco. La orden de trabajo NO recorta el contexto: manda leer los dos documentos completos, porque la traducción existente es la mejor referencia sobre qué terminología y registro usa ESE documento. Lo acotado es la escritura: una edición por bloque, sobre texto copiado verbatim. tools/blocks.mjs - Segmenta markdown en bloques sin partir nunca fences ni contenedores <docs-*>. Maneja las tres formas del corpus, incluida la autocerrada con atributos multilínea. - Validado contra los 345 pares: 98.3% comparten esqueleto de encabezados y número de bloques. - 20 tests. Escribirlos ya valió la pena: destaparon que un fence de 4 backticks se cerraba con el ``` interno, que habría partido bloques en los archivos que muestran markdown dentro de markdown. tools/plan-translation.mjs - Calcula el baseline como el commit más reciente que tocó .md Y .en.md, no solo el .md. Sin eso, un commit de mantenimiento sobre la traducción mueve la referencia y el trabajo pendiente desaparece en silencio. - Expande cada hunk al bloque que lo contiene y localiza el bloque español por ancla de sección más posición, confirmándolo con tres verificaciones. Si alguna falla, marca manual y no entra: nunca adivina. - Rechaza como reestructuración lo que supere 8 bloques cambiados. tools/verify-translation.mjs - AISLAMIENTO: toda línea modificada debe caer en un bloque autorizado. Convierte "no rompas el resto del archivo" de promesa del prompt en invariante verificable, que es justo lo que angular-ja no tiene. Usa las coordenadas del lado viejo del diff, que corresponden exactamente a los rangos registrados: sin heurísticas de desplazamiento. - Estructura, anclas y glosario. - El glosario se verifica SOLO sobre las líneas tocadas. Verificar el archivo entero hace fallar cada edición por deuda que el autor no introdujo, y un chequeo que siempre falla acaba ignorado. Lo encontramos ejecutándolo: selectors.md fallaba por un CONSEJO: de la línea 3. Resultado sobre los dos archivos desactualizados de hoy: selectors.md listo 1 bloque de 37 drag-drop.md manual 42 bloques cambiados, es reestructuración El caso completo se ejecutó de verdad —editar, verificar, y comprobar que el aislamiento detecta una edición fuera de rango— y luego se revirtió el contenido: #192 también modifica selectors.md, así que cualquier cambio al corpus ahora es un conflicto. Este commit es solo herramientas. Nota para la revisión de #192: mergea 406 callouts con el prefijo traducido, que no se renderizan como caja de aviso.
Porta a check-translations el arreglo que ya tenía plan-translation. El baseline era "último commit que tocó el .md". Con esa regla, cualquier commit de mantenimiento sobre la traducción —un typo, un enlace— adelanta la referencia, y todo el inglés que había cambiado antes deja de contarse: el archivo pasa a "sincronizado" sin que nadie lo haya traducido. Ya ocurrió con 01c0889 (chore: remove Twitter/X references), que tocó varios .md sin sus .en.md. Ahora exige el commit más reciente que tocó AMBOS archivos, resuelto en una sola pasada de git log sobre las dos rutas. Si no hay commit conjunto, cae al commit que dio de alta el .en.md: desde ahí, todo cambio del original es trabajo pendiente. Efecto medible: el baseline de selectors.md pasa de ea3e52e a ca450f6. Aquí los blobs del .en.md coinciden entre ambos commits, así que el resultado no cambia; pero en cuanto un update-origin caiga entre medias, sí cambiaría, y en la dirección peligrosa. Además, los dos `continue` mudos pasan a un bucket `skipped` visible. Un archivo que no se pudo analizar NO está sincronizado, y dejarlo caer en silencio era peor que ruidoso: syncStale habría cerrado su issue con "la traducción ya está al día".
…ctos check-translations reconocía dos estados. Faltaban tres, y los tres pasaban por buenos: el archivo se reportaba como sincronizado o no aparecía en absoluto. SIN RESPALDO — el .md está en español pero no tiene .en.md. El próximo update-origin lo trata como no traducido y le escribe inglés encima. Es pérdida de trabajo, no deuda pendiente, así que va primero en el reporte y con el comando para recuperarlo. Se distingue por detección de idioma (palabras funcionales), no por adivinar. DESPAREJADA — existe el .en.md pero no su .md. El bucle recorría solo los .md, así que estos archivos eran invisibles. Salió al verificar lo demás: guide/i18n/translations-files.en.md tiene un typo (translations en plural) y por eso nunca emparejó con translation-files.md. Ese typo explica por qué la traducción llevaba meses sin respaldo. #192 lo arregla con un rename. HUÉRFANA — el original ya no existe upstream. update-origin nunca borra —verificado, no hay ni un rm ni un unlink— así que la página sigue publicada en español y su .en.md se compara consigo mismo para siempre. Requiere el submódulo; si no está inicializado se avisa en vez de dar el chequeo por bueno. No poder comprobarlo no es lo mismo que no tener huérfanas. Además, el listado incluye ahora los archivos NO trackeados sobre el árbol de trabajo. Con ls-tree, las páginas recién copiadas por update-origin eran invisibles: el reporte decía que todo estaba bien justo en el momento con más trabajo pendiente. Verificado con fixtures: origin simulado con 347 páginas omitiendo una —detecta esa y solo esa—, y archivo sin commitear —pasa de invisible a listado—. La primera versión del fixture usaba el glob '**/*.en.md', que se salta los archivos en la raíz de content; la herramienta no tiene ese bug porque lista por prefijo de ruta.
La comprobación de huérfanas solo recorría los .en.md, así que una página eliminada upstream que nunca tuvo respaldo caía en "sin traducir". Es decir, el tracking issue le pedía voluntarios a la comunidad para traducir páginas muertas: las 7 guías de pipes y guide/defer.md, todas eliminadas en el original. Ahora la comprobación se hace sobre el .md y antes que nada, tenga respaldo o no. Sin .en.md acababa en "sin traducir"; con .en.md se contaba como sincronizada para siempre, porque update-origin nunca vuelve a tocar un archivo que ya no existe en el original. Efecto sobre el corpus real, con el submódulo inicializado: antes después sincronizadas 345 338 sin traducir 14 7 huérfanas 7 14 Validado contra el PR #192, que hizo esta misma limpieza a mano: las 14 huérfanas que detectamos están entre las 16 que el PR elimina. Las 2 restantes (cli/index.md, router-tutorial.md) siguen existiendo en el origin al SHA que tenemos fijado, así que no son huérfanas todavía: el PR las elimina por reestructuración de v22.1. Dos de las 14 son mudanzas, no borrados: upstream movió typed-forms de reference/migrations a guide/forms, y convirtió tools/devtools.md en una carpeta. Para el detector un renombrado es borrado más alta, que es justo lo que hay que revisar a mano.
update-origin copia 8 objetivos; check-translations solo miraba src/content/**/*.md. Los otros se traducen igual, se sincronizan igual, y nadie comprobaba si se habían desactualizado: core/layout/navigation/navigation.component.en.html core/layout/footer/footer.component.en.html features/home/home.component.en.html routing/sub-navigation-data.en.ts Son el menú, el pie de página, la portada y la estructura de rutas: lo primero que ve cualquier visitante. La causa raíz era que cada herramienta tenía su propia idea de qué está en juego. Ahora la lista vive en tools/lib/targets.mjs y la usan las dos, así que no pueden discrepar. De paso, la manipulación de rutas .en.* pasa a ser genérica por extensión en vez de asumir .md. Los 4 archivos resultaron estar sincronizados, pero eso no se sabía: no se comprobaba. Además, con el submódulo ya disponible pude validar por primera vez los objetivos de copia contra el origin real, y el guard que añadimos en c5bb460 encontró uno roto: navigation-entries/index.ts no existe en el SHA fijado. Lo añadí yo al portar las correcciones de #192, sin caer en que esa ruta llega con v22.1. Queda comentado; #192 lo reintroduce junto al bump, que es donde es válida. Esa validación pasa a ser un test permanente (tools/targets.test.mjs), que se salta solo si el submódulo no está inicializado. Un objetivo desalineado deja de descubrirse a mitad de un sync.
La regla `señal(?!es)` marcaba también señalar, señalización, señalan y
Señala: cuatro casos en el corpus, todos español correcto.
Hoy solo era ruido en un reporte. Pero la fase 4 del plan quiere --fix, y
ahí la regla habría reescrito "señalización" como "signalización" y
"señalar" como "signalar", corrompiendo texto correcto en un pase masivo
sobre 200 archivos.
La causa es sutil y merece quedar escrita: el \b de JavaScript se define
sobre [A-Za-z0-9_], así que la ñ y las vocales acentuadas cuentan como
separadores y los límites de palabra caen donde no deben. Por eso los
patrones usan (?<!\p{L}) … (?!\p{L}), que sí es Unicode. Los patrones ya se
compilaban con la bandera u, así que no hace falta nada más.
Hallazgos: 738 → 734, y los 4 que desaparecen son:
zone-pollution.md:3 señalización
structural-directives.md:216 señalar
lifecycle-and-events.md:14 Señala
first-app/intro/README.md:28 señalan
La lógica pura (mask + lintText) se mueve a tools/lib/glossary.mjs para
poder testearla, con 13 tests que cubren el enmascarado, este bug concreto,
y dos invariantes del propio glosario: que toda regla compile y traiga su
motivo, y que ninguna marque su propia forma correcta —que con --fix sería
un bucle infinito.
Dos problemas que se descubren al preguntarse por el orden de los pasos.
1. El detector leía HEAD, no el árbol de trabajo. update-origin deja los
.en.* modificados sin commitear, así que al correr check-translations
justo después el reporte decía "todo sincronizado" — en el momento con
más trabajo pendiente. Obligaba a un orden no evidente: update, commit,
check; y saltarse el commit daba un falso OK en silencio.
Sobre HEAD ahora se compara con git hash-object del archivo y el diff se
pide sin segundo extremo, así que incluye lo no commiteado. El orden deja
de importar. Con --ref se sigue leyendo el commit, que es lo correcto
para auditar una rama.
2. El reporte solo listaba problemas, así que era imposible saber qué
entraba en el alcance. Cuando la interfaz del sitio estuvo fuera durante
meses, la salida se veía idéntica a una correcta. Ahora encabeza con:
Vigilando 366 archivos: 361 md · 3 html · 2 ts
Si un tipo desaparece de esa línea, se nota.
Verificado modificando un .en.html, un .en.ts y un .en.md sin commitear:
los tres aparecen ahora como desactualizados y desaparecen al restaurar.
…ieza La regla es que un archivo de adev-es solo es válido si traduce algo que existe en origin en la misma ruta, si es su respaldo .en.*, o si es un recurso localizado al que apunte un parche. Todo lo demás sobra. La comprobación de huérfanos solo recorría lo que está en copyTargets, así que veía las páginas pero no los recursos. Ahora recorre todo adev-es, que es la regla completa. Aparece un archivo más: una textura de la portada traducida en abril de 2024 (feat(homepage): translate build for everyone texture) que quedó atrás cuando upstream rediseñó la home. Ya no existe el original ni ninguna referencia; lleva casi dos años sin uso. Para eso hizo falta que listOrigin liste TODOS los archivos del original y no solo los que se copian: la pregunta es "¿esta ruta existe upstream?", y limitarla a los objetivos daba falsos positivos con cualquier archivo que viva fuera de ellos. Por qué importa, más allá del orden: BUILD.bazel compila todo el markdown de src/ con un glob, sin comprobar si hay ruta que lo alcance. Son 82 KB de HTML inalcanzable viajando a Firebase en cada deploy. Y sus .en.* se comparan consigo mismos para siempre, así que 7 de estas páginas se contaban como sincronizadas. Comprobado que no se muestran: el único caso dudoso era tools/devtools, que sí está en el menú español, pero su entrada usa contentPath 'tools/devtools/overview' — resuelve desde la carpeta nueva, no desde nuestro tools/devtools.md. El workflow abre ahora un único issue de limpieza con todos, no uno por archivo: borrarlas es un PR de limpieza, no trabajo repartible. Se actualiza solo y se cierra cuando no queda ninguna. Avisa de que un renombrado se ve igual que un borrado —ruta vieja huérfana más página nueva sin traducir— y que ahí hay que trasladar la traducción, no tirarla.
Auditar el PR #192 destapó dos bugs del modo --ref, ambos introducidos al generalizar el alcance. 1. El alcance salía del disco, no de la rama. scopeFromTargets glob-eaba el árbol de trabajo, así que los archivos que la rama AÑADE quedaban fuera: contaba 350 archivos y 6 sin traducir cuando son 388 y 38. Justo las páginas nuevas de v22.1, que es lo que más interesa de un PR de bump. Ahora el alcance se decide con matchesTarget, que trabaja sobre cadenas y no toca el disco, aplicado a la lista de archivos de la referencia. 2. Las huérfanas se calculaban contra el origin montado, que corresponde a HEAD. El PR sube de v21.0.1 a v22.1, así que todo lo que esa versión añadió aparecía como huérfano: 39 inventadas. Ahora, si la referencia apunta a otro origin, el chequeo no se hace y se dice por qué. Mejor no responder que responder mal — es el mismo criterio que con el submódulo sin inicializar. También, la detección de idioma leía del disco; en una rama esos archivos no están en el árbol de trabajo y reventaba con ENOENT en ai/agent-skills.md. Ahora lee el contenido de la referencia. matchesTarget entiende **, * y la negación con !, y una exclusión manda sobre cualquier inclusión. Reemplaza al glob de disco también para HEAD, así que ambos modos comparten exactamente la misma definición de alcance.
github.rest.issues.listForRepo devuelve TAMBIÉN los pull requests, y este repo tiene PRs abiertos titulados «translate: …» con la etiqueta docs-translation: #159, #101, #98, #97, #96. Dos consecuencias, la segunda destructiva: - syncTracking los tomaba por declaraciones de trabajo y marcaba archivos como reclamados por un PR que no los reclama. - syncStale cierra los issues que ya no aplican. Cerrar un PR por esa vía lo cierra de verdad, y el bot habría ido cerrando pull requests de contribuidores. Se filtran con !i.pull_request. Además, para poder probar el workflow sin arriesgar el repo: - Modo simulación. El workflow_dispatch trae un input dry_run que viene activado por defecto, así que la primera ejecución manual registra qué haría y no escribe nada. Hay que desmarcarlo a propósito para que actúe. - Tests del script, 9 casos sobre un repo de mentira: creación, idempotencia, cierre al resolverse, reclamo por un contribuidor, el recorte del diff bajo el límite de GitHub, la simulación, y los dos casos de pull requests. Comprobado que fallan si se quita el filtro. npm test cubre ahora también .github/scripts: 47 tests.
El repo ya la hace a mano, y mejor: #185-#190 agrupan las páginas nuevas de v22.1 por sección, con títulos en español y prefijo de versión. Entre esos seis issues y #197 cubren 34 de los 38 archivos que detectamos. Un tracking issue generado habría duplicado ese trabajo y competido como segunda fuente de verdad. Y como esos issues no llevan la etiqueta docs-translation, el bot ni los habría visto: habría creado duplicados sin enterarse. Agrupar por sección es una decisión editorial —qué va junto, cómo se llama, qué versión lo trae— y un script no la acierta. Se elimina el workflow, su script, sus tests y la plantilla de issue. La detección se queda: es lo que produce la lista con la que se componen esos issues a mano. Queda en el historial por si alguna vez se quiere recuperar. UPDATE-ORIGIN.md pasa a documentar el flujo manual, y de paso corrige la descripción del detector, que seguía hablando de dos estados cuando ya distingue cinco.
Estaba comentado porque la ruta no existía en el origin de v21.0.1 y habría hecho fallar el guard. Con el merge de #192 el submódulo pasa a v22.1, donde sí existe, y el test de objetivos lo confirma.
Salen de analizar los 72 issues del historial, no de suposiciones. Lo que muestran los datos: - 67 de 72 (93%) son de traducción. El resto es mantenimiento suelto: actualizar un enlace, el copyright, escribir una convención de commits. - El título dominante es «Traducir - <Sección>», con una variante para los bumps de versión: «[Angular 22.1] Traducir guías de Signal Forms». - El cuerpo evolucionó de una línea señalando una carpeta (#107, #127) a una lista de casillas por archivo (#185-#190). La segunda forma es mejor: permite repartir el trabajo y ver el avance. Las plantillas la adoptan. - 12 issues (17%) no llevan etiqueta, incluidos los seis más recientes. Los formularios la aplican solos, que es la mitad de su valor. Tres formularios: - traducir.yml el caso dominante, con lista de archivos - actualizar-traduccion.yml para las 31 desactualizadas, que no tenían convención porque hasta ahora no se detectaban - error-traduccion.yml para quien lee y encuentra una errata Cada uno lleva al final los comandos concretos del flujo, incluida la advertencia de no repetir el `cp` sobre un archivo que ya tiene .en.md. config.yml deja habilitados los issues en blanco: ese 7% de mantenimiento no encaja en ninguna plantilla y forzarlo sería peor. Con tests, porque un formulario con el esquema mal cae en silencio: GitHub lo ignora y ofrece un issue en blanco. Solo se nota cuando alguien abre uno y no trae ni etiqueta ni estructura.
Las otras dos eran especulativas y los datos del repo lo dicen claro. Reportar erratas: cero issues en 72. Nunca ha pasado. Una plantilla dedicada sería una entrada más en el selector, restándole visibilidad a la única que se usa de verdad. Los issues en blanco siguen habilitados, así que quien quiera reportar una puede; si algún día se vuelve habitual, se añade. Actualizar desactualizadas: también cero, y hay una razón de fondo para no darle plantilla propia. El repo AGRUPA — #185-#190 abren un issue por sección, no por archivo. Con las 31 desactualizadas pasaría lo mismo, y ahí la estructura del issue es idéntica a la de traducir: una lista de archivos con casillas. Lo que cambia es el procedimiento, no el formulario. Así que se colapsa en traducir.yml con un desplegable de tipo, y las instrucciones de ambos caminos van al final, separadas. Una sola entrada en el selector, que es lo que corresponde a un repo donde el 93 % de los issues son lo mismo.
npm run check-translations -- --issues La regla sale de cómo agrupa el repo: por carpeta. El matiz está en qué hacer con las que solo aportan un archivo, y sin resolverlo el agrupado no sirve para nada: las 31 desactualizadas daban 31 grupos de uno, porque cada paso de tutorial vive en su propia carpeta (tutorials/learn-angular/steps/11-optimizing-images/README.md). Se resuelve subiendo de nivel: un grupo que no llega al mínimo cede sus archivos al padre, y se repite hasta que nadie pueda subir más. Lo que llega a la raíz sin agrupar queda como misceláneas. Esto reemplaza al caso especial para tutoriales que había prototipado, y da el mismo resultado sin nombrar a nadie. Sobre el pendiente de hoy: sin traducir 38 archivos → 7 issues desactualizadas 31 archivos → 4 issues Los 7 de traducir reproducen casi exactamente los que ya existen a mano (#185-#190 y #197), que es la señal de que la regla capta la convención real. El comando no crea nada: escribe los borradores. El título lo pone una persona, porque nombrar la sección —«Guías de Errores», no «reference/errors»— es una decisión editorial que un script no acierta. Por eso cada borrador sale marcado con «← renombra esto». En los desactualizados se añade el conteo de líneas de prosa por archivo, que sale gratis del detector y distingue de un vistazo el trabajo de dos minutos del de media hora. Con 11 tests, incluidos los invariantes que importan: que ningún archivo se pierda ni se duplique al subir de nivel, y que no entre en bucle con rutas sin carpeta.
El agrupado ya lo calcula check-translations --issues. Lo que un script no acierta es nombrar cada lote y escribir sus criterios, y de eso trata el skill. La convención de títulos sale del historial, no de suposiciones. Cuatro patrones: Traducir - Guías de <X> una carpeta (Guías de Errores, de SSR) Traducir - Guía de <X> un documento (Guía de Seguridad, Zoneless) Traducir - Tutorial <X> un tutorial (Tutorial Signals) Traducir - <Nombre> nombre propio (Press Kit, Roadmap) Con prefijo de versión cuando las páginas las trae un bump: «[Angular 22.1] Traducir guías de Signal Forms». La regla para nombrar la sección es la misma del glosario: descriptor en español, nombre de producto o API en inglés. Y hay una referencia mejor que cualquier regla escrita — navigation-entries/index.ts, donde esos nombres ya están decididos. De ahí salen los ejemplos: «Enciclopedia de Errores» traduce el descriptor, «Estado dependiente con linkedSignal» deja la API en inglés. Los criterios de aceptación son verificables con un comando, a propósito. «La traducción suena natural» no se puede marcar como cumplido sin discutir; «lint-glossary no reporta problemas» sí. Y son distintos para traducir que para actualizar, porque el trabajo es otro. Dos salvaguardas que importan: - Comprobar qué issues existen ANTES de crear nada. El repo los mantiene a mano y ya cubre buena parte del pendiente; duplicarlos es peor que no crearlos. - Enseñar los borradores y esperar confirmación antes de ejecutar gh issue create. Es una acción visible para toda la comunidad y un lote mal agrupado hay que cerrarlo a mano después.
Claude Code espera .claude/skills/<nombre>/SKILL.md, no archivos .md sueltos. Con el formato viejo ninguno de los cuatro se registraba, así que translate-angular-docs y batch-translate llevaban tiempo sin poder invocarse pese a estar escritos y mantenidos. Se detectó al intentar usar el skill nuevo: comparando con los que sí funcionan en ~/.claude/skills, la única diferencia era la ubicación. El frontmatter ya era correcto. Tras mover, tres de los cuatro se registran. translate-angular-docs sigue sin aparecer, pero por otra causa y ajena al repo: está en "off" dentro de skillOverrides en la configuración global del usuario. Se puede seguir invocando a mano; solo está desactivado para invocación por el modelo. También se corrigen los enlaces entre skills, que apuntaban al archivo plano.
Salió de revisar si el glosario del skill (217 filas) debería pasar a glosario.yml (18 reglas). Midiendo qué pasaría, aparecieron términos ingleses "colados en prosa" que en realidad venían de rutas dentro de atributos: href="tools/cli/deployment", path="src/overview/basic/app.ts". No es un problema con las reglas de hoy —solo desaparece 1 hallazgo real, un `librería-` dentro de un atributo— pero sí lo sería con cualquier regla futura sobre una palabra que aparezca en una ruta. El linter ya enmascaraba código, enlaces markdown y anchors; los atributos y las definiciones de enlace de referencia eran el hueco que quedaba. Con tests que comprueban lo que importa: que el texto visible junto a un atributo se siga revisando.
El commit anterior enmascaraba TODOS los atributos y con eso introdujo un falso negativo: <docs-callout title="Nombrando tu librería"> dejaba de revisarse, cuando ese título se renderiza y sí debe estar traducido. Se vio al mirar qué hallazgo había desaparecido — el único que quitó el cambio era precisamente uno legítimo. Ahora solo se enmascaran los atributos que llevan rutas o identificadores (href, src, path, region, id, class, language…), y quedan fuera title, header, alt y label. El criterio es el mismo que ya sigue el corpus: 237 de 237 `<docs-step title>` están traducidos. Los hallazgos vuelven a 748, que era el número correcto.
Los issues llevaban la metodología dentro: qué es un delta, los comandos, los criterios. Eso no cambia entre issues, así que repetirlo en diez es ruido y obliga a editar diez sitios cuando cambie. En angular-ja el cuerpo del issue es mínimo justamente por esto: el cómo vive en la guía. Se añaden dos secciones con ancla para poder enlazarlas: - #respaldo — por qué el .en.md no es opcional y cuándo NO crearlo. Corrige además el consejo anterior, que decía que las traducciones parciales no necesitan respaldo: es exactamente lo que dejó translation-files.md sin protección durante meses, a un update-origin de perderse. - #actualizar — el flujo de delta, con las tres respuestas que puede dar plan-translation y qué hacer con cada una. - #antes-del-pr — las dos comprobaciones y el porqué de commitear .md y .en.md juntos. La tabla de comandos incorpora las seis herramientas nuevas, que no estaban. Los diez issues abiertos quedan recortados a lo que de verdad es suyo: de dónde salen las páginas y qué archivos son. El resto se enlaza.
Los criterios de aceptación se movieron aquí desde los issues: nadie marca casillas en un issue, pero sí las revisa al mergear. El tamaño lo decide la práctica actual: los diez PRs de traducción del historial tienen cuerpos de ~10 caracteres, literalmente «Fixes #171». Una plantilla larga se borraría, así que son cuatro comprobaciones y un campo de notas — siete líneas visibles, dos más que la de angular-ja. Las cuatro son verificables y cada una responde a un fallo real observado: - .md y .en.md en el mismo commit: separarlos deja el archivo marcado como desactualizado de forma permanente, porque la detección busca el commit que tocó ambos. Es el origen del falso positivo de selectors.md. - Prefijos de alerta en inglés: son claves del tokenizer de adev, no prosa. Traducirlos hace que el aviso salga como párrafo plano; hay 425 así. - Los dos comandos de la guía. El tercero dice «en lo que toqué» a propósito: el repo arrastra 748 hallazgos de glosario, así que exigir el linter limpio sobre todo el corpus sería imposible de cumplir. Los comentarios HTML explican el porqué de cada una sin ocupar sitio en el PR renderizado.
Un archivo sin respaldo puede estar pendiente o ya adaptado, y distinguirlo importa: lo segundo es pérdida de trabajo inminente. Se hacía con detección de idioma, que no sabe clasificar un archivo sin prosa. links.ts pasaba por pendiente cuando en realidad ya está adaptado —apunta al GitHub de Angular Hispano y a su Discord— y le falta el .en.ts. El próximo update-origin lo habría devuelto a los enlaces de Angular. La lógica de copia es agnóstica a la extensión, así que esto no era exclusivo de .md. Comparar contra el original es exacto y vale para cualquier extensión. Sobre los 38 que se reportaban como pendientes: 37 son idénticos al original —pendientes de verdad— y 1 difiere, que es precisamente links.ts. La detección de idioma se conserva como respaldo para cuando el submódulo no está inicializado o se audita otra rama. También se borra adev-es/src/assets/textures/construir-para-todos.png: una textura de la portada traducida en abril de 2024 que quedó huérfana cuando upstream rediseñó la home. Verificado que no existe upstream y que nada la referencia.
links.ts está adaptado a Angular Hispano —nuestro GitHub, nuestro Discord— pero no tenía .en.ts. update-origin lo trataba como pendiente y le habría escrito encima los enlaces de Angular, deshaciendo la adaptación en medio de un commit de cientos de archivos. El respaldo se crea desde origin/adev, NO copiando el archivo actual: debe contener el inglés del que se partió, no la versión ya adaptada. Copiar el adaptado habría dejado un respaldo que no respalda nada. Con esto, «sin respaldo» baja a cero. Queda abierto el #205 para las divergencias menores que salieron al compararlos: la cabecera de licencia que se perdió, tres claves vacías que ninguna plantilla usa, y la indentación.
Sigue la convención que usa el propio angular/angular, que tiene su AGENTS.md en la raíz con frontmatter `trigger: always_on` —lo que esperan Windsurf y Antigravity— y mantiene el archivo corto enlazando a la documentación en vez de duplicarla. CLAUDE.md y GEMINI.md son punteros de tres líneas para las herramientas que buscan un nombre propio. No duplican nada: cualquier cambio va en AGENTS.md, y un test comprueba que sigan siendo punteros y no copias. El contenido es lo que un agente necesita saber y no puede deducir del código: que adev-es es una capa de traducción sobre un overlay, que el respaldo .en.* es lo único que protege una traducción de la próxima sincronización, y las cuatro reglas que rompen cosas si se ignoran —entre ellas los prefijos de alerta, que son claves del tokenizer y no prosa. Con tests, porque un agente no duda del documento: si AGENTS.md nombra un comando que ya no existe o un ancla que se movió, actúa sobre información falsa sin que nada avise. Se verifica que los `npm run` existan, que los enlaces y anclas resuelvan, y que los prefijos de alerta sigan estando en el enum de adev — si upstream añade o quita uno, salta aquí.
.agents/skills/<nombre>/SKILL.md es la ubicación neutra —la que lee Antigravity— y encaja con AGENTS.md, que ya es la convención común para las instrucciones. Claude Code espera .claude/skills, pero usa exactamente el mismo formato, así que basta un enlace simbólico. Git lo guarda como un solo objeto (modo 120000): cero duplicación y nada que sincronizar. Añadir un skill lo hace aparecer en ambas rutas a la vez. Se descarta el soporte para Gemini. Su formato es distinto —TOML con description y prompt, no SKILL.md— así que habría hecho falta generarlos y mantener un comando de sincronización. No se necesita, y el coste no se justificaba. Nota sobre portabilidad: en Windows los enlaces simbólicos exigen core.symlinks activado. Si a alguien no le resuelve .claude/skills, los skills siguen accesibles en .agents/skills.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Herramientas para saber qué falta traducir, qué se desactualizó, y aplicar los
cambios del original sin retraducir el archivo entero.
Nace de una pregunta concreta: cuando Angular cambia una página ya traducida,
¿cómo se detecta y cómo se actualiza sin rehacer el trabajo?
Qué se detecta y cómo
npm run check-translationsreconstruye el estado desde el historial de git,sin metadata adicional. Como
update-originsobrescribe el.en.mdcon elinglés nuevo, ese archivo en el commit donde se tradujo es el inglés vigente
entonces; compararlo con el actual da exactamente lo que falta.
Distingue cinco estados. Los tres últimos eran silenciosos: esos archivos se
contaban como correctos.
.mdCubre también
.tsy.html, que se traducen igual —el menú, el pie, laportada— y no se vigilaban.
Traducción incremental
Para una traducción desactualizada,
plan-translationcalcula qué bloques hayque tocar y
verify-translationcomprueba que no se tocó nada más.El principio es leer mucho y escribir poco: el modelo lee el documento
completo en ambos idiomas —la traducción existente es la mejor referencia de
terminología y registro que hay— pero solo edita los bloques declarados.
La verificación de aislamiento convierte «no rompas el resto del archivo» de
promesa del prompt en invariante comprobable. Sobre los dos archivos
desactualizados de entonces:
selectors.mdsalía como 1 bloque de 37, ydrag-drop.mdcomo reestructuración, que el sistema rechaza en vez de fingirprecisión.
Cambios en el flujo existente
update-originfalla si un objetivo de copia no coincide con nada, en vezde omitirlo en silencio. Es la causa raíz del desfase que feat: update origin to Angular v22.1 #192 corrigió a
mano. La lista de objetivos pasa a
tools/lib/targets.mjs, compartida con eldetector para que ambos no puedan discrepar.
sus copias: el build superpone
adev-essobreorigin, así que dejarlas lascongelaba en una versión vieja.
CONTRIBUTINGdocumenta el flujo y corrige el consejo de que las traduccionesparciales no necesitan
.en.md. Es exactamente lo que dejótranslation-files.mdsin protección durante meses.Corrección de contenido
El skill de traducción mandaba traducir los prefijos de alerta. Son claves del
tokenizer:
docs-alert.mtsconstruye su matcher desde un enum con las clavesen inglés, así que
NOTA:no coincide y el aviso se renderiza como párrafoplano. Hay 425 así, recogidos en #204.
El skill ya no lo hace, y el linter los detecta. Este PR no toca el corpus.
Terminología
lint-glossaryaplicaglosario.ymlignorando código, enlaces, anchors yatributos de ruta. Cada regla lleva su motivo.
Se mantiene deliberadamente pequeño: se midieron los candidatos del glosario del
skill contra el corpus y la mayoría daría falsos positivos —
paquetesonpaquetes npm en 218 casos,
fragmentoson fragmentos de código en 88—. Unlinter que falla siempre acaba ignorado.
Skills
Los cuatro estaban en formato plano y ninguno se cargaba: Claude Code espera
<nombre>/SKILL.md. Se corrige, y se añadetranslate-deltapara el flujoincremental y
crear-issues-traduccion, cuya convención de títulos sale delhistorial del repo.
Verificación
63 tests (
npm test), sobre lo que puede romperse en silencio: el parser debloques, el agrupado, las reglas del glosario, y que los objetivos de copia
coincidan de verdad con el original.
El detector se validó contra #192 antes de mergearse: marcó 31 desactualizadas y
14 huérfanas, y las 14 coincidían una a una con las que su autor había
encontrado a mano.
No incluye
Ninguna automatización de CI ni creación automática de issues: se decidió que
los issues se crean a mano, agrupados por sección, porque nombrarlos es una
decisión editorial. Tampoco toca el contenido traducido.