Skip to content
For AI agents: the complete documentation index is available at llms.txt; this page is also available as Markdown at index.md.

Extend translations management

By extending Translations management, you can 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, field type transformers, exclusion rules, and UI components. In all cases, you follow the same pattern: implement an interface first, 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 configured in your system.

REST API-based provider

To connect a translation service that calls a REST API directly, implement TranslationProviderInterface. For providers that store API keys and other required settings, you can rely on ConfigurableProviderInterface. It extends TranslationProviderInterface and adds getConfiguration() and isConfigured() methods.

The translate() method receives a TranslationDataInterface object that carries the text to translate along with the source and target language codes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

declare(strict_types=1);

namespace App\TranslationsManagement;

use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Provider\TranslationProviderInterface;
use Ibexa\Contracts\TranslationsManagement\AutoTranslate\TranslationDataInterface;

final readonly class MyCustomProvider implements TranslationProviderInterface
{
    /**
     * Replace MyApiClient with your HTTP client, SDK wrapper, or any service
     * that communicates with the external translation API.
     */
    public function __construct(
        private MyApiClient $apiClient,
    ) {
    }

    public function getIdentifier(): string
    {
        return 'my_custom_provider';
    }

    public function getName(): string
    {
        return 'My Translation Service';
    }

    public function getVendorName(): string
    {
        return 'My Company Ltd';
    }

    public function translate(TranslationDataInterface $translationData): string
    {
        return $this->apiClient->translate(
            $translationData->getText(),
            $translationData->getSourceLanguage(),
            $translationData->getTargetLanguage()
        );
    }

    /** @return array<string> */
    public function getSupportedLanguageCodes(): array
    {
        return ['eng-GB', 'ger-DE', 'fre-FR'];
    }
}

Register the provider with the ibexa.translations_management.auto_translate.provider tag. Both identifier and validation_profile attributes are required.

1
2
3
4
5
6
services:
    App\TranslationsManagement\MyCustomProvider:
        tags:
            - name: 'ibexa.translations_management.auto_translate.provider'
              identifier: 'my_custom_provider'
              validation_profile: 'my_custom_profile'

AI-based provider

To connect a translation service that uses the AI Actions framework, implement AiTranslationProviderInterface. This interface extends ConfigurableProviderInterface and serves as a type marker for AI-based providers. The system uses the getConfiguration() and isConfigured() methods to determine whether the provider is available before displaying selectable options in the Create a new translation modal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php

declare(strict_types=1);

namespace App\TranslationsManagement;

use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Provider\AiTranslationProviderInterface;
use Ibexa\Contracts\TranslationsManagement\AutoTranslate\TranslationDataInterface;

final readonly class MyCustomAiProvider implements AiTranslationProviderInterface
{
    /**
     * Replace MyApiClient with your HTTP client, SDK wrapper, or any service
     * that communicates with the external AI translation API.
     */
    public function __construct(
        private MyApiClient $apiClient,
        private string $actionConfigurationIdentifier,
    ) {
    }

    public function getIdentifier(): string
    {
        return 'my_custom_ai_provider';
    }

    public function getName(): string
    {
        return 'My AI Translation Service';
    }

    public function getVendorName(): string
    {
        return 'My Company Ltd';
    }

    public function translate(TranslationDataInterface $translationData): string
    {
        return $this->apiClient->translate(
            $translationData->getText(),
            $translationData->getSourceLanguage(),
            $translationData->getTargetLanguage()
        );
    }

    /** @return array<string> */
    public function getSupportedLanguageCodes(): array
    {
        return ['eng-GB', 'ger-DE', 'fre-FR'];
    }

    /** @return array<string, mixed> */
    public function getConfiguration(): array
    {
        return [
            'actionConfigurationIdentifier' => $this->actionConfigurationIdentifier,
        ];
    }

    public function isConfigured(): bool
    {
        return $this->actionConfigurationIdentifier !== '';
    }
}

