# Guía de traducción (EN/FR/IT/PT) en autoservicio — para Inma / Mixbot

> **Para quién es este documento:** para Inma y Mixbot, a partir de la conversación del
> 2026-08-31 en Buzz: además del audio ([#222](https://gitea.feadulta.com/rafa/feadulta/issues/222),
> ver `docs/guia-tts-audio-autoservicio-inma-mixbot.md`), Rafa confirmó que la traducción se
> traspasa también. El endpoint para enlazar traducciones (PR #225) se hizo bajo el paraguas
> del propio #222.
>
> **Motores autorizados por Rafa (2026-08-31):** un agente local con Haiku desde el propio
> Claude/Cowork de Inma (no la API de pago de Rafa — es otra cuenta, otro caso), o MiniMax
> con el mismo token ya compartido para el audio. **Gemma queda fuera para vosotros**: es un
> modelo local que corre en la GPU del PC de Rafa, no accesible desde fuera.

---

## 0. Crear y enlazar la traducción — ya resuelto

Ya existe un endpoint que crea el post traducido **y** lo enlaza con Polylang (idioma +
grupo de traducción) en una sola llamada — no hace falta tocar nada de bajo nivel:

```
POST https://www.feadulta.com/wp-json/fea/v1/crear-traduccion
Authorization: Basic <usuario:contraseña_de_aplicación>
Content-Type: multipart/form-data

es_id=<ID del post ES>
lang=en|fr|it|pt
title=<título traducido>
content=<HTML traducido>
excerpt=<opcional>
status=draft|publish   (default draft)
model=<opcional, solo trazabilidad — p.ej. "minimax" o "haiku-local">
```

Respuesta (`201` si crea, `200` si ya existía — **es idempotente**, repetir la llamada con el
mismo `es_id`/`lang` nunca duplica):

```json
{"es_id": 56786, "lang": "en", "translation_id": 56787, "created": true,
 "url": "https://www.feadulta.com/en/?p=56787"}
```

Por detrás replica exactamente lo que hacía `scripts/fea_translate_helper.php::create` en
local: asigna el idioma antes de las categorías, mapea cada categoría del ES a su
equivalente traducida (o la deja en español si no existe traducción de esa categoría),
preserva el resto del grupo de traducciones si el ES ya tenía otros idiomas, y añade los
metas de trazabilidad (`traduccion_automatica`, `traduccion_origen`, `traduccion_modelo`,
`traduccion_fecha`).

Verificado por Claudix el 2026-08-31 con una prueba real de extremo a extremo en producción
(post ES desechable → traducción EN creada → confirmado con `pll_get_post_translations` que
el grupo quedó `{es: ..., en: ...}` — todo borrado después).

---

## 1. Las reglas de traducción (igual las use vuestro Claude/Haiku o MiniMax)

Esto es el prompt real que usa el pipeline de Rafa (`scripts/translate_post.py`), verificado
palabra por palabra el 2026-08-31 contra el código — usadlo tal cual, no lo resumáis:

> Eres un traductor profesional de textos religiosos cristianos (espiritualidad y teología
> católica). Traduce del español al {idioma destino}. REGLAS ESTRICTAS:
> 1. Conserva EXACTAMENTE el marcado HTML (etiquetas y atributos) y los shortcodes entre `[ ]`
>    y `{ }`. No los traduzcas ni los reordenes.
> 2. NO traduzcas las referencias bíblicas ni sus abreviaturas (p. ej. "Jn 3, 16", "Isaías 5,
>    1-7", "Mt 5"). Déjalas idénticas.
> 3. Conserva los nombres propios de persona y lugar (salvo exónimos establecidos).
> 4. Glosario fijo: "Fe Adulta" → "Fe Adulta" (en/fr/it/pt, no se traduce); "EFFA" → "EFFA"
>    (igual, en los 4 idiomas).
> 5. Traducción FIEL: no resumas, no añadas, no comentes.

Para el **título**, además: que quede natural en el idioma destino (no lo dejéis en español
salvo que sea un nombre propio o una marca), y si es un modismo, traducid el sentido, no lo
calquéis literal.

## 2. El "learning" del título — dadle el primer párrafo como contexto

Esto es lo que pedía Rafa explícitamente documentar. **Aviso de precisión:** este cambio
existe como commit (`4475ebb`, rama `feat/issue-200-title-context`) pero **no está fusionado
a `main`** — verificado el 2026-08-31 comparando el histórico. O sea: si alguna vez volvéis a
usar `scripts/translate_post.py` tal cual desde main, el título se traduce SIN este contexto.
Para vuestro proceso propio (agente Haiku o llamada directa a MiniMax) aplicad la técnica de
todas formas, es la que ya dio mejor resultado en pruebas:

- **Traducid primero el cuerpo del artículo, el título al final** — necesitáis el cuerpo ya
  traducido (o al menos leído) para poder darle contexto al título.
- Al traducir el título, añadid al prompt el **primer párrafo del cuerpo en español**
  (recortado a 2000 caracteres), como contexto de significado — no se traduce ni se incluye
  literal en la salida, solo ayuda al modelo a entender modismos o dobles sentidos del
  título que sin ese contexto salen mal traducidos.
- Instrucción exacta añadida al prompt del título: *"Si es un modismo, interpreta su sentido
  según el contexto, no lo calques."*

## 3. Troceo de artículos largos

Si usáis MiniMax para traducir (no Haiku, que admite el artículo entero de una vez gracias a
su contexto de 200k), trocead el HTML por párrafos completos (nunca a mitad de una etiqueta)
en bloques de ~5.000 caracteres, y unid las traducciones de cada trozo en el mismo orden.

## 4. QA — checklist antes de dar una traducción por buena

Reformulado en REST puro (el script original, `gate_mec2.php`, corre dentro de WP y usa
funciones de Polylang no disponibles desde fuera — pero las comprobaciones sí son igual de
válidas mirando el JSON de cada post por la API):

Para cada origen ES y cada idioma que hayáis traducido, comparad contra el post ES:

1. **Cobertura**: los 4 idiomas presentes (o los que toque — Pagola nunca lleva traducción
   automática, ver §5).
2. **Estado**: la traducción en `draft` hasta que se revise (salvo que el ES ya esté
   `publish` y la carta lo pida — ver `docs/guia-publicacion-carta-inma.md`).
3. **Sin residuos de generación**: buscar literalmente en el `content` devuelto por la API
   cadenas como `</strong>**`, `</strong></strong>`, `&lt;&gt;`, `<<<INI>>>`, `<<<FIN>>>`,
   `|||FIN`, o cualquier resto de las marcas del prompt.
4. **Sin alfabetos ajenos**: ningún carácter árabe/cirílico/CJK en el resultado (residuo
   típico de generación que se cuela cuando el modelo "piensa" en otro idioma).
5. **Mismo nº de párrafos** que el ES (contar apariciones de `<p` en el HTML). Un desajuste
   suele significar que el modelo se comió contenido en el troceado.
6. **Mismo nº de enlaces `<a href>`** que el ES — **es el check que más falla y el más
   importante**: en la carta 739, el italiano perdió 15 de 39 enlaces en uno de los trozos
   troceados mientras EN/FR salieron bien, y nada posterior lo detecta si no lo comprobáis
   aquí. Si hay menos enlaces en la traducción que en el ES, **no la deis por buena**.
7. **Referencias bíblicas equivalentes**: extraer patrones tipo `capítulo,versículo` de
   ambos textos y comparar solo capítulo:versículo, **no la abreviatura del libro completa**
   (el inglés cambia la coma por dos puntos y traduce el nombre: `Mt 15,21-28` en ES pasa a
   `Matthew 15:21-28` en EN — comparar literal daría un falso positivo de "referencia
   perdida").
8. **Título distinto del ES** (salvo portugués, donde algunos nombres propios/citas
   coinciden de forma legítima).

Capa editorial adicional si tenéis acceso a un modelo capaz de juzgar fidelidad (vuestro
Claude/Haiku local sirve para esto): pedidle que lea origen y traducción y devuelva un
veredicto `approve` / `needs_review` / `rewrite` con la cita exacta del problema — descartad
cualquier hallazgo cuya cita no aparezca literal en el texto (evita ediciones basadas en
alucinaciones del propio revisor).

## 5. Excepciones que no lleváis por la vía automática

- **Autores que aportan su propia traducción humana** (hoy, Pagola: 4 DOCX por carta,
  adjuntos en el issue de la semana) — nunca generéis fallback automático para ellos aunque
  falte alguno de los 4 idiomas.
- **Textos litúrgicos enlazados desde la carta** (evangelio, lecturas) — confirmar con Rafa
  si entran en el alcance de "todo lo de la carta" o se dejan fuera; ha habido confusión
  antes sobre esto (no dar por hecho que "ya deben estar traducidos").

## 6. Cómo leer la cuota de MiniMax

Con el mismo token que ya tenéis para el audio:

```bash
curl -s https://api.minimax.io/v1/token_plan/remains \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json"
```

Del JSON de respuesta:
- `base_resp.status_code` debe ser `0` (cualquier otro valor es error de cuenta/plan, ver
  `base_resp.status_msg`).
- `model_remains` es una lista por modelo. El de texto/traducción es el que tiene
  `model_name == "general"`; los de audio son los `speech-2.8-*` que ya usáis para TTS.
- De cada entrada: `current_interval_remaining_percent` (cuánto queda de la ventana de 5h
  que se resetea sola — `end_time` es cuándo) y `current_weekly_remaining_percent` /
  `weekly_end_time` (la ventana semanal, más amplia). Restad a 100 para saber "cuánto habéis
  gastado" si preferís verlo así.

Igual que con el audio: un HTTP 200 no es garantía de cuota — cuando `t2a_v2` o el endpoint
de traducción devuelvan `base_resp.status_code=2056` (plan agotado) o `1039` (límite de
ritmo), parad y reintentad más tarde, no machaquéis la API.

## 7. Qué falta para que esto sea autoservicio de verdad

Con el endpoint del §0 ya no queda ningún bloqueante de infraestructura conocido para crear
y enlazar traducciones por REST. Lo que sigue siendo trabajo vuestro, no de infraestructura:
generar el texto (motores del principio del documento), aplicar el QA del §4 antes de
llamar al endpoint, y decidir cuándo pasáis una traducción de `draft` a `publish` (mismo
criterio que la carta en español, ver `docs/guia-publicacion-carta-inma.md`).
