Multiidioma: traducciones reales con Gemma 4B local (arreglar fontanería + cola priorizada + on-demand) #11

Open
opened 2026-06-28 19:12:47 +00:00 by rafa · 0 comments
Owner

Labels: area:content, area:plugin, improvement

Resumen

El multiidioma de feadulta "no está bien": la estructura existe (Polylang, 5 idiomas, selector) pero el corpus de artículos no está traducido ni enlazado entre idiomas, así que cambiar de idioma en un artículo no lleva a ninguna parte útil. Este issue analiza el estado real y propone una solución por fases usando Gemma 4 4B local para generar traducciones, sin intentar traducir los 24k artículos de golpe.

Análisis y datos verificados el 2026-06-15 contra el WP local. Es un issue de iniciativa larga (milestone v3); el alcance se ejecuta por fases.


Estado actual verificado

Configuración Polylang

  • 5 idiomas: es (default, sin prefijo), en (/en/), fr (/fr/), it (/it/), pt (/pt/).
  • force_lang=1 (directory mode), hide_default=1, rewrite=1. post_types y taxonomies vacíos (built-in post/page los gestiona Polylang por defecto, OK).

El dato que lo explica todo

Idioma Posts con ese idioma asignado
es 24.261
fr 1.215
en 916
pt 866
it 758

Pero solo hay 189 grupos de traducción (taxonomy=post_translations), de los cuales 187 enlazan los 5 idiomas. Es decir:

  • Esos ~187 grupos son las páginas estructurales ya traducidas: portadas (ES 26542 ↔ EN 43890 ↔ FR 42756 ↔ IT 42757 ↔ PT 42758), cartas recientes, secciones EFFA, evangelios.
  • Los 24.261 artículos ES están huérfanos (sin traducción enlazada).
  • Los ~3.755 posts no-ES (en/fr/it/pt) son contenido original histórico de Joomla, también huérfanos: no son traducciones de un ES, son artículos nativos en ese idioma que existían en el Joomla multiidioma. No están enlazados a ningún equivalente.

