Refactor entity translation into a shared orchestrator service

Summary

This MR refactors ai_translate so entity translation is handled through a shared orchestration layer, and then exposes that shared workflow through both an AI FunctionCall plugin and an optional Tool API integration. The main goal is to stop duplicating entity-translation orchestration across entrypoints and establish one reusable path for:

  • UI batch translation
  • Drush entity translation
  • AI FunctionCall-based translation
  • optional Tool API-based translation

What changed

Shared translation orchestration

Added:

  • EntityTranslationOrchestratorInterface
  • EntityTranslationOrchestrator
  • EntityTranslationResult

This service now owns the shared entity translation flow:

  1. resolve source translation
  2. extract text metadata
  3. translate extracted field values
  4. decode HTML entities
  5. create target translation
  6. apply translation status behavior
  7. insert translated metadata
  8. save and return a structured result

Refactored existing entrypoints

Updated:

  • src/Controller/AiTranslateController.php
  • src/Drush/AiTranslateCommands.php
  • ai_translate.services.yml

Both UI and Drush now delegate entity translation logic to the shared orchestrator instead of keeping separate orchestration code.

Added AI FunctionCall support

Added:

  • src/Plugin/AiFunctionCall/TranslateEntity.php

This provides a FunctionCall entrypoint for translating content entities through the same shared orchestrator service.

The FunctionCall plugin:

  • accepts:
    • entity_type
    • entity_id
    • target_language
    • optional source_language
  • validates:
    • entity type exists
    • entity exists and is a content entity
    • entity type is translatable
  • enforces access through:
    • create ai content translation
    • entity update access
  • returns:
    • readable output
    • structured output
    • status values such as success, failed, skipped, and access_denied

Added optional Tool API integration

Added:

  • modules/ai_translate_tool/ai_translate_tool.info.yml
  • modules/ai_translate_tool/src/Plugin/tool/Tool/TranslateEntityTool.php

This adds an optional submodule that exposes the same translation workflow as a Tool API plugin, without making tool a hard dependency of the main ai_translate module.

The Tool plugin:

  • accepts:
    • entity_type_id
    • entity_id
    • target_language
    • optional source_language
  • validates:
    • entity type ID exists and is a content entity type
    • language inputs are valid installed site languages
    • entity exists and is a content entity
    • entity type is translatable
  • enforces access through:
    • create ai content translation
    • entity update access
  • declares structured output_definitions for:
    • status
    • message
    • entity_type_id
    • entity_id
    • source_language
    • target_language
    • translated_entity_label
    • failures

The Tool integration is packaged as a submodule so the main module remains usable without the Tool API installed.

Test coverage

Added:

  • tests/src/Unit/EntityTranslationOrchestratorTest.php
  • tests/src/Kernel/EntityTranslationOrchestratorKernelTest.php
  • tests/src/Kernel/TranslateEntityFunctionCallKernelTest.php
  • modules/ai_translate_tool/tests/src/Kernel/TranslateEntityToolKernelTest.php

PHPUnit / Composer test support

Added or updated:

  • composer.json
  • phpunit.bootstrap.php
  • phpunit.xml

The Composer PSR-4 autoload entries are needed so the new unit/kernel test classes are available in Drupal.org/GitLab CI when tests are run through Drupal core’s PHPUnit runner. The PHPUnit bootstrap/config is also included because, in this project setup, the default Drupal test bootstrap can otherwise resolve Drupal\ai_translate\... to the bundled ai_translate copy inside drupal/ai instead of this checkout.

For the optional Tool integration:

  • composer.json includes drupal/tool as a require-dev dependency
  • modules/ai_translate_tool/ai_translate_tool.info.yml declares test_dependencies on tool:tool

This allows Tool-specific tests to run in automated test environments without making Tool a runtime dependency of the base module.

Behavior / parity notes

This first draft keeps a few cleanup improvements instead of preserving legacy behavior 1:1.

Drush field failure handling

Previously, Drush could partially translate a field if one column failed partway through. Now, failed fields are skipped rather than partially translated.

Drush now respects translation_status

Previously, the draft/unpublished translation-status behavior was only applied in the UI flow. Now, Drush uses the same shared save logic and also respects that configuration.

Drush messaging changed

Previously, Drush emitted generic translation errors during field/column failures. Now, successful entity saves still report success, with warnings when some fields could not be translated.

Language edge cases

Language handling is now more explicit in the shared orchestrator, so invalid/unknown language edge cases may behave slightly differently than before.

For this first review draft, I am intentionally keeping these cleanups unless reviewers prefer to preserve behavior 1:1.

Edited by Sven Decabooter

Merge request reports

Loading