Skip to content

feat: add support for translating Category elements#119

Open
abhinaykhalatkar wants to merge 1 commit into
digitalpulsebe:developfrom
abhinaykhalatkar:feature/category-element-support
Open

feat: add support for translating Category elements#119
abhinaykhalatkar wants to merge 1 commit into
digitalpulsebe:developfrom
abhinaykhalatkar:feature/category-element-support

Conversation

@abhinaykhalatkar
Copy link
Copy Markdown

@abhinaykhalatkar abhinaykhalatkar commented May 20, 2026

What this does

Adds support for translating Craft Categories. Until now the plugin only handled Entries, Assets, and Commerce Products/Variants : Categories were the obvious missing piece. After this PR they get the same treatment: the Translate button in the sidebar, the bulk action on the index page, the review modal, and the lifecycle events.

Why

Categories in Craft are first-class translatable elements : per-site titles, slugs, custom fields, URIs, the lot. But MultiTranslator::getSupportedElementClasses() didn't include them, so anyone with translatable categories had to write their own module on top of TranslateService to handle them. This was the biggest gap on the free tier and it shouldn't be one.

What changed (6 steps, in this order)

  1. ElementHelper::query(): added an explicit branch for Category::class. The old code fell back to Entry::find() in an else, which was a footgun waiting to happen: any new element type would silently get routed as an Entry. Added a comment so future contributors don't trip on this.

  2. TranslateService::findTargetCategory() : new method, modelled on findTargetEntry(). Uses Elements::propagateElement so the structure topology is preserved. If the source's group isn't enabled for the target site, it throws UnsupportedSiteException instead of silently falling through to duplicateElement (which would orphan the structure).

  3. Debug-log block in TranslateService : was hard-coded to read $source?->section->propagationMethod, which obviously doesn't work for anything that isn't an Entry. Replaced with a match(true) that picks the right descriptor per element class. As a side benefit this also fixes a quiet bug where Assets produced wrong values in the debug log (Assets don't have ->section either).

  4. Save block in TranslateService : Categories are saved with propagate=false. Craft's category propagation silently rejects per-site updates when the translated values diverge, so propagate=true would lose the translation. false updates just the per-site row, which is what we want : the row already exists by this point (either from step 2 or from Craft auto-propagating on first save).

  5. MultiTranslator::getSupportedElementClasses() : added Category::class. This is the actual "switch it on" change, and it lands last on purpose so the sidebar button never shows up before the dispatch underneath it works.

  6. BulkTranslateJob : wrapped each element in a try/catch for UnsupportedSiteException | InvalidConfigException. One bad target site no longer takes down the whole bulk job.

There are two behaviour changes, both strictly more lenient:

  • Bulk jobs now keep going past unsupported elements instead of failing the whole run
  • The debug log's propagationMethod field now emits sensible values instead of null (or throwing) for Categories and Assets

One thing to flag on permissions: anyone who already has Translate access on Entries will automatically have it on Categories. That's intentional and called out in the CHANGELOG, but worth surfacing here too.

How I tested it

Against Craft 5 with DeepL on a 2-site setup (en-US + de-DE), plus a third unsupported site (fr-FR) to exercise the failure paths.

  • Translate sidebar button renders on Category edit pages, root and nested
  • Bulk Translate action shows up on the categories index
  • All 6 sample categories translate cleanly across every field shape: title, intro (PlainText), body (CKEditor), and Matrix content_blocks with nested content (Books→Bücher, Electronics→Elektronik, Fiction→Belletristik, Programming→Programmierung, Phones→Handys, Laptops→Laptops)
  • Parent/child structure is preserved on the target site (Fiction's parent on de is the de Books : same canonical id as on default)
  • With debug=true, the log line for a Category translation contains 'group:testCategories' and doesn't throw
  • When the source category group isn't enabled for the target site, UnsupportedSiteException flashes cleanly on single-translate, and the bulk job logs and moves past it
  • All four lifecycle events fire with Category as sourceElement / targetElement
  • Regression check: Entry / Asset / Product paths unchanged

- new findTargetCategory() in TranslateService with structure-preserving
  propagation (uses propagateElement, never duplicateElement)
- explicit Category branch in ElementHelper::query() — removes a latent
  Entry-fallback footgun for any future supported element type
- type-aware debug-log propagationMethod via match(true) (also unmasks
  a latent Asset bug under debug=true that produced wrong values)
- save Category targets with propagate=false (Craft's category
  propagation pipeline otherwise silently rejects per-site rows with
  diverged translated values)
- BulkTranslateJob: per-element try/catch around UnsupportedSiteException
  and InvalidConfigException so one unsupported target site no longer
  fails the whole bulk job
- README: new "Supported element types" section + Category slug listener
  recipe via afterElementTranslation
- CHANGELOG: Unreleased entry documenting the additions + fixes
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.

1 participant