Skip to content

feat(theme): enqueue AI translations from the meta box instead of translating inline (#156) - #157

Merged
JohnRDOrazio merged 2 commits into
mainfrom
feat/ai-translate-enqueue-156
May 27, 2026
Merged

feat(theme): enqueue AI translations from the meta box instead of translating inline (#156)#157
JohnRDOrazio merged 2 commits into
mainfrom
feat/ai-translate-enqueue-156

Conversation

@JohnRDOrazio

@JohnRDOrazio JohnRDOrazio commented May 26, 2026

Copy link
Copy Markdown
Member

Implements #156.

Problem

The Languages → AI Translation meta box ("Translate All" + per-language buttons) translated synchronously — one blocking OpenAI call per button, fired concurrently (Promise.all). On a long article (e.g. post 1326, ~33 KB) five concurrent OpenAI calls rate-limit/time-out, so typically only one language succeeds and the rest show "failed".

Change

Make the meta box enqueue, exactly like /cdcf/v1/translate — the worker (cdcf_process_translation) does the OpenAI work asynchronously and sequentially, which is reliable at any size and already carries footnote-anchor protection (#152), auto-publish, ACF copy, and language-aware featured-image handling.

  • Shared core: extracted the resolve/create + link + enqueue into cdcf_enqueue_post_translation() (handlers/translate.php), now used by both cdcf_rest_translate() and cdcf_ajax_ai_translate(). Linking goes through the GET_LOCK helper (moved here as cdcf_translate_link_under_lock) with orphan-cleanup on link failure — so /cdcf/v1/translate gains the same race-safety the meta box had (fix(theme): serialize Polylang group linking in AI media translation #150/fix(cdcf-mcp): preserve colon footnote anchors through wp_kses_post #152), and concurrent "Translate All" enqueues stay safe.
  • Thin handler: cdcf_ajax_ai_translate() is now auth + delegate + JSON. The ~200 lines of synchronous OpenAI / ACF / attachment / auto-publish code are removed — that behaviour lives in the worker (covered by ProcessTranslationTest).
  • UX: buttons report "⏳ Queued" (Edit link built via DOM, no innerHTML); "Translate All" → "All queued — translations will appear shortly."

Reconciliation (per the issue's checklist)

Verified the worker (cdcf_process_translation) is a superset of the old synchronous handler: title/content/excerpt (+ footnote protection), attachment alt-text + the "no translatable text" no-op, translatable + non-translatable ACF copy, language-aware featured image, and auto-publish-when-source-published. So enqueuing loses nothing.

Tests

  • AjaxAiTranslateTest rewritten for the thin delegate (auth, param forwarding, queued payload, WP_Error mapping).
  • TranslateHandlerTest gains lock (GET_LOCK/RELEASE_LOCK keyed on source) and link-failure-cleanup coverage; its existing create/reuse/attachment/parent/enqueue tests still pass (behaviour preserved).
  • Theme suite 363 green. Refactored files fully covered (ai-translate.php 15/15; translate.php's only uncovered lines are the WP-Cron fallback, exercised by its RunInSeparateProcess test).

Behaviour note

"Translate All" stays concurrent (fast enqueue), but each request now returns immediately and the worker drains the queue one at a time — so no more OpenAI pile-up, and the group can't corrupt (locked linking). This is the same path the manual sequential re-translations used successfully.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Translation requests are now queued for background processing instead of completing immediately
    • Added "Edit" links for queued draft translations, allowing users to review before publishing
    • UI status updated to show "Queuing…" instead of "Done"
    • "Translate All" messaging updated to indicate translations are queued and will appear shortly
  • Tests

    • Updated test suite to reflect asynchronous queueing behavior

Review Change Stack

…nslating inline (#156)

The "Translate All" / per-language buttons in the AI Translation meta box
ran synchronously: one blocking OpenAI call per button, fired concurrently
(Promise.all). On a long article five concurrent calls rate-limited/timed
out, so most languages showed "failed".

Make the meta box ENQUEUE, exactly like /cdcf/v1/translate, so the worker
(cdcf_process_translation) does the OpenAI work async + sequentially — which
is reliable at any size and already carries the footnote-anchor protection,
auto-publish, ACF copy, and language-aware featured image.

- Extract the create/resolve + link + enqueue core into the shared
  cdcf_enqueue_post_translation() (handlers/translate.php), used by both
  cdcf_rest_translate() and cdcf_ajax_ai_translate(). The linking now goes
  through the GET_LOCK helper (moved here as cdcf_translate_link_under_lock)
  with orphan-cleanup on link failure — so /cdcf/v1/translate gains the same
  race-safety the meta box had (#150/#152), and concurrent "Translate All"
  enqueues stay safe.
- cdcf_ajax_ai_translate() becomes a thin auth + delegate + JSON wrapper; the
  ~200 lines of synchronous OpenAI/ACF/attachment/auto-publish code are gone
  (that behaviour lives in the worker, covered by ProcessTranslationTest).
- Meta-box JS: buttons now report "Queued" (DOM-built Edit link, no innerHTML)
  and "Translate All" says translations will appear shortly.

Tests: AjaxAiTranslateTest rewritten for the thin delegate; TranslateHandlerTest
gains lock + link-failure-cleanup coverage. Theme suite 363 green; refactored
files fully covered (ai-translate 15/15; translate.php's only gaps are the
WP-Cron fallback, exercised by its separate-process test).

Implements #156.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@JohnRDOrazio, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 14 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8abd6ce1-e7b5-478c-b6c5-5e1bef1c662e

📥 Commits

Reviewing files that changed from the base of the PR and between 051c131 and 41874ea.

📒 Files selected for processing (3)
  • wordpress/themes/cdcf-headless/functions.php
  • wordpress/themes/cdcf-headless/includes/handlers/translate.php
  • wordpress/themes/cdcf-headless/tests/TranslateHandlerTest.php
📝 Walkthrough

Walkthrough

This PR refactors AI translation from synchronous in-request processing to asynchronous enqueueing. A new shared cdcf_enqueue_post_translation() function validates inputs, creates or reuses translation posts, links them into Polylang groups under MySQL advisory locks, and queues work for background processing. The AJAX and REST handlers become thin wrappers delegating to this helper. The admin UI updates to show "Queuing…" state and an "Edit" link instead of a completion checkmark.

Changes

Async AI Translation Enqueueing

Layer / File(s) Summary
Enqueue and Polylang Linking Infrastructure
wordpress/themes/cdcf-headless/includes/handlers/translate.php
Introduces cdcf_enqueue_post_translation() to validate inputs, create or reuse translation posts, and coordinate enqueueing; defines cdcf_translate_link_under_lock() to link posts into Polylang groups using MySQL advisory locks (GET_LOCK/RELEASE_LOCK). REST endpoint updated to call the enqueue helper and return queued metadata (post_id, queue, errors).
AJAX Handler Refactoring
wordpress/themes/cdcf-headless/includes/admin/ai-translate.php
Replaces synchronous in-request translation with a thin wrapper: authenticates via nonce, delegates to cdcf_enqueue_post_translation(), and maps results to wp_send_json_success() or wp_send_json_error(). Removed inline OpenAI calls, translation post creation, group linking, and content writing logic (now centralized).
Frontend UI State Updates
wordpress/themes/cdcf-headless/functions.php
Updates admin Polylang meta box JavaScript: in-flight status shows "Queuing…" instead of "Translating…", success response appends DOM-based "Edit" link instead of rendering a "✓ Done" checkmark, error condition recognizes "Queuing…" as the expected pre-failure state, and "Translate All" messaging indicates translations will appear shortly rather than all being done immediately.
Handler Enqueue and Locking Tests
wordpress/themes/cdcf-headless/tests/TranslateHandlerTest.php
Adds test coverage for MySQL lock acquisition during Polylang group linking (verifies GET_LOCK and RELEASE_LOCK SQL) and orphan post cleanup when linking fails (verifies wp_delete_post called and enqueue not triggered). Test teardown now unsets mocked $wpdb to prevent cross-test leakage.
AJAX Adapter and Wrapper Tests
wordpress/themes/cdcf-headless/tests/AjaxAiTranslateTest.php
Updates tests to verify wrapper-level behavior: permission short-circuiting prevents enqueue delegation, success payload maps correctly from enqueue helper, WP_Error responses convert to JSON errors, and provided post_id forwards unchanged for retranslation. Removes prior comprehensive synchronous translation tests (OpenAI, attachment metadata, ACF fields, auto-publish, linking/locking logic), which are now centralized in the handler layer.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related Issues

Possibly Related PRs

  • CatholicOS/cdcf-website#37: Both PRs implement the background translation enqueueing pipeline; PR #37 adds automation to enqueue draft language translations on post status transitions, while this PR refactors the AI translate AJAX/REST/handler flow to use that same enqueueing infrastructure.
  • CatholicOS/cdcf-website#150: This PR extracts Polylang "link translation under a keyed MySQL lock" logic out of the synchronous admin handler into the shared translation enqueue/handler helpers, directly aligning with that PR's goal of serializing Polylang group linking.

Poem

🐰 A queue to honor Polylang's group,
No locks were lost, no posts left looping—
The admin clicks, the worker waits,
Translations soon appear as drafts.
"Queuing now" the button says,
And Edit links guide editors' ways.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: converting from synchronous inline translation to asynchronous enqueueing of AI translations from the meta box interface.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-translate-enqueue-156

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production

codacy-production Bot commented May 26, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics -49 complexity

Metric Results
Complexity -49

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
wordpress/themes/cdcf-headless/includes/handlers/translate.php (2)

82-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate caller-supplied post_id before bypassing resolution.

When post_id is non-zero, this helper skips both source existence checks and any verification that the target post actually belongs to $source_id / $target_lang, then queues work immediately. A stale or arbitrary ID can send the worker at the wrong post. Reject mismatches up front or resolve the expected translation ID from Polylang before enqueueing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress/themes/cdcf-headless/includes/handlers/translate.php` around lines
82 - 149, The function currently trusts a non-zero caller-supplied post_id and
skips validating the source/translation relationship; before enqueueing (where
cdcf_enqueue_translation/wp_schedule_single_event are called) fetch and validate
the provided post_id: ensure get_post($post_id) exists and, if Polylang is
available, verify pll_get_post($source_id, $target_lang) either matches $post_id
or use the pll_get_post result as the canonical translation id; if there is no
matching translation return a WP_Error('invalid_post', ...) (or override
$post_id with the resolved ID) so we never queue work for a stale/arbitrary
post.

141-147: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t report wp-cron if wp_schedule_single_event() fails (wordpress/themes/cdcf-headless/includes/handlers/translate.php:141-147)

wp_schedule_single_event() return value is ignored, so this REST path can still respond 202 with queue => wp-cron even when the cron event wasn’t actually scheduled. Treat scheduling failure as a server error before calling spawn_cron().

🔧 Proposed fix
     if (function_exists('cdcf_enqueue_translation')) {
         $queue = cdcf_enqueue_translation($post_id, $source_id, $target_lang);
     } else {
-        wp_schedule_single_event(time(), 'cdcf_async_translate', [$post_id, $source_id, $target_lang]);
+        $scheduled = wp_schedule_single_event(time(), 'cdcf_async_translate', [$post_id, $source_id, $target_lang], true);
+        if (is_wp_error($scheduled) || !$scheduled) {
+            return new WP_Error('enqueue_failed', 'Failed to schedule translation job.', ['status' => 500]);
+        }
         spawn_cron();
         $queue = 'wp-cron';
     }
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wordpress/themes/cdcf-headless/functions.php`:
- Around line 2147-2152: The current code calls Promise.all with
translateOne(btn).catch(()=>{}) which hides failures and always sets
allBtn.textContent to "All queued…"; update this to detect per-button failures
(use Promise.allSettled or have translateOne return success/failure) by mapping
buttons to promises that resolve with a status object or using
allSettled(buttonPromises), then inspect the results: if every result is
fulfilled/success set allBtn.textContent to the success message, otherwise set a
failure/partial message and re-enable allBtn as appropriate; reference the
translateOne function, the allBtn element and the '.cdcf-ai-translate-btn'
button list when locating the change.

In `@wordpress/themes/cdcf-headless/includes/handlers/translate.php`:
- Around line 157-160: The handler is re-sanitizing request values which
violates the cdcf/v1 REST contract; update the call in translate.php so
cdcf_enqueue_post_translation receives the raw, already-sanitized $request
values instead of wrapping them in intval/absint/sanitize_text_field (i.e. pass
$request['source_id'], $request['target_lang'], $request['post_id'] directly),
and ensure any sanitization remains declared in the register_rest_route() args
block for these fields.

---

Outside diff comments:
In `@wordpress/themes/cdcf-headless/includes/handlers/translate.php`:
- Around line 82-149: The function currently trusts a non-zero caller-supplied
post_id and skips validating the source/translation relationship; before
enqueueing (where cdcf_enqueue_translation/wp_schedule_single_event are called)
fetch and validate the provided post_id: ensure get_post($post_id) exists and,
if Polylang is available, verify pll_get_post($source_id, $target_lang) either
matches $post_id or use the pll_get_post result as the canonical translation id;
if there is no matching translation return a WP_Error('invalid_post', ...) (or
override $post_id with the resolved ID) so we never queue work for a
stale/arbitrary post.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec49b714-71b1-45da-a17a-e3149d6e69d3

📥 Commits

Reviewing files that changed from the base of the PR and between 85b15f5 and 051c131.

📒 Files selected for processing (5)
  • wordpress/themes/cdcf-headless/functions.php
  • wordpress/themes/cdcf-headless/includes/admin/ai-translate.php
  • wordpress/themes/cdcf-headless/includes/handlers/translate.php
  • wordpress/themes/cdcf-headless/tests/AjaxAiTranslateTest.php
  • wordpress/themes/cdcf-headless/tests/TranslateHandlerTest.php

Comment thread wordpress/themes/cdcf-headless/functions.php Outdated
Comment thread wordpress/themes/cdcf-headless/includes/handlers/translate.php Outdated
- functions.php "Translate All": use Promise.allSettled and report the real
  outcome — count rejected enqueues and show a partial-failure message (and
  re-enable the button) instead of swallowing failures into a blanket
  "all queued".
- translate.php cdcf_rest_translate: stop re-sanitizing — the route's
  args-block already declares absint / sanitize_text_field, so pass the
  request values through per the cdcf/v1 contract (#111). The admin-ajax
  caller still sanitizes its raw $_POST on its own side.
- translate.php cdcf_enqueue_post_translation: validate a caller-supplied
  post_id (must exist AND be the source's Polylang translation for the target
  language) before enqueuing, so the worker never writes a translation into a
  stale or arbitrary post. Returns invalid_post (404/400) otherwise.

Tests: provided-post_id test now asserts validation; added missing-post and
not-the-translation rejection tests. Theme suite 365 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnRDOrazio

Copy link
Copy Markdown
Member Author

Addressed the review findings (all three valid against current code):

  1. "Translate All" swallowed failures — switched to Promise.allSettled; it now counts rejected enqueues and shows a partial-failure message (re-enabling the button) instead of a blanket "all queued".
  2. cdcf_rest_translate re-sanitized — the route's args block already declares absint/sanitize_text_field, so the handler now passes $request[...] through per the audit(theme): defense-in-depth re-sanitization of REST inputs inside handlers #111 contract. (The admin-ajax handler still sanitizes its raw $_POST on its own side, since it's not behind REST args.)
  3. Unvalidated caller-supplied post_idcdcf_enqueue_post_translation now validates that a provided post_id exists and is the source's Polylang translation for the target language before enqueuing, returning invalid_post (404/400) otherwise — so the worker can't be pointed at a stale/arbitrary post to overwrite.

Tests updated (provided-post_id now asserts validation; added missing-post and not-the-translation rejection tests). Theme suite 365 green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants