Cómo automatizar la traducción de archivos .po en tu pipeline de CI/CD

Tu equipo fusiona cambios a main catorce veces al día. Cada una de esas fusiones podría añadir una nueva cadena orientada al usuario: la etiqueta de un botón, un mensaje de error, una descripción emergente (tooltip). Y cada una de esas cadenas comienza su vida solo en inglés. En algún punto de tu proceso de lanzamiento, un humano tiene que notar las nuevas cadenas, exportarlas, conseguir que se traduzcan, pegarlas de vuelta y recompilar. Ese humano es un cuello de botella, y en un equipo que lanza rápido, ese cuello de botella significa que tus usuarios alemanes ven marcadores de posición en inglés durante dos sprints.
La solución es automatizar la traducción en tu pipeline de CI/CD para que las nuevas cadenas se traduzcan en la misma fusión que las introdujo, sin la intervención humana en la ruta crítica para el caso rutinario. Esto es totalmente posible hoy en día, pero solo si evitas la trampa en la que caen la mayoría de los equipos: crear llamadas a LLM sin procesar que corrompen silenciosamente tus marcadores de posición %s y la estructura de tus archivos .po. Esta guía te muestra un patrón realista de pipeline de CI, un esqueleto funcional de GitHub Actions y las decisiones de diseño —idempotencia, traducir solo lo nuevo, compuertas de revisión— que distinguen una pipeline que ayuda de una que genera versiones defectuosas.
Por qué la traducción manual es un cuello de botella en los lanzamientos
La respuesta a "¿por qué no traducir simplemente antes de cada lanzamiento?" es que los tiempos nunca coinciden. Los desarrolladores añaden cadenas continuamente, pero la traducción se realiza en lotes por una persona diferente en un calendario distinto. La brecha entre estas dos cadencias es donde se acumula tu deuda de localización.
El problema de las dos cadencias
Imagina el flujo manual. Un desarrollador añade __( 'Export to CSV', 'mytextdomain' ) y fusiona. Nadie regenera el archivo .pot. Dos semanas después, alguien ejecuta wp i18n make-pot, nota cuarenta nuevas cadenas sin traducir (algunas de desarrolladores que ya se fueron de vacaciones), adivina la intención de la mitad de ellas y envía un archivo .po a un traductor. La traducción regresa, se pega, y tal vez los marcadores de posición sobrevivieron o tal vez no. Mientras tanto, se lanzaron tres versiones con esas cadenas en inglés.
Cada paso ahí es manual, en lotes y propenso a errores. El objetivo de la automatización de CI/CD es transformar esto en algo que se ejecuta automáticamente al fusionar, traduce solo lo que realmente cambió y falla de manera notoria cuando algo parece incorrecto, convirtiendo la traducción de una tarea periódica en una parte invisible del pipeline.
El patrón del pipeline: Extraer, Diferenciar, Traducir, Confirmar
A un alto nivel, un trabajo de traducción automatizado ejecuta cuatro pasos al fusionar a main: regenerar la plantilla, detectar qué es nuevo, traducir solo esas cadenas y confirmar los resultados. Todo el proceso debe ser idempotente: ejecutarlo en una fusión sin cambios en las cadenas debe producir cero diferencias y cero ruido.
Extraer y Diferenciar
El primer paso es la extracción. Regenera el archivo .pot desde el código fuente actual para que refleje cada cadena en la base de código:
wp i18n make-pot . languages/myplugin.pot
El segundo paso es la diferenciación. Esta es la decisión de diseño más importante en todo el pipeline. No quieres volver a traducir cada cadena en cada fusión; eso desperdicia llamadas a la API, corre el riesgo de volver a traducir cadenas que un humano ya corrigió y produce enormes diferencias de revisión. En su lugar, fusionas el archivo .pot fresco en cada archivo .po existente y traduces solo las entradas que ahora están vacías (las cadenas realmente nuevas):
# Merge new template into each locale, preserving existing translations.
# Newly-added strings appear with empty msgstr; --backup=none keeps the tree clean.
for po in languages/*.po; do
msgmerge --update --backup=none "$po" languages/myplugin.pot
done
Después de msgmerge, solo las cadenas completamente nuevas tienen un msgstr vacío. Todo lo que se tradujo previamente queda intacto. Esa propiedad es lo que hace que el pipeline sea idempotente y lo que mantiene tus diferencias de revisión pequeñas y revisables.
Traducir y Confirmar
El tercer paso envía solo esas entradas vacías a un paso de traducción. El cuarto paso confirma el archivo .po actualizado, compila el .mo y regenera cualquier JSON. A continuación, conectaremos todo eso a GitHub Actions.
Un esqueleto de GitHub Actions
Aquí está la respuesta a "¿cómo se ve esto realmente en un archivo de flujo de trabajo?": un trabajo que se activa al hacer push a main, que ejecuta el ciclo de extraer-diferenciar-traducir-confirmar y abre una pull request con los resultados en lugar de confirmar directamente en main.
El archivo de flujo de trabajo
name: Translate on merge
on:
push:
branches: [ main ]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install WP-CLI and gettext
run: |
sudo apt-get update && sudo apt-get install -y gettext
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp && sudo chmod +x /usr/local/bin/wp
- name: Regenerate template and merge into locales
run: |
wp i18n make-pot . languages/myplugin.pot
for po in languages/*.po; do
msgmerge --update --backup=none "$po" languages/myplugin.pot
done
- name: Translate only new (empty) strings
env:
TRANSLATE_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
run: ./scripts/translate-new-strings.sh languages/
- name: Compile .mo and JSON
run: |
wp i18n make-mo languages/
wp i18n make-json languages/ --no-purge
- name: Open pull request with translations
uses: peter-evans/create-pull-request@v6
with:
branch: chore/auto-translations
title: "chore: auto-translate new strings"
commit-message: "chore: auto-translate new strings"
Dónde se oculta el trabajo real
El paso translate-new-strings.sh es donde ocurre la traducción real. La versión ingenua de ese script lee cada entrada vacía, la envía a una API de LLM una cadena a la vez y pega la respuesta de vuelta. Esa versión ingenua es exactamente la trampa, y vale la pena ser explícito sobre el porqué.
Por qué las llamadas a LLM hechas a mano rompen tus archivos
La respuesta corta: las llamadas directas a LLM tratan tus cadenas .po como prosa, y tus cadenas .po no son prosa; son datos estructurados con marcadores de posición, formas plurales y contexto que una llamada de autocompletado de chat destruye alegremente.
La corrupción de marcadores de posición que no verás en las pruebas
Envía Deleted %d of %s files a un endpoint de chat general y podrías recibir Supprimé %d des %s fichiers (bien) o Supprimé %d de % s fichiers (un espacio se coló en el marcador de posición, y ahora sprintf() lanza un error en tiempo de ejecución). Envía una entrada plural y el modelo puede fusionar dos formas en una, rompiendo idiomas con más de dos categorías plurales. Envía una cadena con <a href="%s"> y el modelo puede traducir la URL o eliminar la etiqueta. Ninguna de estas fallas aparece en tus pruebas a menos que pruebes específicamente la salida renderizada en cada configuración regional; aparecen como errores en tiempo de ejecución en producción para usuarios de los que no puedes leer informes de errores.
La trampa del mantenimiento
Puedes intentar defenderte de esto con ingeniería de prompts y post-procesamiento con expresiones regulares, y muchos equipos lo hacen. El problema es que ahora estás manteniendo un motor de traducción frágil como proyecto secundario, redescubriendo cada caso extremo de marcador de posición que el ecosistema Gettext ya resolvió. Catalogamos las formas específicas en que las variables se deforman —y cómo prevenirlas— en traducir archivos PO sin romper las variables de código. La lección aquí se aplica directamente: la calidad del modelo rara vez es el problema; el manejo estructural a su alrededor sí lo es.
Integrando un traductor en la nube impulsado por API en CI
Aquí es donde un servicio de traducción impulsado por API reemplaza tus conjeturas de translate-new-strings.sh. En lugar de crear llamadas a LLM y expresiones regulares de post-procesamiento a mano, tu paso de CI sube el archivo .po modificado a un servicio que ya entiende la estructura de Gettext y devuelve una salida limpia. La forma del pipeline permanece idéntica —extraer, diferenciar, traducir, confirmar—, pero el paso intermedio frágil se convierte en una única llamada a la API.
La llamada a la API que reemplaza tu script
SimplePoTranslate está construido exactamente para esto. Expone una API en la nube adecuada para la automatización y CI, por lo que el paso de traducción de tu flujo de trabajo se convierte en una solicitud que entrega el archivo .po y recibe uno traducido, sin bucles por cadena. Su Syntax Locking mantiene %s, %1$s, {count}, HTML y tokens de código en su lugar automáticamente; toda la clase de errores de la sección anterior es manejada por el motor en lugar de por expresiones regulares que tú mantengas. Con soporte completo para plurales de Gettext y msgctxt, las formas plurales y el contexto sobreviven el viaje de ida y vuelta, lo cual una llamada de autocompletado de chat no puede garantizar.
La idempotencia y la compuerta de revisión siguen siendo tuyas
Dos decisiones de diseño aún te pertenecen, independientemente del motor que uses. Primero, la idempotencia: sigue traduciendo solo las entradas vacías después de msgmerge, para que las fusiones que no hacen nada no produzcan diferencias. Segundo, una compuerta de revisión: haz que el trabajo abra una pull request, como lo hace el esqueleto anterior, en lugar de confirmar directamente en main. La traducción automática es excelente para poner las cadenas en vivo rápidamente, pero una mirada humana antes de la fusión detecta el raro error de contexto, y una PR te da esa mirada sin bloquear el caso común. Los equipos que manejan muchos locales o muchos sitios de clientes reconocerán esta forma de el flujo de trabajo de localización ideal para agencias, donde el mismo patrón de automatizar-luego-revisar se escala a través de docenas de proyectos.
Conclusión
Para automatizar la traducción en CI/CD sin enviar versiones rotas, el patrón es consistente: regenera el archivo .pot al fusionar, msgmerge para incorporarlo en cada local para que solo muestre las cadenas nuevas, traduce solo esas cadenas y confirma el resultado detrás de una compuerta de revisión de pull request. La idempotencia mantiene tus diferencias limpias; la compuerta de revisión evita que la rara mala traducción llegue a producción.
La parte a hacer bien es el propio paso de traducción. Las llamadas a LLM hechas a mano corromperán los marcadores de posición y las formas plurales de maneras que tus pruebas no detectarán, y pasarás más tiempo manteniendo el 'pegamento' de traducción que el código de la característica que sirve. Un motor impulsado por API con Syntax Locking elimina todo ese modo de falla, por lo que tu pipeline traduce nuevas cadenas en la misma fusión que las introdujo, y tus usuarios no anglófonos dejan de ver marcadores de posición en inglés.
¿Listo para automatizar la traducción en tu pipeline sin romper los marcadores de posición? Prueba SimplePoTranslate gratis — no se requiere tarjeta de crédito. Comienza en el nivel gratuito, valida la API con tus propios archivos
.poe intégrala en CI cuando estés listo.