Repository: odoo-dev/odoo
Pull request #4598: Do not start all plugins in translate mode
Author: login: loco-odoo
Created: 2025-05-07T12:40:24Z | Merged: 2025-05-14T13:46:14Z



Diff:
diff --git a/addons/html_builder/static/src/builder.js b/addons/html_builder/static/src/builder.js
index a3f68173a2734b..ea66aa9fd09dcd 100644
--- a/addons/html_builder/static/src/builder.js
+++ b/addons/html_builder/static/src/builder.js
@@ -25,6 +25,7 @@ import { CustomizeTab } from "./sidebar/customize_tab";
 import { ThemeTab } from "@html_builder/website_builder/plugins/theme/theme_tab";
 import { CORE_PLUGINS } from "./core/core_plugins";
 import { EDITOR_COLOR_CSS_VARIABLES, getCSSVariableValue } from "./utils/utils_css";
+import { withSequence } from "@html_editor/utils/resource";
 
 export class Builder extends Component {
     static template = "html_builder.Builder";
@@ -76,7 +77,8 @@ export class Builder extends Component {
                 "BannerPlugin",
             ]
         );
-        const Plugins = [...mainPlugins, ...CORE_PLUGINS, ...(this.props.Plugins || [])];
+        const corePlugins = this.props.isTranslation ? [] : CORE_PLUGINS;
+        const Plugins = [...mainPlugins, ...corePlugins, ...(this.props.Plugins || [])];
         // TODO: maybe do a different config for the translate mode and the
         // "regular" mode.
         this.editor = new Editor(
@@ -103,9 +105,9 @@ export class Builder extends Component {
                     trigger_dom_updated: () => {
                         editorBus.trigger("DOM_UPDATED");
                     },
-                    on_mobile_preview_clicked: () => {
+                    on_mobile_preview_clicked: withSequence(20, () => {
                         editorBus.trigger("DOM_UPDATED");
-                    },
+                    }),
                     change_current_options_containers_listeners: (currentOptionsContainers) => {
                         this.state.currentOptionsContainers = currentOptionsContainers;
                         if (!currentOptionsContainers.length) {
diff --git a/addons/html_builder/static/src/builder.xml b/addons/html_builder/static/src/builder.xml
index c5479b3c9a20b0..7a297c6a654f31 100644
--- a/addons/html_builder/static/src/builder.xml
+++ b/addons/html_builder/static/src/builder.xml
@@ -34,7 +34,8 @@
                 <BlockTab />
             </t>
             <t t-if="state.activeTab === 'customize'">
-                <CustomizeTab currentOptionsContainers="state.currentOptionsContainers" snippetModel="snippetModel"/>
+                <t t-if="props.isTranslation" t-call="html_builder.CustomizeTranslationTab"/>
+                <CustomizeTab t-else="" currentOptionsContainers="state.currentOptionsContainers" snippetModel="snippetModel"/>
             </t>
             <t t-if="state.activeTab === 'theme'">
                 <ThemeTab/>
diff --git a/addons/html_builder/static/src/core/builder_options_plugin.js b/addons/html_builder/static/src/core/builder_options_plugin.js
index 612c938d955e91..786ca18566b066 100644
--- a/addons/html_builder/static/src/core/builder_options_plugin.js
+++ b/addons/html_builder/static/src/core/builder_options_plugin.js
@@ -20,6 +20,7 @@ export class BuilderOptionsPlugin extends Plugin {
         "getContainers",
         "updateContainers",
         "deactivateContainers",
+        "getTarget",
         "getPageContainers",
         "getRemoveDisabledReason",
         "getCloneDisabledReason",
@@ -150,6 +151,10 @@ export class BuilderOptionsPlugin extends Plugin {
         this.dispatchTo("change_current_options_containers_listeners", this.lastContainers);
     }
 
+    getTarget() {
+        return this.target;
+    }
+
     deactivateContainers() {
         this.target = null;
         this.lastContainers = [];
diff --git a/addons/html_builder/static/src/core/builder_options_plugin_translate.js b/addons/html_builder/static/src/core/builder_options_plugin_translate.js
new file mode 100644
index 00000000000000..e60db64893d947
--- /dev/null
+++ b/addons/html_builder/static/src/core/builder_options_plugin_translate.js
@@ -0,0 +1,12 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+export class BuilderOptionsPlugin extends Plugin {
+    static id = "builder-options";
+    static shared = ["deactivateContainers", "getTarget"];
+
+    deactivateContainers() {}
+    getTarget() {}
+}
+
+registry.category("translation-plugins").add(BuilderOptionsPlugin.id, BuilderOptionsPlugin);
diff --git a/addons/html_builder/static/src/core/builder_overlay/builder_overlay_plugin.js b/addons/html_builder/static/src/core/builder_overlay/builder_overlay_plugin.js
index c2022afb62542e..440cb92a6a4be3 100644
--- a/addons/html_builder/static/src/core/builder_overlay/builder_overlay_plugin.js
+++ b/addons/html_builder/static/src/core/builder_overlay/builder_overlay_plugin.js
@@ -3,6 +3,7 @@ import { throttleForAnimation } from "@web/core/utils/timing";
 import { getScrollingElement, getScrollingTarget } from "@web/core/utils/scrolling";
 import { checkElement } from "../builder_options_plugin";
 import { BuilderOverlay, sizingY, sizingX, sizingGrid } from "./builder_overlay";
+import { withSequence } from "@html_editor/utils/resource";
 
 function isResizable(el) {
     const isResizableY = el.matches(sizingY.selector) && !el.matches(sizingY.exclude);
@@ -18,7 +19,7 @@ export class BuilderOverlayPlugin extends Plugin {
     resources = {
         step_added_handlers: this.refreshOverlays.bind(this),
         change_current_options_containers_listeners: this.openBuilderOverlays.bind(this),
-        on_mobile_preview_clicked: this.refreshOverlays.bind(this),
+        on_mobile_preview_clicked: withSequence(20, this.refreshOverlays.bind(this)),
         has_overlay_options: { hasOption: (el) => isResizable(el) },
     };
 
diff --git a/addons/html_builder/static/src/core/disable_snippets_plugin.js b/addons/html_builder/static/src/core/disable_snippets_plugin.js
index 0d5fbaebbca3df..c3c0793cbbfa4f 100644
--- a/addons/html_builder/static/src/core/disable_snippets_plugin.js
+++ b/addons/html_builder/static/src/core/disable_snippets_plugin.js
@@ -1,5 +1,6 @@
 import { omit } from "@web/core/utils/objects";
 import { Plugin } from "@html_editor/plugin";
+import { withSequence } from "@html_editor/utils/resource";
 
 export class DisableSnippetsPlugin extends Plugin {
     static id = "disableSnippets";
@@ -7,9 +8,9 @@ export class DisableSnippetsPlugin extends Plugin {
     static shared = ["disableUndroppableSnippets"];
     resources = {
         after_remove_handlers: this.disableUndroppableSnippets.bind(this),
-        on_mobile_preview_clicked: this.disableUndroppableSnippets.bind(this),
         post_undo_handlers: this.disableUndroppableSnippets.bind(this),
         post_redo_handlers: this.disableUndroppableSnippets.bind(this),
+        on_mobile_preview_clicked: withSequence(20, this.disableUndroppableSnippets.bind(this)),
     };
 
     setup() {
diff --git a/addons/html_builder/static/src/core/disable_snippets_plugin_translation.js b/addons/html_builder/static/src/core/disable_snippets_plugin_translation.js
new file mode 100644
index 00000000000000..19116042275405
--- /dev/null
+++ b/addons/html_builder/static/src/core/disable_snippets_plugin_translation.js
@@ -0,0 +1,11 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+export class DisableSnippetsPlugin extends Plugin {
+    static id = "disableSnippets";
+    static shared = ["disableUndroppableSnippets"];
+
+    disableUndroppableSnippets() {}
+}
+
+registry.category("translation-plugins").add(DisableSnippetsPlugin.id, DisableSnippetsPlugin);
diff --git a/addons/html_builder/static/src/core/overlay_buttons/overlay_buttons_plugin.js b/addons/html_builder/static/src/core/overlay_buttons/overlay_buttons_plugin.js
index 278535e0546381..9ce753db2d202b 100644
--- a/addons/html_builder/static/src/core/overlay_buttons/overlay_buttons_plugin.js
+++ b/addons/html_builder/static/src/core/overlay_buttons/overlay_buttons_plugin.js
@@ -4,6 +4,7 @@ import { throttleForAnimation } from "@web/core/utils/timing";
 import { getScrollingElement, getScrollingTarget } from "@web/core/utils/scrolling";
 import { checkElement } from "../builder_options_plugin";
 import { OverlayButtons } from "./overlay_buttons";
+import { withSequence } from "@html_editor/utils/resource";
 
 export class OverlayButtonsPlugin extends Plugin {
     static id = "overlayButtons";
@@ -17,7 +18,7 @@ export class OverlayButtonsPlugin extends Plugin {
     resources = {
         step_added_handlers: this.refreshButtons.bind(this),
         change_current_options_containers_listeners: this.addOverlayButtons.bind(this),
-        on_mobile_preview_clicked: this.refreshButtons.bind(this),
+        on_mobile_preview_clicked: withSequence(20, this.refreshButtons.bind(this)),
     };
 
     setup() {
diff --git a/addons/html_builder/static/src/core/save_plugin.js b/addons/html_builder/static/src/core/save_plugin.js
index ae93f4d6b5eda5..58265fe20e9786 100644
--- a/addons/html_builder/static/src/core/save_plugin.js
+++ b/addons/html_builder/static/src/core/save_plugin.js
@@ -1,5 +1,6 @@
 import { Plugin } from "@html_editor/plugin";
 import { rpc } from "@web/core/network/rpc";
+import { registry } from "@web/core/registry";
 
 const oeStructureSelector = "#wrapwrap .oe_structure[data-oe-xpath][data-oe-id]";
 const oeFieldSelector = "#wrapwrap [data-oe-field]:not([data-oe-sanitize-prevent-edition])";
@@ -222,3 +223,4 @@ export class SavePlugin extends Plugin {
         }
     }
 }
+registry.category("translation-plugins").add(SavePlugin.id, SavePlugin);
diff --git a/addons/html_builder/static/src/core/setup_editor_plugin.js b/addons/html_builder/static/src/core/setup_editor_plugin.js
index a1bebc0659ffed..e9d35850387cf3 100644
--- a/addons/html_builder/static/src/core/setup_editor_plugin.js
+++ b/addons/html_builder/static/src/core/setup_editor_plugin.js
@@ -1,6 +1,7 @@
 import { Plugin } from "@html_editor/plugin";
 import { _t } from "@web/core/l10n/translation";
 import { getTranslationEditableEls } from "@html_builder/website_builder/plugins/translation_plugin";
+import { registry } from "@web/core/registry";
 
 export class SetupEditorPlugin extends Plugin {
     static id = "setup_editor_plugin";
@@ -103,3 +104,4 @@ export class SetupEditorPlugin extends Plugin {
         return editablesAreaEls;
     }
 }
+registry.category("translation-plugins").add(SetupEditorPlugin.id, SetupEditorPlugin);
diff --git a/addons/html_builder/static/src/core/visibility_plugin.js b/addons/html_builder/static/src/core/visibility_plugin.js
index 7805fd3641dae3..c75c6de50b42b1 100644
--- a/addons/html_builder/static/src/core/visibility_plugin.js
+++ b/addons/html_builder/static/src/core/visibility_plugin.js
@@ -1,5 +1,7 @@
 import { Plugin } from "@html_editor/plugin";
 import { isMobileView } from "@html_builder/utils/utils";
+import { registry } from "@web/core/registry";
+import { withSequence } from "@html_editor/utils/resource";
 
 export class VisibilityPlugin extends Plugin {
     static id = "visibility";
@@ -10,7 +12,7 @@ export class VisibilityPlugin extends Plugin {
         "onOptionVisibilityUpdate",
     ];
     resources = {
-        on_mobile_preview_clicked: this.onMobilePreviewClicked.bind(this),
+        on_mobile_preview_clicked: withSequence(10, this.onMobilePreviewClicked.bind(this)),
         system_attributes: ["data-invisible"],
         system_classes: ["o_snippet_override_invisible"],
     };
@@ -60,17 +62,21 @@ export class VisibilityPlugin extends Plugin {
     }
 
     onMobilePreviewClicked() {
-        const isMobilePreview = isMobileView(this.editable);
         const invisibleOverrideEls = this.editable.querySelectorAll(
             ".o_snippet_mobile_invisible, .o_snippet_desktop_invisible"
         );
         for (const invisibleOverrideEl of [...invisibleOverrideEls]) {
-            const isMobileHidden = invisibleOverrideEl.classList.contains(
-                "o_snippet_mobile_invisible"
-            );
             invisibleOverrideEl.classList.remove("o_snippet_override_invisible");
-            const show = isMobilePreview !== isMobileHidden;
-            this.toggleVisibilityStatus(invisibleOverrideEl, show);
+            const show = this.toggleVisibilityStatus({
+                editingEl: invisibleOverrideEl,
+                considerDeviceVisibility: true,
+            });
+            if (
+                !show &&
+                invisibleOverrideEl.contains(this.dependencies["builder-options"].getTarget())
+            ) {
+                this.dependencies["builder-options"].deactivateContainers();
+            }
         }
     }
 
@@ -83,7 +89,7 @@ export class VisibilityPlugin extends Plugin {
      * @returns {Boolean}
      */
     toggleTargetVisibility(editingEl, show, considerDeviceVisibility) {
-        show = this.toggleVisibilityStatus(editingEl, show, considerDeviceVisibility);
+        show = this.toggleVisibilityStatus({ editingEl, show, considerDeviceVisibility });
         const dispatchName = show ? "target_show" : "target_hide";
         this.dispatchTo(dispatchName, editingEl);
         return show;
@@ -96,8 +102,7 @@ export class VisibilityPlugin extends Plugin {
      * @param {Boolean} show true/false if the element was shown/hidden
      */
     onOptionVisibilityUpdate(editingEl, show) {
-        const isShown = this.toggleVisibilityStatus(editingEl, show);
-
+        const isShown = this.toggleVisibilityStatus({ editingEl, show });
         if (!isShown) {
             this.dependencies["builder-options"].deactivateContainers();
         }
@@ -114,18 +119,14 @@ export class VisibilityPlugin extends Plugin {
      * @param {Boolean} considerDeviceVisibility
      * @returns {Boolean}
      */
-    toggleVisibilityStatus(editingEl, show, considerDeviceVisibility) {
+    toggleVisibilityStatus({ editingEl, show, considerDeviceVisibility = false }) {
         if (
             considerDeviceVisibility &&
             editingEl.matches(".o_snippet_mobile_invisible, .o_snippet_desktop_invisible")
         ) {
             const isMobilePreview = isMobileView(editingEl);
             const isMobileHidden = editingEl.classList.contains("o_snippet_mobile_invisible");
-            if (isMobilePreview === isMobileHidden) {
-                // If the preview mode and the hidden type are the same, the
-                // element is considered as hidden.
-                show = false;
-            }
+            show = isMobilePreview !== isMobileHidden;
         }
 
         if (show === undefined) {
@@ -143,3 +144,5 @@ export class VisibilityPlugin extends Plugin {
 function isTargetVisible(editingEl) {
     return editingEl.dataset.invisible !== "1";
 }
+
+registry.category("translation-plugins").add(VisibilityPlugin.id, VisibilityPlugin);
diff --git a/addons/html_builder/static/src/sidebar/customize_translation_tab.xml b/addons/html_builder/static/src/sidebar/customize_translation_tab.xml
new file mode 100644
index 00000000000000..76beceae86f39c
--- /dev/null
+++ b/addons/html_builder/static/src/sidebar/customize_translation_tab.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+<t t-name="html_builder.CustomizeTranslationTab">
+    <div class="o_customize_tab h-100">
+        <div class="text-center pt-5">
+            Select content on your page to translate it.
+        </div>
+    </div>
+</t>
+
+</templates>
diff --git a/addons/html_builder/static/src/website_builder/plugins/options/popup_option_plugin.js b/addons/html_builder/static/src/website_builder/plugins/options/popup_option_plugin.js
index 84cc239f221fa5..8f0e22199f01d4 100644
--- a/addons/html_builder/static/src/website_builder/plugins/options/popup_option_plugin.js
+++ b/addons/html_builder/static/src/website_builder/plugins/options/popup_option_plugin.js
@@ -9,7 +9,7 @@ export const COOKIES_BAR = SNIPPET_SPECIFIC_END;
 
 class PopupOptionPlugin extends Plugin {
     static id = "PopupOption";
-    static dependencies = ["anchor", "visibility", "history"];
+    static dependencies = ["anchor", "visibility", "history", "popupVisibilityPlugin"];
 
     resources = {
         builder_options: [
@@ -34,24 +34,8 @@ class PopupOptionPlugin extends Plugin {
         on_cloned_handlers: this.onCloned.bind(this),
         on_remove_handlers: this.onRemove.bind(this),
         on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),
-        target_show: this.onTargetShow.bind(this),
-        target_hide: this.onTargetHide.bind(this),
-        clean_for_save_handlers: this.cleanForSave.bind(this),
     };
 
-    setup() {
-        this.addDomListener(this.editable, "click", (ev) => {
-            // Note: links are excluded here so that internal modal buttons do
-            // not close the popup as we want to allow edition of those buttons.
-            if (ev.target.matches(".s_popup .js_close_popup:not(a, .btn)")) {
-                ev.stopPropagation();
-                const popupEl = ev.target.closest(".s_popup");
-                this.onTargetHide(popupEl);
-                this.dependencies.visibility.onOptionVisibilityUpdate(popupEl, false);
-            }
-        });
-    }
-
     getActions() {
         return {
             // Moves the snippet in #o_shared_blocks to be common to all pages
@@ -115,11 +99,11 @@ class PopupOptionPlugin extends Plugin {
     }
 
     onRemove(el) {
-        this.onTargetHide(el);
+        this.dependencies.popupVisibilityPlugin.onTargetHide(el);
         this.dependencies.history.addCustomMutation({
             apply: () => {},
             revert: () => {
-                this.onTargetShow(el);
+                this.dependencies.popupVisibilityPlugin.onTargetShow(el);
             },
         });
     }
@@ -129,10 +113,10 @@ class PopupOptionPlugin extends Plugin {
             this.assignUniqueID(snippetEl);
             this.dependencies.history.addCustomMutation({
                 apply: () => {
-                    this.onTargetShow(snippetEl);
+                    this.dependencies.popupVisibilityPlugin.onTargetShow(snippetEl);
                 },
                 revert: () => {
-                    this.onTargetHide(snippetEl);
+                    this.dependencies.popupVisibilityPlugin.onTargetHide(snippetEl);
                 },
             });
         }
@@ -142,32 +126,6 @@ class PopupOptionPlugin extends Plugin {
         );
     }
 
-    onTargetShow(target) {
-        // Check if the popup is within the editable, because it is cloned on
-        // save (see save plugin) and Bootstrap moves it if it is not within the
-        // document (see Bootstrap Modal's _showElement).
-        if (target.matches(".s_popup") && this.editable.contains(target)) {
-            this.window.Modal.getOrCreateInstance(target.querySelector(".modal")).show();
-        }
-    }
-
-    onTargetHide(target) {
-        if (target.matches(".s_popup")) {
-            this.window.Modal.getOrCreateInstance(target.querySelector(".modal")).hide();
-        }
-    }
-
-    cleanForSave({ root }) {
-        for (const modalEl of root.querySelectorAll(".s_popup .modal.show")) {
-            modalEl.parentElement.dataset.invisible = "1";
-            // Do not call .hide() directly, because it is queued whereas
-            // .dispose() is not.
-            modalEl.classList.remove("show");
-            this.window.Modal.getOrCreateInstance(modalEl)._hideModal();
-            this.window.Modal.getInstance(modalEl).dispose();
-        }
-    }
-
     assignUniqueID(editingElement) {
         editingElement.closest(".s_popup").id = `sPopup${Date.now()}`;
     }
diff --git a/addons/html_builder/static/src/website_builder/plugins/options/visibility_option_plugin.js b/addons/html_builder/static/src/website_builder/plugins/options/visibility_option_plugin.js
index b0a9c565b93cbf..2b348ff7b475aa 100644
--- a/addons/html_builder/static/src/website_builder/plugins/options/visibility_option_plugin.js
+++ b/addons/html_builder/static/src/website_builder/plugins/options/visibility_option_plugin.js
@@ -2,7 +2,7 @@ import { registry } from "@web/core/registry";
 import { Plugin } from "@html_editor/plugin";
 import { selectElements } from "@html_editor/utils/dom_traversal";
 import { pyToJsLocale } from "@web/core/l10n/utils";
-import { getElementsWithOption, isMobileView } from "@html_builder/utils/utils";
+import { getElementsWithOption } from "@html_builder/utils/utils";
 import { VisibilityOption } from "./visibility_option";
 import { withSequence } from "@html_editor/utils/resource";
 import {
@@ -10,8 +10,8 @@ import {
     DEVICE_VISIBILITY,
 } from "@html_builder/website_builder/option_sequence";
 
-const visibilityOptionSelector = "section, .s_hr";
-const deviceVisibilityOptionSelector = "section .row > div";
+export const VISIBILITY_OPTION_SELECTOR = "section, .s_hr";
+export const DEVICE_VISIBILITY_OPTION_SELECTOR = "section .row > div";
 
 class VisibilityOptionPlugin extends Plugin {
     static id = "visibilityOption";
@@ -23,19 +23,17 @@ class VisibilityOptionPlugin extends Plugin {
                 props: {
                     websiteSession: this.dependencies.websiteSession.getSession(),
                 },
-                selector: visibilityOptionSelector,
+                selector: VISIBILITY_OPTION_SELECTOR,
                 cleanForSave: this.dependencies.visibility.cleanForSaveVisibility,
             }),
             withSequence(DEVICE_VISIBILITY, {
                 template: "html_builder.DeviceVisibilityOption",
-                selector: deviceVisibilityOptionSelector,
+                selector: DEVICE_VISIBILITY_OPTION_SELECTOR,
                 exclude: ".s_col_no_resize.row > div, .s_masonry_block .s_col_no_resize",
                 cleanForSave: this.dependencies.visibility.cleanForSaveVisibility,
             }),
         ],
         builder_actions: this.getActions(),
-        target_show: this.onTargetShow.bind(this),
-        target_hide: this.onTargetHide.bind(this),
         on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),
         normalize_handlers: this.normalizeCSSSelectors.bind(this),
         visibility_selector_parameters: [
@@ -145,38 +143,8 @@ class VisibilityOptionPlugin extends Plugin {
         return false;
     }
 
-    onTargetHide(editingEl) {
-        if (
-            editingEl.matches(deviceVisibilityOptionSelector) ||
-            editingEl.matches(visibilityOptionSelector)
-        ) {
-            editingEl.classList.remove("o_snippet_override_invisible");
-
-            const isConditionalHidden = editingEl.matches("[data-visibility='conditional']");
-            if (isConditionalHidden) {
-                editingEl.classList.add("o_conditional_hidden");
-            }
-        }
-    }
-
-    onTargetShow(editingEl) {
-        if (
-            editingEl.matches(deviceVisibilityOptionSelector) ||
-            editingEl.matches(visibilityOptionSelector)
-        ) {
-            const isMobilePreview = isMobileView(editingEl);
-            const isMobileHidden = editingEl.classList.contains("o_snippet_mobile_invisible");
-            const isDesktopHidden = editingEl.classList.contains("o_snippet_desktop_invisible");
-            if ((isMobileHidden && isMobilePreview) || (isDesktopHidden && !isMobilePreview)) {
-                editingEl.classList.add("o_snippet_override_invisible");
-            }
-
-            editingEl.classList.remove("o_conditional_hidden");
-        }
-    }
-
     onSnippetDropped({ snippetEl }) {
-        const selector = [visibilityOptionSelector, deviceVisibilityOptionSelector].join(", ");
+        const selector = [VISIBILITY_OPTION_SELECTOR, DEVICE_VISIBILITY_OPTION_SELECTOR].join(", ");
         const droppedEls = getElementsWithOption(snippetEl, selector);
         droppedEls.forEach((droppedEl) =>
             this.dependencies.visibility.toggleTargetVisibility(droppedEl, true, true)
@@ -184,7 +152,7 @@ class VisibilityOptionPlugin extends Plugin {
     }
 
     normalizeCSSSelectors(rootEl) {
-        for (const el of selectElements(rootEl, visibilityOptionSelector)) {
+        for (const el of selectElements(rootEl, VISIBILITY_OPTION_SELECTOR)) {
             this.updateCSSSelectors(el);
         }
     }
diff --git a/addons/html_builder/static/src/website_builder/plugins/popup_visibility_plugin.js b/addons/html_builder/static/src/website_builder/plugins/popup_visibility_plugin.js
new file mode 100644
index 00000000000000..930e978bd5fc2a
--- /dev/null
+++ b/addons/html_builder/static/src/website_builder/plugins/popup_visibility_plugin.js
@@ -0,0 +1,56 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class PopupVisibilityPlugin extends Plugin {
+    static id = "popupVisibilityPlugin";
+    static dependencies = ["visibility"];
+    static shared = ["onTargetShow", "onTargetHide"];
+
+    resources = {
+        target_show: this.onTargetShow.bind(this),
+        target_hide: this.onTargetHide.bind(this),
+        clean_for_save_handlers: this.cleanForSave.bind(this),
+    };
+
+    setup() {
+        this.addDomListener(this.editable, "click", (ev) => {
+            // Note: links are excluded here so that internal modal buttons do
+            // not close the popup as we want to allow edition of those buttons.
+            if (ev.target.matches(".s_popup .js_close_popup:not(a, .btn)")) {
+                ev.stopPropagation();
+                const popupEl = ev.target.closest(".s_popup");
+                this.onTargetHide(popupEl);
+                this.dependencies.visibility.onOptionVisibilityUpdate(popupEl, false);
+            }
+        });
+    }
+
+    onTargetShow(target) {
+        // Check if the popup is within the editable, because it is cloned on
+        // save (see save plugin) and Bootstrap moves it if it is not within the
+        // document (see Bootstrap Modal's _showElement).
+        if (target.matches(".s_popup") && this.editable.contains(target)) {
+            this.window.Modal.getOrCreateInstance(target.querySelector(".modal")).show();
+        }
+    }
+
+    onTargetHide(target) {
+        if (target.matches(".s_popup")) {
+            this.window.Modal.getOrCreateInstance(target.querySelector(".modal")).hide();
+        }
+    }
+
+    cleanForSave({ root }) {
+        for (const modalEl of root.querySelectorAll(".s_popup .modal.show")) {
+            modalEl.parentElement.dataset.invisible = "1";
+            // Do not call .hide() directly, because it is queued whereas
+            // .dispose() is not.
+            modalEl.classList.remove("show");
+            this.window.Modal.getOrCreateInstance(modalEl)._hideModal();
+            this.window.Modal.getInstance(modalEl).dispose();
+        }
+    }
+}
+
+registry.category("website-plugins").add(PopupVisibilityPlugin.id, PopupVisibilityPlugin);
+registry.category("translation-plugins").add(PopupVisibilityPlugin.id, PopupVisibilityPlugin);
diff --git a/addons/html_builder/static/src/website_builder/plugins/translation_plugin.js b/addons/html_builder/static/src/website_builder/plugins/translation_plugin.js
index 57fc8eb570e0c0..1855ca11e855c9 100644
--- a/addons/html_builder/static/src/website_builder/plugins/translation_plugin.js
+++ b/addons/html_builder/static/src/website_builder/plugins/translation_plugin.js
@@ -57,10 +57,8 @@ export class TranslationPlugin extends Plugin {
         clean_for_save_handlers: this.cleanForSave.bind(this),
         get_dirty_els: this.getDirtyTranslations.bind(this),
         after_setup_editor_handlers: () => {
-            if (this.config.isTranslation) {
-                this.setupServicesIfNotSet();
-                this.prepareTranslation();
-            }
+            this.setupServicesIfNotSet();
+            this.prepareTranslation();
         },
     };
 
@@ -306,9 +304,6 @@ export class TranslationPlugin extends Plugin {
      * @returns {HTMLElement[]}
      */
     getDirtyTranslations() {
-        if (!this.config.isTranslation) {
-            return [];
-        }
         const dirtyEls = [];
         for (const [translateEl, translationInfo] of this.elToTranslationInfoMap) {
             for (const [attr, data] of Object.entries(translationInfo)) {
@@ -351,4 +346,4 @@ export class TranslationPlugin extends Plugin {
     }
 }
 
-registry.category("website-plugins").add(TranslationPlugin.id, TranslationPlugin);
+registry.category("translation-plugins").add(TranslationPlugin.id, TranslationPlugin);
diff --git a/addons/html_builder/static/src/website_builder/plugins/website_visibility_plugin.js b/addons/html_builder/static/src/website_builder/plugins/website_visibility_plugin.js
new file mode 100644
index 00000000000000..96ee02af801bf7
--- /dev/null
+++ b/addons/html_builder/static/src/website_builder/plugins/website_visibility_plugin.js
@@ -0,0 +1,49 @@
+import { isMobileView } from "@html_builder/utils/utils";
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+import {
+    DEVICE_VISIBILITY_OPTION_SELECTOR,
+    VISIBILITY_OPTION_SELECTOR,
+} from "./options/visibility_option_plugin";
+
+class WebsiteVisibilityPlugin extends Plugin {
+    static id = "websiteVisibilityPlugin";
+
+    resources = {
+        target_show: this.onTargetShow.bind(this),
+        target_hide: this.onTargetHide.bind(this),
+    };
+
+    onTargetHide(editingEl) {
+        if (
+            editingEl.matches(DEVICE_VISIBILITY_OPTION_SELECTOR) ||
+            editingEl.matches(VISIBILITY_OPTION_SELECTOR)
+        ) {
+            editingEl.classList.remove("o_snippet_override_invisible");
+
+            const isConditionalHidden = editingEl.matches("[data-visibility='conditional']");
+            if (isConditionalHidden) {
+                editingEl.classList.add("o_conditional_hidden");
+            }
+        }
+    }
+
+    onTargetShow(editingEl) {
+        if (
+            editingEl.matches(DEVICE_VISIBILITY_OPTION_SELECTOR) ||
+            editingEl.matches(VISIBILITY_OPTION_SELECTOR)
+        ) {
+            const isMobilePreview = isMobileView(editingEl);
+            const isMobileHidden = editingEl.classList.contains("o_snippet_mobile_invisible");
+            const isDesktopHidden = editingEl.classList.contains("o_snippet_desktop_invisible");
+            if ((isMobileHidden && isMobilePreview) || (isDesktopHidden && !isMobilePreview)) {
+                editingEl.classList.add("o_snippet_override_invisible");
+            }
+
+            editingEl.classList.remove("o_conditional_hidden");
+        }
+    }
+}
+
+registry.category("website-plugins").add(WebsiteVisibilityPlugin.id, WebsiteVisibilityPlugin);
+registry.category("translation-plugins").add(WebsiteVisibilityPlugin.id, WebsiteVisibilityPlugin);
diff --git a/addons/html_builder/static/src/website_preview/website_builder_action.js b/addons/html_builder/static/src/website_preview/website_builder_action.js
index c2b5c1b1f15742..20db8b059b5838 100644
--- a/addons/html_builder/static/src/website_preview/website_builder_action.js
+++ b/addons/html_builder/static/src/website_preview/website_builder_action.js
@@ -158,7 +158,10 @@ export class WebsiteBuilder extends Component {
     }
 
     get menuProps() {
-        const WebsitePlugins = registry.category("website-plugins").getAll();
+        const websitePlugins = this.translation
+            ? registry.category("translation-plugins").getAll()
+            : registry.category("website-plugins").getAll();
+
         return {
             closeEditor: this.reloadIframeAndCloseEditor.bind(this),
             reloadEditor: this.reloadEditor.bind(this),
@@ -168,7 +171,7 @@ export class WebsiteBuilder extends Component {
             isTranslation: this.translation,
             iframeLoaded: this.iframeLoaded,
             isMobile: this.websiteContext.isMobile,
-            Plugins: WebsitePlugins,
+            Plugins: websitePlugins,
             config: { initialTarget: this.target, initialTab: this.initialTab },
         };
     }
diff --git a/addons/html_builder/static/tests/invisible_elements.test.js b/addons/html_builder/static/tests/invisible_elements.test.js
index 1bdda4042eaf72..1893fc55be6a39 100644
--- a/addons/html_builder/static/tests/invisible_elements.test.js
+++ b/addons/html_builder/static/tests/invisible_elements.test.js
@@ -92,7 +92,7 @@ test("Add an element on the invisible elements tab", async () => {
     ).toHaveCount(1);
 });
 
-test("mobile and desktop invisible elements", async () => {
+test("mobile and desktop invisible elements panel", async () => {
     await setupWebsiteBuilder(`
         <div class="o_snippet_invisible" data-name="Popup1"></div>
         <div class="o_snippet_mobile_invisible" data-name="Popup2"></div>
@@ -109,6 +109,31 @@ test("mobile and desktop invisible elements", async () => {
     ]);
 });
 
+test("mobile and desktop option container", async () => {
+    await setupWebsiteBuilder(`
+        <section class="o_snippet_desktop_invisible"></section>
+    `);
+    await contains(".o_we_invisible_el_panel .o_we_invisible_entry").click();
+    expect(".options-container").toBeDisplayed();
+    await contains("button[data-action='mobile']").click();
+    expect(".options-container").toBeDisplayed();
+    await contains("button[data-action='mobile']").click();
+    expect(".options-container").not.toBeDisplayed();
+});
+
+test("keep the option container of a visible snippet even if there are hidden snippet on the page", async () => {
+    await setupWebsiteBuilder(`
+        <section id="my_el">
+            <p>TEST</p>
+        </section>
+        <section class="o_snippet_mobile_invisible"></section>
+    `);
+    await contains(":iframe #my_el").click();
+    expect(".options-container").toBeDisplayed();
+    await contains("button[data-action='mobile']").click();
+    expect(".options-container").toBeDisplayed();
+});
+
 test("invisible elements efficiency", async () => {
     patchWithCleanup(InvisibleElementsPanel.prototype, {
         updateInvisibleElementsPanel() {


Reviews:
user: login: FrancoisGe
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-08T08:27:02Z
user: login: FrancoisGe
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-13T11:52:42Z

Review comments:
diff_hunk: @@ -99,10 +99,10 @@ export class VisibilityPlugin extends Plugin {
         const isShown = this.toggleVisibilityStatus(editingEl, show);
 
         if (!isShown) {
-            this.dependencies["builder-options"].deactivateContainers();
+            this.dispatchTo("deactivate_containers");
path: addons/html_builder/static/src/core/visibility_plugin.js
user: login: FrancoisGe
body: this.dispatchTo("element_toggle_visibility_handlers", { el, isShown });
created_at: 2025-05-08T08:20:42Z
author_association: NONE
diff_hunk: @@ -80,12 +84,11 @@ export class InvisibleElementsPanel extends Component {
         const toggleVisibility = (snippetEl) => {
             const show = this.shared.visibility.toggleTargetVisibility(snippetEl);
             invisibleEntry.isVisible = show;
-
-            this.shared.disableSnippets.disableUndroppableSnippets();
+            this.getResource("disable_undroppable_snippets").forEach((handler) => handler());
             if (show) {
-                this.shared["builder-options"].updateContainers(snippetEl);
+                this.getResource("update_containers").forEach((handler) => handler(snippetEl));
path: addons/html_builder/static/src/sidebar/invisible_elements_panel.js
user: login: FrancoisGe
body: maybe duplicate code
created_at: 2025-05-08T08:25:15Z
author_association: NONE
diff_hunk: @@ -99,6 +102,21 @@ export class BuilderOptionsPlugin extends Plugin {
         return null;
     }
 
+    /**
+     * Deactivates the containers if the current target is not visible anymore.
+     *
+     * @param {Map<HTMLElement, boolean>} elToVisibilityMap - A map that
+     * provides information on the visibility of elements on the page.
+     */
+    deactivateContainersIfTargetHidden(elToVisibilityMap) {
+        for (const [el, visibility] of elToVisibilityMap.entries()) {
+            if (el.contains(this.target) && !visibility) {
+                this.deactivateContainers();
+                return;
+            }
+        }
+    }
path: addons/html_builder/static/src/core/builder_options_plugin.js
user: login: FrancoisGe
body: We need something more generic. Why not use onSelectedElVisibilityUpdated ? 
Maybe rename onElVisibilityUpdated. 
Check if el.contains(this.target) || el === this.target  && !show then  this.deactivateContainers
created_at: 2025-05-13T11:51:11Z
author_association: NONE

PR comments:
user: login: robodoo
created_at: 2025-05-07T12:40:27Z
body: This PR targets the un-managed branch odoo-dev/odoo:master-mysterious-egg, it needs to be retargeted before it can be merged.
author_association: NONE
user: login: robodoo
created_at: 2025-05-14T13:46:14Z
body: This PR targets the un-managed branch odoo-dev/odoo:master-mysterious-egg, it needs to be retargeted before it can be merged.
author_association: NONE