From 2471ee506f38f5ca54d7bd54084b0ac3d74abeae Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 20 Sep 2019 12:46:07 +0000 Subject: [PATCH] [FIX] web: translate html fields This commit fixes: - body of a email.template in a small field - translation of website_description of a product does not show the source of the translation - translation of website_description of a product shows the full content for the current language HTML fields are typically fields with longer of content. If the field is an HTML field, it should be displayed in a textarea, like the text fields. Most of HTML fields are split in meaningful chunks of content (using xml_translate or html_translate method). For these fields, the source must be displayed. Also sort the entries on the source to have a consistent order closes odoo/odoo#37215 Signed-off-by: Martin Trigaux (mat) --- .../src/js/views/basic/basic_controller.js | 3 +- .../src/js/widgets/translation_dialog.js | 13 +- .../web/static/src/xml/translation_dialog.xml | 9 +- .../static/tests/fields/basic_fields_tests.js | 129 +++++++++++++++++- addons/web/static/tests/views/list_tests.js | 1 + odoo/addons/base/models/ir_translation.py | 4 + 6 files changed, 150 insertions(+), 9 deletions(-) diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index be0b9a00bf3..21ce85ce025 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -833,7 +833,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { userLanguageValue: ev.target.value || '', dataPointID: record.id, isComingFromTranslationAlert: ev.data.isComingFromTranslationAlert, - isText: record.fields[ev.data.fieldName].type === 'text', + isText: result.context.translation_type === 'text', + showSrc: result.context.translation_show_src, }); return this.translationDialog.open(); }, diff --git a/addons/web/static/src/js/widgets/translation_dialog.js b/addons/web/static/src/js/widgets/translation_dialog.js index 39d841a48d0..426ef68cb35 100644 --- a/addons/web/static/src/js/widgets/translation_dialog.js +++ b/addons/web/static/src/js/widgets/translation_dialog.js @@ -23,6 +23,7 @@ odoo.define('web.TranslationDialog', function (require) { * @param {string} [options.dataPointID] the data point id of the record for which we do the translations * @param {boolean} [options.isComingFromTranslationAlert] the initiator of the dialog, might be a link on a field or the translation alert on top of the form * @param {boolean} [options.isText] is the field a text field (multiline) or char (single line) + * @param {boolean} [options.showSrc] is the source of the translation should be rendered (for partial translations, i.e. XML content) * */ init: function (parent, options) { @@ -37,6 +38,7 @@ odoo.define('web.TranslationDialog', function (require) { this.isComingFromTranslationAlert = options.isComingFromTranslationAlert; this.currentInterfaceLanguage = session.user_context.lang; this.isText = options.isText; + this.showSrc = options.showSrc; this._super(parent, _.extend({ size: 'large', @@ -66,15 +68,18 @@ odoo.define('web.TranslationDialog', function (require) { id: term.id, lang: term.lang, langName: relatedLanguage[1], + source: term.src, // we set the translation value coming from the database, except for the language // the user is currently utilizing. Then we set the translation value coming // from the value of the field in the form value: (term.lang === this.currentInterfaceLanguage && + !this.showSrc && !this.isComingFromTranslationAlert) ? this.userLanguageValue : term.value || '' }; }); - this.data.sort((left, right) => left.langName < right.langName ? -1 : 1); + this.data.sort((left, right) => + (left.langName < right.langName || (left.langName === right.langName && left.source < right.source)) ? -1 : 1); }); }, @@ -89,7 +94,7 @@ odoo.define('web.TranslationDialog', function (require) { return this._rpc({ model: 'ir.translation', method: 'search_read', - fields: ['lang', 'value'], + fields: ['lang', 'src', 'value'], domain: this.domain, }); }, @@ -132,10 +137,12 @@ odoo.define('web.TranslationDialog', function (require) { if (initialValue.value !== t.value) { updatedTerm[t.dataset.id] = t.value; - if (initialValue.lang === this.currentInterfaceLanguage) { + if (initialValue.lang === this.currentInterfaceLanguage && !this.showSrc) { // when the user has changed the term for the language he is // using in the interface, this change should be reflected // in the form view + // partial translations being handled server side are + // also ignored var changes = {}; changes[this.fieldName] = updatedTerm[initialValue.id]; updateFormViewField = { diff --git a/addons/web/static/src/xml/translation_dialog.xml b/addons/web/static/src/xml/translation_dialog.xml index 32e2a036e45..7ee3e2f4a8b 100644 --- a/addons/web/static/src/xml/translation_dialog.xml +++ b/addons/web/static/src/xml/translation_dialog.xml @@ -2,10 +2,13 @@
-
+
-
+
+ +
+
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index e8db402ec49..5b5b5196e90 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1109,7 +1109,7 @@ QUnit.module('basic_fields', { }); QUnit.test('char field translatable', async function (assert) { - assert.expect(4); + assert.expect(12); this.data.partner.fields.foo.translate = true; @@ -1128,6 +1128,9 @@ QUnit.module('basic_fields', { '' + '', res_id: 1, + session: { + user_context: {lang: 'en_US'}, + }, mockRPC: function (route, args) { if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route'); @@ -1137,7 +1140,18 @@ QUnit.module('basic_fields', { }); } if (route === "/web/dataset/call_kw/res.lang/get_installed") { - return Promise.resolve([["en_US"], ["fr_BE"]]); + return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); + } + if (args.method === "search_read" && args.model == "ir.translation") { + return Promise.resolve([ + {lang: 'en_US', src: 'yop', value: 'yop', id: 42}, + {lang: 'fr_BE', src: 'yop', value: 'valeur français', id: 43} + ]); + } + if (args.method === "write" && args.model == "ir.translation") { + assert.deepEqual(args.args[1], {value: "english value"}, + "the new translation value should be written"); + return Promise.resolve(); } return this._super.apply(this, arguments); }, @@ -1150,6 +1164,117 @@ QUnit.module('basic_fields', { await testUtils.nextTick(); assert.containsOnce($(document), '.modal', 'a translate modal should be visible'); + assert.containsN($('.modal .o_translation_dialog'), '.translation', 2, + 'two rows should be visible'); + + var $enField = $('.modal .o_translation_dialog .translation:first() input'); + assert.strictEqual($enField.val(), 'yop', + 'English translation should be filled'); + assert.strictEqual($('.modal .o_translation_dialog .translation:last() input').val(), 'valeur français', + 'French translation should be filled'); + + await testUtils.fields.editInput($enField, "english value"); + await testUtils.dom.click($('.modal button.btn-primary')); // save + await testUtils.nextTick(); + + var $foo = form.$('input[type="text"].o_field_char'); + assert.strictEqual($foo.val(), "english value", + "the new translation was not transfered to modified record"); + + await testUtils.fields.editInput($foo, "new english value"); + + await testUtils.dom.click($button); + await testUtils.nextTick(); + + assert.strictEqual($('.modal .o_translation_dialog .translation:first() input').val(), 'new english value', + 'Modified value should be used instead of translation'); + assert.strictEqual($('.modal .o_translation_dialog .translation:last() input').val(), 'valeur français', + 'French translation should be filled'); + + form.destroy(); + + _t.database.multi_lang = multiLang; + }); + + QUnit.test('html field translatable', async function (assert) { + assert.expect(6); + + this.data.partner.fields.foo.translate = true; + + var multiLang = _t.database.multi_lang; + _t.database.multi_lang = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + session: { + user_context: {lang: 'en_US'}, + }, + mockRPC: function (route, args) { + if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { + assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route'); + return Promise.resolve({ + domain: [], + context: { + search_default_name: 'partner,foo', + translation_type: 'char', + translation_show_src: true, + }, + }); + } + if (route === "/web/dataset/call_kw/res.lang/get_installed") { + return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); + } + if (args.method === "search_read" && args.model == "ir.translation") { + return Promise.resolve([ + {lang: 'en_US', src: 'first paragraph', value: 'first paragraph', id: 42}, + {lang: 'en_US', src: 'second paragraph', value: 'second paragraph', id: 43}, + {lang: 'fr_BE', src: 'first paragraph', value: 'premier paragraphe', id: 44}, + {lang: 'fr_BE', src: 'second paragraph', value: 'deuxième paragraphe', id: 45}, + ]); + } + if (args.method === "write" && args.model == "ir.translation") { + assert.deepEqual(args.args[1], {value: "first paragraph modified"}, + "Wrong update on translation"); + return Promise.resolve(); + } + return this._super.apply(this, arguments); + }, + }); + await testUtils.form.clickEdit(form); + var $foo = form.$('input[type="text"].o_field_char'); + + // this will not affect the translate_fields effect until the record is + // saved but is set for consistency of the test + await testUtils.fields.editInput($foo, "

first paragraph

second paragraph

"); + + var $button = form.$('input[type="text"].o_field_char + .o_field_translate'); + await testUtils.dom.click($button); + await testUtils.nextTick(); + + assert.containsOnce($(document), '.modal', 'a translate modal should be visible'); + assert.containsN($('.modal .o_translation_dialog'), '.translation', 4, + 'four rows should be visible'); + + var $enField = $('.modal .o_translation_dialog .translation:first() input'); + assert.strictEqual($enField.val(), 'first paragraph', + 'first part of english translation should be filled'); + + await testUtils.fields.editInput($enField, "first paragraph modified"); + await testUtils.dom.click($('.modal button.btn-primary')); // save + await testUtils.nextTick(); + + assert.strictEqual($foo.val(), "

first paragraph

second paragraph

", + "the new partial translation should not be transfered"); form.destroy(); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 98f45209943..3d96c77cb95 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -98,6 +98,7 @@ QUnit.module('Views', { "ir.translation": { fields: { lang_code: {type: "char"}, + src: {type: "char"}, value: {type: "char"}, res_id: {type: "integer"}, name: {type: "char"}, diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py index 16603a5a8af..caf6be2a4a5 100644 --- a/odoo/addons/base/models/ir_translation.py +++ b/odoo/addons/base/models/ir_translation.py @@ -745,6 +745,7 @@ class IrTranslation(models.Model): 'target': 'current', 'flags': {'search_view': True, 'action_buttons': True}, 'domain': domain, + 'context': {}, } if field: fld = record._fields[field] @@ -763,8 +764,11 @@ class IrTranslation(models.Model): pass action['target'] = 'new' + action['context']['translation_type'] = 'text' if fld.type in ['text', 'html'] else 'char' + action['context']['translation_show_src'] = False if callable(fld.translate): action['view_id'] = self.env.ref('base.view_translation_lang_src_value_tree').id, + action['context']['translation_show_src'] = True else: action['view_id'] = self.env.ref('base.view_translation_lang_value_tree').id,