From e91396718261a9dae9db623d0f9512effb6023c7 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Wed, 10 Jun 2026 23:52:08 -0400 Subject: [PATCH 1/2] feat(cli): expose /cdcf/v1/translate-all as first-class scripts/cdcf_api.py command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The REST endpoint has existed since #156 and we hit a real use case for it today (post 1508 Eucharistic Miracles — needed to refire the atomic 5-language fan-out after the publish hook left the polylang group disconnected). The workaround was: scripts/.venv/bin/python scripts/cdcf_api.py --target production \ rest-post cdcf/v1/translate-all --data '{"source_id":1508}' which works but is awkward for a documented endpoint we expect to use repeatedly (admin-authored CPTs that bypass the publish hook, recovery from any future polylang-group disconnect, etc.). The Python client already exposes translate-post (per-language) and link-translations (post-side) and link-term-translations (term-side, from #205); adding translate-all rounds out the translation surface. Changes: - CdcfClient.translate_all(source_id) → POST /cdcf/v1/translate-all - CLI subcommand `translate-all --source-id N` - Help text documents the atomic shape + recovery use cases Usage: scripts/.venv/bin/python scripts/cdcf_api.py --target production \ translate-all --source-id 1508 Scripts-only change — no backend deploy needed. Smoke-tested locally; the existing translate-post / deploy-translation patterns are followed verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/cdcf_api.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/cdcf_api.py b/scripts/cdcf_api.py index 58edcae..5e98e1d 100644 --- a/scripts/cdcf_api.py +++ b/scripts/cdcf_api.py @@ -400,6 +400,24 @@ def translate_post(self, source_id: int, target_lang: str, post_id: int = 0) -> data["post_id"] = post_id return self._wp_post("cdcf/v1/translate", data) + def translate_all(self, source_id: int) -> dict: + """POST /cdcf/v1/translate-all + + Atomic 5-language fan-out. Creates-or-reuses drafts in every + non-source Polylang language of `source_id`, links them all into + one Polylang group via a single pll_save_post_translations() call, + and enqueues each translation. Same shape as clicking "Translate + All" in the wp-admin Languages meta-box but scriptable. + + Use cases: + - Re-fan-out translations on an admin-authored CPT (the publish + hook only fires for public-referral posts, so admin-created + posts need this explicit invocation). + - Recover from a publish-flow hook that didn't fire or whose + Polylang group got disconnected mid-flight. + """ + return self._wp_post("cdcf/v1/translate-all", {"source_id": source_id}) + def deploy_translation(self, source_id: int, target_lang: str, content: str, title: str | None = None) -> dict: """POST /cdcf/v1/deploy-translation""" @@ -582,6 +600,13 @@ def _build_parser() -> argparse.ArgumentParser: p.add_argument("--target-lang", required=True) p.add_argument("--post-id", type=int, default=0) + # translate-all + p = sub.add_parser( + "translate-all", + help="Atomic 5-language fan-out: create-or-reuse drafts for every non-source Polylang language and queue all translations", + ) + p.add_argument("--source-id", type=int, required=True) + # deploy-translation p = sub.add_parser("deploy-translation", help="Deploy translated content to a post") p.add_argument("--source-id", type=int, required=True) @@ -764,6 +789,9 @@ def _run_cli(args: argparse.Namespace, client: CdcfClient) -> dict: if cmd == "translate-post": return client.translate_post(args.source_id, args.target_lang, args.post_id) + if cmd == "translate-all": + return client.translate_all(args.source_id) + if cmd == "deploy-translation": kwargs = {} if args.title: From 0818d182bbc37f2e4da8964dfc8fcf799a1d47f3 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Thu, 11 Jun 2026 09:07:34 -0400 Subject: [PATCH 2/2] fix(cli): validate translate_all source_id > 0 at client boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit on #207 flagged that translate_all accepts any integer and forwards it directly to /cdcf/v1/translate-all. The backend's absint() sanitization silently coerces a negative ID into a different positive ID, which would enqueue translations for the wrong source post — a real silent-data-corruption defect. Adds an explicit `source_id <= 0` guard that raises ValueError with a clear message. The CLI (argparse type=int) already constrained input to integers, but library callers had no protection; this closes that gap. Other client methods that take IDs (link_translations, link_term_translations) are already protected by #205's cdcf_sanitize_translations_map dropping non-positive values — only translate_all sent a single scalar through with no sanitizer in path. Smoke-tested: source_id=0, -1, -1000 all raise ValueError before any HTTP call. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/cdcf_api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/cdcf_api.py b/scripts/cdcf_api.py index 5e98e1d..34039a3 100644 --- a/scripts/cdcf_api.py +++ b/scripts/cdcf_api.py @@ -415,7 +415,14 @@ def translate_all(self, source_id: int) -> dict: posts need this explicit invocation). - Recover from a publish-flow hook that didn't fire or whose Polylang group got disconnected mid-flight. + + Raises ValueError if source_id is not a positive integer — the + backend's absint() sanitization would otherwise silently coerce + a negative ID into a different positive ID and enqueue + translations for the wrong source post. """ + if source_id <= 0: + raise ValueError(f"source_id must be a positive integer, got {source_id!r}") return self._wp_post("cdcf/v1/translate-all", {"source_id": source_id}) def deploy_translation(self, source_id: int, target_lang: str, content: str,