Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# Automatic Translation
## [1.11.2] - 12/03/2026
### Fixed
- Fixed widget content_settings translation using JSON decode/encode instead of unreliable regex on raw encoded string
- Added translation of repeatable items (title, content, button, image_alt) inside content_settings
- Added translation of widget preview HTML inside content_settings for Page Builder editor consistency

## [1.11.1] - 12/03/2026
### Fixed
- Fixed encodePageBuilderHtmlBox corrupting text/heading content types (double HTML-encoding, style block encoding, spurious newlines)
Expand Down
178 changes: 172 additions & 6 deletions Service/TranslateParsedContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,39 @@

use MageOS\AutomaticTranslation\Helper\Service;
use MageOS\AutomaticTranslation\Model\Translator;
use Magento\Framework\Serialize\Serializer\Json;
use RuntimeException;
use Exception;

class TranslateParsedContent
{
const TRANSLATABLE_WIDGET_PARAMS = ['anchor_text', 'title', 'description'];
const WIDGET_PATTERN = '/\{\{widget\s[^}]*\}\}/';
const TRANSLATABLE_WIDGET_PARAMS = ['anchor_text', 'title', 'description'];
const TRANSLATABLE_REPEATABLE_PARAMS = ['title', 'content', 'button', 'image_alt'];
const JSON_ENCODE_FLAGS = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;

/** @var Service */
protected Service $serviceHelper;

/** @var Translator */
protected Translator $translator;

/** @var Json */
protected Json $json;

/**
* @param Service $serviceHelper
* @param Translator $translator
* @param Json $json
*/
public function __construct(
protected Service $serviceHelper,
protected Translator $translator,
Service $serviceHelper,
Translator $translator,
Json $json
) {
$this->serviceHelper = $serviceHelper;
$this->translator = $translator;
$this->json = $json;
}

/**
Expand All @@ -35,7 +52,7 @@ public function execute(
$parsedContent,
string $requestPostValue,
string $destinationLanguage
): mixed {
) {
if (is_string($parsedContent)) {
return $this->translateWidgetDirectives($parsedContent, $destinationLanguage);
}
Expand Down Expand Up @@ -95,7 +112,7 @@ function (array $matches) use (&$contentSettingsMap): string {
foreach ($contentSettingsMap as $key => $value) {
$result = str_replace(
'content_settings="' . $key . '"',
'content_settings="' . $value . '"',
'content_settings="' . $this->translateContentSettings($value, $destinationLanguage) . '"',
$result
);
}
Expand Down Expand Up @@ -161,6 +178,138 @@ function (array $matches) use (&$map, $prefix): string {
return [$result, $map];
}

/**
* @param string $contentSettings
* @param string $destinationLanguage
* @return string
* @throws Exception
*/
protected function translateContentSettings(
string $contentSettings,
string $destinationLanguage
): string {
$decoded = html_entity_decode($contentSettings, ENT_QUOTES, 'UTF-8');

try {
$outer = $this->json->unserialize($decoded);
} catch (Exception $e) {
return $contentSettings;
}

if (!is_array($outer) || !isset($outer['data'])) {
return $contentSettings;
}

try {
$data = $this->json->unserialize($outer['data']);
} catch (Exception $e) {
return $contentSettings;
}

if (!is_array($data) || !isset($data['values'])) {
return $contentSettings;
}

$translations = [];

foreach ($data['values'] as $key => &$value) {
if (!is_string($value) || trim($value) === '') {
continue;
}

if (strncmp($key, 'repeatable_', 11) === 0 && substr($key, -6) === '_items') {
$original = $value;
$value = $this->translateRepeatableItems($value, $destinationLanguage);
$this->collectRepeatableTranslations($original, $value, $translations);
continue;
}

foreach (self::TRANSLATABLE_WIDGET_PARAMS as $suffix) {
$needle = '_' . $suffix;
if (substr($key, -strlen($needle)) === $needle) {
$original = $value;
$value = $this->translator->translate($value, $destinationLanguage);
if ($original !== $value) {
$translations[$original] = $value;
}
break;
}
}
}
unset($value);

$outer['data'] = json_encode($data, self::JSON_ENCODE_FLAGS);

if (isset($outer['preview']) && !empty($translations)) {
foreach ($translations as $original => $translated) {
$outer['preview'] = str_replace($original, $translated, $outer['preview']);
}
}

return htmlspecialchars(
json_encode($outer, self::JSON_ENCODE_FLAGS),
ENT_COMPAT,
'UTF-8'
);
}

/**
* @param string $original
* @param string $translated
* @param array &$translations
*/
protected function collectRepeatableTranslations(
string $original,
string $translated,
array &$translations
): void {
foreach (self::TRANSLATABLE_REPEATABLE_PARAMS as $param) {
preg_match_all('/`' . preg_quote($param, '/') . '`:`([^`]*)`/u', $original, $origMatches);
preg_match_all('/`' . preg_quote($param, '/') . '`:`([^`]*)`/u', $translated, $transMatches);

foreach ($origMatches[1] as $j => $origVal) {
if (isset($transMatches[1][$j]) && $origVal !== $transMatches[1][$j] && trim($origVal) !== '') {
$translations[$origVal] = $transMatches[1][$j];
}
}
}
}

/**
* @param string $repeatableString
* @param string $destinationLanguage
* @return string
* @throws RuntimeException
* @throws Exception
*/
protected function translateRepeatableItems(
string $repeatableString,
string $destinationLanguage
): string {
foreach (self::TRANSLATABLE_REPEATABLE_PARAMS as $param) {
$result = preg_replace_callback(
'/(`' . preg_quote($param, '/') . '`:`)([^`]*)(`)/u',
function (array $matches) use ($destinationLanguage): string {
if (trim($matches[2]) === '') {
return $matches[0];
}
return $matches[1]
. $this->translator->translate($matches[2], $destinationLanguage)
. $matches[3];
},
$repeatableString
);

if ($result === null) {
throw new RuntimeException(sprintf('preg_replace_callback failed with error %d', preg_last_error()));
}

$repeatableString = $result;
}

return $repeatableString;
}

/**
* @param string $widget
* @param string $destinationLanguage
Expand Down Expand Up @@ -193,6 +342,23 @@ function (array $matches) use ($destinationLanguage): string {
$widget = $result;
}

return $widget;
$result = preg_replace_callback(
'/(repeatable_[a-z_]+_items=")([^"]*)(")/',
function (array $matches) use ($destinationLanguage): string {
if (trim($matches[2]) === '') {
return $matches[0];
}
return $matches[1]
. $this->translateRepeatableItems($matches[2], $destinationLanguage)
. $matches[3];
},
$widget
);

if ($result === null) {
throw new RuntimeException(sprintf('preg_replace_callback failed with error %d', preg_last_error()));
}

return $result;
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"MIT"
],
"require": {
"php": ">=7.4",
"magento/framework": "*",
"magento/module-catalog": "^104.0.0",
"deeplcom/deepl-php": "^1",
Expand Down