diff --git a/code_samples/multisite/automated_translation/config/services.yaml b/code_samples/multisite/automated_translation/config/services.yaml new file mode 100644 index 0000000000..85bdd6f69e --- /dev/null +++ b/code_samples/multisite/automated_translation/config/services.yaml @@ -0,0 +1,31 @@ +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/' + exclude: + - '../src/DependencyInjection/' + - '../src/Entity/' + - '../src/Kernel.php' + + App\AutomatedTranslation\AiClient: + tags: + - ibexa.automated_translation.client + + App\AutomatedTranslation\ImageFieldEncoder: + tags: + - ibexa.automated_translation.field_encoder + +ibexa_automated_translation: + system: + default: + configurations: + aiclient: + languages: + - 'en_GB' + - 'fr_FR' diff --git a/code_samples/multisite/automated_translation/src/AutomatedTranslation/AiClient.php b/code_samples/multisite/automated_translation/src/AutomatedTranslation/AiClient.php new file mode 100644 index 0000000000..e423edb643 --- /dev/null +++ b/code_samples/multisite/automated_translation/src/AutomatedTranslation/AiClient.php @@ -0,0 +1,68 @@ + */ + private array $supportedLanguages; + + private ActionServiceInterface $actionService; + + private ActionConfigurationServiceInterface $actionConfigurationService; + + public function __construct(ActionServiceInterface $actionService, ActionConfigurationServiceInterface $actionConfigurationService) + { + $this->actionService = $actionService; + $this->actionConfigurationService = $actionConfigurationService; + } + + public function setConfiguration(array $configuration): void + { + if (!array_key_exists('languages', $configuration)) { + throw new ClientNotConfiguredException('List of supported languages is missing in the configuration under the "languages" key'); + } + $this->supportedLanguages = $configuration['languages']; + } + + public function translate(string $payload, ?string $from, string $to): string + { + $action = new TranslateAction(new Text([$payload])); + $action->setRuntimeContext( + new RuntimeContext( + [ + 'from' => $from, + 'to' => $to, + ] + ) + ); + $actionConfiguration = $this->actionConfigurationService->getActionConfiguration('translate'); + $actionResponse = $this->actionService->execute($action, $actionConfiguration)->getOutput(); + + assert($actionResponse instanceof Text); + + return $actionResponse->getText(); + } + + public function supportsLanguage(string $languageCode): bool + { + return in_array($languageCode, $this->supportedLanguages, true); + } + + public function getServiceAlias(): string + { + return 'aiclient'; + } + + public function getServiceFullName(): string + { + return 'Custom AI Automated Translation'; + } +} diff --git a/code_samples/multisite/automated_translation/src/AutomatedTranslation/ImageFieldEncoder.php b/code_samples/multisite/automated_translation/src/AutomatedTranslation/ImageFieldEncoder.php new file mode 100644 index 0000000000..bb229ddc39 --- /dev/null +++ b/code_samples/multisite/automated_translation/src/AutomatedTranslation/ImageFieldEncoder.php @@ -0,0 +1,39 @@ +fieldTypeIdentifier === 'ezimage'; + } + + public function canDecode(string $type): bool + { + return $type === 'ezimage'; + } + + public function encode(Field $field): string + { + /** @var \Ibexa\Core\FieldType\Image\Value $value */ + $value = $field->getValue(); + + return $value->alternativeText ?? ''; + } + + /** + * @param string $value + * @param \Ibexa\Core\FieldType\Image\Value $previousFieldValue + */ + public function decode(string $value, $previousFieldValue): Value + { + $previousFieldValue->alternativeText = $value; + + return $previousFieldValue; + } +} diff --git a/code_samples/multisite/automated_translation/src/AutomatedTranslation/TranslateAction.php b/code_samples/multisite/automated_translation/src/AutomatedTranslation/TranslateAction.php new file mode 100644 index 0000000000..8d2e14c74d --- /dev/null +++ b/code_samples/multisite/automated_translation/src/AutomatedTranslation/TranslateAction.php @@ -0,0 +1,13 @@ + ['all' => true], + Ibexa\Bundle\AdminUi\IbexaAdminUiBundle::class => ['all' => true], + // ... + ]; + ``` + +### Configure access to translation services + +Before you can start using the feature, you must configure access to your Google and/or DeepL account. + +1\. Get the [Google API key](https://developers.google.com/maps/documentation/javascript/get-api-key) and/or [DeepL Pro key](https://support.deepl.com/hc/en-us/articles/360020695820-API-Key-for-DeepL-s-API). + +2\. Set these values in the YAML configuration files, under the `ibexa_automated_translation.system.default.configurations` key: + +``` yaml +ibexa_automated_translation: + system: + default: + configurations: + google: + apiKey: "google-api-key" + deepl: + authKey: "deepl-pro-key" +``` + +The configuration is SiteAccess-aware, therefore, you can configure different engines to be used for different sites. + +## Translate content items with CLI + +To create a machine translation of a specific content item, you can use the `ibexa:automated:translate` command. + +The following arguments and options are supported: + +- `--from` - the source language +- `--to` - the target language +- `contentId` - ID of the content to translate +- `serviceName` - the service to use for translation + +For example, to translate the root content item from English to French with the help of Google Translate, run: + +``` bash +php bin/console ibexa:automated:translate --from=eng-GB --to=fre-FR 52 google +``` + +## Extend automated content translations + +### Add a custom machine translation service + +By default, the automated translation package can connect to Google Translate or DeepL, but you can configure it to use a custom machine translation service. +You would do it, for example, when a new service emerges on the market, or your company requires that a specific service is used. + +The following example adds a new translation service. +It uses the [AI actions framework](ai_actions_md) and assumes a custom `TranslateAction` AI Action exists. +To learn how to build custom AI actions see [Extending AI actions](extend_ai_actions.md#custom-action-type-use-case). + +1. Create a service that implements the [`\Ibexa\AutomatedTranslation\Client\ClientInterface`](../../api/php_api/php_api_reference/classes/Ibexa-Contracts-AutomatedTranslation-Client-ClientInterface.html) interface: + +``` php hl_lines="35-52" +[[= include_file('code_samples/multisite/automated_translation/src/AutomatedTranslation/AiClient.php') =]] +``` + +2\. Tag the service as `ibexa.automated_translation.client` in the Symfony container: + +``` yaml +[[= include_file('code_samples/multisite/automated_translation/config/services.yaml', 15, 18) =]] +``` + +3\. Specify the configuration under the `ibexa_automated_translation.system.default.configurations` key: + +``` yaml +[[= include_file('code_samples/multisite/automated_translation/config/services.yaml', 23, 32) =]] +``` + +### Create custom field or block attribute encoder + +You can expand the list of supported field types and block attributes for automated translation, adding support for even more use cases than the ones built into [[= product_name =]]. + +The whole automated translation process consists of 3 phases: + +1. **Encoding** - data is extracted from the field types and block attributes and serialized into XML format +1. **Translating** - the serialized XML is sent into specified translation service +1. **Decoding** - the translated response is deserialized into the original data structures for storage in [[= product_name =]] + +The following example adds support for automatically translating alternative text in image fields. + +1. Create a class implementing the [`FieldEncoderInterface`](../../api/php_api/php_api_reference/classes/Ibexa-Contracts-AutomatedTranslation-Encoder-Field-FieldEncoderInterface.html) and add the required methods: + +``` php hl_lines="11-14 16-19 21-27 33-38" +[[= include_file('code_samples/multisite/automated_translation/src/AutomatedTranslation/ImageFieldEncoder.php') =]] +``` +In this example, the methods are responsible for: + +- `canEncode` - deciding whether the field to be encoded is an [Image](imagefield.md) field +- `canDecode` - deciding whether the field to be decoded is an [Image](imagefield.md) field +- `encode` - extracting the alternative text from the field type +- `decode` - saving the translated alternative text in the field type's value object + +2\. Register the class as a service. +If you're not using [Symfony's autoconfiguration]([[= symfony_doc =]]/service_container.html#the-autoconfigure-option), use the `ibexa.automated_translation.field_encoder` service tag. + +``` yaml +[[= include_file('code_samples/multisite/automated_translation/config/services.yaml', 19, 22) =]] +``` + +For custom block attributes, the appropriate interface is [`BlockAttributeEncoderInterface`](../../api/php_api/php_api_reference/classes/Ibexa-Contracts-AutomatedTranslation-Encoder-BlockAttribute-BlockAttributeEncoderInterface.html) and the service tag is `ibexa.automated_translation.block_attribute_encoder`. diff --git a/mkdocs.yml b/mkdocs.yml index 10b6678ef6..a2ecde136c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -418,6 +418,7 @@ nav: - Languages: multisite/languages/languages.md - Language API: multisite/languages/language_api.md - Back office translations: multisite/languages/back_office_translations.md + - Automated content translation: multisite/languages/automated_translations.md - Permissions: - Permissions: permissions/permissions.md - Permission overview: permissions/permission_overview.md