From 8d19f0f1fde6fe271db348cae47128157d079292 Mon Sep 17 00:00:00 2001
From: xO-Tx <mou@odoo.com>
Date: Fri, 28 Apr 2023 13:48:36 +0000
Subject: [PATCH] [FIX] website, tools: make select options translatable

Steps to reproduce:

- Go to a website page > Add a 'Form' block > Add a new 'Selection'
field.
- Go to the page (in 'edit_translations' mode) > The selection field
options are not translatable.

The goal of this commit is to make the select options translatable
by adding an intermediate `.o_translation_select` element.

This element will handle option's text translations from the linked
`<select/>`. The final values are copied to the original element
right before save.

opw-3233360

closes odoo/odoo#120028

X-original-commit: 57d7e75b030ed34992865170ce349442fb3c1d03
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
---
 addons/website/i18n/website.pot               |  7 ++
 .../website/static/src/js/menu/translate.js   | 96 ++++++++++++++++---
 addons/website/static/src/scss/website.scss   | 15 +++
 .../static/src/scss/website.wysiwyg.scss      | 10 +-
 odoo/tools/translate.py                       |  2 +-
 5 files changed, 115 insertions(+), 15 deletions(-)

diff --git a/addons/website/i18n/website.pot b/addons/website/i18n/website.pot
index b2e36122d34f..6b349790cee8 100644
--- a/addons/website/i18n/website.pot
+++ b/addons/website/i18n/website.pot
@@ -9726,6 +9726,13 @@ msgstr ""
 msgid "Translate Attribute"
 msgstr ""
 
+#. module: website
+#. openerp-web
+#: code:addons/website/static/src/js/menu/translate.js:0
+#, python-format
+msgid "Translate Selection Option"
+msgstr ""
+
 #. module: website
 #. openerp-web
 #: code:addons/website/static/src/js/menu/translate.js:0
diff --git a/addons/website/static/src/js/menu/translate.js b/addons/website/static/src/js/menu/translate.js
index f432f0696878..1be1b30c630c 100644
--- a/addons/website/static/src/js/menu/translate.js
+++ b/addons/website/static/src/js/menu/translate.js
@@ -96,6 +96,41 @@ var AttributeTranslateDialog = weDialog.extend({
     },
 });
 
+// Used to translate the text of `<select/>` options since it should not be
+// possible to interact with the content of `.o_translation_select` elements.
+const SelectTranslateDialog = weDialog.extend({
+    /**
+     * @constructor
+     */
+    init(parent, options) {
+        this._super(parent, {
+            ...options,
+            title: _t("Translate Selection Option"),
+            buttons: [
+                {text: _t("Close"), click: this.save}
+            ],
+        });
+        this.optionEl = this.options.targetEl;
+        this.translationObject = this.optionEl.closest('[data-oe-translation-id]');
+    },
+    /**
+     * @override
+     */
+    start() {
+        const inputEl = document.createElement('input');
+        inputEl.className = 'form-control my-3';
+        inputEl.value = this.optionEl.textContent;
+        inputEl.addEventListener('keyup', () => {
+            this.optionEl.textContent = inputEl.value;
+            const translationUpdated = inputEl.value !== this.optionEl.dataset.initialTranslationValue;
+            this.translationObject.classList.toggle('o_dirty', translationUpdated);
+            this.optionEl.classList.add('o_option_translated');
+        });
+        this.el.appendChild(inputEl);
+        return this._super(...arguments);
+    },
+});
+
 const savableSelector = '[data-oe-translation-id], ' +
     '[data-oe-model][data-oe-id][data-oe-field], ' +
     '[placeholder*="data-oe-translation-id="], ' +
@@ -269,6 +304,25 @@ var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
                     }
                 });
 
+                // Hack: we add a temporary element to handle option's text
+                // translations from the linked <select/>. The final values are
+                // copied to the original element right before save.
+                self.selectTranslationEls = [];
+                $editable.filter('[data-oe-translation-id] > select').each((index, select) => {
+                    const selectTranslationEl = document.createElement('div');
+                    selectTranslationEl.className = 'o_translation_select';
+                    const optionNames = [...select.options].map(option => option.text);
+                    optionNames.forEach(option => {
+                        const optionEl = document.createElement('div');
+                        optionEl.textContent = option;
+                        optionEl.dataset.initialTranslationValue = option;
+                        optionEl.className = 'o_translation_select_option';
+                        selectTranslationEl.appendChild(optionEl);
+                    });
+                    select.before(selectTranslationEl);
+                    self.selectTranslationEls.push(selectTranslationEl);
+                });
+
                 self.translations = [];
                 self.$translations = self._getEditableArea().filter('.o_translatable_attribute, .o_translatable_text');
                 self.$editables = $('.o_editable_translatable_attribute, .o_editable_translatable_text');
@@ -370,18 +424,25 @@ var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
             });
         });
 
-        this.$translations.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
-            if (ev.ctrlKey) {
-                return;
-            }
-            ev.preventDefault();
-            ev.stopPropagation();
-            if (ev.type !== 'mousedown') {
-                return;
-            }
+        this.$translations
+            .add(this._getEditableArea().filter('.o_translation_select_option'))
+            .prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
+                if (ev.ctrlKey) {
+                    return;
+                }
+                ev.preventDefault();
+                ev.stopPropagation();
+                if (ev.type !== 'mousedown') {
+                    return;
+                }
 
-            new AttributeTranslateDialog(self, {}, ev.target).open();
-        });
+                const targetEl = ev.target;
+                if (targetEl.closest('.o_translation_select')) {
+                    new SelectTranslateDialog(self, {size: 'medium', targetEl}).open();
+                } else {
+                    new AttributeTranslateDialog(self, {}, ev.target).open();
+                }
+            });
     },
 
     //--------------------------------------------------------------------------
@@ -390,6 +451,19 @@ var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
 
     _onSave: function (ev) {
         ev.stopPropagation();
+        // Adapt translation values for `select` > `options`s and remove all
+        // temporary `.o_translation_select` elements.
+        for (const optionsEl of this.selectTranslationEls) {
+            const selectEl = optionsEl.nextElementSibling;
+            const translatedOptions = optionsEl.children;
+            const selectOptions = selectEl.tagName === 'SELECT' ? [...selectEl.options] : [];
+            if (selectOptions.length === translatedOptions.length) {
+                selectOptions.map((option, i) => {
+                    option.text = translatedOptions[i].textContent;
+                });
+            }
+            optionsEl.remove();
+        }
     },
 });
 
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 78fda3581fbd..7b77734109cf 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -1639,6 +1639,21 @@ ul.o_checklist > li.o_checked::after {
 input[value*="data-oe-translation-id"] {
     @extend .o_text_content_invisible;
 }
+[data-oe-translation-id] {
+    > .o_translation_select {
+        border: $input-border-width solid $input-border-color;
+        @include border-radius($input-border-radius, 0);
+
+        // Hide translatable `<select/>`s since we use `.o_translation_select`
+        // elements to handle translations.
+        + select {
+            display: none !important;
+        }
+        > div:not(:last-child) {
+            border-bottom: inherit;
+        }
+    }
+}
 
 //------------------------------------------------------------------------------
 // Website Animate
diff --git a/addons/website/static/src/scss/website.wysiwyg.scss b/addons/website/static/src/scss/website.wysiwyg.scss
index e45f7447741c..f5460cf19a24 100644
--- a/addons/website/static/src/scss/website.wysiwyg.scss
+++ b/addons/website/static/src/scss/website.wysiwyg.scss
@@ -85,13 +85,17 @@
 }
 
 html[lang] > body.editor_enable [data-oe-translation-state] {
-    background: rgba($o-we-content-to-translate-color, 0.5) !important;
+    &, .o_translation_select_option {
+        background: rgba($o-we-content-to-translate-color, 0.5) !important;
+    }
 
     &[data-oe-translation-state="translated"] {
-        background: rgba($o-we-translated-content-color, 0.5) !important;
+        &, .o_translation_select_option {
+            background: rgba($o-we-translated-content-color, 0.5) !important;
+        }
     }
 
-    &.o_dirty {
+    &.o_dirty, .o_option_translated {
         background: rgba($o-we-translated-content-color, 0.25) !important;
     }
 }
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index 22f5f5e56fad..b5620a6c2d1d 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -141,7 +141,7 @@ TRANSLATED_ELEMENTS = {
     'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'del', 'dfn', 'em',
     'font', 'i', 'ins', 'kbd', 'keygen', 'mark', 'math', 'meter', 'output',
     'progress', 'q', 'ruby', 's', 'samp', 'small', 'span', 'strong', 'sub',
-    'sup', 'time', 'u', 'var', 'wbr', 'text',
+    'sup', 'time', 'u', 'var', 'wbr', 'text', 'select', 'option',
 }
 
 # Which attributes must be translated. This is a dict, where the value indicates
-- 
GitLab