Register the provider with the ibexa.translations_management.auto_translate.provider tag, with ai_generic as the validation profile. The ai_generic validation profile is meant to be used by default for AI providers, but you can implement your own.

1
2
3
4
5
6
services:
    App\TranslationsManagement\MyCustomAiProvider:
        tags:
            - name: 'ibexa.translations_management.auto_translate.provider'
              identifier: 'my_custom_ai_provider'
              validation_profile: 'ai_generic'

If your custom provider integrates with the AI Actions framework, isConfigured() should check whether the actionConfigurationIdentifier resolves to an existing and enabled Action Configuration.

The validation_profile, supportedLanguageCodes, and languageCodesMap options work the same way as for REST API-based providers.

Language code normalizer

If your provider uses language codes that differ from the ones used by Ibexa DXP and the languageCodesMap configuration is insufficient, implement a custom LanguageNormalizerInterface to handle the conversion:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

declare(strict_types=1);

namespace App\TranslationsManagement;

use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Exception\UnsupportedLanguageException;
use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Provider\LanguageNormalizer\LanguageNormalizerInterface;
use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Provider\TranslationProviderInterface;

final class MyCustomLanguageCodeNormalizer implements LanguageNormalizerInterface
{
    private const array LANGUAGE_MAP = [
        'eng-GB' => 'en-GB',
        'ger-DE' => 'de',
        'fre-FR' => 'fr',
    ];

    public function supports(TranslationProviderInterface $provider): bool
    {
        return $provider->getIdentifier() === 'my_custom_ai_provider';
    }

    public function normalize(
        TranslationProviderInterface $provider,
        string $languageCode
    ): string {
        if (isset(self::LANGUAGE_MAP[$languageCode])) {
            return self::LANGUAGE_MAP[$languageCode];
        }

        throw new UnsupportedLanguageException(
            $languageCode,
            $provider->getIdentifier(),
            array_values(self::LANGUAGE_MAP)
        );
    }
}

The supports() method is a way to bind the normalizer to a provider. When a translation is triggered, the system checks the registered normalizers, and it uses the first one whose supports() method returns true for the current provider.

Register the normalizer with the ibexa.translations_management.auto_translate.provider.language_normalizer tag:

1
2
3
4
5
services:
    App\TranslationsManagement\MyCustomLanguageNormalizer:
        tags:
            - name: 'ibexa.translations_management.auto_translate.provider.language_normalizer'
              priority: 10

If multiple normalizers are registered, use priority to control the order in which they're checked.

Validation profiles

The validation_profile attribute links the provider to a validator that checks language codes and payload size 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 and register it:

1
2
3
4
5
services:
    App\TranslationsManagement\MyProviderValidator:
        tags:
            - name: 'ibexa.translations_management.auto_translate.provider.validator'
              profile: 'my_custom_profile'

You can reuse the DefaultProviderValidator class if it meets your requirements or implement your own. It exposes configurable maximum payload size and language code regex patterns.

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 text, RichText, and ibexa_landing_page fields.

To add support for a custom or non-standard field type, implement FieldValueTransformerInterface:

  • getFieldTypeIdentifier() - returns the field type identifier that this transformer handles
  • encode(Field $field): EncodedFieldValue - extracts the translatable string from the field and wraps it in an EncodedFieldValue. The constructor takes the extracted string as its first argument and an optional metadata array as the second.
  • decode(string $value, mixed $previousFieldValue, array $metadata): Value - receives the translated string, the previous field value, and any metadata. Returns the updated field value.

The following example adds support for automatically translating the alternative text of an image:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php

declare(strict_types=1);

namespace App\TranslationsManagement;

use Ibexa\Contracts\Core\Repository\Values\Content\Field;
use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Transformer\Field\EncodedFieldValue;
use Ibexa\Contracts\TranslationsManagement\AutoTranslate\Transformer\Field\FieldValueTransformerInterface;
use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
use Ibexa\Core\FieldType\Image\Value as ImageValue;
use Ibexa\Core\FieldType\Value;

final class ImageAltTextTransformer implements FieldValueTransformerInterface
{
    public function getFieldTypeIdentifier(): string
    {
        return 'ibexa_image';
    }

    public function encode(Field $field): EncodedFieldValue
    {
        $value = $field->getValue();
        if (!$value instanceof ImageValue) {
            throw new InvalidArgumentException(
                '$field',
                sprintf('Expected %s, got %s.', ImageValue::class, get_debug_type($value))
            );
        }

        return new EncodedFieldValue($value->alternativeText ?? '');
    }

    /**
     * @param array<string, mixed> $metadata
     */
    public function decode(string $value, mixed $previousFieldValue, array $metadata): Value
    {
        if (!$previousFieldValue instanceof ImageValue) {
            throw new InvalidArgumentException(
                '$previousFieldValue',
                sprintf('Expected %s, got %s.', ImageValue::class, get_debug_type($previousFieldValue))
            );
        }

        return new ImageValue([
            'id' => $previousFieldValue->id,
            'fileName' => $previousFieldValue->fileName,
            'fileSize' => $previousFieldValue->fileSize,
            'uri' => $previousFieldValue->uri,
            'imageId' => $previousFieldValue->imageId,
            'inputUri' => $previousFieldValue->inputUri,
            'width' => $previousFieldValue->width,
            'height' => $previousFieldValue->height,
            'alternativeText' => $value,
            'additionalData' => $previousFieldValue->additionalData,
            'mime' => $previousFieldValue->mime,
        ]);
    }
}

Register the new 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:

1
2
3
4
5
services:
    App\TranslationsManagement\ImageAltTextTransformer:
        tags:
            - name: 'ibexa.translations_management.auto_translate.field_value_transformer'
              field_type_identifier: 'ibexa_image'

Advanced metadata handling

When metadata is required for decoding or when you need to control what happens if metadata encoding fails, implement MetadataAwareFieldValueTransformerInterface. With this interface, you can fail the translation when metadata encoding fails and indicate that metadata is required for decoding. Without it, the field is skipped instead.

Define custom exclusion rules

Use exclusion rules to identify content 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 content from side-by-side view, for example, content types whose fields render incorrectly in the side-by-side layout, implement SideBySideExclusionRuleInterface. The isExcluded() method receives a ContentInfo object, which gives you access to different criteria, including content type, section, owner, main language, publication status, visibility, and main location of the content item. If the content item should be excluded, the method should return true.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php

declare(strict_types=1);

namespace App\TranslationsManagement;

use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo;
use Ibexa\Contracts\TranslationsManagement\SideBySide\Service\SideBySideExclusionRuleInterface;

final class MyCustomExclusionRule implements SideBySideExclusionRuleInterface
{
    public function isExcluded(ContentInfo $contentInfo): bool
    {
        return $contentInfo->getContentType()->identifier === 'my_excluded_type';
    }
}

Register the rule with the ibexa.translations_management.side_by_side.exclusion_rule tag. This interface is not registered for Symfony autoconfiguration, so the tag is required.

1
2
3
4
5
services:
    App\TranslationsManagement\MyCustomExclusionRule:
        tags:
            - { name: 'ibexa.translations_management.side_by_side.exclusion_rule' }
    app.translations_management.exclusion_rule.custom_field_types:

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 element could be, for example, a disclaimer or policy notice that the editor must acknowledge before a translation is created.

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 of the content edit confirmation screen.

Register a component with the ibexa.twig.component tag:

1
2
3
4
5
6
services:
    App\TranslationsManagement\TwigComponent\MyTranslationModalFooter:
        tags:
            - name: ibexa.twig.component
              group: 'admin-ui-content-translation-modal-footer'
              priority: 10

Note

The admin-ui-content-translation-modal-footer group receives a location variable that may be null for an unpublished draft. Always check for null before you access location properties in your component template.