diff --git a/code_samples/translations_management/config/services.yaml b/code_samples/translations_management/config/services.yaml new file mode 100644 index 0000000000..b198c91ba6 --- /dev/null +++ b/code_samples/translations_management/config/services.yaml @@ -0,0 +1,31 @@ +services: + App\TranslationsManagement\MyCustomProvider: + tags: + - name: 'ibexa.translations_management.auto_translate.provider' + identifier: 'my_custom_provider' + validation_profile: 'ai_generic' + App\TranslationsManagement\MyProviderValidator: + tags: + - name: 'ibexa.translations_management.auto_translate.provider.validator' + profile: 'my_custom_profile' + App\TranslationsManagement\MyTranslationAddExtension: + tags: + - { name: form.type_extension } + App\TranslationsManagement\ImageAltTextTransformer: + tags: + - name: 'ibexa.translations_management.auto_translate.field_value_transformer' + field_type_identifier: 'ibexa_image' + App\TranslationsManagement\MyCustomExclusionRule: + tags: + - { name: 'ibexa.translations_management.side_by_side.exclusion_rule' } + app.translations_management.exclusion_rule.custom_field_types: + class: Ibexa\TranslationsManagement\SideBySide\Service\UnsupportedFieldTypeExclusionRule + arguments: + $excludedFieldTypeIdentifiers: ['custom_blog_post', 'custom_landing_page'] + tags: + - { name: 'ibexa.translations_management.side_by_side.exclusion_rule' } + App\TranslationsManagement\TwigComponent\MyTranslationModalFooter: + tags: + - name: ibexa.twig.component + group: 'admin-ui-content-translation-modal-footer' + priority: 10 diff --git a/code_samples/translations_management/src/TranslationsManagement/ContentProxyTranslateSubscriber.php b/code_samples/translations_management/src/TranslationsManagement/ContentProxyTranslateSubscriber.php new file mode 100644 index 0000000000..e9442a3115 --- /dev/null +++ b/code_samples/translations_management/src/TranslationsManagement/ContentProxyTranslateSubscriber.php @@ -0,0 +1,39 @@ + ['onProxyTranslate', 200], + ]; + } + + public function onProxyTranslate(ContentProxyTranslateEvent $event): void + { + // Read the translation context: + $event->getContentId(); + $event->getFromLanguageCode(); // ?string — null when no source language exists + $event->getToLanguageCode(); + $event->getLocationId(); // ?int — null when no location context is available + + $url = $this->urlGenerator->generate('your_custom_route', [ + 'contentId' => $event->getContentId(), + ]); + + $event->setResponse(new RedirectResponse($url)); + $event->stopPropagation(); + } +} diff --git a/code_samples/translations_management/src/TranslationsManagement/ImageAltTextTransformer.php b/code_samples/translations_management/src/TranslationsManagement/ImageAltTextTransformer.php new file mode 100644 index 0000000000..380932de0d --- /dev/null +++ b/code_samples/translations_management/src/TranslationsManagement/ImageAltTextTransformer.php @@ -0,0 +1,33 @@ +getValue()->alternativeText ?? ''); + } + + /** + * @param array $metadata + */ + public function decode(string $value, mixed $previousFieldValue, array $metadata): Value + { + $previousFieldValue->alternativeText = $value; + + return $previousFieldValue; + } +} diff --git a/code_samples/translations_management/src/TranslationsManagement/MyApiClient.php b/code_samples/translations_management/src/TranslationsManagement/MyApiClient.php new file mode 100644 index 0000000000..4091bb5285 --- /dev/null +++ b/code_samples/translations_management/src/TranslationsManagement/MyApiClient.php @@ -0,0 +1,15 @@ +getContentType()->identifier === 'my_excluded_type'; + } +} diff --git a/code_samples/translations_management/src/TranslationsManagement/MyCustomProvider.php b/code_samples/translations_management/src/TranslationsManagement/MyCustomProvider.php new file mode 100644 index 0000000000..947d6f3e04 --- /dev/null +++ b/code_samples/translations_management/src/TranslationsManagement/MyCustomProvider.php @@ -0,0 +1,50 @@ +apiClient->translate( + $translationData->getText(), + $translationData->getSourceLanguage(), + $translationData->getTargetLanguage() + ); + } + + /** @return array */ + public function getSupportedLanguageCodes(): array + { + return ['en_GB', 'de_DE', 'fr_FR']; + } +} diff --git a/code_samples/translations_management/src/TranslationsManagement/MyTranslationAddExtension.php b/code_samples/translations_management/src/TranslationsManagement/MyTranslationAddExtension.php new file mode 100644 index 0000000000..5a3c4834e8 --- /dev/null +++ b/code_samples/translations_management/src/TranslationsManagement/MyTranslationAddExtension.php @@ -0,0 +1,22 @@ +add('my_custom_field'/* ... */); + } +} diff --git a/code_samples/translations_management/src/TranslationsManagement/TranslationPairManager.php b/code_samples/translations_management/src/TranslationsManagement/TranslationPairManager.php new file mode 100644 index 0000000000..e0324871ff --- /dev/null +++ b/code_samples/translations_management/src/TranslationsManagement/TranslationPairManager.php @@ -0,0 +1,36 @@ +languageService->loadLanguage($sourceLanguageCode); + $targetLanguage = $this->languageService->loadLanguage($targetLanguageCode); + + return $this->languagePairService->createLanguagePair( + $sourceLanguage, + $targetLanguage, + $provider, + $replaceExisting, + ); + } +} diff --git a/composer.json b/composer.json index d229c7579a..a05f70e221 100644 --- a/composer.json +++ b/composer.json @@ -86,7 +86,8 @@ "ibexa/cdp": "~5.0.x-dev", "ibexa/connector-raptor": "~5.0.x-dev", "ibexa/image-editor": "~5.0.x-dev", - "ibexa/integrated-help": "~5.0.x-dev" + "ibexa/integrated-help": "~5.0.x-dev", + "ibexa/translations-management": "~5.0.x-dev" }, "scripts": { "fix-cs": "php-cs-fixer fix --config=.php-cs-fixer.php -v --show-progress=dots", diff --git a/deptrac.baseline.yaml b/deptrac.baseline.yaml index b465d1d963..664593c7d1 100644 --- a/deptrac.baseline.yaml +++ b/deptrac.baseline.yaml @@ -245,6 +245,8 @@ deptrac: App\Tab\Dashboard\Everyone\EveryoneArticleTab: - Ibexa\AdminUi\Tab\Dashboard\PagerLocationToDataMapper - Ibexa\Core\Pagination\Pagerfanta\LocationSearchAdapter + App\TranslationsManagement\MyTranslationAddExtension: + - Ibexa\AdminUi\Form\Type\Content\Translation\TranslationAddType App\View\Matcher\Owner: - Ibexa\Core\MVC\Symfony\Matcher\ContentBased\MatcherInterface - Ibexa\Core\MVC\Symfony\View\ContentValueView diff --git a/docs/api/event_reference/event_reference.md b/docs/api/event_reference/event_reference.md index 6cc5792d69..49396a4264 100644 --- a/docs/api/event_reference/event_reference.md +++ b/docs/api/event_reference/event_reference.md @@ -37,6 +37,7 @@ For example, copying a content item is connected with two events: `BeforeCopyCon "api/event_reference/segmentation_events", "api/event_reference/site_events", "api/event_reference/taxonomy_events", + "api/event_reference/translations_management_events", "api/event_reference/trash_events", "api/event_reference/twig_component_events", "api/event_reference/url_events", diff --git a/docs/api/event_reference/translations_management_events.md b/docs/api/event_reference/translations_management_events.md new file mode 100644 index 0000000000..ca9e482b94 --- /dev/null +++ b/docs/api/event_reference/translations_management_events.md @@ -0,0 +1,29 @@ +--- +description: Events that are triggered when working with translations management. +edition: lts-update +page_type: reference +--- + +# Translations management events + +The [Translations management](configure_translations_management.md) package dispatches events at two levels. + +## Translation events + +Translation events are thrown once per field value per translation operation. +They are used for logging, analytics, and observability. +Both events are read-only, you can't use them to override the translation result. + +| Event | Dispatched by | Dispatched when | Properties | +|---|---|---|----| +| [`BeforeTranslateEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Event-BeforeTranslateEvent.html) | `TranslationService` | Before a translation request is sent to the provider | `TranslationProviderInterface $provider`
`string $text`
`string $sourceLanguage`
`string $targetLanguage` | +| [`TranslateEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Event-TranslateEvent.html) | `TranslationService` | After a translation response is received | `string $result`
`TranslationProviderInterface $provider`
`string $text`
`string $sourceLanguage`
`string $targetLanguage` | + +## Side-by-side creation events + +Side-by-side creation events are dispatched when a new translation draft is being prepared. + +| Event | Dispatched by | Dispatched when | Properties | +|---|---|---|---| +| [`OnContentSideBySideTranslationCreateEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-SideBySide-Event-OnContentSideBySideTranslationCreateEvent.html) | `ContentTranslationCreateController` | When a draft side-by-side translation of a content item is being created | `Request $request`
`Content $sourceContent`
`string $sourceLanguageCode`
`string $targetLanguageCode`
`?Content $targetDraft` | +| [`OnProductSideBySideTranslationCreateEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-SideBySide-Event-OnProductSideBySideTranslationCreateEvent.html) | `ProductTranslationViewController` | When a draft side-by-side translation of a product is being created | `Request $request`
`ContentAwareProductInterface $sourceProduct`
`ContentAwareProductInterface $targetProduct`
`string $sourceLanguageCode`
`string $targetLanguageCode`
`?ProductUpdateData $productUpdateData` | diff --git a/docs/ibexa_products/editions.md b/docs/ibexa_products/editions.md index fc5296196e..5bc7c73bf9 100644 --- a/docs/ibexa_products/editions.md +++ b/docs/ibexa_products/editions.md @@ -71,3 +71,4 @@ The features brought by LTS Updates become standard parts of the next LTS releas | [Integrated help](integrated_help.md) | ✔ | ✔ | ✔ | | [MCP servers](mcp_guide.md) | ✔ | ✔ | ✔ | | [Shopping list](shopping_list_guide.md) | | | ✔ | +| [Translations management](translations_management_guide.md) | ✔ | ✔ | ✔ | diff --git a/docs/multisite/img/diagram_source/translations_management_flow.drawio b/docs/multisite/img/diagram_source/translations_management_flow.drawio new file mode 100644 index 0000000000..e289d5a54e --- /dev/null +++ b/docs/multisite/img/diagram_source/translations_management_flow.drawio @@ -0,0 +1 @@ +5Zptc6M2EIB/DTPtB2d4MRh/tB3Hcc5pPfW0l/uUkUEG9WTkCuGX+/WVQNiAyJn0iJ1cZzxjWEmw++xqWQk0a7TeTyjYhI/Eh1gzdX+vWbeaaRqGaWrip/sHKbFtJ5MEFPlSdhIs0DcohbqUJsiHcakjIwQztCkLPRJF0GMlGaCU7MrdVgSX77oBAVQECw9gVfoZ+SzMDdP1U8M9REEob+3asmEN8s5SEIfAJ7uCyBpr1ogSwrKj9X4EsaCXc8nG3b3QelSMwog1GTDIBmwBTqRtA3+NIqEZZDH/SzYCLgVRjAFDRLRsKNly/jSWJrBDzoWSJPKhuLShWcNdiBhcbIAnWnc8FLgsZGssm1cI4xHBhKZjLR9Ad+Vxecwo+QoLLY7nwuWKt6jGSXu3kDK4L4iksRNI1pDRA+8iW007GyEjr2PkIbUr+NGVsrDgQkfKgAyd4HjpE11+IAHXwx4qsMc+YtxMU6fwnwTGKfEI7qrI28RsQ9fv1mF2zaXlOC1h7l+T80jh/AeMCd7CuBC9mulgfs/hUhwF4ugXDKIg4VO3swFIOEXckM+AWHTm8wHzVMLd8Wur/litVqZXG/a+s3TslvxhdPWKP1R3HHNS0R29Ftxxp4b9noe3l0Z7HudgyWnyVIwg9ttNLJchXEks/QuG+0Thu4CRn8IlLybvj4i4fz3G9zUpxYMoSyk5YU5PIKUoCj5iCBvlHGFahpoj9DfiO1X4jijkRFO8gAaQdfLsLApHClbsAxI23Ssifvhe9bFFcBenj7zqUxFEIqrXxEc8N6fuCOGHr0+MbjmVWOYFHfFJzddgC7OLvkVsXwhpObS7NUQNu4ao3QLRmUo05LbE6e353fMIb/mxdyGwPfs82d4bkX1UyM6TJUZxCFte/V0EpeU0CNK6MrgNlLBmcS5FMUmol6+/M1H20NOK6xnol3YlVCspFAl5W96k+CGVzUYqD9+TylYjlUeqyndtq5wOHVAKDoUOG4IiFheuPBeC4jqiPNtdvWJ9dsETi6NmzfB0G+G5U/FMruZRW8lB2sjUBr1GlrSudr3bjlt/x8qukjEylHLUyfjXhofllsPD6Ff2987oVen/4/HkNPLCvRpP06vFU7PAmaoqP7yLDGFVFhF57dqaS91GfB5UPp+u5tL+f1V5djWV83r59To/XktnFPT8ER3iu0n09LsxGuyecNLJi7BCdq4WhnEINuLQSyg+DCnwvgpTzlWI5XJyhdHmXh5r9WVfDcsXK0G7slzpHMu+0gpQUyrBrtlCKVgL0vopQFp63Upa5Wj0+m/EsatwnM/+nEx/U2i+ClsbqKwyKkeNuNrFRxt7DrWkHIXUYno77gy/dMQ/b/lrOv787rDxRdyNfWVy7vm5KmxGHsAzsIR4TmKUbpVZt0vCGFlzNHmHAUaBaGCkQjKf7et9IN6j3yxBjLyb7nNaIDzH/JHwzLEN0/fq+o3bDu3jZD2+m3cU1k4N6jZeFNWi7v9/UNckhLqofrMnUJ7KC2hFkbGQp4SykAQkAnh8khbmu87PTn1mREBO6f4NGTvIrzhAwkiZPdwj9pRyteXZl0LL7V5eOT05FE7mkCJuN6RSptVVRWfrq+9N72LR9XK/qxVeDSqvn2RmHLfi258Z/PT0tU22Wjp9tGSN/wU= \ No newline at end of file diff --git a/docs/multisite/img/managing_translations_sxs_view.png b/docs/multisite/img/managing_translations_sxs_view.png new file mode 100644 index 0000000000..8a27800397 Binary files /dev/null and b/docs/multisite/img/managing_translations_sxs_view.png differ diff --git a/docs/multisite/img/translations_management_flow.png b/docs/multisite/img/translations_management_flow.png new file mode 100644 index 0000000000..61a7d04471 Binary files /dev/null and b/docs/multisite/img/translations_management_flow.png differ diff --git a/docs/multisite/img/translations_management_language_pairs.png b/docs/multisite/img/translations_management_language_pairs.png new file mode 100644 index 0000000000..a15adf7af5 Binary files /dev/null and b/docs/multisite/img/translations_management_language_pairs.png differ diff --git a/docs/multisite/translations_management/configure_translations_management.md b/docs/multisite/translations_management/configure_translations_management.md new file mode 100644 index 0000000000..32c7939e3a --- /dev/null +++ b/docs/multisite/translations_management/configure_translations_management.md @@ -0,0 +1,308 @@ +--- +description: Install translations management configure translation providers, language pairs, and more. +edition: lts-update +month_change: true +--- + +# Configure translations management + +`ibexa/translations-management` extends [[= product_name =]]'s built-in language management tools that editors use for content translation. +It introduces a plugin that handles the translation provider system by connecting to REST APIs and AI services, a [side-by-side editing interface](#side-by-side-translation-view) where editors can compare source and target languages and provide translations in a single view, and multiple extension points that you can use to [customize different areas of the translation workflow](extend_translations_management.md). + +The package is standalone and does not require the `ibexa/automated-translation` add-on package to run. + +## Install package + +The Translations management LTS Update is optional. +To enable it, run the following command: + +```bash +composer require ibexa/translations-management +``` + +After installation, run the [[= product_name_base =]] data migrations to complete the setup: + +```bash +php bin/console ibexa:migrations:import --from-bundle=IbexaTranslationsManagementBundle +php bin/console ibexa:migrations:migrate +``` + +This copies the migration files into the project's migrations directory and adds the default action configurations in the database. + +## Configure translation providers + +Translation providers are the services that perform the actual text translation. +The Translations management package comes with two types of translation providers: + +- REST API-based providers call a translation service such as Google Translate or DeepL directly by using an API key. +- AI-based providers send translation requests through the [AI Actions](configure_ai_actions.md) framework, relying on the same model selection and policy controls as other AI features in [[= product_name =]]. + +Out of the box, Translations management can support the following translation providers: + +| Provider | Type | Configuration | +|---|---|---| +| Google Translate | REST API | API key | +| DeepL | REST API | API key | +| OpenAI | AI Actions | Action Configuration identifier | +| Anthropic (Claude) | AI Actions | Action Configuration identifier | +| Google Gemini | AI Actions | Action Configuration identifier | + +!!! note "Prerequisites for the default translation providers" + + Before you can configure translation providers, you must fulfill the following prerequisites: + + - For the REST API-based translation providers, add API keys that you obtain from the machine translation services to the `.env` file in the root directory of your project. + + - For the AI-based translation providers, [install and configure](configure_ai_actions.md) the `ibexa/connector-ai` package and their corresponding connectors. + +**Built-in AI providers** + +When you install the Translations management package, the installation process automatically creates AI [Action Configurations](extend_ai_actions.md#action-configurations) for OpenAI (`auto_translate_openai`), Google Gemini (`auto_translate_gemini`), and Anthropic Claude (`auto_translate_anthropic`). + +You can use them directly in provider configuration: + +| Action Configuration identifier | Handler | Default model | +|---|---|---| +| `auto_translate_openai` | `openai-text-to-text` | `gpt-5` | +| `auto_translate_gemini` | `gemini-text-to-text` | `gemini-pro-latest` | +| `auto_translate_anthropic` | `anthropic-text-to-text` | `claude-sonnet-4-20250514` | + +You can then [customize these configurations in the UI]([[= user_doc =]]/ai_actions/work_with_ai_actions/#edit-existing-ai-actions). + +### Add YAML configuration + +In `config/packages`, create a `translations_management.yaml` file. +You configure the providers in the SiteAccess-aware `translations_management` namespace. + +``` yaml +ibexa: + system: + default: + translations_management: + auto_translate: + providers: + google: + apiKey: '%env(GOOGLE_TRANSLATE_API_KEY)%' + deepl: + apiKey: '%env(DEEPL_API_KEY)%' + openai: + actionConfigurationIdentifier: 'auto_translate_openai' + anthropic: + actionConfigurationIdentifier: 'auto_translate_anthropic' + gemini: + actionConfigurationIdentifier: 'auto_translate_gemini' +``` + +The `apiKey` values must reference API key values that you added to the `.env` file. +The `actionConfigurationIdentifier` values must reference existing Action Configurations. +If a value is missing or empty, the provider doesn't appear in the UI as a selectable option. + +!!! caution "AI policies required" + + AI-based providers require that AI policies are assigned to user roles. + If an editor can't see AI providers in the translation provider dropdown, check if the appropriate AI policies are granted in their role definition. + +If you fail to configure the providers, the Translations management feature disables itself in the editor's UI. +The **Use automatic translation** checkbox is disabled, and a message is displayed that prompts the user to contact the administrator + +This state is controlled by `TranslationProviderFormFieldsConfigurator::isAutomaticTranslationDisabled()`, which returns `true` when the provider registry is empty. + +### Advanced translation provider options + +In addition to their required authentication keys, all providers support two optional ones: + +- `supportedLanguageCodes` - overrides the default list of language codes this provider accepts +- `languageCodesMap` - maps language codes used by [[= product_name =]], for example, `eng-GB`, to the provider-specific codes the API expects + +``` yaml +ibexa: + system: + default: + translations_management: + auto_translate: + providers: + # ... + openai: + actionConfigurationIdentifier: 'auto_translate_openai' + supportedLanguageCodes: + - 'eng-GB' + - 'ger-DE' + - 'fre-FR' + languageCodesMap: + eng-GB: 'en' + ger-DE: 'de' + fre-FR: 'fr' +``` + +The `supportedLanguageCodes` setting controls which languages are available when creating [language pairs](#define-language-pairs) for this provider. + +!!! note "Identifier normalization" + + Provider identifiers are normalized from hyphens to underscores during configuration processing. + Use one format consistently. + If you mix `my-provider` and `my_provider` for the same provider, it results in an exception. + +## Define language pairs + +Language pair definitions decide which provider handles each source-to-target language combination by default. +For example, you can decide that English to French translations should use DeepL. +When an editor [opens the translation modal]([[= user_doc =]]/content_management/translate_content/#add-new-translation) and selects a matching language combination, the provider that you chose is pre-selected in the dropdown. +The editor can override the pre-selection. + +The list of languages available when creating a language pair is determined by what each provider supports. +You can only select the languages that are present in a provider's [supported list](#advanced-translation-provider-options) for that provider's pairs. + +The configurations are persisted by `SettingService` and stored in the `ibexa_setting` database table under group `translations_management` with identifier `language_pairs`. + +You can manage language pairs in [[= product_name_base =]]'s back office or programmatically. + +### Manage language pairs in UI + +To manage language pairs in the back office, go to **Admin** -> **Languages** -> **Language pairs** tab. +Here you can create, edit and delete language pairs. + +To add a language pair, click **+ Add language pair**. +Then, pick a source language and one or more target languages from their respective drop-down lists. +Finally, from the **Translation service** list, pick a translation provider and click **Save and close** + +![Creating a language pair](translations_management_language_pairs.png "Creating a language pair") + +This adds as many language pairs as you picked target languages. + +!!! note + + The **Add language pair** action is disabled if no translation providers are [configured](#configure-translation-providers). + + If a language pair already exists and is associated with a translation provider, you can't create another language pair with a different provider. + Edit the existing language pair instead. + +## Manage language pairs programmatically + +To manage language pairs programmatically, create a service class and inject `LanguagePairServiceInterface` into its constructor. +Symfony autowires it automatically so no manual service configuration is needed. + +``` php hl_lines="2" +[[= include_code('code_samples/translations_management/src/TranslationsManagement/TranslationPairManager.php', 14, 35) =]] +``` + +The service exposes the following methods: + +| Method | Description | +|---|---| +| `createLanguagePair()` | Create a new language pair. Pass `true` as the fourth argument to overwrite an existing pair with the same source and target. | +| `updateLanguagePair()` | Update an existing language pair by ID. | +| `syncLanguagePairsForSourceAndProvider()` | Synchronize all target languages for a given source language and provider. | +| `loadLanguagePairs()` | Load all configured language pairs. | +| `deleteLanguagePairById()` | Delete a language pair by ID. | +| `deleteLanguagePairsForProvider()` | Delete all language pairs associated with a given provider. | + +## User settings + +The Translations management package adds preferences that editors can configure under their [user settings](getting_started/get_started/#browsing). +Each editor can configure them independently, and they do not affect other users. + +For example, editors can choose whether the target language column appears on the left or right in the side-by-side view. +By default, the target is on the right, and each editor can override this default. + +You can change the system-wide default in configuration: + +``` yaml +ibexa: + system: + default: + translations_management: + default_side_by_side_column_order: 'source_left_target_right' +``` + +The accepted values are `source_left_target_right` (default) and `source_right_target_left`. + +## Side-by-side translation view + +The [side-by-side translation view]([[= user_doc =]]/content_management/translate_content/#side-by-side-translation-view) is a two-column content editing interface where the source column is read-only and the target column is an editable form. + +Content types that contain the `ibexa_landing_page` or `ibexa_form` fields are not supported, and editors can open them in the standard single-language editor only. +You can exclude support for additional content types if needed. +To do it, [define custom exclusion rules](extend_translations_management.md#define-custom-exclusion-rules). + +### Architecture + +The side-by-side view consists of three forms placed in a single Twig template: + +- `view.sourcePreviewForm` — the source language content, rendered as read-only fields +- `view.form` — the target language content, rendered as editable fields +- `view.copyAllForm` — the **Copy all from source** action + +To assemble the view, `SideBySideEditContextBuilder` performs the following actions: + +1. Resolves source and target languages +2. Loads the correct content version +3. Groups fields by their content type field groups + +!!! note "Meta fields" + + The builder excludes the fields that are marked marked as `meta: true` or belong to a field group that is listed in `admin_ui_forms.content_edit.meta_field_groups_list`, and does not render them. + +To resolve the column order, `SideBySideTargetLanguagePositionResolver` reads the user setting and falls back to `source_left_target_right` when the setting is not made. +The Twig template applies `order-xl-*` classes for responsive column placement. + +### Side-by-side view behavior + +Editors have multiple ways to arrive at the side-by-side translation view, for example: + +- From the **Create a new translation** modal, by clicking the **Open side-by-side** action. + This submits the modal to the `ibexa.translations_management.side_by_side_create` route, which creates a new draft and redirects to `side_by_side_view` with the resolved `versionNo`. + +- From the **Versions** tab, by clicking the **Edit side-by-side** action next to a draft whose source and target languages differ. + This doesn't create a new draft, and the existing version number is used. + +!!! tip "Routes" + + The Translations management package registers internal back office routes. + To list them with their current paths, run: + + ``` bash + php bin/console debug:router | grep translations_management + ``` + +### Side-by-side view functions + +The side-by-side translation view has several functions, including: + +- Copy all from source + +When an editor clicks the **Copy all from source** action, all translatable field values are copied from the source to target column. +It's a single server-side operation handled by `SideBySideFieldCopyService::copyAllFields()` after which the view is reloaded. + +- Draft conflict warning + +When an user opens the translation modal and selects a target language which already has a draft translation, a warning appears in the modal. +The warning is shown or hidden dynamically by `side-by-side-translation-modal-warning.js` when the user changes the target language selection. + +## Translate content items with CLI + +For the purposes of batch processing, automation and other scripted actions, the Translations management package exposes a command that translates content items by using any of the configured providers: + +``` bash +php bin/console ibexa:translations:auto-translate-content \ + --content-id=42 \ + --provider=deepl \ + --from=eng-GB \ + --to=fre-FR +``` + +!!! tip "Command alias" + + You can use `ibexa:translations:translate-content` as an alias. + +The command uses the same provider configuration and field value transformers as the UI, so the results are the same if an editor triggered the translation manually. + +### CLI command options + +| Option | Required | Description | +|---|---|---| +| `--content-id` | Yes | ID of the content item to translate | +| `--provider` | Yes | Identifier of the translation provider to use | +| `--from` | Yes | Source language code | +| `--to` | Yes | Target language code | +| `--user-id` | No | Repository user ID to run the translation (default: `14`, which is the Administrator user) | +| `--draft-only` | No | Create a translated draft without publishing it | diff --git a/docs/multisite/translations_management/extend_translations_management.md b/docs/multisite/translations_management/extend_translations_management.md new file mode 100644 index 0000000000..cd0f02e245 --- /dev/null +++ b/docs/multisite/translations_management/extend_translations_management.md @@ -0,0 +1,212 @@ +--- +description: Extend translations management - add custom classes, exclude custom content types and intercept the flow. +edition: lts-update +month_change: true +--- + +# Extend translations management + +By extending [Translations management](translations_management_guide.md), you can build custom translation workflows and adapt the package's behavior to your specific requirements. +The package is designed to be extended in multiple ways. +You can create custom [translation providers](configure_translations_management.md#configure-translation-providers), field type transformers, exclusion rules, and UI components. +In all cases, you follow the same pattern: implement an interface or extend a base class, then register the service with a service tag. +The package discovers and registers tagged services automatically. + +## Add custom translation provider + +Before you build a custom translation provider, if your provider uses the AI Actions framework, make sure that the `ibexa/connector-ai` package is installed in your system. + +To connect a translation service that is not built into the package, implement [`TranslationProviderInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Provider-TranslationProviderInterface.html). +The `translate()` method receives a `TranslationDataInterface` object that carries the text to translate along with the source and target language codes: + +``` php hl_lines="36-49" +[[= include_code('code_samples/translations_management/src/TranslationsManagement/MyCustomProvider.php') =]] +``` + +Register the provider with the `ibexa.translations_management.auto_translate.provider` tag. +Both `identifier` and `validation_profile` are required attributes. + +``` yaml +[[= include_code('code_samples/translations_management/config/services.yaml', 1, 6) =]] +``` + +The `validation_profile` attribute links the provider to a validator that checks language codes and payload size before each before each translation request. +By default, three profiles are available: + +| Profile | Used by | +|---|---| +| `google` | Google Translate provider | +| `deepl` | DeepL provider | +| `ai_generic` | All built-in AI providers. Suitable for custom AI providers. | + +To define a custom validation profile, implement [`ProviderValidatorInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Validator-ProviderValidatorInterface.html) and register it: + +``` yaml +[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]] +[[= include_code('code_samples/translations_management/config/services.yaml', 7, 10) =]] +``` + +You can extend [`DefaultProviderValidator`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Validator-DefaultProviderValidator.html) as base class. +It exposes configurable maximum payload size and language code regex patterns. + +The package also provides several specialized interfaces for providers with specific requirements: + +| Interface | Purpose | +|---|---| +| [`ConfigurableProviderInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Provider-ConfigurableProviderInterface.html) | Extends `TranslationProviderInterface`. Adds `getConfiguration()` and `isConfigured()` for providers that store API keys and other settings | +| [`AiTranslationProviderInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Provider-AiTranslationProviderInterface.html) | Extends `ConfigurableProviderInterface`. Used as a type marker for AI-based providers, it inherits the configuration methods | +| [`TranslationHttpClientInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Http-TranslationHttpClientInterface.html) | For HTTP-based providers that use a REST API pattern | + +## Add support for custom field types + +The translation engine works by extracting translatable text from fields, sending it to the provider, and writing the translated text back. +Field value transformers handle this encode/decode cycle, one per field type. +The package includes transformers for standard text and RichText fields. +To add support for a custom or non-standard field type, implement [`FieldValueTransformerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Transformer-Field-FieldValueTransformerInterface.html): + +- `getFieldTypeIdentifier()` - returns the field type identifier this transformer handles +- `encode(Field $field): EncodedFieldValue` - extracts the translatable string from the field and wraps it in an [`EncodedFieldValue`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Transformer-Field-EncodedFieldValue.html). The constructor takes the extracted string as its first argument and an optional metadata array as its second. +- `decode(string $value, mixed $previousFieldValue, array $metadata): Value` - receives the translated string, the previous field value, and any metadata, and returns the updated field value + +``` php hl_lines="19 24" +[[= include_code('code_samples/translations_management/src/TranslationsManagement/ImageAltTextTransformer.php') =]] +``` + +Register the transformer with the `ibexa.translations_management.auto_translate.field_value_transformer` tag. +The `field_type_identifier` attribute is required. +It must match the value that `getFieldTypeIdentifier()` returns: + +``` yaml +[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]] +[[= include_code('code_samples/translations_management/config/services.yaml', 14, 17) =]] +``` + +If a field type requires metadata, for example, RichText fields with embedded objects that you must preserve after translation, implement [`MetadataAwareFieldValueTransformerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Transformer-Field-MetadataAwareFieldValueTransformerInterface.html) instead. + +## Define custom exclusion rules + +Use exclusion rules to identify content types that cannot use the side-by-side view. +The Translations management package ships with one rule that excludes content types that contain `ibexa_landing_page` or `ibexa_form` fields. + +### Exclude with custom class + +To exclude additional content types, for example, content types whose fields render incorrectly in the side-by-side layout, implement [`SideBySideExclusionRuleInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-SideBySide-Service-SideBySideExclusionRuleInterface.html). +The `isExcluded()` method receives a [`ContentInfo`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html) object and returns `true` if the content item should be excluded. +Classes that implement this interface are automatically tagged with `autoconfigure`: + +``` php +[[= include_code('code_samples/translations_management/src/TranslationsManagement/MyCustomExclusionRule.php') =]] +``` + +If `autoconfigure` is not available, register the tag explicitly: + +``` yaml +[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]] +[[= include_code('code_samples/translations_management/config/services.yaml', 18, 20) =]] +``` + +### Exclude with existing class + +`MyCustomExclusionRule` targets one specific content type by name. +To exclude any content type that contain specific field types without the need to write a custom class, register an additional instance of the built-in [`UnsupportedFieldTypeExclusionRule`](https://github.com/ibexa/translations-management/blob/main/src/lib/SideBySide/Service/UnsupportedFieldTypeExclusionRule.php). +Because this registers a second instance of the service with different arguments, you can't use the class name as the service ID. +Use an arbitrary string ID instead to avoid a service definition conflict: + +``` yaml +[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]] +[[= include_code('code_samples/translations_management/config/services.yaml', 21, 26) =]] +``` + +## Use Twig component extension points + +Two Twig component groups allow you to inject custom UI elements into the translation interface without the need to override their templates. +Such custom elements could be: + +- buttons that allow the editor to create a new translation either in the side-by-side view or the standard single-panel editor +- a disclaimer or policy notice that the editor must acknowledge before a translation is created + +| Component group | Location | Variables available | +|---|---|---| +| `admin-ui-content-translation-modal-footer` | Footer of the **Add translation** modal | `form`, `content_id`, `location`, `allow_placeholder` | +| `admin-ui-content-edit-translation-select-footer` | Footer of the **Select translation** panel on the content edit screen | `form`, `content_id`, `main_language_code` | + +The two groups behave differently: + +- `admin-ui-content-translation-modal-footer` — if any of the components renders output that is not empty, it entirely replaces the default footer buttons. +Your component template must therefore include its own action buttons. +- `admin-ui-content-edit-translation-select-footer` — component output is inserted between the existing **Edit** and **Discard** buttons. + +Register a component with the `ibexa.twig.component` tag: + +``` yaml +[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]] +[[= include_code('code_samples/translations_management/config/services.yaml', 27, 31) =]] +``` + +!!! note + + The `admin-ui-content-translation-modal-footer` group receives a `location` variable that may be `null` when the modal is rendered outside a location context. + Always check for `null` before you access location properties in your component template. + +## Extend modal + +If injecting custom UI elements is not sufficient, you can extend the modal itself. +To add a field to the **Add translation** modal, for example, to let the editor choose a custom workflow or pass extra parameters along with the translation request, extend [`TranslationAddType`](https://github.com/ibexa/admin-ui/blob/main/src/lib/Form/Type/Content/Translation/TranslationAddType.php) with a [Symfony's Form Type extension](https://symfony.com/doc/current/form/create_form_type_extension.html). +It's the same mechanism the translations management package uses internally to inject its provider selector into the modal. + +Create a class that extends [`AbstractTypeExtension`](https://symfony.com/doc/current/reference/forms/types/form.html) and declare the extended type: + +``` php +[[= include_code('code_samples/translations_management/src/TranslationsManagement/MyTranslationAddExtension.php') =]] +``` + +Register it as a service: + +``` yaml +[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]] +[[= include_code('code_samples/translations_management/config/services.yaml', 11, 13) =]] +``` + +The extra field is then available in the submitted form data, which the standard `admin-ui` controller includes in the translation request data. +Use this approach when you need to read extra input from the editor, not to redirect or replace the response. + +!!! caution "Internal `TranslationAddType`" + + `TranslationAddType` is marked `@internal` in `ibexa/admin-ui`. + While it functions as an extension point in practice, its name and signature may change. + It may even be removed entirely without a deprecation notice. + +## Intercept translation flow + +The `BeforeTranslateEvent` and `TranslateEvent` [events](translations_management_events.md#translation-events) operate at the field-value level and cannot redirect the HTTP flow. +To intercept the "Add translation" action at the HTTP level, for example, to trigger auto-translation and redirect to a custom view, or to bypass the default flow entirely, subscribe to `admin-ui`'s `ContentProxyTranslateEvent`. + +The `translations-management` package listens to this event at priority `100`. +Subscribe at a higher priority to act before the package does: + +``` php hl_lines="35 36" +[[= include_code('code_samples/translations_management/src/TranslationsManagement/ContentProxyTranslateSubscriber.php') =]] +``` + +Both highlighted calls are required: + +- `setResponse()` alone does not prevent the Translations management listener at priority 100 from running and overwriting the response. +- `stopPropagation()` stops all lower-priority listeners from executing. + +When a response is set on the event, `admin-ui` uses it and doesn't proceed with the standard translation editor. + +!!! caution "Internal `ContentProxyTranslateEvent`" + + `ContentProxyTranslateEvent` is marked `@internal` in `ibexa/admin-ui`. + While it functions as an extension point in practice, its name and signature may change. + It may even be removed entirely without a deprecation notice. + +## Service tags reference + +The following service tags expose additional extension points that you can use to customize and extend translations management behavior. + +| Tag | Purpose | Required attributes | +|---|---|---| +| `ibexa.translations_management.auto_translate.provider.language_normalizer` | Register a language code normalizer for a provider | none | +| `ibexa.translations_management.auto_translate.provider.ai.translation_strategy` | Register a custom AI translation strategy (prompt structure) | `priority` | +| `ibexa.translations_management.auto_translate.metadata_validation.retry_policy` | Register a metadata validation retry policy | `priority` | diff --git a/docs/multisite/translations_management/translations_management.md b/docs/multisite/translations_management/translations_management.md new file mode 100644 index 0000000000..468165d855 --- /dev/null +++ b/docs/multisite/translations_management/translations_management.md @@ -0,0 +1,16 @@ +--- +description: Translations management brings multiple features that help managers, developers and localization teams automated multilingual content delivery. +edition: lts-update +page_type: landing_page +--- + +# Translations management + +Translations management helps [[= product_name =]] developers and users deliver automated content item, product and product catalog translations. + +[[= cards([ + "multisite/translations_management/translations_management_guide", + "multisite/translations_management/configure_translations_management", + "multisite/translations_management/extend_translations_management", + "api/event_reference/translations_management_events", +], columns=3) =]] diff --git a/docs/multisite/translations_management/translations_management_guide.md b/docs/multisite/translations_management/translations_management_guide.md new file mode 100644 index 0000000000..cebb95ddf1 --- /dev/null +++ b/docs/multisite/translations_management/translations_management_guide.md @@ -0,0 +1,91 @@ +--- +description: Translations management helps managers, developers and localization teams with multilingual content delivery. +edition: lts-update +month_change: true +--- + +# Translations management product guide + +## What is Translations management + +Content managers, translators, and proofreaders who work with multilingual content in [[= product_name =]] often face a common set of challenges: + +- context is lost when the source text isn't visible alongside the translation +- translating long and complex content items is time-consuming +- quality assurance is slow and error-prone without a direct comparison view +- switching between tools or tabs to cross-reference languages disrupts focus and slows down publishing + +The Translations management package addresses these pain points through a side-by-side view, machine translation and the ability to invite reviewers to collaborate on the translation of a content items or products. + +The package integrates with the [AI Actions framework](ai_actions.md) to support machine translation providers such as Google Translate and DeepL, and AI powered translation services like OpenAI, Anthropic, and Google Gemini. + +Administrators can manage providers and configure default provider-to-language-pair mappings directly in [[= product_name =]]'s user interface, while editors can trigger machine translation from the content editing interface. + +!!! note + + Translations management is a standalone set of features. + Although some views are similar to those delivered by the [Automated translations](automated_translations.md) opt-in package, Translations management does not require the `ibexa/automated-translation` package to run. + These two packages use different namespaces, service tags, and provider interfaces. + +## Availability + +Translations management is an [LTS Update](editions.md#lts-updates) available in all [[= product_name =]] editions. + +## How it works + +Before the translation flow can happen, an administrator sets up the translation providers and assigns language pairs to them. +Then, when an editor opens a content item and requests a new machine translation, the plugin resolves which provider to use. +If no language-pair rule matches, it falls back to the user's manual selection. +The plugin then extracts the translatable fields from the source language version of a content item and sends them to the configured provider's API. +The system writes the translated strings into a target-language draft of the content item, and opens it in a side-by-side view for the editor to review and refine. +The editor can save the result as a draft, share it with a reviewer or publish it. + +![Translations management flow](translations_management_flow.png "Translations management flow") + +## Capabilities + +### Translation provider management + +Administrators can manage translation providers and configure translation provider/language combination assignments ([language pairs](configure_translations_management.md#define-language-pairs)). +This allows administrators to define which provider handles which language combination. +Editors see the configured provider pre-selected when creating a new translation, but can override it if needed. + +![Creating a language pair](translations_management_language_pairs.png "Creating a language pair") + +The package provides integrations with several translation providers, including REST API-based services such as Google Translate and DeepL, and AI-powered services through the [AI Actions](ai_actions.md). + +### Side-by-side translation view + +Translations management introduces a [side-by-side translation view]([[= user_doc =]]/content_management/translate_content/#side-by-side-translation-view) that displays the read-only source language content next to an editable target language form. +In this view, editors can provide and review translations in context, without having to leave the content editing interface. + +![Side-by-side translation view](managing_translations_sxs_view.png "Side-by-side translation view") + +Editors can: + +- access the side-by-side view when creating a new translation, reviewing an existing one, or editing a draft +- compare source and target content field by field while editing +- copy all content from the source column to the target column with a single action +- provide localized versions of media assets and their alternative text +- use the distraction-free mode for focused editing of individual fields, with AI actions available inline +- choose whether the source column appears on the left or right in user settings + +!!! note "Excluded content types" + + Content types that are editable in Page builder or Form builder are excluded from side-by-side editing. + + Products are editable in the side-by-side view, but product attributes are not translatable. + +### Command-line translation + +The Translations management package exposes a [console command](configure_translations_management.md#translate-content-items-with-cli) for translating content items from the command line. +You can use it for batch processing or automated workflows. + +### Extensibility + +Developers can [extend the translations management](extend_translations_management.md) package: + +- create custom translation providers +- add support for custom fields +- add custom content type exclusion rules +- tap into the translation lifecycle with [events](translations_management_events.md) diff --git a/docs/release_notes/ibexa_dxp_v5.0.md b/docs/release_notes/ibexa_dxp_v5.0.md index 02469e686b..8a976a1754 100644 --- a/docs/release_notes/ibexa_dxp_v5.0.md +++ b/docs/release_notes/ibexa_dxp_v5.0.md @@ -10,6 +10,53 @@ month_change: true
+[[% set version = 'v5.0.10' %]] +[[% set date = '2026-07-30' %]] + +[[= release_note_entry_begin( + 'Translations management ' + version, + date, + ['Headless', 'Experience', 'Commerce', 'LTS Update', 'New feature', 'First release'] +) =]] + +Translations management is a new LTS Update that extends [[= product_name =]]'s built-in language management tools with machine translation, a side-by-side editing view, and a command-line translation utility. + +### Machine translation providers + +Translation providers are the services that perform the actual text translation. +Translations management uses two provider types to connect to the translation services: + +- REST API-based providers: Google Translate and DeepL, configured with API keys +- AI-based providers: OpenAI, Anthropic Claude, and Google Gemini, routed through AI Actions + +For more information, see [Configure translation providers](configure_translations_management.md#configure-translation-providers). + +### Side-by-side translation view + +A [side-by-side translation view]([[= user_doc =]]/content_management/translate_content/#side-by-side-translation-view) displays the source and target text of the content item or product on one screen. +Editors can translate or compare source and target content, copy all content from the source column to the target column in a single action, and use the distraction-free mode for focused editing of individual fields. + +For more information, see [User Documentation]([[= user_doc =]]/content_management/translate_content/#side-by-side-translation-view). + +### CLI translation command + +A new console command translates content items from the command line, enabling batch processing and automated workflows. + +For more information, see [Translate content items with CLI](configure_translations_management.md#translate-content-items-with-cli). + +### Developer experience + +The package exposes multiple extension points for custom translation workflows, including: + +- Custom translation providers through `TranslationProviderInterface` +- Custom field type support through `FieldValueTransformerInterface` +- Custom content type exclusion rules through `SideBySideExclusionRuleInterface` +- extension points for adding UI elements and fields to the views used by the feature + +For more information, see [Extend translations management](https://doc.ibexa.co/en/5.0/translations/extend_translations_management/). + +[[= release_note_entry_end() =]] + [[% set version = 'v5.0.8' %]] [[% set date = '2026-05-21' %]] diff --git a/mkdocs.yml b/mkdocs.yml index 6ecfc86c8c..dc16cdb66a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,7 @@ nav: - Discounts events: api/event_reference/discounts_events.md - Collaboration events: api/event_reference/collaboration_events.md - Integrated help events: api/event_reference/integrated_help_events.md + - Translations management events: api/event_reference/translations_management_events.md - Other events: api/event_reference/other_events.md - Notification channels: api/notification_channels.md - Administration: @@ -479,6 +480,11 @@ nav: - Language API: multisite/languages/language_api.md - Back office translations: multisite/languages/back_office_translations.md - Automated content translation: multisite/languages/automated_translations.md + - Translations management: + - Translations management: multisite/translations_management/translations_management.md + - Translations management guide: multisite/translations_management/translations_management_guide.md + - Configure translations management: multisite/translations_management/configure_translations_management.md + - Extend translations management: multisite/translations_management/extend_translations_management.md - Permissions: - Permissions: permissions/permissions.md - Permission overview: permissions/permission_overview.md