Files
feadulta/scripts/translate_lectura_titles.php
T

219 lines
10 KiB
PHP

<?php
/**
* translate_lectura_titles.php (issue Gitea #140)
*
* Traduce SOLO el nombre del libro bíblico en el INICIO del post_title de los
* posts no-ES (EN/FR/IT/PT) cuyo título es una cita bíblica «<LIBRO> <num>, ...».
* El cuerpo ya está traducido; esto es title-only.
*
* - Determinista: mapa fijo de libros ES -> {en,fr,it,pt}.
* - Idempotente: si el token inicial ya está en el idioma destino, no toca nada.
* - Seguro: exige número tras el libro (excluye «Juan Pablo II», «Domingo 30...»,
* etc. — DOMINGO/SEMANA no son libros, no están en el mapa).
* - Cotejo insensible a acentos (fold a ASCII-mayúsculas) para casar variantes;
* el valor canónico por idioma garantiza que ES==destino sea un no-op.
*
* Uso (local): docker exec wordpress-web php /var/www/html/scripts/... (o vía cwd)
* php scripts/translate_lectura_titles.php # dry-run + reporte
* APPLY=1 php scripts/translate_lectura_titles.php # aplica
* Prod: FEA_WP_LOAD=/web/wp-nuevo/wp-load.php php translate_lectura_titles.php
*/
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
$WP_LOAD = getenv('FEA_WP_LOAD') ?: '/var/www/html/wp-load.php';
if (!file_exists($WP_LOAD)) {
fwrite(STDERR, "No encuentro wp-load.php en $WP_LOAD\n");
exit(1);
}
define('WP_USE_THEMES', false);
require $WP_LOAD;
global $wpdb;
$APPLY = getenv('APPLY') === '1';
$LANGS = ['en', 'fr', 'it', 'pt'];
/*
* Mapa de libros: clave en español (display) => [en, fr, it, pt] (Title case canónico).
* El cotejo es accent-insensitive; los valores destino son los litúrgicos católicos.
*/
$BOOKS = [
// --- Antiguo Testamento ---
'Génesis' => ['Genesis', 'Genèse', 'Genesi', 'Gênesis'],
'Éxodo' => ['Exodus', 'Exode', 'Esodo', 'Êxodo'],
'Levítico' => ['Leviticus', 'Lévitique', 'Levitico', 'Levítico'],
'Números' => ['Numbers', 'Nombres', 'Numeri', 'Números'],
'Deuteronomio' => ['Deuteronomy', 'Deutéronome', 'Deuteronomio', 'Deuteronômio'],
'Josué' => ['Joshua', 'Josué', 'Giosuè', 'Josué'],
'Jueces' => ['Judges', 'Juges', 'Giudici', 'Juízes'],
'Rut' => ['Ruth', 'Ruth', 'Rut', 'Rute'],
'Samuel' => ['Samuel', 'Samuel', 'Samuele', 'Samuel'],
'Reyes' => ['Kings', 'Rois', 'Re', 'Reis'],
'Crónicas' => ['Chronicles', 'Chroniques', 'Cronache', 'Crônicas'],
'Esdras' => ['Ezra', 'Esdras', 'Esdra', 'Esdras'],
'Nehemías' => ['Nehemiah', 'Néhémie', 'Neemia', 'Neemias'],
'Tobías' => ['Tobit', 'Tobie', 'Tobia', 'Tobias'],
'Judit' => ['Judith', 'Judith', 'Giuditta', 'Judite'],
'Ester' => ['Esther', 'Esther', 'Ester', 'Ester'],
'Macabeos' => ['Maccabees', 'Maccabées', 'Maccabei', 'Macabeus'],
'Job' => ['Job', 'Job', 'Giobbe', 'Jó'],
'Salmos' => ['Psalms', 'Psaumes', 'Salmi', 'Salmos'],
'Salmo' => ['Psalm', 'Psaume', 'Salmo', 'Salmo'],
'Proverbios' => ['Proverbs', 'Proverbes', 'Proverbi', 'Provérbios'],
'Eclesiastés' => ['Ecclesiastes', 'Ecclésiaste', 'Ecclesiaste', 'Eclesiastes'],
'Eclesiástico' => ['Ecclesiasticus', 'Siracide', 'Siracide', 'Eclesiástico'],
'Sabiduría' => ['Wisdom', 'Sagesse', 'Sapienza', 'Sabedoria'],
'Isaías' => ['Isaiah', 'Isaïe', 'Isaia', 'Isaías'],
'Jeremías' => ['Jeremiah', 'Jérémie', 'Geremia', 'Jeremias'],
'Lamentaciones' => ['Lamentations', 'Lamentations', 'Lamentazioni', 'Lamentações'],
'Baruc' => ['Baruch', 'Baruch', 'Baruc', 'Baruc'],
'Ezequiel' => ['Ezekiel', 'Ézéchiel', 'Ezechiele', 'Ezequiel'],
'Daniel' => ['Daniel', 'Daniel', 'Daniele', 'Daniel'],
'Oseas' => ['Hosea', 'Osée', 'Osea', 'Oseias'],
'Joel' => ['Joel', 'Joël', 'Gioele', 'Joel'],
'Amós' => ['Amos', 'Amos', 'Amos', 'Amós'],
'Abdías' => ['Obadiah', 'Abdias', 'Abdia', 'Abdias'],
'Jonás' => ['Jonah', 'Jonas', 'Giona', 'Jonas'],
'Miqueas' => ['Micah', 'Michée', 'Michea', 'Miqueias'],
'Nahúm' => ['Nahum', 'Nahum', 'Naum', 'Naum'],
'Habacuc' => ['Habakkuk', 'Habacuc', 'Abacuc', 'Habacuc'],
'Sofonías' => ['Zephaniah', 'Sophonie', 'Sofonia', 'Sofonias'],
'Ageo' => ['Haggai', 'Aggée', 'Aggeo', 'Ageu'],
'Zacarías' => ['Zechariah', 'Zacharie', 'Zaccaria', 'Zacarias'],
'Malaquías' => ['Malachi', 'Malachie', 'Malachia', 'Malaquias'],
// --- Nuevo Testamento ---
'Mateo' => ['Matthew', 'Matthieu', 'Matteo', 'Mateus'],
'Marcos' => ['Mark', 'Marc', 'Marco', 'Marcos'],
'Lucas' => ['Luke', 'Luc', 'Luca', 'Lucas'],
'Juan' => ['John', 'Jean', 'Giovanni', 'João'],
'Hechos' => ['Acts', 'Actes', 'Atti', 'Atos'],
'Romanos' => ['Romans', 'Romains', 'Romani', 'Romanos'],
'Corintios' => ['Corinthians', 'Corinthiens', 'Corinzi', 'Coríntios'],
'Gálatas' => ['Galatians', 'Galates', 'Galati', 'Gálatas'],
'Efesios' => ['Ephesians', 'Éphésiens', 'Efesini', 'Efésios'],
'Filipenses' => ['Philippians', 'Philippiens', 'Filippesi', 'Filipenses'],
'Colosenses' => ['Colossians', 'Colossiens', 'Colossesi', 'Colossenses'],
'Tesalonicenses' => ['Thessalonians', 'Thessaloniciens', 'Tessalonicesi', 'Tessalonicenses'],
'Timoteo' => ['Timothy', 'Timothée', 'Timoteo', 'Timóteo'],
'Tito' => ['Titus', 'Tite', 'Tito', 'Tito'],
'Filemón' => ['Philemon', 'Philémon', 'Filemone', 'Filêmon'],
'Hebreos' => ['Hebrews', 'Hébreux', 'Ebrei', 'Hebreus'],
'Santiago' => ['James', 'Jacques', 'Giacomo', 'Tiago'],
'Pedro' => ['Peter', 'Pierre', 'Pietro', 'Pedro'],
'Judas' => ['Jude', 'Jude', 'Giuda', 'Judas'],
'Apocalipsis' => ['Revelation', 'Apocalypse', 'Apocalisse', 'Apocalipse'],
];
// fold a ASCII-mayúsculas (quita acentos) para cotejo robusto
function fold($s) {
$s = trim($s);
$map = ['Á'=>'A','À'=>'A','Ä'=>'A','Â'=>'A','Ã'=>'A','É'=>'E','È'=>'E','Ë'=>'E','Ê'=>'E',
'Í'=>'I','Ì'=>'I','Ï'=>'I','Î'=>'I','Ó'=>'O','Ò'=>'O','Ö'=>'O','Ô'=>'O','Õ'=>'O',
'Ú'=>'U','Ù'=>'U','Ü'=>'U','Û'=>'U','Ñ'=>'N','Ç'=>'C'];
$s = mb_strtoupper($s, 'UTF-8');
return strtr($s, $map);
}
$langIdx = array_flip($LANGS); // en=>0,...
// índice de búsqueda: foldedSpanish => [en,fr,it,pt]
$LOOKUP = [];
foreach ($BOOKS as $es => $tr) {
$LOOKUP[fold($es)] = $tr;
}
// Todos los posts no-ES (filtramos/transformamos en PHP, regex /u fiable).
$placeholders = implode(',', array_fill(0, count($LANGS), '%s'));
$sql = $wpdb->prepare(
"SELECT p.ID, t.slug AS lang, p.post_title
FROM {$wpdb->posts} p
JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy='language'
JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
WHERE p.post_type='post'
AND p.post_status IN ('publish','draft','future','pending','private')
AND t.slug IN ($placeholders)",
$LANGS
);
$rows = $wpdb->get_results($sql);
$changes = []; // [ID => [lang, old, new]]
$per_lang = array_fill_keys($LANGS, 0);
$skipped_already = 0;
$candidates = 0; // títulos con al menos una cita bíblica detectada
/*
* Traduce CADA token de libro de una cita bíblica dentro del título:
* - una palabra (letras) NO precedida por letra/número
* - seguida de «<espacios><dígito>» (el capítulo de la cita).
* Cubre el inicio, los compuestos «1ª lectura / 2ª lectura / evangelio» (tras «/»),
* el ordinal previo («2 Timoteo 4») y prefijos de fiesta («Epifanía - Isaías 60»).
* Como SOLO casa ortografías españolas (las del mapa), en posts no-ES únicamente
* toca citas heredadas del ES; las descripciones van en el idioma destino.
*/
$BOOK_RE = '/(?<![\p{L}\p{N}])(\p{L}[\p{L}.]*)(?=\s+\d)/u';
foreach ($rows as $r) {
$lang = $r->lang;
$li = $langIdx[$lang];
$hit = false;
$new_title = preg_replace_callback($BOOK_RE, function ($m) use ($LOOKUP, $li, &$hit) {
$book = $m[1];
$key = fold($book);
if (!isset($LOOKUP[$key])) {
return $m[0]; // no es libro conocido -> intacto
}
$hit = true;
$canon = $LOOKUP[$key][$li];
$isUpper = (mb_strtoupper($book, 'UTF-8') === $book);
return $isUpper ? mb_strtoupper($canon, 'UTF-8') : $canon;
}, $r->post_title);
if (!$hit) { continue; }
$candidates++;
if ($new_title === $r->post_title) { $skipped_already++; continue; } // idempotente
$changes[$r->ID] = [$lang, $r->post_title, $new_title];
$per_lang[$lang]++;
}
// --- Reporte ---
echo "== translate_lectura_titles.php (issue #140) ==\n";
echo "WP_LOAD: $WP_LOAD\n";
echo "Posts no-ES escaneados: " . count($rows) . "\n";
echo " - con cita bíblica detectada: $candidates\n";
echo " - ya en idioma destino (idempotente): $skipped_already\n";
echo " - A CAMBIAR: " . count($changes) . "\n";
echo " por idioma: ";
foreach ($LANGS as $l) echo strtoupper($l) . "=" . $per_lang[$l] . " ";
echo "\n\n";
$SAMPLE = (int)(getenv('SAMPLE') ?: 8);
foreach ($LANGS as $l) {
$shown = 0;
echo "--- muestra $l ---\n";
foreach ($changes as $id => $c) {
if ($c[0] !== $l) continue;
echo sprintf(" %d «%s» -> «%s»\n", $id, $c[1], $c[2]);
if ($SAMPLE > 0 && ++$shown >= $SAMPLE) break;
}
}
echo "\n";
if (!$APPLY) {
echo "DRY-RUN (no se ha tocado nada). Ejecuta con APPLY=1 para aplicar.\n";
exit(0);
}
// --- Aplica (title-only, sin tocar slug/cuerpo) ---
$done = 0;
foreach ($changes as $id => $c) {
$ok = $wpdb->update($wpdb->posts, ['post_title' => $c[2]], ['ID' => $id], ['%s'], ['%d']);
if ($ok !== false) {
clean_post_cache($id);
$done++;
} else {
fwrite(STDERR, "ERROR al actualizar ID $id\n");
}
}
echo "APLICADOS: $done de " . count($changes) . "\n";