From 57d7e75b030ed34992865170ce349442fb3c1d03 Mon Sep 17 00:00:00 2001
From: xO-Tx <mou@odoo.com>
Date: Mon, 17 Apr 2023 16:29:06 +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#117519

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
---
 addons/website/i18n/website.pot               |  7 ++
 .../js/editor/wysiwyg_multizone_translate.js  | 99 ++++++++++++++++---
 addons/website/static/src/scss/website.scss   | 15 +++
 .../static/src/scss/website.wysiwyg.scss      | 10 +-
 odoo/tools/translate.py                       |  2 +-
 5 files changed, 117 insertions(+), 16 deletions(-)

diff --git a/addons/website/i18n/website.pot b/addons/website/i18n/website.pot
index bb3e37c42d02..891c277be713 100644
--- a/addons/website/i18n/website.pot
+++ b/addons/website/i18n/website.pot
@@ -7453,6 +7453,13 @@ msgstr ""
 msgid "Translate Attribute"
 msgstr ""
 
+#. module: website
+#. openerp-web
+#: code:addons/website/static/src/js/editor/wysiwyg_multizone_translate.js:0
+#, python-format
+msgid "Translate Selection Option"
+msgstr ""
+
 #. module: website
 #. openerp-web
 #: code:addons/website/static/src/xml/translator.xml:0
diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js
index 978ac4d98a6d..02e506d4a476 100644
--- a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js
+++ b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js
@@ -69,6 +69,41 @@ var AttributeTranslateDialog = Dialog.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 = Dialog.extend({
+    /**
+     * @constructor
+     */
+    init: function (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: function () {
+        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);
+    },
+});
+
 var WysiwygTranslate = WysiwygMultizone.extend({
     custom_events: _.extend({}, WysiwygMultizone.prototype.custom_events || {}, {
         ready_to_save: '_onSave',
@@ -101,8 +136,9 @@ var WysiwygTranslate = WysiwygMultizone.extend({
         return promise.then(function () {
             self._relocateEditorBar();
             var attrs = ['placeholder', 'title', 'alt'];
+            const $editable = self._getEditableArea();
             _.each(attrs, function (attr) {
-                self._getEditableArea().filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
+                $editable.filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
                     var $node = $(this);
                     var translation = $node.data('translation') || {};
                     var trans = $node.attr(attr);
@@ -125,6 +161,25 @@ var WysiwygTranslate = WysiwygMultizone.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.
+            $editable.filter('[data-oe-translation-id] > select').each((index, select) => {
+                // Keep the default width of the translation `<span/>`.
+                select.parentElement.classList.remove('o_is_inline_editable');
+                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.translations = [];
             self.$editables_attr = self._getEditableArea().filter('.o_translatable_attribute');
             self.$editables_attribute = $('.o_editable_translatable_attribute');
@@ -274,18 +329,25 @@ var WysiwygTranslate = WysiwygMultizone.extend({
             });
         });
 
-        this.$editables_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
-            if (ev.ctrlKey) {
-                return;
-            }
-            ev.preventDefault();
-            ev.stopPropagation();
-            if (ev.type !== 'mousedown') {
-                return;
-            }
+        this.$editables_attr
+            .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, {}, targetEl).open();
+                }
+            });
     },
 
     //--------------------------------------------------------------------------
@@ -294,6 +356,19 @@ var WysiwygTranslate = WysiwygMultizone.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.el.querySelectorAll('.o_translation_select')) {
+            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 7ef3d5562af2..c75b07c6ef6d 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -1551,3 +1551,18 @@ $ribbon-padding: 100px;
         opacity: 0;
     }
 }
+[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;
+        }
+    }
+}
diff --git a/addons/website/static/src/scss/website.wysiwyg.scss b/addons/website/static/src/scss/website.wysiwyg.scss
index 8bb4f1dcfcb7..e6edd90358b3 100644
--- a/addons/website/static/src/scss/website.wysiwyg.scss
+++ b/addons/website/static/src/scss/website.wysiwyg.scss
@@ -81,13 +81,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 a6735cbaf3dc..6ba57274d9f6 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -140,7 +140,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
-- 
GitLab