Skip to content

IBX-10684: Document translation management - #3249

Open
dabrt wants to merge 19 commits into
5.0from
translations-management
Open

IBX-10684: Document translation management#3249
dabrt wants to merge 19 commits into
5.0from
translations-management

Conversation

@dabrt

@dabrt dabrt commented Jun 18, 2026

Copy link
Copy Markdown
Contributor
Question Answer
JIRA Ticket IBX-10684
Versions 5.0 and up
Edition all

Document translation management

Checklist

  • Text renders correctly
  • Text has been checked with vale
  • Description metadata is up to date
  • Code samples are working
  • PHP code samples have been fixed with PHP CS fixer
  • Added link to this PR in relevant JIRA ticket or code PR

@adriendupuis
adriendupuis marked this pull request as draft June 19, 2026 07:12
Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
Comment thread docs/api/event_reference/translations_management_events.md Outdated
@dabrt
dabrt requested a review from mikadamczyk June 24, 2026 07:02
@dabrt
dabrt marked this pull request as ready for review June 25, 2026 12:41
dabrt and others added 5 commits June 25, 2026 16:34
Co-Authored-By: dabrt <dabrt@users.noreply.github.com>
Co-Authored-By: Marek Nocoń <mnocon@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/translations_management/config/services.yaml


code_samples/translations_management/config/services.yaml

docs/multisite/translations_management/extend_translations_management.md@29:``` yaml
docs/multisite/translations_management/extend_translations_management.md@30:[[= include_code('code_samples/translations_management/config/services.yaml', 1, 6) =]]
docs/multisite/translations_management/extend_translations_management.md@31:```

001⫶services:
002⫶ App\TranslationsManagement\MyCustomProvider:
003⫶ tags:
004⫶ - name: 'ibexa.translations_management.auto_translate.provider'
005⫶ identifier: 'my_custom_provider'
006⫶ validation_profile: 'ai_generic'

docs/multisite/translations_management/extend_translations_management.md@44:``` yaml
docs/multisite/translations_management/extend_translations_management.md@45:[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]]
docs/multisite/translations_management/extend_translations_management.md@46:[[= include_code('code_samples/translations_management/config/services.yaml', 7, 10) =]]
docs/multisite/translations_management/extend_translations_management.md@47:```

001⫶services:
002⫶ App\TranslationsManagement\MyProviderValidator:
003⫶ tags:
004⫶ - name: 'ibexa.translations_management.auto_translate.provider.validator'
005⫶ profile: 'my_custom_profile'

docs/multisite/translations_management/extend_translations_management.md@79:``` yaml
docs/multisite/translations_management/extend_translations_management.md@80:[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]]
docs/multisite/translations_management/extend_translations_management.md@81:[[= include_code('code_samples/translations_management/config/services.yaml', 14, 17) =]]
docs/multisite/translations_management/extend_translations_management.md@82:```

001⫶services:
002⫶ App\TranslationsManagement\ImageAltTextTransformer:
003⫶ tags:
004⫶ - name: 'ibexa.translations_management.auto_translate.field_value_transformer'
005⫶ field_type_identifier: 'ibexa_image'

docs/multisite/translations_management/extend_translations_management.md@103:``` yaml
docs/multisite/translations_management/extend_translations_management.md@104:[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]]
docs/multisite/translations_management/extend_translations_management.md@105:[[= include_code('code_samples/translations_management/config/services.yaml', 18, 20) =]]
docs/multisite/translations_management/extend_translations_management.md@106:```

001⫶services:
002⫶ App\TranslationsManagement\MyCustomExclusionRule:
003⫶ tags:
004⫶ - { name: 'ibexa.translations_management.side_by_side.exclusion_rule' }

docs/multisite/translations_management/extend_translations_management.md@115:``` yaml
docs/multisite/translations_management/extend_translations_management.md@116:[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]]
docs/multisite/translations_management/extend_translations_management.md@117:[[= include_code('code_samples/translations_management/config/services.yaml', 21, 26) =]]
docs/multisite/translations_management/extend_translations_management.md@118:```

