Skip to content

fix(theme): read translation source lang from source post, not site default#171

Merged
JohnRDOrazio merged 1 commit into
mainfrom
fix/translation-source-language
Jun 5, 2026
Merged

fix(theme): read translation source lang from source post, not site default#171
JohnRDOrazio merged 1 commit into
mainfrom
fix/translation-source-language

Conversation

@JohnRDOrazio
Copy link
Copy Markdown
Member

@JohnRDOrazio JohnRDOrazio commented Jun 5, 2026

Summary

cdcf_process_translation() was assembling the OpenAI prompt with the source language hardcoded to the site default (EN) via pll_default_language('slug'). For a Polylang-linked source post in any other language, this silently produced a "translate from English to X" prompt body even though the source post was actually German/Italian/Portuguese/etc.

It happens to work today because every existing entry point seeds the source in EN before fanning out (the /cdcf/v1/team-member and /cdcf/v1/community-channel etc. handlers all create EN first and then call translate per target lang). The bug was latent.

The linking layer in cdcf_translate_link_under_lock (translate.php:45) already reads the source's actual language correctly via pll_get_post_language($source_id). This brings the prompt-assembly layer in line.

Why now

Precondition for issue #2 — letting team members edit their bios via passkey login. The plan is to let authors draft in their native language (a German author drafts in German, an Italian author in Italian) and fan out re-translation to the other 5 langs from whatever they save. That UX requires the source-lang detection to actually read the source post.

Ships standalone, regardless of when the rest of issue #2 lands.

Change

- $source_lang = pll_default_language('slug');
+ $source_lang = pll_get_post_language($source_id) ?: pll_default_language('slug');

The ?: fallback preserves legacy behaviour for posts not yet linked into a Polylang group — no flag, no callsite churn.

Tests

Two new PHPUnit tests in ProcessTranslationTest:

  • test_source_language_read_from_source_post_not_site_default — German source post → captured $source_name === 'German' in the OpenAI call
  • test_source_language_falls_back_to_site_default_when_unset — empty pll_get_post_language → falls back to 'English'

Full theme suite passes locally:

$ composer test --working-dir=wordpress/themes/cdcf-headless
OK (393 tests, 929 assertions)

Test plan

  • CI's theme PHPUnit job goes green
  • Manual smoke after deploy: re-translate an existing EN-source post via wp-admin "Translate All" — output identical to current behaviour (negative regression check)
  • Manual smoke: temporarily create a DE source post via Polylang, queue a translation, confirm the OpenAI call's messages field shows "from German to ..." rather than "from English to ..." (error_log debug or worker log inspection)

Deploy

Theme change — needs gh workflow run deploy.yml -f environment=production to ship. A staging-target run skips theme/plugin steps and the bug would persist on live.

Summary by CodeRabbit

Bug Fixes

  • Improved source language detection for translations — The system now correctly identifies the source language from each post's assigned language instead of always using the site default, resulting in more accurate AI-powered translations. Falls back to site default when the source language is not yet assigned.

…efault

cdcf_process_translation() assembles the OpenAI prompt using
pll_default_language('slug') as the source language — which is always
the site default (EN). For a Polylang-linked source post in a different
language, this produces a "translate from English to X" prompt body
even though the source is actually German/Italian/etc.

It happens to work today because every existing entry point seeds the
source in EN before fanning out, but it would silently mistranslate any
non-EN source. Issue #2 (team-member bio self-edit) needs source-lang
parity for the author's chosen drafting language.

Read pll_get_post_language($source_id) first, falling back to
pll_default_language('slug') for posts not yet linked into a Polylang
group — preserves legacy behaviour without a flag.

The linking layer in cdcf_translate_link_under_lock already reads the
source's actual language correctly (translate.php:45); this brings the
prompt layer in line with it.

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

coderabbitai Bot commented Jun 5, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f67c2021-40f2-4820-8bb1-50eb1d1c6c61

📥 Commits

Reviewing files that changed from the base of the PR and between c241cd5 and 2859d4c.

📒 Files selected for processing (2)
  • wordpress/themes/cdcf-headless/includes/translation.php
  • wordpress/themes/cdcf-headless/tests/ProcessTranslationTest.php

📝 Walkthrough

Walkthrough

The translation system now determines OpenAI source language from each post's assigned Polylang language, falling back to the site default when the post has no language assignment. Implementation changes the language detection logic, and test updates add setup stubs and two validation cases confirming both the post-specific and fallback paths.

Changes

Post-level source language detection

