From 7211a5ae9b78ba0864719c20c40805d30d6879a6 Mon Sep 17 00:00:00 2001 From: Samuele Martini Date: Thu, 12 Mar 2026 17:03:40 +0100 Subject: [PATCH 1/2] Fix content_settings and widget params translation in Page Builder --- Service/TranslateParsedContent.php | 159 ++++++++++++++++++++++++++++- 1 file changed, 156 insertions(+), 3 deletions(-) diff --git a/Service/TranslateParsedContent.php b/Service/TranslateParsedContent.php index 0a1b702..11111c2 100644 --- a/Service/TranslateParsedContent.php +++ b/Service/TranslateParsedContent.php @@ -5,21 +5,26 @@ 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; /** * @param Service $serviceHelper * @param Translator $translator + * @param Json $json */ public function __construct( protected Service $serviceHelper, protected Translator $translator, + protected Json $json, ) { } @@ -95,7 +100,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 ); } @@ -161,6 +166,137 @@ 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) { + return $contentSettings; + } + + if (!is_array($outer) || !isset($outer['data'])) { + return $contentSettings; + } + + try { + $data = $this->json->unserialize($outer['data']); + } catch (Exception) { + 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 (str_starts_with($key, 'repeatable_') && str_ends_with($key, '_items')) { + $original = $value; + $value = $this->translateRepeatableItems($value, $destinationLanguage); + $this->collectRepeatableTranslations($original, $value, $translations); + continue; + } + + foreach (self::TRANSLATABLE_WIDGET_PARAMS as $suffix) { + if (str_ends_with($key, '_' . $suffix)) { + $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 @@ -193,6 +329,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; } } From b60d36f030164ff690571b235c14ed1feb5fae00 Mon Sep 17 00:00:00 2001 From: Samuele Martini Date: Thu, 12 Mar 2026 17:20:31 +0100 Subject: [PATCH 2/2] Add PHP 7.4 compatibility and content_settings translation fixes --- CHANGELOG.md | 6 ++++++ Service/TranslateParsedContent.php | 29 +++++++++++++++++++++-------- composer.json | 1 + 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3d39c..f419fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Service/TranslateParsedContent.php b/Service/TranslateParsedContent.php index 11111c2..28b0932 100644 --- a/Service/TranslateParsedContent.php +++ b/Service/TranslateParsedContent.php @@ -16,16 +16,28 @@ class TranslateParsedContent 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, - protected Json $json, + Service $serviceHelper, + Translator $translator, + Json $json ) { + $this->serviceHelper = $serviceHelper; + $this->translator = $translator; + $this->json = $json; } /** @@ -40,7 +52,7 @@ public function execute( $parsedContent, string $requestPostValue, string $destinationLanguage - ): mixed { + ) { if (is_string($parsedContent)) { return $this->translateWidgetDirectives($parsedContent, $destinationLanguage); } @@ -180,7 +192,7 @@ protected function translateContentSettings( try { $outer = $this->json->unserialize($decoded); - } catch (Exception) { + } catch (Exception $e) { return $contentSettings; } @@ -190,7 +202,7 @@ protected function translateContentSettings( try { $data = $this->json->unserialize($outer['data']); - } catch (Exception) { + } catch (Exception $e) { return $contentSettings; } @@ -205,7 +217,7 @@ protected function translateContentSettings( continue; } - if (str_starts_with($key, 'repeatable_') && str_ends_with($key, '_items')) { + if (strncmp($key, 'repeatable_', 11) === 0 && substr($key, -6) === '_items') { $original = $value; $value = $this->translateRepeatableItems($value, $destinationLanguage); $this->collectRepeatableTranslations($original, $value, $translations); @@ -213,7 +225,8 @@ protected function translateContentSettings( } foreach (self::TRANSLATABLE_WIDGET_PARAMS as $suffix) { - if (str_ends_with($key, '_' . $suffix)) { + $needle = '_' . $suffix; + if (substr($key, -strlen($needle)) === $needle) { $original = $value; $value = $this->translator->translate($value, $destinationLanguage); if ($original !== $value) { diff --git a/composer.json b/composer.json index e554510..6f5f66f 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "MIT" ], "require": { + "php": ">=7.4", "magento/framework": "*", "magento/module-catalog": "^104.0.0", "deeplcom/deepl-php": "^1",