, ...». * 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";