feat(submissions): translate featured-media attachment before queueing post translations#208
feat(submissions): translate featured-media attachment before queueing post translations#208JohnRDOrazio wants to merge 1 commit into
Conversation
…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>
📝 WalkthroughWalkthroughThis 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. ChangesAttachment translation prerequisite flow
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 42 |
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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.phpwordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php
| $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); | ||
| } |
There was a problem hiding this comment.
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.
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:quietly assigns the EN attachment when no sibling exists. That leaves non-EN posts with:
nullfeaturedImage 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:
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): arraypll_set_post_languagepll_save_post_translationswith the full mapfalse, every just-created sibling iswp_delete_post'd withforce=true(same shape as PR fix(submissions): atomic Polylang group save to eliminate lost-update race #203 for posts)grep 'cdcf_ensure_attachment'yields the complete per-publish attachment timeline){lang => attachment_id}map on success,[]on bailoutcdcf_create_attachment_translation($source, $source_lang, $target_lang): ?inttitle/caption/description/alt_textvia the existingcdcf_openai_translatehelperwp_postsrow pointing at the source's underlying_wp_attached_file— no new bytes uploaded, Polylang media translation is metadata-only_wp_attachment_metadata(image dimensions, EXIF, etc.) verbatim_wp_attachment_image_altmeta (lives in postmeta, not the posts table)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 placenullonly onwp_insert_posthard failureTests
578/578 theme suite green (was 569 pre-PR, +9 new tests + 1 stub addition):
bails_when_polylang_inactive— function_exists guardbails_when_source_isnt_attachment— post_type checkskips_langs_already_linked— pre-seed honored, only missing langs get new draftsskips_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 ONEpll_save_post_translationscall with the full 6-language map (regression guard for the lost-update race in the attachment context)rolls_back_on_atomic_save_failure—pll_save_post_translationsreturningfalseforce-deletes every just-created siblingcreate_helper_uses_openai_translated_strings— title/caption/description/alt-text all flow through correctly; alt-text written via the right meta keycreate_helper_falls_back_to_source_on_openai_error— OpenAI error → source strings verbatim, not failurecreate_helper_returns_null_on_wp_insert_post_failure— hard failure pathExisting tests preserved:
stubCommonFunctions()now stubsget_post_thumbnail_idto0(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
Deploy
Backend (theme) change — ship via
gh workflow run deploy.yml -f environment=productionafter merge.Related
/link-term-translationscompanion endpoint🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests