Skip to content

feat(rest): POST /cdcf/v1/link-term-translations + Python client method#205

Merged
JohnRDOrazio merged 2 commits into
mainfrom
feat/link-term-translations-endpoint
Jun 11, 2026
Merged

feat(rest): POST /cdcf/v1/link-term-translations + Python client method#205
JohnRDOrazio merged 2 commits into
mainfrom
feat/link-term-translations-endpoint

Conversation

@JohnRDOrazio

@JohnRDOrazio JohnRDOrazio commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds the term equivalent of the existing /cdcf/v1/link-translations post endpoint. Polylang's term-side language + group helpers (pll_set_term_language, pll_save_term_translations) are PHP-only — there's no native REST surface for them — so corrupted Polylang term groups (like the post-#201 ConfessIt state where term 171 examen ended up language=fr with no en entry in its group) can't be repaired from a Python script alone. This is the thin REST wrapper that closes the gap.

Endpoint

POST /cdcf/v1/link-term-translations
Auth: edit_posts capability (Application Password or Zitadel bearer)
Body: {
  "taxonomy": "project_tag",
  "translations": { "en": 169, "it": 231, "es": 241, ... }
}

Returns { success: true, taxonomy, translations } on success, or WP_Error with appropriate HTTP status on validation/save failure.

Validation mirrors /link-translations:

Error code HTTP Trigger
polylang_missing 500 pll_set_term_language or pll_save_term_translations undefined
invalid_taxonomy 400 taxonomy slug missing or doesn't exist
invalid_translations 400 not an array, or fewer than 2 entries
invalid_term 400 any term_id doesn't exist in the named taxonomy (also catches get_term WP_Error)
link_failed 500 pll_save_term_translations returned false

Test plan

  • php -l clean on the new handler + functions.php
  • composer test --working-dir=wordpress/themes/cdcf-headless557/557 pass (+9 new tests covering each WP_Error path + happy path with per-term language set order + atomic save with the full map)
  • After merge + production deploy: use it to repair ConfessIt's term groups (171 + 173) and atomically link new examination siblings for IT/ES/FR/PT

Python client + CLI

CdcfClient.link_term_translations(taxonomy: str, translations: dict[str, int]) -> dict
scripts/cdcf_api.py link-term-translations \
  --taxonomy project_tag \
  --translations '{"en":169,"it":231,"es":241,"fr":252,"pt":264,"de":275}'

Deploy

Backend (theme) change — ship via gh workflow run deploy.yml -f environment=production after merge. A bare deploy.yml run defaults to staging and skips the WP theme steps.

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added REST API endpoint to atomically link multi-language term translations within taxonomies.
    • Added CLI command for managing term translations, accepting language-to-term ID mappings in JSON format.
  • Tests

    • Added comprehensive test suite validating successful linking scenarios, error handling for invalid inputs, and edge cases.

Term equivalent of /cdcf/v1/link-translations. Polylang's term-side
language + group helpers (pll_set_term_language, pll_save_term_translations)
are PHP-only — there's no native REST surface for them, so corrupted
term Polylang groups (like the post-#201 ConfessIt state where term 171
"examen" ended up language=fr with no en sibling in its translation
group) can't be repaired from a Python script alone. This adds the
thin REST wrapper that fixes that.

Endpoint shape:
  POST /cdcf/v1/link-term-translations
  Auth: edit_posts capability (same baseline as /link-translations)
  Body: {
    "taxonomy": "project_tag",
    "translations": { "en": 169, "it": 231, "es": 241, ... }
  }
  Returns: { success: true, taxonomy, translations } on success;
           WP_Error with appropriate HTTP status on validation/save failure.

Validation matches /link-translations' shape:
  - polylang_missing (500): pll_set_term_language or
    pll_save_term_translations not defined
  - invalid_taxonomy (400): taxonomy slug missing or doesn't exist
  - invalid_translations (400): not an array, or fewer than 2 entries
  - invalid_term (400): any term_id doesn't exist in the named taxonomy
    (also catches get_term WP_Error return)
  - link_failed (500): pll_save_term_translations returned false

9 tests covering each WP_Error path + the happy path (sets language on
each term in order, then one atomic save with the full map, returns
the validated translations dict). 557/557 theme suite green.

Python client + CLI:
  - CdcfClient.link_term_translations(taxonomy, translations) wraps the POST
  - `scripts/cdcf_api.py link-term-translations --taxonomy X --translations '{...}'`

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

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 17 minutes and 16 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: 741c73b3-97ac-42cf-bb27-4e086e2540c7

📥 Commits

Reviewing files that changed from the base of the PR and between 31f7c2b and 6ac40c8.

📒 Files selected for processing (4)
  • wordpress/themes/cdcf-headless/functions.php
  • wordpress/themes/cdcf-headless/includes/sanitizers.php
  • wordpress/themes/cdcf-headless/tests/SanitizersTest.php
  • wordpress/themes/cdcf-headless/tests/bootstrap.php
📝 Walkthrough

Walkthrough

This PR introduces a new feature for atomically linking Polylang translation term IDs within a taxonomy. The change spans a REST handler, endpoint registration, Python CLI client, and comprehensive test coverage across all layers.

Changes

Polylang Term Translation Linking

Layer / File(s) Summary
REST handler implementation
wordpress/themes/cdcf-headless/includes/handlers/link-term-translations.php
Core cdcf_rest_link_term_translations() function validates Polylang availability, checks taxonomy and translations input, verifies all referenced terms exist in the taxonomy, sets Polylang language per term, and persists the translation group atomically. Returns structured WP_Error responses on validation/persistence failure or a normalized success payload on completion.
REST API endpoint registration
wordpress/themes/cdcf-headless/functions.php
Registers POST /cdcf/v1/link-term-translations endpoint with edit_posts capability check, sanitizes and validates taxonomy and translations arguments, and includes the handler file.
Python CLI client method and command
scripts/cdcf_api.py
New CdcfClient.link_term_translations() method POSTs to the endpoint with taxonomy slug and language→term-ID mapping. CLI is extended with link-term-translations subcommand accepting --taxonomy and JSON --translations arguments, parsed and routed through _run_cli.
Handler test suite
wordpress/themes/cdcf-headless/tests/LinkTermTranslationsHandlerTest.php, wordpress/themes/cdcf-headless/tests/bootstrap.php
PHPUnit test class with Brain Monkey mocking validates error handling for missing Polylang (HTTP 500), invalid taxonomy (HTTP 400), non-array translations (HTTP 400), fewer than two translations (HTTP 400), missing/invalid terms (HTTP 400 with error message), persistence failure (HTTP 500), and happy path that verifies Polylang language setting per term and translation group linking with normalized response.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • CatholicOS/cdcf-website#15: Both PRs modify scripts/cdcf_api.py's CLI argument parsing and command dispatch logic to add new subcommands.

Poem

🐰 Translations linked with atomic care,
Terms bound together, a perfect pair,
Polylang speaks in languages true,
From Python to REST, the pipeline flows through.
thump thump

🚥 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 directly and concisely summarizes the main change: adding a new REST endpoint for linking Polylang term translations and its corresponding Python client method.
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/link-term-translations-endpoint

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.

@codecov-commenter

codecov-commenter commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...dless/includes/handlers/link-term-translations.php 94.73% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codacy-production

codacy-production Bot commented Jun 11, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 27 complexity

Metric Results
Complexity 27

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.

@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/functions.php`:
- Around line 442-458: Update the register_rest_route args for the translations
parameter to add a sanitize_callback that enforces and returns a safe
associative array: inside the callback (referenced from the
cdcf/v1/link-term-translations and cdcf/v1/link-translations route registrations
in functions.php) first decode/ensure the input is an associative array with at
least two entries, then validate each language key against a strict pattern or
whitelist (e.g., /^[a-z]{2}(-[A-Z]{2})?$/ or your Polylang-allowed codes) and
remove/reject invalid keys, and for each value cast/validate IDs using absint or
numeric checks (dropping non-numeric/zero values); return the sanitized map or
null/wp_error on failure so includes/handlers/link-term-translations.php and the
link-translations handler receive only validated language codes and integer IDs.
🪄 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: 635fc0b5-bfdf-433d-beff-1450cc209719

📥 Commits

Reviewing files that changed from the base of the PR and between fef66d3 and 31f7c2b.

📒 Files selected for processing (5)
  • scripts/cdcf_api.py
  • wordpress/themes/cdcf-headless/functions.php
  • wordpress/themes/cdcf-headless/includes/handlers/link-term-translations.php
  • wordpress/themes/cdcf-headless/tests/LinkTermTranslationsHandlerTest.php
  • wordpress/themes/cdcf-headless/tests/bootstrap.php

Comment thread wordpress/themes/cdcf-headless/functions.php
CodeRabbit on #205 flagged that the `translations` arg on both
/cdcf/v1/link-translations and /cdcf/v1/link-term-translations had only
type=object declared at register_rest_route — no sanitize_callback —
which violates the convention CLAUDE.md settled in #111 ("Every
cdcf/v1 route declares its sanitize_callback per field in the args
block; handlers trust the sanitized input and do NOT re-sanitize").

Adds a shared helper cdcf_sanitize_translations_map() in a new
includes/sanitizers.php:
  - Non-array input → empty array
  - Each key must match /^[a-z]{2}(-[A-Z]{2})?$/ (ISO 639-1 alone plus
    optional ISO 3166-1 region; matches Polylang slug shape)
  - Each value coerced via absint(); zero/negative dropped silently
  - Invalid entries are silently dropped (the handlers already return
    contextual WP_Error on count < 2, so the registration-time drop
    plus handler-level structural check still produces meaningful
    400s on bad input)

Applied as sanitize_callback to BOTH route registrations — the helper
is shared, so adding to both is trivial overhead. Handlers unchanged
per CLAUDE.md's no-defense-in-depth-re-sanitization rule (the existing
(int) casts in the handlers are now redundant but harmless; leaving
them avoids a wider change to this PR's scope).

8 new tests in SanitizersTest.php covering:
  - non-array/null/int/object input → empty array
  - empty array → empty array
  - well-formed map passes through with int coercion (string + float)
  - ISO 639-1 alone + ISO 639-1 + ISO 3166-1 region (en-US, pt-BR, etc.)
  - drops malformed keys (3-letter, uppercase, digits, underscore-region,
    3-letter-region, empty)
  - drops non-string keys (int, numeric-string PHP-coerced to int)
  - drops zero/negative/non-numeric values via absint
  - 6-lang production map round-trips intact

565/565 theme suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnRDOrazio JohnRDOrazio merged commit bcdac84 into main Jun 11, 2026
13 checks passed
@JohnRDOrazio JohnRDOrazio deleted the feat/link-term-translations-endpoint branch June 11, 2026 03:48
JohnRDOrazio added a commit that referenced this pull request Jun 11, 2026
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) <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