Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions code_samples/translations_management/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
services:
App\TranslationsManagement\MyCustomProvider:
tags:
- name: 'ibexa.translations_management.auto_translate.provider'
identifier: 'my_custom_provider'
validation_profile: 'my_custom_profile'
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
App\TranslationsManagement\MyCustomAiProvider:
tags:
- name: 'ibexa.translations_management.auto_translate.provider'
identifier: 'my_custom_ai_provider'
validation_profile: 'ai_generic'
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types=1);

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
<?php declare(strict_types=1);
<?php
declare(strict_types=1);


namespace App\TranslationsManagement\EventSubscriber;

use Ibexa\Contracts\AdminUi\Event\ContentProxyTranslateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

final readonly class ContentProxyTranslateSubscriber implements EventSubscriberInterface
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
) {
}

public static function getSubscribedEvents(): array
{
return [
ContentProxyTranslateEvent::class => ['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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,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,
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\TranslationsManagement;

/** Placeholder for your own HTTP client or third-party SDK wrapper. */
final class MyApiClient
{
public function translate(string $text, string $sourceLanguage, string $targetLanguage): string
{
// Your implementation here.
return '';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,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 MyAiClient with your HTTP client, SDK wrapper, or any service
* that communicates with the external AI translation API.
*/
public function __construct(
private MyAiClient $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 !== '';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,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';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,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'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace App\TranslationsManagement;

Comment thread
dabrt marked this conversation as resolved.
use Ibexa\AdminUi\Form\Type\Content\Translation\TranslationAddType;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;

final class MyTranslationAddExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [TranslationAddType::class];
}

public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('my_custom_field'/* ... */);
}
}
Comment on lines +1 to +22

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.

I would recommend to drop this class and references to it, because it is not a TM contract. Class TranslationAddType comes from admin-ui so I don't see any value to keep it here and inform devs that you can extend form type from other package.

3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions deptrac.baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/api/event_reference/event_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions docs/api/event_reference/translations_management_events.md
Original file line number Diff line number Diff line change
@@ -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) | `EventDispatchingProviderTranslator` | Before a translation request is sent to the provider | `TranslationProviderInterface $provider`</br>`string $text`</br>`string $sourceLanguage`</br>`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`</br>`TranslationProviderInterface $provider`</br>`string $text`</br>`string $sourceLanguage`</br>`string $targetLanguage` |

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
| [`TranslateEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Event-TranslateEvent.html) | `TranslationService` | After a translation response is received | `string $result`</br>`TranslationProviderInterface $provider`</br>`string $text`</br>`string $sourceLanguage`</br>`string $targetLanguage` |
| [`TranslateEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Event-TranslateEvent.html) | `EventDispatchingProviderTranslator` | After a translation response is received | `string $result`</br>`TranslationProviderInterface $provider`</br>`string $text`</br>`string $sourceLanguage`</br>`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`</br>`Content $sourceContent`</br>`string $sourceLanguageCode`</br>`string $targetLanguageCode`</br>`?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`</br>`ContentAwareProductInterface $sourceProduct`</br>`ContentAwareProductInterface $targetProduct`</br>`string $sourceLanguageCode`</br>`string $targetLanguageCode`</br>`?ProductUpdateData $productUpdateData` |
1 change: 1 addition & 0 deletions docs/ibexa_products/editions.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ The features brought by LTS Updates become standard parts of the next LTS releas
| [Integrated help](integrated_help.md) | &#10004; | &#10004; | &#10004; |
| [MCP servers](mcp_guide.md) | &#10004; | &#10004; | &#10004; |
| [Shopping list](shopping_list_guide.md) | | | &#10004; |
| [Translations management](translations_management_guide.md) | &#10004; | &#10004; | &#10004; |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mxfile host="Electron" modified="2026-06-16T10:51:05.860Z" agent="5.0 (Macintosh; Intel Mac OS X 26_3_1) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="j-W4lXxHCJBOFpcNCibg" version="14.6.13" type="device"><diagram id="1kQWOgmGZ1G1xJNYzsRM" name="Page-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=</diagram></mxfile>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading