diff --git a/scripts/apply_lecturas_wp.php b/scripts/apply_lecturas_wp.php new file mode 100644 index 0000000..a16e8f1 --- /dev/null +++ b/scripts/apply_lecturas_wp.php @@ -0,0 +1,41 @@ + $es_id]; + $group = $existing; + $es_cats = wp_get_post_categories($es_id); + foreach (['en', 'fr', 'it', 'pt'] as $L) { + if (!empty($existing[$L])) { $group[$L] = $existing[$L]; continue; } + if (empty($row['langs'][$L])) continue; + $id = wp_insert_post([ + 'post_title' => $es->post_title, // referencia bíblica (igual en todos) + 'post_content' => $row['langs'][$L], + 'post_status' => $status, + 'post_type' => 'post', + 'comment_status' => 'closed', + ], true); + if (is_wp_error($id)) continue; + pll_set_post_language($id, $L); + $cats = []; + foreach ($es_cats as $c) { $t = pll_get_term($c, $L); if ($t) $cats[] = $t; } + if ($cats) wp_set_post_categories($id, $cats); + $group[$L] = $id; + $created++; + } + if (function_exists('pll_save_post_translations')) pll_save_post_translations($group); + $posts_done++; +} +echo "posts ES procesados: $posts_done | traducciones creadas: $created | status=$status | skip=$skipped\n"; diff --git a/scripts/build_lectionary_index.py b/scripts/build_lectionary_index.py new file mode 100644 index 0000000..5f16bd6 --- /dev/null +++ b/scripts/build_lectionary_index.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +build_lectionary_index.py — Descarga el leccionario de evangelizo.ws para un rango de +fechas (un ciclo litúrgico completo cubre todas las lecturas) en es/en/fr/it/pt y +construye un índice POR REFERENCIA bíblica, para casar lecturas sin depender de fechas. + +Salida: /tmp/lectionary_index.json { "LIBRO|cap|vers": {es,en,fr,it,pt: html} } +Cache por día/idioma en /tmp/evangelizo_cache (resumible). + +Uso: python3 build_lectionary_index.py 2023-01-01 2025-12-31 +""" +import sys, os, re, json, time, html, unicodedata, urllib.request +from datetime import date, timedelta + +LANGS = {"SP": "es", "AM": "en", "FR": "fr", "IT": "it", "PT": "pt"} +CACHE = "/tmp/evangelizo_cache" +os.makedirs(CACHE, exist_ok=True) +INDEX = "/tmp/lectionary_index.json" + + +def norm_book(full_title): + # "Libro de Jeremías" / "Carta de san Pablo a los Romanos" -> "JEREMIAS"/"ROMANOS" + s = unicodedata.normalize("NFKD", full_title).encode("ascii", "ignore").decode().upper() + s = re.sub(r"[^A-Z0-9 ]", " ", s) + toks = [t for t in s.split() if t] + return toks[-1] if toks else "" + + +def clean(raw): + raw = html.unescape(raw or "") + raw = re.sub(r"\[\[[^\]]+\]\]", "", raw) + paras = [p.strip() for p in raw.split("\n") if p.strip()] + return "".join(f"
{p}
\n" for p in paras) + + +def fetch(date_s, lang_code): + fp = os.path.join(CACHE, f"{date_s}_{lang_code}.json") + if os.path.exists(fp): + try: + return json.load(open(fp)) + except Exception: + pass + url = f"https://publication.evangelizo.ws/{lang_code}/days/{date_s}" + for a in range(3): + try: + req = urllib.request.Request(url, headers={"User-Agent": "fea-lect/1.0"}) + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + out = [] + for rd in data.get("data", {}).get("readings", []) or []: + out.append({ + "code": rd.get("reading_code", ""), + "ref": (rd.get("reference_displayed") or "").strip().rstrip("."), + "book": (rd.get("book") or {}).get("full_title", ""), + "text": clean(rd.get("text", "")), + }) + json.dump(out, open(fp, "w"), ensure_ascii=False) + time.sleep(0.15) + return out + except Exception: + if a == 2: + json.dump([], open(fp, "w")) + return [] + time.sleep(1.0) + + +def key_from(book_full, ref): + m = re.match(r"(\d{1,3})\s*,\s*(\d{1,3})", ref) + if not m: + return None + return f"{norm_book(book_full)}|{int(m.group(1))}|{int(m.group(2))}" + + +def main(): + d0 = date.fromisoformat(sys.argv[1]) + d1 = date.fromisoformat(sys.argv[2]) + days = (d1 - d0).days + 1 + + # Las fiestas trasladadas caen en fechas distintas por país/idioma → NO se puede + # casar dentro del mismo día. Indexamos por reading_code (estable entre idiomas) + # acumulando el texto de cada idioma desde CUALQUIER día donde aparezca. + code_text = {wl: {} for wl in LANGS.values()} # lang -> {code: text} + code_book = {} # code -> norm_book (del SP) + cur, n = d0, 0 + while cur <= d1: + ds = cur.isoformat() + for lc, wl in LANGS.items(): + for rd in fetch(ds, lc): + code = rd["code"] + if not code: + continue + if rd["text"] and code not in code_text[wl]: + code_text[wl][code] = rd["text"] + if wl == "es" and code not in code_book: + nb = norm_book(rd["book"]) + m = re.search(r"(\d{1,3})\s*,\s*(\d{1,3})", code) + if nb and m: + code_book[code] = f"{nb}|{int(m.group(1))}|{int(m.group(2))}" + n += 1 + if n % 60 == 0: + print(f" {n}/{days} días codes_es={len(code_text['es'])}", flush=True) + cur += timedelta(days=1) + + # combinar: para cada code con clave-ES y texto en los 4 idiomas + index = {} + for code, key in code_book.items(): + if key in index: + continue + entry = {} + ok = True + for wl in ("es", "en", "fr", "it", "pt"): + t = code_text[wl].get(code) + if not t: + ok = (wl == "es") and ok # es siempre presente; faltar otro descarta + if wl != "es": + ok = False + break + entry[wl] = t + if ok and all(l in entry for l in ("en", "fr", "it", "pt")): + index[key] = entry + json.dump(index, open(INDEX, "w"), ensure_ascii=False) + print(f"FIN. {n} días. codes_es={len(code_book)} → índice {len(index)} referencias en {INDEX}") + + +if __name__ == "__main__": + main() diff --git a/scripts/lecturas_apply.py b/scripts/lecturas_apply.py new file mode 100644 index 0000000..4f40660 --- /dev/null +++ b/scripts/lecturas_apply.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +lecturas_apply.py — Casa las lecturas ES sin traducir contra el índice del leccionario +(build_lectionary_index.py) POR REFERENCIA bíblica y vuelca las traducciones a crear. + +Entrada: /tmp/lectionary_index.json , /tmp/lecturas_todo.json +Salida: /tmp/lecturas_creadas.json (para que un wp eval cree+asocie+publique) + /tmp/lecturas_skip.json + +Uso: python3 lecturas_apply.py [--limit N] +""" +import sys, re, json, unicodedata +from collections import Counter + +# Alias de nombre de libro: feadulta -> token usado por evangelizo (último token full_title ES) +ALIAS = { + "HECHOS": "APOSTOLES", "HCH": "APOSTOLES", + "CANTAR": "CANTARES", + "APOC": "APOCALIPSIS", "AP": "APOCALIPSIS", + "QOHELET": "ECLESIASTES", + # abreviaturas litúrgicas + "MT": "MATEO", "MC": "MARCOS", "LC": "LUCAS", "JN": "JUAN", + "RM": "ROMANOS", "GA": "GALATAS", "EF": "EFESIOS", "FLP": "FILIPENSES", + "COL": "COLOSENSES", "HB": "HEBREOS", "ST": "SANTIAGO", + "IS": "ISAIAS", "JR": "JEREMIAS", "EZ": "EZEQUIEL", "GN": "GENESIS", + "EX": "EXODO", "DT": "DEUTERONOMIO", "SAL": "SALMOS", "PR": "PROVERBIOS", +} + + +def norm(s): + s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().upper() + return re.sub(r"[^A-Z]", "", s) # solo letras → descarta el número del libro + + +def title_keys(title): + keys = [] + for part in re.split(r"\s*/\s*", title): + m = re.search(r"([0-9]?\s*[A-Za-zÀ-ÿ][A-Za-zÀ-ÿ.\s]+?)\s+(\d{1,3})\s*,\s*(\d{1,3})", part) + if not m: + return None # parte no parseable → no casar el post entero + book = norm(m.group(1)) + book = ALIAS.get(book, book) + keys.append(f"{book}|{int(m.group(2))}|{int(m.group(3))}") + return keys or None + + +def main(): + limit = 0 + if "--limit" in sys.argv: + limit = int(sys.argv[sys.argv.index("--limit") + 1]) + idx = json.load(open("/tmp/lectionary_index.json")) + todo = json.load(open("/tmp/lecturas_todo.json")) + if limit: + todo = todo[:limit] + + creadas, skip = [], [] + for t in todo: + keys = title_keys(t["title"]) + if not keys: + skip.append({**t, "why": "título no parseable"}) + continue + if not all(k in idx for k in keys): + missing = [k for k in keys if k not in idx] + skip.append({**t, "why": "ref no en índice", "missing": missing}) + continue + langs = {} + for wl in ("en", "fr", "it", "pt"): + langs[wl] = "".join(idx[k][wl] for k in keys) + creadas.append({"es_id": t["id"], "title": t["title"], "langs": langs}) + + json.dump(creadas, open("/tmp/lecturas_creadas.json", "w"), ensure_ascii=False) + json.dump(skip, open("/tmp/lecturas_skip.json", "w"), ensure_ascii=False) + print(f"CASADAS: {len(creadas)} / {len(todo)} SKIP: {len(skip)}") + print("motivos skip:", dict(Counter(s["why"] for s in skip))) + # muestra de refs que faltan (para ampliar alias/rango) + missing = Counter() + for s in skip: + for k in s.get("missing", []): + missing[k.split("|")[0]] += 1 + print("libros con más misses:", dict(missing.most_common(12))) + + +if __name__ == "__main__": + main() diff --git a/tools/e2e/verify/lecturas_pendientes.txt b/tools/e2e/verify/lecturas_pendientes.txt new file mode 100644 index 0000000..5de2599 --- /dev/null +++ b/tools/e2e/verify/lecturas_pendientes.txt @@ -0,0 +1,62 @@ +# Lecturas sin traducir tras lote evangelizo (61) — ref no hallada en ciclo 2023-2025 +1199 MATEO 27, 11-54 ref no en índice +52 JUAN 18, 1-40 / JUAN 19, 1-42 ref no en índice +286 DEUTERONOMIO, 2-3 y 14-16 / 1 CORINTIOS 10, 16-17 título no parseable +2988 JUAN 18, 28-40 ref no en índice +3478 LUCAS 19, 28-44 ref no en índice +2687 MATEO 21, 1-11 ref no en índice +5532 SANTIAGO 2, 1-9 ref no en índice +5539 PUNTOS DE REFLEXIÓN (Mt 23, 8-24) ref no en índice +5778 HECHOS 1, 12-14 ref no en índice +5842 1 PEDRO 1, 8 ref no en índice +5866 HECHOS 10, 37-39 ref no en índice +6000 GÁLATAS 5, 19-20, 22-23 ref no en índice +6015 ROMANOS 8, 15-17 ref no en índice +6087 SABIDURÍA 11, 23-26 y 12, 1-2 ref no en índice +57 MARCOS 16, 1-7 ref no en índice +4843 ÉXODO 15, 22 a 16, 35 ref no en índice +4846 HECHOS 2, 14-36 ref no en índice +6163 1 JUAN 4, 20-21 y 3, 16-18 ref no en índice +7661 1 REYES 17, 17-24 ref no en índice +7662 GÁLATAS 1, 11-19 ref no en índice +7704 ZACARÍAS 12, 10-11 y 13, 1 ref no en índice +7705 GÁLATAS 3, 26-29 ref no en índice +8152 MALAQUÍAS 4, 1-2 ref no en índice +4675 ISAÍAS 49, 14-15 ref no en índice +10527 SAMUEL 27, 4-7 ref no en índice +13022 HECHOS 4,33;5,12.27-33;12,2 ref no en índice +13289 DEUTERONOMIO 6, 1-9 ref no en índice +2217 LUCAS 3, 1-6 ref no en índice +7070 BARUC 5, 1-9 ref no en índice +7071 FILIPENSES 1, 3-11 ref no en índice +1996 LUCAS 4, 21-30 ref no en índice +7266 JEREMÍAS 1, 4-19 ref no en índice +7958 TIMOTEO 1, 12-17 ref no en índice +8105 SABIDURÍA 11, 23 - 12, 2 ref no en índice +8106 2 TESALONICENSES 1, 11 - 2, 2 ref no en índice +8131 2 TESALONICENSES 2, 15 - 3, 5 ref no en índice +11260 MALAQUIAS 3,19-20a ref no en índice +8195 ROMANOS 13, 11-14 ref no en índice +4701 GÉNESIS 2, 7-9 y 3, 1-7 ref no en índice +4932 1 PEDRO 2, 20-25 ref no en índice +5218 ROMANOS 8, 35-39 ref no en índice +9039 ECLESIÁSTICO 27, 33 a 28, 9 ref no en índice +5549 SABIDURÍA 6, 13-17 ref no en índice +5550 1 TESALONICENSES 4, 12-17 ref no en índice +5640 ISAÍAS 63, 16 a 64, 8 ref no en índice +5657 2 PEDRO 3, 8-14 ref no en índice +5954 LEVÍTICO 13. 1-2, 44-46 título no parseable +71 MARCOS 15, 1-39 ref no en índice +15823 SEGUNDO ‘ANUNCIO PASCUAL’: LO FEMENINO DEL ALMA DISPONE A LA EXPERIENCIA PASCUAL LC 24, 6-10 ref no en índice +6302 HECHOS 10, 25-48 ref no en índice +6644 1 REYES 19, 4-8 ref no en índice +10026 SANTIAGO 2, 1-5 ref no en índice +6818 NÚMEROS 11, 25-29 ref no en índice +6896 ISAÍAS 53, 10-11 ref no en índice +2849 MARCOS 10, 46-52 ref no en índice +16393 DEUTERONOMIO 6, 2-6 ref no en índice +6970 1 REYES 17, 10-16 ref no en índice +2001 LUCAS 1, 1-4 / LUCAS 4, 14-21 ref no en índice +7746 ISAÍAS 66, 10-14 ref no en índice +1648 LUCAS 17, 5-10 ref no en índice +17511 “NO NOS DEJES CAER EN LA TENTACIÓN” (MT 6, 13) ref no en índice