diff --git a/scripts/translate_lectura_titles.php b/scripts/translate_lectura_titles.php new file mode 100644 index 0000000..1316f24 --- /dev/null +++ b/scripts/translate_lectura_titles.php @@ -0,0 +1,218 @@ + , ...». + * 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 «» (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 = '/(?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";