Layer / File(s) Summary
Source language detection logic
wordpress/themes/cdcf-headless/includes/translation.php
cdcf_process_translation() now reads the source post's assigned Polylang language via pll_get_post_language($source_id) instead of always using the site default, falling back to the site default when the post has no language assignment.
Test infrastructure and validation
wordpress/themes/cdcf-headless/tests/ProcessTranslationTest.php
Test setup adds a default stub for pll_get_post_language() returning empty. Two new test cases verify the OpenAI prompt receives the source post's language name and that unassigned posts correctly fall back to the site default language name.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A post finds its voice in its own language true,
No more default assumptions—we read what fits you,
From German to English, each tongue gets its way,
The tests bless this logic—hip hop, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 summarizes the main change: the fix switches from using the site default language to reading the source language from the source post itself for translation prompts.
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 fix/translation-source-language

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 0 complexity

Metric Results
Complexity 0

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!

@JohnRDOrazio
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@JohnRDOrazio JohnRDOrazio merged commit cf73abc into main Jun 5, 2026
13 checks passed
@JohnRDOrazio JohnRDOrazio deleted the fix/translation-source-language branch June 5, 2026 02:35
JohnRDOrazio added a commit that referenced this pull request Jun 5, 2026
…ints

GET  /cdcf/v1/my-team-member
PATCH /cdcf/v1/my-team-member/{lang}

The authenticated user's bio self-edit surface. Discovery resolves the
caller's author_team_member ACF link, pulls the full Polylang group,
and reports which language versions are editable. Edit accepts a
partial PATCH against any language in that group, persists to the
target post, and fans out re-translation to the OTHER 5 langs from
the just-saved source — reusing cdcf_enqueue_post_translation() so
all the existing redis-queue + Polylang-link + advisory-lock infra
works as-is.

Authorization model
-------------------
- Bearer-validated session via the Phase 1b authenticator (or cookie
  / Application Password for non-frontend callers).
- author_team_member ACF user-field is the canonical ownership signal.
  It's admin-managed (set via /cdcf/v1/author-team-member, PR #160),
  so end users can never widen their own access from inside this
  endpoint.
- Permission gate: logged-in AND has any link → admits.
- Per-language gate inside the handler: the link must point at SOME
  post in the {lang} target's Polylang group. Covers the case where
  the admin set the link at the EN post but the user edits the DE
  sibling.

Translation strategy (issue #2 locked decision #3)
--------------------------------------------------
- Edit in any language — no privileged "primary language". Whichever
  language gets saved becomes the new source-of-truth for that cycle.
- Re-translation fan-out covers the other 5 langs in the group, NOT
  including the just-saved one. Each enqueue is independent; a single
  enqueue failure is recorded in `errors` without aborting siblings
  (the just-saved source-language post is already persisted at that
  point — we don't roll it back for a downstream queue blip).
- The OpenAI prompt reads source-lang from the source post (PR #171
  fix), so editing in any locale produces correct translations.

URL host allowlist
------------------
- member_linkedin_url: linkedin.com (or any subdomain) or empty.
- member_github_url:   github.com   (or any subdomain) or empty.
- esc_url_raw sanitization runs first via the args block; the
  hostname constraint can't be expressed there so it's enforced in
  the handler body, returning 400 on violation.
- Suffix match is dot-boundary anchored — linkedin.com.evil.org
  does NOT pass.

Partial PATCH semantics
-----------------------
- A field is updated only when the request actually carried a string
  value for it (caught via `is_string($request->get_param(…))`). A
  PATCH that only changes member_title does NOT clobber post_content.
- Empty string IS a valid value and clears the field (used by URL
  fields and member_title alike).

Tests (27 new in MyTeamMemberHandlerTest)
-----------------------------------------
- resolve_link:    scalar id / array of ids / WP_Post / false / anonymous
- collect_group:   normal / fallback to pll_get_post_language / invalid
- url_host_ok:     empty / exact / subdomain / unrelated / suffix-
                   lookalike / malformed
- permission:      401 anonymous / 403 no link / true linked
- GET happy:       full group with two languages
- GET filter:      polluted group with non-team_member post stripped
- PATCH happy:     DE edit fans out to en/it/es/fr/pt, all three ACF
                   fields written to the DE post, wp_update_post
                   carries the right ID + content
- PATCH partial:   member_title-only PATCH leaves wp_update_post
                   uncalled
- PATCH 404:       lang not in Polylang group
- PATCH 400:       LinkedIn URL pointing at evil.example.org
- PATCH 400:       GitHub URL pointing at gitlab.com
- PATCH 500:       wp_update_post WP_Error bubbles up
- PATCH partial-fail: one enqueue WP_Error recorded, siblings still
                      queued, no exception
- PATCH ownership: link points outside the resolved Polylang group →
                   403

Test bootstrap also gains a minimal WP_Post shim (4 fields) and the
new handler include is required-up-front like the others.

Theme suite: 420 → 447 tests (+27), 970 → 1033 assertions (+63).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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