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:
EntityTranslationOrchestratorInterfaceEntityTranslationOrchestratorEntityTranslationResult
This service now owns the shared entity translation flow:
- resolve source translation
- extract text metadata
- translate extracted field values
- decode HTML entities
- create target translation
- apply translation status behavior
- insert translated metadata
- save and return a structured result
Refactored existing entrypoints
Updated:
src/Controller/AiTranslateController.phpsrc/Drush/AiTranslateCommands.phpai_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_typeentity_idtarget_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, andaccess_denied
Added optional Tool API integration
Added:
modules/ai_translate_tool/ai_translate_tool.info.ymlmodules/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_identity_idtarget_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_definitionsfor:statusmessageentity_type_identity_idsource_languagetarget_languagetranslated_entity_labelfailures
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.phptests/src/Kernel/EntityTranslationOrchestratorKernelTest.phptests/src/Kernel/TranslateEntityFunctionCallKernelTest.phpmodules/ai_translate_tool/tests/src/Kernel/TranslateEntityToolKernelTest.php
PHPUnit / Composer test support
Added or updated:
composer.jsonphpunit.bootstrap.phpphpunit.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.jsonincludesdrupal/toolas arequire-devdependencymodules/ai_translate_tool/ai_translate_tool.info.ymldeclarestest_dependenciesontool: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.