001⫶services:
002⫶ app.translations_management.exclusion_rule.custom_field_types:
003⫶ class: Ibexa\TranslationsManagement\SideBySide\Service\UnsupportedFieldTypeExclusionRule
004⫶ arguments:
005⫶ $excludedFieldTypeIdentifiers: ['custom_blog_post', 'custom_landing_page']
006⫶ tags:
007⫶ - { name: 'ibexa.translations_management.side_by_side.exclusion_rule' }

docs/multisite/translations_management/extend_translations_management.md@141:``` yaml
docs/multisite/translations_management/extend_translations_management.md@142:[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]]
docs/multisite/translations_management/extend_translations_management.md@143:[[= include_code('code_samples/translations_management/config/services.yaml', 27, 31) =]]
docs/multisite/translations_management/extend_translations_management.md@144:```

001⫶services:
002⫶ App\TranslationsManagement\TwigComponent\MyTranslationModalFooter:
003⫶ tags:
004⫶ - name: ibexa.twig.component
005⫶ group: 'admin-ui-content-translation-modal-footer'
006⫶ priority: 10

docs/multisite/translations_management/extend_translations_management.md@165:``` yaml
docs/multisite/translations_management/extend_translations_management.md@166:[[= include_code('code_samples/translations_management/config/services.yaml', 1, 1) =]]
docs/multisite/translations_management/extend_translations_management.md@167:[[= include_code('code_samples/translations_management/config/services.yaml', 11, 13) =]]
docs/multisite/translations_management/extend_translations_management.md@168:```

001⫶services:
002⫶ App\TranslationsManagement\MyTranslationAddExtension:
003⫶ tags:
004⫶ - { name: form.type_extension }


code_samples/translations_management/src/TranslationsManagement/ContentProxyTranslateSubscriber.php


code_samples/translations_management/src/TranslationsManagement/ContentProxyTranslateSubscriber.php

docs/multisite/translations_management/extend_translations_management.md@187:``` php hl_lines="35 36"
docs/multisite/translations_management/extend_translations_management.md@188:[[= include_code('code_samples/translations_management/src/TranslationsManagement/ContentProxyTranslateSubscriber.php') =]]
docs/multisite/translations_management/extend_translations_management.md@189:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\TranslationsManagement\EventSubscriber;
004⫶
005⫶use Ibexa\Contracts\AdminUi\Event\ContentProxyTranslateEvent;
006⫶use Symfony\Component\EventDispatcher\EventSubscriberInterface;
007⫶use Symfony\Component\HttpFoundation\RedirectResponse;
008⫶use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
009⫶
010⫶final readonly class ContentProxyTranslateSubscriber implements EventSubscriberInterface
011⫶{
012⫶ public function __construct(
013⫶ private UrlGeneratorInterface $urlGenerator,
014⫶ ) {
015⫶ }
016⫶
017⫶ public static function getSubscribedEvents(): array
018⫶ {
019⫶ return [
020⫶ ContentProxyTranslateEvent::class => ['onProxyTranslate', 200],
021⫶ ];
022⫶ }
023⫶
024⫶ public function onProxyTranslate(ContentProxyTranslateEvent $event): void
025⫶ {
026⫶ // Read the translation context:
027⫶ $event->getContentId();
028⫶ $event->getFromLanguageCode(); // ?string — null when no source language exists
029⫶ $event->getToLanguageCode();
030⫶ $event->getLocationId(); // ?int — null when no location context is available
031⫶
032⫶ $url = $this->urlGenerator->generate('your_custom_route', [
033⫶ 'contentId' => $event->getContentId(),
034⫶ ]);
035❇️
036❇️ $event->setResponse(new RedirectResponse($url));
037⫶ $event->stopPropagation();
038⫶ }
039⫶}


code_samples/translations_management/src/TranslationsManagement/ImageAltTextTransformer.php


code_samples/translations_management/src/TranslationsManagement/ImageAltTextTransformer.php

docs/multisite/translations_management/extend_translations_management.md@71:``` php hl_lines="19 24"
docs/multisite/translations_management/extend_translations_management.md@72:[[= include_code('code_samples/translations_management/src/TranslationsManagement/ImageAltTextTransformer.php') =]]
docs/multisite/translations_management/extend_translations_management.md@73:```

001⫶<?php
002⫶
003⫶declare(strict_types=1);
004⫶
005⫶namespace App\TranslationsManagement;
006⫶
007⫶use Ibexa\Contracts\Core\FieldType\Value;
008⫶use Ibexa\Contracts\Core\Repository\Values\Content\Field;
009⫶use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Transformer\Field\EncodedFieldValue;
010⫶use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Transformer\Field\FieldValueTransformerInterface;
011⫶
012⫶final class ImageAltTextTransformer implements FieldValueTransformerInterface
013⫶{
014⫶ public function getFieldTypeIdentifier(): string
015⫶ {
016⫶ return 'ibexa_image';
017⫶ }
018⫶
019❇️ public function encode(Field $field): EncodedFieldValue
020⫶ {
021⫶ return new EncodedFieldValue($field->getValue()->alternativeText ?? '');
022⫶ }
023⫶
024❇️ /**
025⫶ * @param array<string, mixed> $metadata
026⫶ */
027⫶ public function decode(string $value, mixed $previousFieldValue, array $metadata): Value
028⫶ {
029⫶ $previousFieldValue->alternativeText = $value;
030⫶
031⫶ return $previousFieldValue;
032⫶ }
033⫶}


code_samples/translations_management/src/TranslationsManagement/MyApiClient.php


code_samples/translations_management/src/TranslationsManagement/MyCustomExclusionRule.php


code_samples/translations_management/src/TranslationsManagement/MyApiClient.php


code_samples/translations_management/src/TranslationsManagement/MyCustomExclusionRule.php

docs/multisite/translations_management/extend_translations_management.md@97:``` php
docs/multisite/translations_management/extend_translations_management.md@98:[[= include_code('code_samples/translations_management/src/TranslationsManagement/MyCustomExclusionRule.php') =]]
docs/multisite/translations_management/extend_translations_management.md@99:```

001⫶<?php
002⫶
003⫶declare(strict_types=1);
004⫶
005⫶namespace App\TranslationsManagement;
006⫶
007⫶use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo;
008⫶use Ibexa\Contracts\TranslationsManagement\SideBySide\Service\SideBySideExclusionRuleInterface;
009⫶
010⫶final class MyCustomExclusionRule implements SideBySideExclusionRuleInterface
011⫶{
012⫶ public function isExcluded(ContentInfo $contentInfo): bool
013⫶ {
014⫶ return $contentInfo->getContentType()->identifier === 'my_excluded_type';
015⫶ }
016⫶}


code_samples/translations_management/src/TranslationsManagement/MyCustomProvider.php


code_samples/translations_management/src/TranslationsManagement/MyCustomProvider.php

docs/multisite/translations_management/extend_translations_management.md@22:``` php hl_lines="36-49"
docs/multisite/translations_management/extend_translations_management.md@23:[[= include_code('code_samples/translations_management/src/TranslationsManagement/MyCustomProvider.php') =]]
docs/multisite/translations_management/extend_translations_management.md@24:```

001⫶<?php
002⫶
003⫶declare(strict_types=1);
004⫶
005⫶namespace App\TranslationsManagement;
006⫶
007⫶use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Provider\TranslationProviderInterface;
008⫶use Ibexa\Contracts\TranslationsManagement\AutoTranslate\TranslationDataInterface;
009⫶
010⫶final readonly class MyCustomProvider implements TranslationProviderInterface
011⫶{
012⫶ /**
013⫶ * Replace MyApiClient with your HTTP client, SDK wrapper, or any service
014⫶ * that communicates with the external translation API.
015⫶ */
016⫶ public function __construct(
017⫶ private MyApiClient $apiClient,
018⫶ ) {
019⫶ }
020⫶
021⫶ public function getIdentifier(): string
022⫶ {
023⫶ return 'my_custom_provider';
024⫶ }
025⫶
026⫶ public function getName(): string
027⫶ {
028⫶ return 'My Translation Service';
029⫶ }
030⫶
031⫶ public function getVendorName(): string
032⫶ {
033⫶ return 'My Company Ltd';
034⫶ }
035⫶
036❇️ public function translate(TranslationDataInterface $translationData): string
037❇️ {
038❇️ return $this->apiClient->translate(
039❇️ $translationData->getText(),
040❇️ $translationData->getSourceLanguage(),
041❇️ $translationData->getTargetLanguage()
042❇️ );
043❇️ }
044❇️
045❇️ /** @return array<string> */
046❇️ public function getSupportedLanguageCodes(): array
047❇️ {
048❇️ return ['en_GB', 'de_DE', 'fr_FR'];
049❇️ }
050⫶}


code_samples/translations_management/src/TranslationsManagement/MyTranslationAddExtension.php


code_samples/translations_management/src/TranslationsManagement/MyTranslationAddExtension.php

docs/multisite/translations_management/extend_translations_management.md@159:``` php
docs/multisite/translations_management/extend_translations_management.md@160:[[= include_code('code_samples/translations_management/src/TranslationsManagement/MyTranslationAddExtension.php') =]]
docs/multisite/translations_management/extend_translations_management.md@161:```

001⫶<?php
002⫶
003⫶declare(strict_types=1);
004⫶
005⫶namespace App\TranslationsManagement;
006⫶
007⫶use Ibexa\AdminUi\Form\Type\Content\Translation\TranslationAddType;
008⫶use Symfony\Component\Form\AbstractTypeExtension;
009⫶use Symfony\Component\Form\FormBuilderInterface;
010⫶
011⫶final class MyTranslationAddExtension extends AbstractTypeExtension
012⫶{
013⫶ public static function getExtendedTypes(): iterable
014⫶ {
015⫶ return [TranslationAddType::class];
016⫶ }
017⫶
018⫶ public function buildForm(FormBuilderInterface $builder, array $options): void
019⫶ {
020⫶ $builder->add('my_custom_field'/* ... */);
021⫶ }
022⫶}


code_samples/translations_management/src/TranslationsManagement/TranslationPairManager.php


code_samples/translations_management/src/TranslationsManagement/TranslationPairManager.php

docs/multisite/translations_management/configure_translations_management.md@184:``` php hl_lines="2"
docs/multisite/translations_management/configure_translations_management.md@185:[[= include_code('code_samples/translations_management/src/TranslationsManagement/TranslationPairManager.php', 14, 35) =]]
docs/multisite/translations_management/configure_translations_management.md@186:```

001⫶ public function __construct(
002❇️ private LanguagePairServiceInterface $languagePairService,
003⫶ private LanguageService $languageService,
004⫶ ) {
005⫶ }
006⫶
007⫶ public function addPair(
008⫶ string $sourceLanguageCode,
009⫶ string $targetLanguageCode,
010⫶ TranslationProviderInterface $provider,
011⫶ bool $replaceExisting = false,
012⫶ ): LanguagePairInterface {
013⫶ $sourceLanguage = $this->languageService->loadLanguage($sourceLanguageCode);
014⫶ $targetLanguage = $this->languageService->loadLanguage($targetLanguageCode);
015⫶
016⫶ return $this->languagePairService->createLanguagePair(
017⫶ $sourceLanguage,
018⫶ $targetLanguage,
019⫶ $provider,
020⫶ $replaceExisting,
021⫶ );
022⫶ }

Download colorized diff

@dabrt
dabrt requested a review from AnnaKaih July 22, 2026 12:53
Comment thread docs/api/event_reference/translations_management_events.md Outdated
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
Comment thread code_samples/translations_management/config/services.yaml Outdated
Comment on lines +179 to +198
## 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. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this should be dropped because LanguagePairServiceInterface is not a contract and should not be exposed as example to manage Language Pairs outside of the translations management bundle.


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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should to clarify that excluding a content type from the side-by-side view does not make it untranslatable. Landing Page and other excluded content types can still be auto-translated through the standard translation flow and the CLI command - only the side-by-side editing interface is unavailable for them.

Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
@dabrt
dabrt requested a review from ciastektk August 11, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants