Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f0df726
IBX-10684: Document translation management
dabrt Jun 18, 2026
2aec539
Add events and extensions
dabrt Jun 19, 2026
bffca77
PHP & JS CS Fixes
dabrt Jun 19, 2026
6f128e0
Fix formatting in events
dabrt Jun 19, 2026
07583df
Add "Configure..." stem to enable building
dabrt Jun 22, 2026
9a00463
Implement reviewer comments
dabrt Jun 23, 2026
1196977
PHP & JS CS Fixes
dabrt Jun 23, 2026
97c540b
Add the "Configura" article
dabrt Jun 24, 2026
510b6bb
Clarify configuration article
dabrt Jun 25, 2026
9f3f304
Fix issues
dabrt Jun 25, 2026
5739e69
Remove duplicate blanks
dabrt Jun 25, 2026
45be4c6
Add a release notes entry
dabrt Jun 25, 2026
866d19c
Fix code issues
dabrt Jun 26, 2026
00ebddc
PHP & JS CS Fixes
dabrt Jun 26, 2026
45ce125
Add TranslationAddType to deptrack baseline
dabrt Jun 26, 2026
f51b281
Apply suggestions from code review
dabrt Aug 10, 2026
53570f8
Add translation review description.
dabrt Aug 10, 2026
bedfd31
Implement reviewer comments
dabrt Aug 11, 2026
b9f08ca
Minor fix
dabrt Aug 11, 2026
55a11e7
Implement reviewer comments, improve installation procedure
dabrt Aug 13, 2026
2133328
Apply suggestion from @ciastekk
dabrt Aug 17, 2026
74b684a
Apply suggestions from @ciastekk
dabrt Aug 17, 2026
c37003b
Apply suggestions from @ciastekk
dabrt Aug 17, 2026
ce0425f
Implement review comments from @mnocon
dabrt Aug 17, 2026
dacea12
Improve the review status assignment description
dabrt Aug 18, 2026
d0cd8b6
Merge branch 'release_5.0.10' into translations-management
dabrt Aug 19, 2026
684b3a3
Fix broken links
dabrt Aug 19, 2026
741b111
Fix markdown issues
dabrt Aug 19, 2026
0081b9a
RNs moved to release_5.0.10 branch
dabrt Aug 19, 2026
6451522
Apply suggestions from code review
dabrt Aug 19, 2026
3d55428
Changes from review to extending... pt 1
dabrt Aug 19, 2026
92243bc
Apply changes from review pt 3
dabrt Aug 19, 2026
fff9d83
Implement reviewer comments
dabrt Aug 20, 2026
c1fd099
Remove unnecessary section
dabrt Aug 20, 2026
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
37 changes: 37 additions & 0 deletions code_samples/translations_management/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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\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_point2d', 'custom_value']
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'
App\TranslationsManagement\MyCustomLanguageNormalizer:
tags:
- name: 'ibexa.translations_management.auto_translate.provider.language_normalizer'
priority: 10
35 changes: 35 additions & 0 deletions code_samples/translations_management/install/schema.mysql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
CREATE TABLE IF NOT EXISTS ibexa_auto_translation (
id INT AUTO_INCREMENT NOT NULL,
provider_identifier VARCHAR(190) NOT NULL,
content_id INT NOT NULL,
version_no INT NOT NULL,
source_language_id BIGINT NOT NULL,
target_language_id BIGINT NOT NULL,
review_status VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)',
updated_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)',
INDEX ibexa_auto_translation_content_version_idx (content_id, version_no),
INDEX ibexa_auto_translation_target_language_idx (target_language_id),
INDEX ibexa_auto_translation_review_status_idx (review_status),
UNIQUE INDEX ibexa_auto_translation_context_uidx (content_id, version_no, source_language_id, target_language_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_520_ci` ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS ibexa_auto_translation_review_log (
id INT AUTO_INCREMENT NOT NULL,
auto_translation_id INT DEFAULT NULL,
user_id INT NOT NULL,
status VARCHAR(64) NOT NULL,
operation VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)',
INDEX IDX_325A3B737CE350E8 (auto_translation_id),
INDEX ibexa_auto_translation_review_log_auto_translation_created_idx (auto_translation_id, created_at, id),
INDEX ibexa_auto_translation_review_log_status_created_idx (status, created_at),
INDEX ibexa_auto_translation_review_log_user_idx (user_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_520_ci` ENGINE = InnoDB;

ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_auto_translation_fk
FOREIGN KEY (auto_translation_id) REFERENCES ibexa_auto_translation (id) ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_user_fk
FOREIGN KEY (user_id) REFERENCES ibexa_user (contentobject_id) ON UPDATE CASCADE ON DELETE RESTRICT;
40 changes: 40 additions & 0 deletions code_samples/translations_management/install/schema.postgresql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
CREATE TABLE IF NOT EXISTS ibexa_auto_translation (
id SERIAL NOT NULL,
provider_identifier VARCHAR(190) NOT NULL,
content_id INT NOT NULL,
version_no INT NOT NULL,
source_language_id BIGINT NOT NULL,
target_language_id BIGINT NOT NULL,
review_status VARCHAR(64) NOT NULL,
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY(id)
);

CREATE INDEX IF NOT EXISTS ibexa_auto_translation_content_version_idx ON ibexa_auto_translation (content_id, version_no);
CREATE INDEX IF NOT EXISTS ibexa_auto_translation_target_language_idx ON ibexa_auto_translation (target_language_id);
CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_status_idx ON ibexa_auto_translation (review_status);
CREATE UNIQUE INDEX IF NOT EXISTS ibexa_auto_translation_context_uidx ON ibexa_auto_translation (content_id, version_no, source_language_id, target_language_id);
COMMENT ON COLUMN ibexa_auto_translation.created_at IS '(DC2Type:datetime_immutable)';
COMMENT ON COLUMN ibexa_auto_translation.updated_at IS '(DC2Type:datetime_immutable)';

CREATE TABLE IF NOT EXISTS ibexa_auto_translation_review_log (
id SERIAL NOT NULL,
auto_translation_id INT DEFAULT NULL,
user_id INT NOT NULL,
status VARCHAR(64) NOT NULL,
operation VARCHAR(64) NOT NULL,
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY(id)
);

CREATE INDEX IF NOT EXISTS IDX_325A3B737CE350E8 ON ibexa_auto_translation_review_log (auto_translation_id);
CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_log_auto_translation_created_idx ON ibexa_auto_translation_review_log (auto_translation_id, created_at, id);
CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_log_status_created_idx ON ibexa_auto_translation_review_log (status, created_at);
CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_log_user_idx ON ibexa_auto_translation_review_log (user_id);
COMMENT ON COLUMN ibexa_auto_translation_review_log.created_at IS '(DC2Type:datetime_immutable)';

ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_auto_translation_fk
FOREIGN KEY (auto_translation_id) REFERENCES ibexa_auto_translation (id) ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_user_fk
FOREIGN KEY (user_id) REFERENCES ibexa_user (contentobject_id) ON UPDATE CASCADE ON DELETE RESTRICT;
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 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 !== '';
}
}
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,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 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)
);
}
}
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'];
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"ibexa/connector-raptor": "~5.0.x-dev",
"ibexa/image-editor": "~5.0.x-dev",
"ibexa/integrated-help": "~5.0.x-dev",
"ibexa/translations-management": "~5.0.x-dev",
"ibexa/site-context": "~5.0.x-dev",
"ibexa/fieldtype-richtext-rte": "~5.0.x-dev",
"ibexa/site-factory": "~5.0.x-dev",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,10 @@ For more information, see [this example using few of those components](component
|`admin-ui-discount-condition-code-usage-summary`| `vendor/ibexa/discounts/src/bundle/Resources/views/themes/admin/discounts/tab/details.html.twig` |
|`admin-ui-discount-condition-code-summary`| `vendor/ibexa/discounts/src/bundle/Resources/views/themes/admin/discounts/tab/details.html.twig` |
|`admin-ui-discount-condition-code-usage-limit-summary`| `vendor/ibexa/discounts/src/bundle/Resources/views/themes/admin/discounts/tab/details.html.twig` |

## Translations management [[% include 'snippets/lts-update_badge.md' %]]

| Group name | Template file |
|---|---|
|`admin-ui-content-translation-modal-footer`| `vendor/ibexa/translations-management/src/bundle/Resources/views/themes/admin/translations_management/component/side_by_side_modal_footer.html.twig` |
|`admin-ui-content-edit-translation-select-footer`| `vendor/ibexa/translations-management/src/bundle/Resources/views/themes/admin/translations_management/component/side_by_side_content_edit_footer.html.twig` |
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
Loading
Loading