-
Notifications
You must be signed in to change notification settings - Fork 81
IBX-10684: Document translation management #3249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 5.0
Are you sure you want to change the base?
Changes from all commits
f0df726
2aec539
bffca77
6f128e0
07583df
9a00463
1196977
97c540b
510b6bb
9f3f304
5739e69
45be4c6
866d19c
00ebddc
45ce125
f51b281
53570f8
bedfd31
b9f08ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
|
|
||
| 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; | ||
|
|
||
|
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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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` | | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| ## 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` | | ||||||
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.