Skip to content

IBX-10684: Document translation management - #3249

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

IBX-10684: Document translation management#3249
dabrt wants to merge 23 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 thread docs/multisite/translations_management/configure_translations_management.md Outdated
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
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
Comment thread docs/api/event_reference/translations_management_events.md Outdated
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
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 12, 2026 10:58
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
Comment thread docs/multisite/translations_management/configure_translations_management.md Outdated
Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
Comment thread docs/multisite/translations_management/extend_translations_management.md Outdated
dabrt and others added 2 commits August 17, 2026 11:09
Co-authored-by: Tomasz Kryszan <tomasz.kryszan@ibexa.co>
Co-authored-by: Tomasz Dąbrowski <64841871+dabrt@users.noreply.github.com>
Co-authored-by: Tomasz Kryszan <tomasz.kryszan@ibexa.co>
@dabrt
dabrt requested a review from ciastektk August 17, 2026 09:11

@mnocon mnocon left a comment

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.

Review without the "Extend..." article (will do the review of it soon)

app.translations_management.exclusion_rule.custom_field_types:
class: Ibexa\TranslationsManagement\SideBySide\Service\UnsupportedFieldTypeExclusionRule
arguments:
$excludedFieldTypeIdentifiers: ['custom_blog_post', 'custom_landing_page']

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.

Both 'custom_blog_post' and 'custom_landing_page' sound more like content types than field types - I'd use the examples we have in other parts in the doc for custom field types (https://doc.ibexa.co/en/5.0/content_management/field_types/create_custom_generic_field_type/#define-value-object , https://doc.ibexa.co/en/5.0/tutorials/generic_field_type/creating_a_point2d_field_type/ )

$event->getContentId();
$event->getFromLanguageCode(); // ?string — null when no source language exists
$event->getToLanguageCode();
$event->getLocationId(); // ?int — null when no location context is available

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.

Suggested change
$event->getLocationId(); // ?int — null when no location context is available
$event->getLocationId(); // ?int — null when the content item hasn't been published yet

To make it more meaningful - I assume null happens only in this case? That's https://doc.ibexa.co/en/5.0/content_management/locations/#:~:text=A%20content%20item%20receives%20a%20location%20only%20once%20it%20has%20been%20published%2E%20This%20means%20that%20a%20new%20unpublished%20draft%20doesn%27t%20have%20a%20location%20yet%2E%20You%20can%20find%20drafts%20in%20the%20Drafts%20tab%20in%20the%20Content%20menu

{
// Read the translation context:
$event->getContentId();
$event->getFromLanguageCode(); // ?string — null when no source language exists

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.

Suggested change
$event->getFromLanguageCode(); // ?string — null when no source language exists
$event->getFromLanguageCode(); // ?string — null when no source language was selected


## Translation events

Translation events are thrown once per field value per translation operation.

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.

Suggested change
Translation events are thrown once per field value per translation operation.
Translation events are dispatched once per field value per translation operation.

Exceptions are thrown (throw new InvalidArgumentException), event are dispatched by the event dispatcher ( https://github.com/ibexa/translations-management/blob/9e62c293b8085443c16f10a34de3b089f79b126b/src/lib/AutoTranslate/Event/EventDispatchingProviderTranslator.php#L45 )

I'm not sure I understand this sentence correctly - for each field type to be translated and for each language that field is translated into?

3 fields, 2 languages - 6 before and after events?

How about:

Suggested change
Translation events are thrown once per field value per translation operation.
Translation events are dispatched for every field in each selected target language.

## Translation events

Translation events are thrown once per field value per translation operation.
They are used for logging, analytics, and observability.

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.

Are they used, or can they be used?

I think the system uses them only for logging by default (https://github.com/ibexa/translations-management/blob/9e62c293b8085443c16f10a34de3b089f79b126b/src/bundle/AutoTranslate/EventSubscriber/TranslationEventSubscriber.php#L11 ? )

Suggested change
They are used for logging, analytics, and observability.
Use them for logging, analytics, and observability.


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.

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.

Good remark, might be worth to link to the product attributes note explaining this (https://doc.ibexa.co/en/5.0/product_catalog/products/#product-attributes:~:text=Attribute%20translations )


!!! note "Excluded content types"

Content types that are editable in Page builder or Form builder are excluded from side-by-side editing.

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.

Comment on lines +90 to +92
!!! note "No review for manual translations"

Draft translations that were created manually don't have a review status.

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.

This seems like a difference - coming back to the mention of "triggering the translation from CLI has the same results as triggering from UI" in another article.


## Translation review

When a draft translation of a content item or product is created by going through the automatic translation process, the system creates a review status record and marks the draft `for_review`.

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.

This one doesn't mention that CLI does not trigger it.

| `--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 |

## Translation review

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.

This workflow doesn't seem configurable, can we find a better place to describe this than the "Configure..." article?

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.

4 participants