Skip to content

feat(submissions): translate featured-media attachment before queueing post translations#208

Open
JohnRDOrazio wants to merge 1 commit into
mainfrom
feat/translate-featured-media-on-publish
Open

feat(submissions): translate featured-media attachment before queueing post translations#208
JohnRDOrazio wants to merge 1 commit into
mainfrom
feat/translate-featured-media-on-publish

Conversation

@JohnRDOrazio

@JohnRDOrazio JohnRDOrazio commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds Phase 0 to the publish hook: when a source has a featured_image, synchronously ensure attachment translation siblings exist for every target language before the post translation drafts are created. By the time the worker handles each post translation, pll_get_post($source_thumbnail_id, $target_lang) already returns the matching-language attachment sibling — no more EN-image fallback on non-EN posts.

Motivation

The existing fallback in cdcf_process_translation:

$lang_image_id = pll_get_post($source_thumbnail_id, $target_lang);
set_post_thumbnail($post_id, $lang_image_id ?: $source_thumbnail_id);

quietly assigns the EN attachment when no sibling exists. That leaves non-EN posts with:

  • English alt-text rendered to Italian/Spanish/etc. users (a11y + SEO regression)
  • Potentially null featuredImage from WPGraphQL depending on the Polylang resolver's cross-language behavior (the image bytes are fine — the file is shared — but the attachment post is in a different language and may be filtered at query time)

The fallback was the right defensive choice short-term, but the proper fix is to ensure same-language siblings actually exist.

Architecture

Phase 0 in the publish hook, before Phase 1 creates post drafts:

Phase 0 (NEW): ensure attachment translations exist for source's featured_image
Phase 1: create post sibling drafts + per-post language (existing)
Phase 2: ONE atomic pll_save_post_translations for posts (existing, PR #203)
Phase 3: enqueue post translation jobs (existing)

This is atomic by construction — the publish hook runs once per source publish; no Redis worker races possible. Mirrors the same atomic shape PR #203 established for post groups, just one level up the dependency chain.

Implementation

cdcf_ensure_attachment_translations($source_attachment_id, $target_langs): array

cdcf_create_attachment_translation($source, $source_lang, $target_lang): ?int

  • OpenAI-translates title / caption / description / alt_text via the existing cdcf_openai_translate helper
  • Creates a new wp_posts row pointing at the source's underlying _wp_attached_fileno new bytes uploaded, Polylang media translation is metadata-only
  • Copies _wp_attachment_metadata (image dimensions, EXIF, etc.) verbatim
  • Sets translated alt-text via _wp_attachment_image_alt meta (lives in postmeta, not the posts table)
  • OpenAI error → source-string fallback, not null. A sibling with source-language metadata still beats no sibling — the latter would regress to the EN-image fallback this PR is fixing in the first place
  • Returns null only on wp_insert_post hard failure

Tests

578/578 theme suite green (was 569 pre-PR, +9 new tests + 1 stub addition):

  • bails_when_polylang_inactive — function_exists guard
  • bails_when_source_isnt_attachment — post_type check
  • skips_langs_already_linked — pre-seed honored, only missing langs get new drafts
  • skips_source_lang_in_target_list — caller-error tolerance ('en' in target_langs is silently skipped, not duplicated)
  • atomic_save_then_returns_full_group — happy path; exactly ONE pll_save_post_translations call with the full 6-language map (regression guard for the lost-update race in the attachment context)
  • rolls_back_on_atomic_save_failurepll_save_post_translations returning false force-deletes every just-created sibling
  • create_helper_uses_openai_translated_strings — title/caption/description/alt-text all flow through correctly; alt-text written via the right meta key
  • create_helper_falls_back_to_source_on_openai_error — OpenAI error → source strings verbatim, not failure
  • create_helper_returns_null_on_wp_insert_post_failure — hard failure path

Existing tests preserved: stubCommonFunctions() now stubs get_post_thumbnail_id to 0 (default = no thumbnail = Phase 0 is a no-op), so all publish-flow tests that don't care about featured media continue to pass unchanged.

Trade-offs

  • Publish latency: +5 OpenAI calls per source publish (one per non-source language) when the source has a featured image. Per-call latency is small (1-2s); +5-10s total for the publish action. Acceptable for the once-per-public-referral-approval cadence.
  • OpenAI cost: ~$0.0001 per attachment per language for 4 short strings. Negligible.

Deploy

Backend (theme) change — ship via gh workflow run deploy.yml -f environment=production after merge.

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced featured image translation support in multi-language submissions by ensuring all required image translations are created before post publication, with automatic translation of image metadata (title, caption, description, alt text).
  • Tests

    • Added comprehensive test coverage for attachment translation workflows, including error handling and fallback scenarios.

…g post translations

Phase 0 of the publish hook now synchronously ensures attachment
translation siblings exist for the source's featured_image BEFORE
Phase 1 creates the post translation drafts. By the time the worker
later handles each post-translation job, pll_get_post(thumbnail, lang)
already returns the matching-language attachment sibling — no more
EN-image fallback on non-EN posts.

Motivation: the existing fallback in cdcf_process_translation
  $lang_image_id = pll_get_post($source_thumbnail_id, $target_lang);
  set_post_thumbnail($post_id, $lang_image_id ?: $source_thumbnail_id);
quietly assigned the EN attachment when no sibling existed. That left
non-EN posts with EN-language alt-text (a11y + SEO regression) and —
depending on the WPGraphQL+Polylang resolver behavior — could surface
as a null featuredImage on the frontend (cross-language attachment
filtered out at query time).

Implementation:
  cdcf_ensure_attachment_translations($source_attachment_id, $target_langs)
    - Phase 1: per-lang create attachment sibling + pll_set_post_language
    - Phase 2: ONE atomic pll_save_post_translations with the full map
      (mirrors PR #203's atomic shape for posts — same lost-update race
      avoidance, same rollback-on-failure semantics)
    - Phase 3 (n/a — no queueing; attachment translation is inline)

  cdcf_create_attachment_translation($source, $source_lang, $target_lang)
    - OpenAI-translates title/caption/description/alt-text via the
      existing cdcf_openai_translate helper
    - Creates a new wp_posts row pointing at the source's underlying
      _wp_attached_file (no new bytes uploaded — Polylang media
      translation is metadata-only)
    - Copies _wp_attachment_metadata + sets translated alt-text via
      _wp_attachment_image_alt meta
    - Falls back to source strings on OpenAI error rather than failing
      the create — a sibling with source-language metadata still beats
      no sibling (the latter regresses to the EN-image fallback we're
      fixing)
    - Returns null only on wp_insert_post hard failure

Diagnostic logging follows the PR #206 ENTER/PHASE_1_DONE/PHASE_2_OK
/PHASE_2_FAIL pattern with source_id keyed lines (single grep
'cdcf_ensure_attachment' yields the full per-publish attachment
timeline).

Integration point: cdcf_enqueue_translations_for_submission's existing
ENTER log now sees the source thumbnail check, calls Phase 0 if
present, then proceeds to Phase 1 unchanged. Phase 0 is a no-op when
the source has no thumbnail.

9 new tests cover:
  - bails when Polylang inactive
  - bails when source isn't an attachment
  - skips already-linked languages (pre-seed honored)
  - skips source_lang in target_langs (caller-error tolerance)
  - exactly-once atomic save with full final group (regression guard
    for the lost-update race in the attachment context)
  - rollback: pll_save_post_translations returning false force-deletes
    every just-created sibling (mirrors PR #203 shape)
  - create helper: OpenAI strings flow through to wp_insert_post +
    alt-text meta correctly
  - create helper: OpenAI error → source-string fallback (not null)
  - create helper: wp_insert_post failure → return null

578/578 theme suite green (was 569 pre-PR; +9 new tests + 1 stub
addition to stubCommonFunctions for the Phase 0 get_post_thumbnail_id
call — all existing publish-flow tests preserve original behavior with
the default thumbnail=0 stub meaning Phase 0 is a no-op).

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

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a pre-step in submission translation enqueue to ensure featured-image attachment translations exist before post translations. It introduces helpers for attachment sibling creation and atomic Polylang group saving with rollback, plus tests for bailouts, language skipping, success/failure save paths, translation fallback, and insert failures.

Changes

Attachment translation prerequisite flow

Layer / File(s) Summary
Phase 0 enqueue wiring and atomic attachment group save
wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.php, wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php
cdcf_enqueue_translations_for_submission() now calls attachment seeding before post sibling creation. cdcf_ensure_attachment_translations() validates Polylang/source attachment state, skips source and already-linked languages, creates missing siblings, issues one atomic pll_save_post_translations() call for the full map, and deletes newly created siblings if the save fails. Tests cover bailouts, skipping behavior, single atomic save, and rollback deletion.
Attachment sibling creation and translation fallback paths
wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.php, wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php
cdcf_create_attachment_translation() creates one attachment sibling with inherit status, copies source media linkage/metadata, translates text fields via cdcf_openai_translate when available, falls back to source text on WP_Error, and returns null if wp_insert_post() fails. Tests validate translated writes, fallback values, and null-return failure behavior.
Test scaffolding for thumbnail and attachment fixtures
wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php
Test stubs now default get_post_thumbnail_id() to 0 in common and wp-cron fallback paths, and add fakeAttachment() for WP_Post-like attachment fixtures used by the new helper tests.

Sequence Diagram(s)

sequenceDiagram
  participant EnqueueFn
  participant EnsureFn
  participant AttachmentCreator
  participant PolylangAPI

  EnqueueFn->>EnqueueFn: Read source thumbnail attachment ID
  EnqueueFn->>EnsureFn: Ensure attachment translations for target languages
  EnsureFn->>PolylangAPI: Read existing attachment translation group
  loop each target language
    EnsureFn->>AttachmentCreator: Create missing attachment sibling
    AttachmentCreator-->>EnsureFn: Return new attachment ID or null
  end
  EnsureFn->>PolylangAPI: Save full translation group once
  alt save succeeds
    PolylangAPI-->>EnsureFn: Return saved group
    EnsureFn-->>EnqueueFn: Return full attachment language map
  else save fails
    PolylangAPI-->>EnsureFn: Return failure
    EnsureFn->>EnsureFn: Delete newly created siblings
    EnsureFn-->>EnqueueFn: Return empty map
  end
  EnqueueFn->>EnqueueFn: Continue post translation enqueue phases
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • CatholicOS/cdcf-website#203: Both modify cdcf_enqueue_translations_for_submission() and Polylang translation group save behavior, including single-save and rollback expectations in tests.
  • CatholicOS/cdcf-website#206: Both touch phase sequencing in submission lifecycle translation flow and the atomic pll_save_post_translations() path.
  • CatholicOS/cdcf-website#150: Both update Polylang translation group creation/saving behavior around media translation linkage and merged-group persistence.

Poem

🐇 I hopped through Phase Zero at dawn,
and found each image sibling neatly drawn.
One atomic save, no strays in the warren,
rollback sweeps paths when links are torn.
In every tongue, the captions now sing—
carrot cheers for this multilingual spring!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% 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 and concisely summarizes the main change: adding attachment translation before post translations are queued, which is the primary purpose of this PR.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/translate-featured-media-on-publish

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

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 42 complexity

Metric Results
Complexity 42

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

❌ Patch coverage is 94.49541% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...f-headless/includes/admin/submission-lifecycle.php 94.49% 6 Missing ⚠️

📢 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: 1

🤖 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/includes/admin/submission-lifecycle.php`:
- Around line 112-115: The call to
cdcf_ensure_attachment_translations($source_thumbnail_id, $target_langs)
discards its return value so partial translation maps (or failures) allow posts
to proceed without same-language featured media; update submission-lifecycle.php
to capture the function's return, validate that a complete per-target-language
map was produced for all $target_langs (and/or that each target lang has a
corresponding attachment ID), and if not, halt/skip further Phase 1/3 processing
for this post (or queue a retry/error) to enforce Phase 0 coverage; reference
the call site using $source_thumbnail_id and $target_langs and the helper
function cdcf_ensure_attachment_translations to implement the check and early
exit.
🪄 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: b31523d7-1541-4699-a0a5-b1b17a45e314

📥 Commits

Reviewing files that changed from the base of the PR and between a13dde5 and 084a52a.

📒 Files selected for processing (2)
  • wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.php
  • wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php

Comment on lines +112 to +115
$source_thumbnail_id = (int) get_post_thumbnail_id($en_post_id);
if ($source_thumbnail_id > 0) {
cdcf_ensure_attachment_translations($source_thumbnail_id, $target_langs);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce Phase 0 coverage before Phase 1/3 continue.

Line 114 discards the result of cdcf_ensure_attachment_translations(). In the helper, Lines 286-305 can still return a partial/source-only map when per-language attachment creation fails, so post siblings can still be created/queued without same-language featured media — reintroducing the EN fallback/null featured-image behavior this phase is meant to eliminate.

Suggested fix
-    $source_thumbnail_id = (int) get_post_thumbnail_id($en_post_id);
-    if ($source_thumbnail_id > 0) {
-        cdcf_ensure_attachment_translations($source_thumbnail_id, $target_langs);
-    }
+    $source_thumbnail_id = (int) get_post_thumbnail_id($en_post_id);
+    if ($source_thumbnail_id > 0) {
+        $attachment_map = cdcf_ensure_attachment_translations($source_thumbnail_id, $target_langs);
+        $missing_attachment_langs = array_values(array_diff($target_langs, array_keys($attachment_map)));
+        if (!empty($missing_attachment_langs)) {
+            error_log(sprintf(
+                'cdcf_enqueue_translations_for_submission: Phase 0 incomplete for post %d thumbnail %d; missing attachment langs=%s; skipping enqueue.',
+                $en_post_id,
+                $source_thumbnail_id,
+                implode(',', $missing_attachment_langs)
+            ));
+            return;
+        }
+    }

Also applies to: 286-305

🤖 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/admin/submission-lifecycle.php`
around lines 112 - 115, The call to
cdcf_ensure_attachment_translations($source_thumbnail_id, $target_langs)
discards its return value so partial translation maps (or failures) allow posts
to proceed without same-language featured media; update submission-lifecycle.php
to capture the function's return, validate that a complete per-target-language
map was produced for all $target_langs (and/or that each target lang has a
corresponding attachment ID), and if not, halt/skip further Phase 1/3 processing
for this post (or queue a retry/error) to enforce Phase 0 coverage; reference
the call site using $source_thumbnail_id and $target_langs and the helper
function cdcf_ensure_attachment_translations to implement the check and early
exit.

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