Síntomas / lo que está roto

  1. Selector inútil en artículos: fea-homepage.php:296 usa pll_the_languages(['raw'=>1,'hide_if_no_translation'=>0]) → muestra los 5 idiomas siempre, aunque el artículo no tenga traducción. Al pulsar EN en un artículo ES sin traducción, Polylang lleva a la portada del idioma (o a la home), no a una traducción → UX confusa.
  2. Corpus no-ES desconectado: 3.755 artículos válidos en otros idiomas que casi nadie encuentra (no hay puente desde el ES ni listados claros por idioma).
  3. Bugs Polylang+queries ya documentados:
    • WP_Tax_Query genera AND 0=1 con get_posts(['lang'=>$lang,'category__in'=>[$cat]]) → no pasar lang explícito, Polylang ya filtra.
    • get_term_by('slug',...) devuelve false en idioma ≠ ES → usar $wpdb directo.
    • Carta de la semana da 404 en EN/FR/IT/PT (issue #2, mismo origen).

Decisión: motor y modalidad (2026-06-16)

Motor = Gemma 4B local. Modalidad = pre-generar páginas reales (NO traducción on-the-fly).

Se valoró meter un traductor on-the-fly (widget tipo Google Translate / GTranslate free, que traduce en el navegador al vuelo). Descartado por:

  • Cero SEO, que es medio motivo de tener multiidioma en un sitio de contenido: el widget on-the-fly no genera páginas reales ni indexables (sin URLs por idioma, sin hreflang). Google indexa solo el ES.
  • Choca con Polylang: tendríamos dos sistemas de idioma a la vez (el widget JS + las páginas Polylang ya traducidas), con conflictos de selector y de URLs.
  • Calidad/coste no lo justifican: la API de calidad (Google Cloud / DeepL) traduce algo más fluido que Gemma, pero cuesta dinero a escala (~20 €/$ por millón de caracteres → miles de € para el corpus completo). Teniendo Gemma local gratis, no se va a gastar en traducción. El widget gratis sí evita el coste, pero es justo el que no da SEO.

→ Conclusión: Gemma 4B local, pre-generando posts reales de Polylang (indexables, con hreflang). La fluidez es algo menor que DeepL/Google pero es gratis, privada y da el SEO que importa. La modalidad "bajo demanda" de abajo sigue generando un post real cacheado (no es on-the-fly de navegador).

Solución propuesta (por fases)

Premisa estratégica: NO traducir los 24.261 artículos × 4 idiomas (= ~97k traducciones; a ~45s/artículo con Gemma serían ~50 días de cómputo y poco valor para la mayoría del archivo histórico). Se prioriza y se traduce bajo demanda + lo central.

Fase 0 — Arreglar la fontanería (lo que falla hoy, sin traducir nada)

  • Selector inteligente: decidir comportamiento cuando no hay traducción. Opciones:
    • (a) hide_if_no_translation=1 → en artículos mono-idioma no se muestra el conmutador (limpio, pero "esconde" el multiidioma).
    • (b) Mostrar los idiomas pero, si no hay traducción, ofrecer "Traducir automáticamente" (dispara Fase 1 on-demand) o enviar a la portada del idioma con un aviso. Recomendado (b) una vez exista Fase 1; mientras tanto (a).
  • Arreglar los bugs de queries (carta 404 idiomas — issue #2; get_term_by/WP_Tax_Query).
  • Hacer navegable el corpus no-ES: garantizar que los archivos /en/ /fr/ /it/ /pt/ listan el contenido nativo de ese idioma (categorías/archivos por idioma), para que esos 3.755 posts dejen de ser invisibles.
  • hreflang correcto: Polylang añade hreflang en su walker; verificar que sale en <head> para SEO y que no apunta a traducciones inexistentes.

Fase 1 — Traducción con Gemma 4B local (priorizada + on-demand)

Pipeline translate_post.py (patrón de backfill_bilingual.py de ytsummaries, que ya hace ES→EN con Polylang):

  1. Lee el post ES (título + post_content HTML).
  2. Gemma 4 E4B (LM Studio http://172.19.128.1:1234/v1) traduce preservando: marcado HTML, shortcodes, citas bíblicas (no traducir referencias tipo "Jn 3,16"), nombres propios, y un glosario fijo del proyecto ("fe adulta", términos teológicos, nombres de secciones).
  3. Crea el post traducido (estado configurable: borrador para revisión, o publish con badge).
  4. Enlaza: pll_set_post_language($id,$lang) + pll_save_post_translations(['es'=>$es,'en'=>$en,...]).
  5. Marca meta traduccion_automatica=1 + traduccion_modelo/traduccion_fecha (transparencia + permite regenerar).
  6. Idempotente y reanudable (estado en JSON + log), notifica a Telegram al acabar (como ytsummaries).

Priorización (qué traducir, en orden):

  1. Cartas de la semana (contenido editorial central; varias ya traducidas).
  2. Artículos destacados / autores habituales / más vistos.
  3. Bajo demanda: botón "Traducir este artículo" en el front → genera, cachea, enlaza. Cubre el long tail sin pre-traducir 24k.

Calidad: Gemma 4B validado para traducción feadulta (bench #52, milestone modelos). Para textos doctrinalmente sensibles, considerar revisión humana o escalado puntual a frontier (Opus/GPT-5) en cartas clave. Mostrar badge "traducción automática".

Fase 2 — On-demand en vivo (opcional)

  • Si se pulsa un idioma sin traducción, generar al vuelo y cachear. Latencia Gemma = varios segundos → mejor patrón "pre-generar al primer clic + spinner + email/aviso cuando esté", o pre-traducir la cola priorizada para que casi siempre exista.

Consideraciones técnicas

  • Coste/tiempo: documentar throughput real de Gemma (medir palabras/seg). Definir cuántos miles entran en cada fase. No comprometer "todo el archivo".
  • Almacenamiento: traducciones son posts → crecen wp_posts/postmeta; asumible.
  • SEO: hreflang + sitemaps por idioma (Yoast). Evitar contenido duplicado marcando traducción automática.
  • Sincronización local→prod: las traducciones se generan en local y se despliegan a /web/wp-nuevo (sin scp → ssh 'cat > ruta'; sin proc_open; mysql --default-character-set=utf8mb4). Decidir si se generan directamente en prod por cron Hermes.
  • Polylang Free no trae comandos wp pll: usar wp eval-file con funciones pll_* (igual que ytsummaries).

Plan de ejecución

  1. Fase 0 (fontanería + selector + bugs + navegabilidad no-ES + hreflang).
  2. translate_post.py + glosario + idempotencia (validar con 10-20 cartas).
  3. Cola priorizada por cron/loop reanudable; badge "traducción automática".
  4. Botón "Traducir este artículo" (on-demand) para el long tail.
  5. Medir calidad (spot-check) y throughput; ajustar alcance.

Ficheros y referencias

  • Selector: wordpress/wp-content/mu-plugins/fea-homepage.php:231-330 (CSS + pll_the_languages).
  • Patrón pipeline: ytsummaries/scripts/backfill_bilingual.py (ES→EN Polylang, estado/reanudable).
  • Bugs relacionados: issue #2 (carta 404 idiomas).
  • Polylang options en wp_options (polylang).

Criterios de aceptación (incrementales)

  • El selector ya no lleva a callejones sin salida (oculta o ofrece traducir).
  • El corpus no-ES (3.755 posts) es navegable desde los archivos por idioma.
  • translate_post.py traduce y enlaza correctamente una carta de muestra en los 4 idiomas, preservando HTML/citas/nombres.
  • Existe una cola priorizada reanudable y un badge "traducción automática".
  • hreflang correcto en <head> para los posts traducidos.
**Labels:** area:content, area:plugin, improvement ## Resumen El multiidioma de feadulta "no está bien": la estructura existe (Polylang, 5 idiomas, selector) pero **el corpus de artículos no está traducido ni enlazado entre idiomas**, así que cambiar de idioma en un artículo no lleva a ninguna parte útil. Este issue analiza el estado real y propone una solución por fases usando **Gemma 4 4B local** para generar traducciones, sin intentar traducir los 24k artículos de golpe. > Análisis y datos **verificados el 2026-06-15** contra el WP local. Es un issue de iniciativa larga (milestone v3); el alcance se ejecuta por fases. --- ## Estado actual verificado ### Configuración Polylang - 5 idiomas: **es** (default, sin prefijo), **en** (`/en/`), **fr** (`/fr/`), **it** (`/it/`), **pt** (`/pt/`). - `force_lang=1` (directory mode), `hide_default=1`, `rewrite=1`. `post_types` y `taxonomies` vacíos (built-in `post`/`page` los gestiona Polylang por defecto, OK). ### El dato que lo explica todo | Idioma | Posts con ese idioma asignado | |--------|------------------------------| | es | 24.261 | | fr | 1.215 | | en | 916 | | pt | 866 | | it | 758 | Pero **solo hay 189 grupos de traducción** (`taxonomy=post_translations`), de los cuales **187 enlazan los 5 idiomas**. Es decir: - Esos ~187 grupos son las **páginas estructurales** ya traducidas: portadas (ES 26542 ↔ EN 43890 ↔ FR 42756 ↔ IT 42757 ↔ PT 42758), cartas recientes, secciones EFFA, evangelios. - Los **24.261 artículos ES están huérfanos** (sin traducción enlazada). - Los **~3.755 posts no-ES** (en/fr/it/pt) son **contenido original histórico de Joomla**, también huérfanos: no son traducciones de un ES, son artículos nativos en ese idioma que existían en el Joomla multiidioma. No están enlazados a ningún equivalente. ### Síntomas / lo que está roto 1. **Selector inútil en artículos**: `fea-homepage.php:296` usa `pll_the_languages(['raw'=>1,'hide_if_no_translation'=>0])` → muestra **los 5 idiomas siempre**, aunque el artículo no tenga traducción. Al pulsar EN en un artículo ES sin traducción, Polylang lleva a la **portada del idioma** (o a la home), no a una traducción → UX confusa. 2. **Corpus no-ES desconectado**: 3.755 artículos válidos en otros idiomas que casi nadie encuentra (no hay puente desde el ES ni listados claros por idioma). 3. **Bugs Polylang+queries** ya documentados: - `WP_Tax_Query` genera `AND 0=1` con `get_posts(['lang'=>$lang,'category__in'=>[$cat]])` → no pasar `lang` explícito, Polylang ya filtra. - `get_term_by('slug',...)` devuelve false en idioma ≠ ES → usar `$wpdb` directo. - **Carta de la semana da 404 en EN/FR/IT/PT** (issue #2, mismo origen). --- ## Decisión: motor y modalidad (2026-06-16) **Motor = Gemma 4B local. Modalidad = pre-generar páginas reales (NO traducción on-the-fly).** Se valoró meter un traductor **on-the-fly** (widget tipo Google Translate / GTranslate free, que traduce en el navegador al vuelo). **Descartado** por: - **Cero SEO**, que es medio motivo de tener multiidioma en un sitio de contenido: el widget on-the-fly **no genera páginas reales ni indexables** (sin URLs por idioma, sin hreflang). Google indexa solo el ES. - **Choca con Polylang**: tendríamos dos sistemas de idioma a la vez (el widget JS + las páginas Polylang ya traducidas), con conflictos de selector y de URLs. - **Calidad/coste no lo justifican**: la API de calidad (Google Cloud / DeepL) traduce algo más fluido que Gemma, pero **cuesta dinero a escala** (~20 €/$ por millón de caracteres → miles de € para el corpus completo). Teniendo Gemma local gratis, **no se va a gastar en traducción**. El widget *gratis* sí evita el coste, pero es justo el que no da SEO. → Conclusión: **Gemma 4B local, pre-generando posts reales de Polylang** (indexables, con hreflang). La fluidez es algo menor que DeepL/Google pero es gratis, privada y da el SEO que importa. La modalidad "bajo demanda" de abajo **sigue generando un post real cacheado** (no es on-the-fly de navegador). ## Solución propuesta (por fases) **Premisa estratégica:** NO traducir los 24.261 artículos × 4 idiomas (= ~97k traducciones; a ~45s/artículo con Gemma serían ~50 días de cómputo y poco valor para la mayoría del archivo histórico). Se prioriza y se traduce **bajo demanda + lo central**. ### Fase 0 — Arreglar la fontanería (lo que falla hoy, sin traducir nada) - **Selector inteligente**: decidir comportamiento cuando no hay traducción. Opciones: - (a) `hide_if_no_translation=1` → en artículos mono-idioma no se muestra el conmutador (limpio, pero "esconde" el multiidioma). - (b) Mostrar los idiomas pero, si no hay traducción, ofrecer **"Traducir automáticamente"** (dispara Fase 1 on-demand) o enviar a la portada del idioma con un aviso. **Recomendado (b)** una vez exista Fase 1; mientras tanto (a). - **Arreglar los bugs de queries** (carta 404 idiomas — issue #2; `get_term_by`/`WP_Tax_Query`). - **Hacer navegable el corpus no-ES**: garantizar que los archivos `/en/ /fr/ /it/ /pt/` listan el contenido nativo de ese idioma (categorías/archivos por idioma), para que esos 3.755 posts dejen de ser invisibles. - **hreflang correcto**: Polylang añade hreflang en su walker; verificar que sale en `<head>` para SEO y que no apunta a traducciones inexistentes. ### Fase 1 — Traducción con Gemma 4B local (priorizada + on-demand) Pipeline `translate_post.py` (patrón de `backfill_bilingual.py` de ytsummaries, que ya hace ES→EN con Polylang): 1. Lee el post ES (título + `post_content` HTML). 2. **Gemma 4 E4B** (LM Studio `http://172.19.128.1:1234/v1`) traduce **preservando**: marcado HTML, shortcodes, **citas bíblicas** (no traducir referencias tipo "Jn 3,16"), **nombres propios**, y un **glosario fijo** del proyecto ("fe adulta", términos teológicos, nombres de secciones). 3. Crea el post traducido (estado configurable: borrador para revisión, o publish con badge). 4. Enlaza: `pll_set_post_language($id,$lang)` + `pll_save_post_translations(['es'=>$es,'en'=>$en,...])`. 5. Marca meta `traduccion_automatica=1` + `traduccion_modelo`/`traduccion_fecha` (transparencia + permite regenerar). 6. Idempotente y **reanudable** (estado en JSON + log), notifica a Telegram al acabar (como ytsummaries). **Priorización (qué traducir, en orden):** 1. **Cartas de la semana** (contenido editorial central; varias ya traducidas). 2. **Artículos destacados / autores habituales / más vistos**. 3. **Bajo demanda**: botón "Traducir este artículo" en el front → genera, cachea, enlaza. Cubre el long tail sin pre-traducir 24k. **Calidad:** Gemma 4B validado para traducción feadulta (bench #52, milestone modelos). Para textos doctrinalmente sensibles, considerar revisión humana o escalado puntual a frontier (Opus/GPT-5) en cartas clave. Mostrar badge "traducción automática". ### Fase 2 — On-demand en vivo (opcional) - Si se pulsa un idioma sin traducción, generar al vuelo y cachear. Latencia Gemma = varios segundos → mejor patrón "pre-generar al primer clic + spinner + email/aviso cuando esté", o pre-traducir la cola priorizada para que casi siempre exista. --- ## Consideraciones técnicas - **Coste/tiempo:** documentar throughput real de Gemma (medir palabras/seg). Definir cuántos miles entran en cada fase. No comprometer "todo el archivo". - **Almacenamiento:** traducciones son posts → crecen wp_posts/postmeta; asumible. - **SEO:** hreflang + sitemaps por idioma (Yoast). Evitar contenido duplicado marcando traducción automática. - **Sincronización local→prod:** las traducciones se generan en local y se despliegan a `/web/wp-nuevo` (sin scp → `ssh 'cat > ruta'`; sin `proc_open`; `mysql --default-character-set=utf8mb4`). Decidir si se generan directamente en prod por cron Hermes. - **Polylang Free** no trae comandos `wp pll`: usar `wp eval-file` con funciones `pll_*` (igual que ytsummaries). ## Plan de ejecución 1. Fase 0 (fontanería + selector + bugs + navegabilidad no-ES + hreflang). 2. `translate_post.py` + glosario + idempotencia (validar con 10-20 cartas). 3. Cola priorizada por cron/loop reanudable; badge "traducción automática". 4. Botón "Traducir este artículo" (on-demand) para el long tail. 5. Medir calidad (spot-check) y throughput; ajustar alcance. ## Ficheros y referencias - Selector: `wordpress/wp-content/mu-plugins/fea-homepage.php:231-330` (CSS + `pll_the_languages`). - Patrón pipeline: `ytsummaries/scripts/backfill_bilingual.py` (ES→EN Polylang, estado/reanudable). - Bugs relacionados: issue #2 (carta 404 idiomas). - Polylang options en `wp_options` (`polylang`). ## Criterios de aceptación (incrementales) - [ ] El selector ya no lleva a callejones sin salida (oculta o ofrece traducir). - [ ] El corpus no-ES (3.755 posts) es navegable desde los archivos por idioma. - [ ] `translate_post.py` traduce y enlaza correctamente una carta de muestra en los 4 idiomas, preservando HTML/citas/nombres. - [ ] Existe una cola priorizada reanudable y un badge "traducción automática". - [ ] hreflang correcto en `<head>` para los posts traducidos.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rafa/feadulta#11