From 97bd3453e191ab860aa66a842bf3b7d466637557 Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 25 Jun 2026 17:41:03 +0300 Subject: [PATCH 01/12] 15597-move-store-switcher-for-cms-page-and-block-to-basic --- view/adminhtml/layout/cms_block_edit.xml | 19 ++++ view/adminhtml/layout/cms_page_edit.xml | 19 ++++ .../templates/cms/store-switcher-promo.phtml | 90 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 view/adminhtml/layout/cms_block_edit.xml create mode 100644 view/adminhtml/layout/cms_page_edit.xml create mode 100644 view/adminhtml/templates/cms/store-switcher-promo.phtml diff --git a/view/adminhtml/layout/cms_block_edit.xml b/view/adminhtml/layout/cms_block_edit.xml new file mode 100644 index 0000000..349c455 --- /dev/null +++ b/view/adminhtml/layout/cms_block_edit.xml @@ -0,0 +1,19 @@ + + + + + + + + 1 + + + + + diff --git a/view/adminhtml/layout/cms_page_edit.xml b/view/adminhtml/layout/cms_page_edit.xml new file mode 100644 index 0000000..349c455 --- /dev/null +++ b/view/adminhtml/layout/cms_page_edit.xml @@ -0,0 +1,19 @@ + + + + + + + + 1 + + + + + diff --git a/view/adminhtml/templates/cms/store-switcher-promo.phtml b/view/adminhtml/templates/cms/store-switcher-promo.phtml new file mode 100644 index 0000000..b3eeab8 --- /dev/null +++ b/view/adminhtml/templates/cms/store-switcher-promo.phtml @@ -0,0 +1,90 @@ + +getWebsites()): ?> +
+ escapeHtml(__('Scope:')) ?> + + getHintHtml() ?> +
+ + + + renderTag('script', [], $script, false) ?> + + From 39a614216783172490226fca72ad2f254645dd8e Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 25 Jun 2026 17:48:09 +0300 Subject: [PATCH 02/12] 15597 update notice --- Controller/Adminhtml/Mass/Translate/Locked.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Controller/Adminhtml/Mass/Translate/Locked.php b/Controller/Adminhtml/Mass/Translate/Locked.php index 3198948..3e934d0 100644 --- a/Controller/Adminhtml/Mass/Translate/Locked.php +++ b/Controller/Adminhtml/Mass/Translate/Locked.php @@ -22,7 +22,8 @@ public function execute() $this->messageManager->addNotice( __( - 'This option is available in Magefan Translation Extra plan only. Please upgrade at ' .$url. '.', + 'This option is available in Magefan Translation Extra plan only. + Please upgrade plan to use this feature.', $url . $params ) ); From a9a65a24dbc0d01646757051f952862b6a301c4a Mon Sep 17 00:00:00 2001 From: "Magefan - Magento 2 Extensions for Performance, SEO & Growth" Date: Fri, 3 Jul 2026 18:16:24 +0300 Subject: [PATCH 03/12] Change button attribute in mfPopup call --- view/adminhtml/templates/form/js.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view/adminhtml/templates/form/js.phtml b/view/adminhtml/templates/form/js.phtml index c3d3e45..9462794 100644 --- a/view/adminhtml/templates/form/js.phtml +++ b/view/adminhtml/templates/form/js.phtml @@ -40,7 +40,7 @@ if ($block->getRequest()->getParam('store')) { var a = \$('" . $escaper->escapeHtml(__('Translate')) . "').click(function(e){ e.preventDefault(); require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function(mfPopup) { - mfPopup('Plus or Extra', 'auto-translate', 'button'); + mfPopup('Plus or Extra', 'auto-translate', 'button-attr'); }); return false; }); From fe086914644f1f923de21d791f2ea3600cf6e771 Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 20 Aug 2026 11:58:13 +0300 Subject: [PATCH 04/12] 13855-review-translation --- Model/Config/Source/ScheduleSource.php | 2 ++ Model/EntityManager.php | 24 +++++++++++++++++++++++- Model/TranslatableData.php | 25 +++++++++++++++++++++++++ Setup/Recurring.php | 6 ++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Model/Config/Source/ScheduleSource.php b/Model/Config/Source/ScheduleSource.php index b651fa1..7ac267e 100644 --- a/Model/Config/Source/ScheduleSource.php +++ b/Model/Config/Source/ScheduleSource.php @@ -39,6 +39,8 @@ public function toOptionArray() ['value' => EntityManager::SECONDBLOG_CATEGORY_ID, 'label' => __('Magefan Second Blog Category')], ['value' => EntityManager::SECONDBLOG_AUTHOR_ID, 'label' => __('Magefan Second Blog Author')], + ['value' => EntityManager::REVIEW_ID, 'label' => __('Product Review')], + ['value' => EntityManager::TRANSLATE_ID, 'label' => __('Phrases from Search And Translate')] ]; diff --git a/Model/EntityManager.php b/Model/EntityManager.php index f770a49..f136862 100644 --- a/Model/EntityManager.php +++ b/Model/EntityManager.php @@ -28,6 +28,8 @@ class EntityManager public const SECONDBLOG_CATEGORY_ID = 13; public const SECONDBLOG_AUTHOR_ID = 14; + public const REVIEW_ID = 15; + public const TYPE_CATEGORY = 'category'; public const TYPE_PRODUCT = 'product'; public const TYPE_PAGE = 'page'; @@ -45,6 +47,8 @@ class EntityManager public const TYPE_SECONDBLOG_CATEGORY = 'secondblog_category'; public const TYPE_SECONDBLOG_AUTHOR = 'secondblog_author'; + public const TYPE_REVIEW = 'review'; + public const TYPES = [ self::TYPE_PRODUCT => self::PRODUCT_ID, self::TYPE_CATEGORY => self::CATEGORY_ID, @@ -61,7 +65,9 @@ class EntityManager self::TYPE_SECONDBLOG_POST => self::SECONDBLOG_POST_ID, self::TYPE_SECONDBLOG_TAG => self::SECONDBLOG_TAG_ID, self::TYPE_SECONDBLOG_CATEGORY => self::SECONDBLOG_CATEGORY_ID, - self::TYPE_SECONDBLOG_AUTHOR => self::SECONDBLOG_AUTHOR_ID + self::TYPE_SECONDBLOG_AUTHOR => self::SECONDBLOG_AUTHOR_ID, + + self::TYPE_REVIEW => self::REVIEW_ID ]; /** @@ -78,6 +84,22 @@ public function __construct( $this->entityPool = $entityPool; } + /** + * Whether an adapter is registered in the pool for this entity type. + * + * A type can be listed in self::TYPES while its adapter is not available, + * because adapters are contributed by Magefan_TranslationExtra, which is an + * optional module. Callers iterating over self::TYPES should check this + * before calling getEntityAdapter(). + * + * @param int $entityTypeId + * @return bool + */ + public function hasEntityAdapter(int $entityTypeId): bool + { + return isset($this->entityPool[$entityTypeId]); + } + /** * @param int $entityTypeId * @return mixed diff --git a/Model/TranslatableData.php b/Model/TranslatableData.php index 9b21b73..c6d6361 100644 --- a/Model/TranslatableData.php +++ b/Model/TranslatableData.php @@ -136,6 +136,10 @@ public function getList(?string $type = null): array $data[EntityManager::TYPE_SECONDBLOG_AUTHOR] = $this->getBlogAuthorFields(); break; + case EntityManager::TYPE_REVIEW: + $data[EntityManager::TYPE_REVIEW] = $this->getReviewFields(); + break; + default: // If no type or unrecognized type is passed, return all $data[EntityManager::TYPE_CATEGORY] = $this->getCatalogAttributes($this->categoryAttributeRepository); @@ -157,6 +161,10 @@ public function getList(?string $type = null): array $data[EntityManager::TYPE_SECONDBLOG_AUTHOR] = $this->getBlogAuthorFields(); } + if ($this->moduleManager->isEnabled('Magento_Review')) { + $data[EntityManager::TYPE_REVIEW] = $this->getReviewFields(); + } + break; } @@ -351,6 +359,23 @@ private function getBlogCategoryFields(): array ]; } + /** + * Product review fields. + * + * Nickname is intentionally not listed - it holds the reviewer's name, + * which must not be machine translated. It is still stored per store view + * so it can be corrected manually. + * + * @return array[] + */ + private function getReviewFields(): array + { + return [ + ['label' => __('Summary of Review'), 'code' => 'title', 'type' => 'text'], + ['label' => __('Review'), 'code' => 'detail', 'type' => 'text'] + ]; + } + /** * @return array[] */ diff --git a/Setup/Recurring.php b/Setup/Recurring.php index cceaf60..3d0b4fa 100644 --- a/Setup/Recurring.php +++ b/Setup/Recurring.php @@ -30,6 +30,12 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con 'magefan_second_blog_category', 'magefan_second_blog_tag', 'magefan_second_blog_author', + // "review" and not "review_detail": ResourceModel\Review::_afterSave() + // writes only title/detail/nickname to review_detail, while the main table + // is saved through AbstractDb::_prepareDataForSave() and picks the column up. + // Handled here rather than in db_schema.xml because Magento_Review can be + // disabled, and declaring a column on an undeclared table breaks upgrade. + 'review', ]; $localizationTables = [ From 85e3028b586852fa01520fec4fdb367c79d30f85 Mon Sep 17 00:00:00 2001 From: bb Date: Fri, 21 Aug 2026 11:28:58 +0300 Subject: [PATCH 05/12] 13855 add review entity support to Auto Translate mass action button Plugs the review grid's Massaction\Extended block (the legacy Widget\Grid variant used by review_product_index/pending, distinct from the UI-component Massaction the other grids use) and adds the Auto Translate button to Magento\Review\Block\Adminhtml\Edit, mirroring the ProductAttribute Edit plugin. Co-Authored-By: Claude Sonnet 5 --- .../Backend/Block/Widget/Grid/Massaction.php | 2 + .../Magento/Review/Block/Adminhtml/Edit.php | 68 +++++++++++++++++++ etc/adminhtml/di.xml | 10 +++ 3 files changed, 80 insertions(+) create mode 100644 Plugin/Backend/Magento/Review/Block/Adminhtml/Edit.php diff --git a/Plugin/Backend/Magento/Backend/Block/Widget/Grid/Massaction.php b/Plugin/Backend/Magento/Backend/Block/Widget/Grid/Massaction.php index ef168e9..6e2ca3a 100644 --- a/Plugin/Backend/Magento/Backend/Block/Widget/Grid/Massaction.php +++ b/Plugin/Backend/Magento/Backend/Block/Widget/Grid/Massaction.php @@ -24,6 +24,8 @@ class Massaction 'secondblog_category_index', 'secondblog_tag_index', 'secondblogauthor_author_index', + 'review_product_index', + 'review_product_pending', ]; /** diff --git a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit.php b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit.php new file mode 100644 index 0000000..d783e66 --- /dev/null +++ b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit.php @@ -0,0 +1,68 @@ +config = $config; + } + + /** + * @param \Magento\Review\Block\Adminhtml\Edit $subject + * @param \Magento\Framework\View\Layout $layout + * @return array + */ + public function beforeSetLayout( + \Magento\Review\Block\Adminhtml\Edit $subject, + $layout + ) { + if ($this->config->isEnabled() && $subject->getRequest()->getParam('id')) { + $subject->addButton( + 'mftranslation_locked_button', + [ + 'label' => __('Auto Translate'), + 'on_click' => $this->getOnClick(), + 'class' => 'mf_auto_translate', + ], + 10 + ); + } + + return [$layout]; + } + + /** + * @return string + */ + private function getOnClick(): string + { + return "require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function(mfPopup){" + . "mfPopup('Extra', 'auto-translate', 'button');" + . "});"; + } +} diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index 5c22063..f0f6898 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -13,4 +13,14 @@ + + + + + + + + + From dfaf79eab7d5fd14d04610f0ceba2d540cce928f Mon Sep 17 00:00:00 2001 From: bb Date: Fri, 21 Aug 2026 11:37:31 +0300 Subject: [PATCH 06/12] 13855 give the Extended-grid plugin its own name instead of reusing the base one Same-name reuse only needs to hold across modules on the same intercepted type (that's what lets TranslationExtra's plugin override this one) - it doesn't need to match this module's own entry for the base Massaction type. Reusing it there was just confusing on review, not a functional collision (plugin config is keyed by type+name), but distinct names read better. Co-Authored-By: Claude Sonnet 5 --- etc/adminhtml/di.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index f0f6898..76a2f1b 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -17,7 +17,7 @@ - + From 6cdd6b668152f169274ad2d21e21fa7b8d854366 Mon Sep 17 00:00:00 2001 From: bb Date: Fri, 21 Aug 2026 12:01:39 +0300 Subject: [PATCH 07/12] 13855 remove hasEntityAdapter() - dead in its only caller TranslationExtra's own entityPool declares every id in EntityManager::TYPES unconditionally, so isset() on it is never false from within TranslationExtra itself - the isset() check in AddEntityItems::execute() this was added for could never actually skip anything. The real "adapter unavailable" case (config declares an adapter class that fails to instantiate, e.g. an optional module like Magefan_SecondBlog being disabled) was already handled by getEntityAdapter() returning null, which addEntityIdsToSchedule() already checks. Co-Authored-By: Claude Sonnet 5 --- Model/EntityManager.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Model/EntityManager.php b/Model/EntityManager.php index f136862..aa83f1f 100644 --- a/Model/EntityManager.php +++ b/Model/EntityManager.php @@ -84,22 +84,6 @@ public function __construct( $this->entityPool = $entityPool; } - /** - * Whether an adapter is registered in the pool for this entity type. - * - * A type can be listed in self::TYPES while its adapter is not available, - * because adapters are contributed by Magefan_TranslationExtra, which is an - * optional module. Callers iterating over self::TYPES should check this - * before calling getEntityAdapter(). - * - * @param int $entityTypeId - * @return bool - */ - public function hasEntityAdapter(int $entityTypeId): bool - { - return isset($this->entityPool[$entityTypeId]); - } - /** * @param int $entityTypeId * @return mixed From 0b093f3a56be2693947d0ff4b3b1a5cebe9bab25 Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 3 Sep 2026 11:09:08 +0300 Subject: [PATCH 08/12] 13855 basic-tier teaser for the review Exclude From Auto Translation toggle --- .../Block/Adminhtml/Edit/FormPlugin.php | 143 ++++++++++++++++++ etc/adminhtml/di.xml | 6 + 2 files changed, 149 insertions(+) create mode 100644 Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php diff --git a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php new file mode 100644 index 0000000..bc7e3d7 --- /dev/null +++ b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php @@ -0,0 +1,143 @@ +config = $config; + $this->mfSecureRenderer = $mfSecureRenderer; + } + + /** + * @param \Magento\Review\Block\Adminhtml\Edit\Form $subject + * @param \Magento\Framework\Data\Form|null $form + * @return \Magento\Framework\Data\Form|null + */ + public function afterGetForm($subject, $form) + { + if (!$form || !$this->config->isEnabled()) { + return $form; + } + + // getForm() runs more than once per page render - without this guard the + // fieldset and its click handler script would be emitted multiple times. + if (!$form->getElement('review_details') || $form->getElement('mf_auto_translation')) { + return $form; + } + + $fieldset = $form->addFieldset( + 'mf_auto_translation', + ['legend' => __('Auto Translation (Extra)')], + 'review_details' + ); + + $fieldset->addField( + 'mf_exclude_auto_translation', + 'note', + [ + 'label' => __('Exclude From Auto Translation'), + 'text' => $this->getLockedContentHtml(), + ] + ); + + return $form; + } + + /** + * The toggle and its "Use Default Value" checkbox are both left disconnected from + * any submitted field name - there is nothing to persist on this tier - and the + * transparent overlay drawn on top intercepts every click on either of them. + * + * @return string + */ + private function getLockedContentHtml() + { + return '
' + . '
' + . '
' + . '' + . '' + . '
' + . '' + . '
' + . '
' + . '
' + . $this->getClickHandlerHtml(); + } + + /** + * @param \Magento\Framework\Phrase $phrase + * @return string + */ + private function escapeAttr($phrase) + { + return htmlspecialchars((string)$phrase, ENT_QUOTES); + } + + /** + * Delegated on document rather than an inline onclick attribute, same as + * Magefan\TranslationPlus\Plugin\Magento\Review\Block\Adminhtml\Edit\FormPlugin::getToggleScriptHtml() - + * inline handlers are blocked under a strict CSP. + * + * @return string + */ + private function getClickHandlerHtml() + { + $script = "document.addEventListener('click', function (event) {" + . "if (!event.target.className || event.target.className.indexOf('mf-auto-translation-lock-overlay') === -1) {" + . " return; }" + . "require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function (mfPopup) {" + . " mfPopup('Extra', 'review-edit', 'fieldset');" + . "});" + . "});"; + + return $this->mfSecureRenderer->renderTag('script', [], $script, false); + } +} diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index 76a2f1b..df85055 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -23,4 +23,10 @@ + + + + + From 4e6e8682a16aeece286d172533387d1ffa181778 Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 3 Sep 2026 12:00:54 +0300 Subject: [PATCH 09/12] 13855 add locked store switcher to the review edit page --- view/adminhtml/layout/review_product_edit.xml | 19 ++++ .../templates/cms/store-switcher-promo.phtml | 90 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 view/adminhtml/layout/review_product_edit.xml create mode 100644 view/adminhtml/templates/cms/store-switcher-promo.phtml diff --git a/view/adminhtml/layout/review_product_edit.xml b/view/adminhtml/layout/review_product_edit.xml new file mode 100644 index 0000000..349c455 --- /dev/null +++ b/view/adminhtml/layout/review_product_edit.xml @@ -0,0 +1,19 @@ + + + + + + + + 1 + + + + + diff --git a/view/adminhtml/templates/cms/store-switcher-promo.phtml b/view/adminhtml/templates/cms/store-switcher-promo.phtml new file mode 100644 index 0000000..b3eeab8 --- /dev/null +++ b/view/adminhtml/templates/cms/store-switcher-promo.phtml @@ -0,0 +1,90 @@ + +getWebsites()): ?> +
+ escapeHtml(__('Scope:')) ?> + + getHintHtml() ?> +
+ + + + renderTag('script', [], $script, false) ?> + + From e5dbe2b5f4886df3de65d63ee0a44e4c98c4efaf Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 3 Sep 2026 12:40:08 +0300 Subject: [PATCH 10/12] 13855 extract shared locked-toggle markup into LockedToggleFieldHtml, always label the review fieldset Extra --- Model/LockedToggleFieldHtml.php | 99 ++++++++++++++++ .../Block/Adminhtml/Edit/FormPlugin.php | 107 ++++++------------ 2 files changed, 133 insertions(+), 73 deletions(-) create mode 100644 Model/LockedToggleFieldHtml.php diff --git a/Model/LockedToggleFieldHtml.php b/Model/LockedToggleFieldHtml.php new file mode 100644 index 0000000..16da5a2 --- /dev/null +++ b/Model/LockedToggleFieldHtml.php @@ -0,0 +1,99 @@ +mfSecureRenderer = $mfSecureRenderer; + } + + /** + * @param string $plan Plan name shown in the popup, e.g. "Extra" or "Plus or Extra" + * @param string $utmMedium + * @param string $utmCampaign + * @return string + */ + public function render(string $plan, string $utmMedium, string $utmCampaign): string + { + $yes = $this->escapeAttr(__('Yes')); + $no = $this->escapeAttr(__('No')); + + return '
' + . '
' + . '
' + . '' + . '' + . '
' + . '' + . '
' + . '
' + . '
' + . $this->getClickHandlerHtml($plan, $utmMedium, $utmCampaign); + } + + /** + * @param \Magento\Framework\Phrase $phrase + * @return string + */ + private function escapeAttr($phrase): string + { + return htmlspecialchars((string)$phrase, ENT_QUOTES); + } + + /** + * Delegated on document rather than an inline onclick attribute - inline handlers + * are blocked under a strict CSP. + * + * @param string $plan + * @param string $utmMedium + * @param string $utmCampaign + * @return string + */ + private function getClickHandlerHtml(string $plan, string $utmMedium, string $utmCampaign): string + { + $script = "document.addEventListener('click', function (event) {" + . "if (!event.target.className || event.target.className.indexOf('mf-locked-toggle-overlay') === -1) {" + . " return; }" + . "require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function (mfPopup) {" + . " mfPopup(" . json_encode($plan) . ", " . json_encode($utmMedium) . ", " . json_encode($utmCampaign) . ");" + . "});" + . "});"; + + return $this->mfSecureRenderer->renderTag('script', [], $script, false); + } +} diff --git a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php index bc7e3d7..01134a8 100644 --- a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php +++ b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php @@ -7,16 +7,20 @@ namespace Magefan\Translation\Plugin\Backend\Magento\Review\Block\Adminhtml\Edit; -use Magefan\Community\Api\SecureHtmlRendererInterface; use Magefan\Translation\Model\Config; +use Magefan\Translation\Model\LockedToggleFieldHtml; /** * Basic-tier teaser for the "Exclude From Auto Translation" review field. Registered * under the same plugin name ("magefan_translation_plus_review_edit_form_use_default") * as Magefan\TranslationPlus\Plugin\Magento\Review\Block\Adminhtml\Edit\FormPlugin on * the same target class - TranslationPlus loads after Translation, so its plugin - * definition overrides this one entirely (the real, working fieldset) when Plus is - * installed. + * definition overrides this one entirely (the real, working fieldset) when Plus and + * Extra are both installed. + * + * That subclass also falls back to addLockedExcludeAutoTranslationField() below + * whenever Extra specifically is missing (Plus alone doesn't act on this flag - see + * that class), which is why the shared bits here are protected rather than private. * * Mirrors Magefan\Translation\Plugin\Backend\Magento\Review\Block\Adminhtml\Edit (the * "Auto Translate" button teaser) and the locked "Auto Translation (Extra)" fieldset @@ -29,23 +33,23 @@ class FormPlugin /** * @var Config */ - private $config; + protected $config; /** - * @var SecureHtmlRendererInterface + * @var LockedToggleFieldHtml */ - private $mfSecureRenderer; + protected $lockedToggleFieldHtml; /** * @param Config $config - * @param SecureHtmlRendererInterface $mfSecureRenderer + * @param LockedToggleFieldHtml $lockedToggleFieldHtml */ public function __construct( Config $config, - SecureHtmlRendererInterface $mfSecureRenderer + LockedToggleFieldHtml $lockedToggleFieldHtml ) { $this->config = $config; - $this->mfSecureRenderer = $mfSecureRenderer; + $this->lockedToggleFieldHtml = $lockedToggleFieldHtml; } /** @@ -59,10 +63,27 @@ public function afterGetForm($subject, $form) return $form; } - // getForm() runs more than once per page render - without this guard the - // fieldset and its click handler script would be emitted multiple times. + $this->addLockedExcludeAutoTranslationField($form); + + return $form; + } + + /** + * Adds the locked "Exclude From Auto Translation" fieldset: the same toggle a + * working install would show, disabled, with a click-anywhere overlay that opens + * the upgrade popup instead of doing anything. + * + * Also called by Magefan\TranslationPlus\...\FormPlugin, which extends this class, + * for its own Extra-less fallback - guarded the same way afterGetForm() above + * guards it, so either caller can call it unconditionally. + * + * @param \Magento\Framework\Data\Form $form + * @return void + */ + protected function addLockedExcludeAutoTranslationField($form) + { if (!$form->getElement('review_details') || $form->getElement('mf_auto_translation')) { - return $form; + return; } $fieldset = $form->addFieldset( @@ -76,68 +97,8 @@ public function afterGetForm($subject, $form) 'note', [ 'label' => __('Exclude From Auto Translation'), - 'text' => $this->getLockedContentHtml(), + 'text' => $this->lockedToggleFieldHtml->render('Extra', 'review-edit', 'fieldset'), ] ); - - return $form; - } - - /** - * The toggle and its "Use Default Value" checkbox are both left disconnected from - * any submitted field name - there is nothing to persist on this tier - and the - * transparent overlay drawn on top intercepts every click on either of them. - * - * @return string - */ - private function getLockedContentHtml() - { - return '
' - . '
' - . '
' - . '' - . '' - . '
' - . '' - . '
' - . '
' - . '
' - . $this->getClickHandlerHtml(); - } - - /** - * @param \Magento\Framework\Phrase $phrase - * @return string - */ - private function escapeAttr($phrase) - { - return htmlspecialchars((string)$phrase, ENT_QUOTES); - } - - /** - * Delegated on document rather than an inline onclick attribute, same as - * Magefan\TranslationPlus\Plugin\Magento\Review\Block\Adminhtml\Edit\FormPlugin::getToggleScriptHtml() - - * inline handlers are blocked under a strict CSP. - * - * @return string - */ - private function getClickHandlerHtml() - { - $script = "document.addEventListener('click', function (event) {" - . "if (!event.target.className || event.target.className.indexOf('mf-auto-translation-lock-overlay') === -1) {" - . " return; }" - . "require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function (mfPopup) {" - . " mfPopup('Extra', 'review-edit', 'fieldset');" - . "});" - . "});"; - - return $this->mfSecureRenderer->renderTag('script', [], $script, false); } } From 6739d32bab4fa5f7ea878c687f993f3f6dbb3a1a Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 3 Sep 2026 12:45:04 +0300 Subject: [PATCH 11/12] 13855 fold locked-toggle markup into FormPlugin itself, drop the separate model now that TranslationPlus extends this class --- Model/LockedToggleFieldHtml.php | 99 ------------------- .../Block/Adminhtml/Edit/FormPlugin.php | 83 ++++++++++++++-- 2 files changed, 76 insertions(+), 106 deletions(-) delete mode 100644 Model/LockedToggleFieldHtml.php diff --git a/Model/LockedToggleFieldHtml.php b/Model/LockedToggleFieldHtml.php deleted file mode 100644 index 16da5a2..0000000 --- a/Model/LockedToggleFieldHtml.php +++ /dev/null @@ -1,99 +0,0 @@ -mfSecureRenderer = $mfSecureRenderer; - } - - /** - * @param string $plan Plan name shown in the popup, e.g. "Extra" or "Plus or Extra" - * @param string $utmMedium - * @param string $utmCampaign - * @return string - */ - public function render(string $plan, string $utmMedium, string $utmCampaign): string - { - $yes = $this->escapeAttr(__('Yes')); - $no = $this->escapeAttr(__('No')); - - return '
' - . '
' - . '
' - . '' - . '' - . '
' - . '' - . '
' - . '
' - . '
' - . $this->getClickHandlerHtml($plan, $utmMedium, $utmCampaign); - } - - /** - * @param \Magento\Framework\Phrase $phrase - * @return string - */ - private function escapeAttr($phrase): string - { - return htmlspecialchars((string)$phrase, ENT_QUOTES); - } - - /** - * Delegated on document rather than an inline onclick attribute - inline handlers - * are blocked under a strict CSP. - * - * @param string $plan - * @param string $utmMedium - * @param string $utmCampaign - * @return string - */ - private function getClickHandlerHtml(string $plan, string $utmMedium, string $utmCampaign): string - { - $script = "document.addEventListener('click', function (event) {" - . "if (!event.target.className || event.target.className.indexOf('mf-locked-toggle-overlay') === -1) {" - . " return; }" - . "require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function (mfPopup) {" - . " mfPopup(" . json_encode($plan) . ", " . json_encode($utmMedium) . ", " . json_encode($utmCampaign) . ");" - . "});" - . "});"; - - return $this->mfSecureRenderer->renderTag('script', [], $script, false); - } -} diff --git a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php index 01134a8..2e31719 100644 --- a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php +++ b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php @@ -7,8 +7,8 @@ namespace Magefan\Translation\Plugin\Backend\Magento\Review\Block\Adminhtml\Edit; +use Magefan\Community\Api\SecureHtmlRendererInterface; use Magefan\Translation\Model\Config; -use Magefan\Translation\Model\LockedToggleFieldHtml; /** * Basic-tier teaser for the "Exclude From Auto Translation" review field. Registered @@ -36,20 +36,20 @@ class FormPlugin protected $config; /** - * @var LockedToggleFieldHtml + * @var SecureHtmlRendererInterface */ - protected $lockedToggleFieldHtml; + protected $mfSecureRenderer; /** * @param Config $config - * @param LockedToggleFieldHtml $lockedToggleFieldHtml + * @param SecureHtmlRendererInterface $mfSecureRenderer */ public function __construct( Config $config, - LockedToggleFieldHtml $lockedToggleFieldHtml + SecureHtmlRendererInterface $mfSecureRenderer ) { $this->config = $config; - $this->lockedToggleFieldHtml = $lockedToggleFieldHtml; + $this->mfSecureRenderer = $mfSecureRenderer; } /** @@ -97,8 +97,77 @@ protected function addLockedExcludeAutoTranslationField($form) 'note', [ 'label' => __('Exclude From Auto Translation'), - 'text' => $this->lockedToggleFieldHtml->render('Extra', 'review-edit', 'fieldset'), + 'text' => $this->getLockedToggleHtml('Extra', 'review-edit', 'fieldset'), ] ); } + + /** + * Renders a locked stand-in for a toggle-switch field: the same toggle + + * "Use Default Value" markup the real, working field would use, both inert, with a + * transparent overlay on top that shows the upgrade popup on any click. + * + * Protected (not the fieldset-adding method's own private helper) so + * Magefan\TranslationPlus\...\FormPlugin could call it directly too, if it ever + * needs a locked toggle outside of addLockedExcludeAutoTranslationField()'s shape. + * + * @param string $plan Plan name shown in the popup, e.g. "Extra" or "Plus or Extra" + * @param string $utmMedium + * @param string $utmCampaign + * @return string + */ + protected function getLockedToggleHtml($plan, $utmMedium, $utmCampaign) + { + $yes = $this->escapeAttr(__('Yes')); + $no = $this->escapeAttr(__('No')); + + return '
' + . '
' + . '
' + . '' + . '' + . '
' + . '' + . '
' + . '
' + . '
' + . $this->getLockedClickHandlerHtml($plan, $utmMedium, $utmCampaign); + } + + /** + * @param \Magento\Framework\Phrase $phrase + * @return string + */ + private function escapeAttr($phrase) + { + return htmlspecialchars((string)$phrase, ENT_QUOTES); + } + + /** + * Delegated on document rather than an inline onclick attribute - inline handlers + * are blocked under a strict CSP. + * + * @param string $plan + * @param string $utmMedium + * @param string $utmCampaign + * @return string + */ + private function getLockedClickHandlerHtml($plan, $utmMedium, $utmCampaign) + { + $script = "document.addEventListener('click', function (event) {" + . "if (!event.target.className || event.target.className.indexOf('mf-locked-toggle-overlay') === -1) {" + . " return; }" + . "require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function (mfPopup) {" + . " mfPopup(" . json_encode($plan) . ", " . json_encode($utmMedium) . ", " . json_encode($utmCampaign) . ");" + . "});" + . "});"; + + return $this->mfSecureRenderer->renderTag('script', [], $script, false); + } } From c8080d97199e8ba8e9559b41221cace6f4935bfd Mon Sep 17 00:00:00 2001 From: bb Date: Thu, 3 Sep 2026 12:52:14 +0300 Subject: [PATCH 12/12] 13855 fix SVG className bug in the locked overlay click handler; extract addAutoTranslationFieldset() --- .../Block/Adminhtml/Edit/FormPlugin.php | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php index 2e31719..42918af 100644 --- a/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php +++ b/Plugin/Backend/Magento/Review/Block/Adminhtml/Edit/FormPlugin.php @@ -18,9 +18,10 @@ * definition overrides this one entirely (the real, working fieldset) when Plus and * Extra are both installed. * - * That subclass also falls back to addLockedExcludeAutoTranslationField() below - * whenever Extra specifically is missing (Plus alone doesn't act on this flag - see - * that class), which is why the shared bits here are protected rather than private. + * That subclass also reuses addAutoTranslationFieldset() and + * addLockedExcludeAutoTranslationField() below for its own Extra-less fallback (Plus + * alone doesn't act on this flag - see that class), which is why they're protected + * rather than private. * * Mirrors Magefan\Translation\Plugin\Backend\Magento\Review\Block\Adminhtml\Edit (the * "Auto Translate" button teaser) and the locked "Auto Translation (Extra)" fieldset @@ -69,35 +70,52 @@ public function afterGetForm($subject, $form) } /** - * Adds the locked "Exclude From Auto Translation" fieldset: the same toggle a - * working install would show, disabled, with a click-anywhere overlay that opens - * the upgrade popup instead of doing anything. + * Adds the "Auto Translation (Extra)" fieldset right after review_details, or + * returns null without touching the form if it's already there (getForm() runs + * more than once per page render) or review_details itself isn't present to + * insert after. * - * Also called by Magefan\TranslationPlus\...\FormPlugin, which extends this class, - * for its own Extra-less fallback - guarded the same way afterGetForm() above - * guards it, so either caller can call it unconditionally. + * Shared between this class's own locked field below and + * Magefan\TranslationPlus\...\FormPlugin's real one, so the guard only lives once. * * @param \Magento\Framework\Data\Form $form - * @return void + * @return \Magento\Framework\Data\Form\Element\Fieldset|null */ - protected function addLockedExcludeAutoTranslationField($form) + protected function addAutoTranslationFieldset($form) { if (!$form->getElement('review_details') || $form->getElement('mf_auto_translation')) { - return; + return null; } - $fieldset = $form->addFieldset( + return $form->addFieldset( 'mf_auto_translation', ['legend' => __('Auto Translation (Extra)')], 'review_details' ); + } + + /** + * Adds the locked "Exclude From Auto Translation" field: the same toggle a working + * install would show, disabled, with a click-anywhere overlay that opens the + * upgrade popup instead of doing anything. + * + * @param \Magento\Framework\Data\Form $form + * @return void + */ + protected function addLockedExcludeAutoTranslationField($form) + { + $fieldset = $this->addAutoTranslationFieldset($form); + + if (!$fieldset) { + return; + } $fieldset->addField( 'mf_exclude_auto_translation', 'note', [ 'label' => __('Exclude From Auto Translation'), - 'text' => $this->getLockedToggleHtml('Extra', 'review-edit', 'fieldset'), + 'text' => $this->getLockedToggleHtml(), ] ); } @@ -107,16 +125,9 @@ protected function addLockedExcludeAutoTranslationField($form) * "Use Default Value" markup the real, working field would use, both inert, with a * transparent overlay on top that shows the upgrade popup on any click. * - * Protected (not the fieldset-adding method's own private helper) so - * Magefan\TranslationPlus\...\FormPlugin could call it directly too, if it ever - * needs a locked toggle outside of addLockedExcludeAutoTranslationField()'s shape. - * - * @param string $plan Plan name shown in the popup, e.g. "Extra" or "Plus or Extra" - * @param string $utmMedium - * @param string $utmCampaign * @return string */ - protected function getLockedToggleHtml($plan, $utmMedium, $utmCampaign) + private function getLockedToggleHtml() { $yes = $this->escapeAttr(__('Yes')); $no = $this->escapeAttr(__('No')); @@ -137,7 +148,7 @@ protected function getLockedToggleHtml($plan, $utmMedium, $utmCampaign) . '
' . '' - . $this->getLockedClickHandlerHtml($plan, $utmMedium, $utmCampaign); + . $this->getLockedClickHandlerHtml(); } /** @@ -151,20 +162,19 @@ private function escapeAttr($phrase) /** * Delegated on document rather than an inline onclick attribute - inline handlers - * are blocked under a strict CSP. + * are blocked under a strict CSP. classList.contains() rather than a className + * substring match: className is an SVGAnimatedString (no .indexOf) on an SVG + * target, e.g. any of the admin icon sprites elsewhere on the page. * - * @param string $plan - * @param string $utmMedium - * @param string $utmCampaign * @return string */ - private function getLockedClickHandlerHtml($plan, $utmMedium, $utmCampaign) + private function getLockedClickHandlerHtml() { $script = "document.addEventListener('click', function (event) {" - . "if (!event.target.className || event.target.className.indexOf('mf-locked-toggle-overlay') === -1) {" + . "if (!event.target.classList || !event.target.classList.contains('mf-locked-toggle-overlay')) {" . " return; }" . "require(['Magefan_Translation/js/mf-upgrade-plan-popup'], function (mfPopup) {" - . " mfPopup(" . json_encode($plan) . ", " . json_encode($utmMedium) . ", " . json_encode($utmCampaign) . ");" + . " mfPopup('Extra', 'review-edit', 'fieldset');" . "});" . "});";