[IMP] web, web_editor: translation dialog for new translate api

This commit is contained in:
Chong Wang (cwg)
2022-09-15 22:37:50 +02:00
parent 1b473cf0db
commit b43d117e0e
13 changed files with 237 additions and 443 deletions
@@ -944,25 +944,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
var record = this.model.get(ev.data.id, { raw: true });
var res_id = record.res_id || record.res_ids[0];
var result = await this._rpc({
route: '/web/dataset/call_button',
params: {
model: 'ir.translation',
method: 'translate_fields',
args: [record.model, res_id, ev.data.fieldName],
kwargs: { context: record.getContext() },
}
});
this.translationDialog = new TranslationDialog(this, {
domain: result.domain,
searchName: result.context.search_default_name,
fieldName: ev.data.fieldName,
resId: res_id,
userLanguageValue: ev.target.value || '',
dataPointModel: record.model,
dataPointID: record.id,
isComingFromTranslationAlert: ev.data.isComingFromTranslationAlert,
isText: result.context.translation_type === 'text',
showSrc: result.context.translation_show_src,
context: record.getContext(),
});
return this.translationDialog.open();
},
@@ -14,29 +14,24 @@ odoo.define('web.TranslationDialog', function (require) {
* @constructor
* @param {Widget} parent
* @param {Object} [options]
* @param {string} [options.domain] the domain needed to get the translation terms
* @param {string} [options.fieldName] the name of the field currently translated (from the model of the form view)
* @param {string} [options.searchName] the name of the actual field that is the reference for translation (in the form of model,field)
* @param {integer} [options.resId] the ID of record currently translated
* @param {string} [options.userLanguageValue] the value of the translation in the language of the user, as seen in the from view (might be empty)
* @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) {
options = options || {};
this.fieldName = options.fieldName;
this.domain = options.domain;
this.searchName = options.searchName;
this.resId = options.resId;
this.userLanguageValue = options.userLanguageValue;
this.domain.push(['name', "=", `${this.searchName}`]);
this.dataPointModel = options.dataPointModel;
this.dataPointID = options.dataPointID;
this.isComingFromTranslationAlert = options.isComingFromTranslationAlert;
this.currentInterfaceLanguage = session.user_context.lang;
this.isText = options.isText;
this.showSrc = options.showSrc;
this.context = options.context;
this._super(parent, _.extend({
size: 'large',
@@ -56,31 +51,35 @@ odoo.define('web.TranslationDialog', function (require) {
this._loadLanguages().then((l) => {
this.languages = l;
return this._loadTranslations().then((t) => {
this.translations = t;
[this.translations, this.context] = t;
let id = 1;
this.translations.forEach((t) => t['id'] = id++);
this.isText = this.context.translation_type === 'text';
this.showSource = this.context.translation_show_source;
});
}),
]).then(() => {
this.data = this.translations.map((term) => {
let relatedLanguage = this.languages.find((language) => language[0] === term.lang);
if (!term.value && !this.showSrc) {
term.value = term.src;
if (!term.value && !this.showSource) {
term.value = term.source;
}
return {
id: term.id,
lang: term.lang,
langName: relatedLanguage[1],
source: term.src,
source: term.source,
// 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.showSource &&
!this.isComingFromTranslationAlert) ?
this.userLanguageValue : term.value || ''
};
});
this.data.sort((left, right) =>
(left.langName < right.langName || (left.langName === right.langName && left.source < right.source)) ? -1 : 1);
left.langName.localeCompare(right.langName));
});
},
@@ -92,12 +91,11 @@ odoo.define('web.TranslationDialog', function (require) {
* @private
*/
_loadTranslations: function () {
const domain = [...this.domain, ['lang', 'in', this.languages.map(l => l[0])]];
return this._rpc({
model: 'ir.translation',
method: 'search_read',
fields: ['lang', 'src', 'value'],
domain: domain,
model: this.dataPointModel,
method: 'get_field_translations',
args: [[this.resId], this.fieldName],
context: this.context,
});
},
/**
@@ -137,16 +135,16 @@ odoo.define('web.TranslationDialog', function (require) {
this.el.querySelectorAll('input[type=text],textarea').forEach((t) => {
var initialValue = this.data.find((d) => d.id == t.dataset.id);
if (initialValue.value !== t.value) {
updatedTerm[t.dataset.id] = t.value;
updatedTerm[t.dataset.id] = {lang: initialValue.lang, source: initialValue.source, value: t.value};
if (initialValue.lang === this.currentInterfaceLanguage && !this.showSrc) {
if (initialValue.lang === this.currentInterfaceLanguage && !this.showSource) {
// 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];
changes[this.fieldName] = t.value;
updateFormViewField = {
dataPointID: this.dataPointID,
changes: changes,
@@ -158,16 +156,23 @@ odoo.define('web.TranslationDialog', function (require) {
// updatedTerm only contains the id and values of the terms that
// have been updated by the user
var saveUpdatedTermsProms = Object.keys(updatedTerm).map((id) => {
var writeTranslation = {
model: 'ir.translation',
method: 'write',
context: this.context,
args: [[parseInt(id, 10)], { value: updatedTerm[id] }]
};
return this._rpc(writeTranslation);
});
return Promise.all(saveUpdatedTermsProms).then(() => {
const translations = {};
if (this.showSource) { // model terms translation
Object.entries(updatedTerm).forEach(([id, term]) => {
if (!translations[term.lang]) {
translations[term.lang] = {};
}
translations[term.lang][term.source] = term.value;
});
}
else { // model translation
Object.entries(updatedTerm).forEach(([id, term]) => translations[term.lang] = term.value);
}
return this._rpc({
model: this.dataPointModel,
method: 'update_field_translations',
args: [[this.resId], this.fieldName, translations],
}).then(() => {
// we might have to update the value of the field on the form
// view that opened the translation dialog
if (updateFormViewField) {
@@ -2,13 +2,13 @@
<templates id="template" xml:space="preserve">
<div t-name="web.TranslationDialogWidget" class="o_translation_dialog">
<div class="row" t-foreach="widget.data" t-as="term">
<div t-attf-class="col-12 #{widget.showSrc ? 'col-lg-2' : 'col-lg-3'} #{widget.currentInterfaceLanguage == term.lang ? 'o_language_current' : ''}">
<div t-attf-class="col-12 #{widget.showSource ? 'col-lg-2' : 'col-lg-3'} #{widget.currentInterfaceLanguage == term.lang ? 'o_language_current' : ''}">
<t t-esc="term.langName"></t>
</div>
<div t-if="widget.showSrc" class="col-12 col-lg-3 source">
<div t-if="widget.showSource" class="col-12 col-lg-3 source">
<t t-esc="term.source"></t>
</div>
<div t-attf-class="col-12 #{widget.showSrc ? 'col-lg-7' : 'col-lg-9'} translation">
<div t-attf-class="col-12 #{widget.showSource ? 'col-lg-7' : 'col-lg-9'} translation">
<input t-if="!widget.isText" type="text"
class="o_field_char o_input"
t-att-value="term.value"
@@ -17,7 +17,7 @@
<textarea t-if="widget.isText"
t-esc="term.value"
t-att-data-id="term.id"
t-att-rows="widget.showSrc ? 2 : 5"
t-att-rows="widget.showSource ? 2 : 5"
class="o_field_text o_field_translate o_field_widget o_input">
</textarea>
</div>
@@ -17,7 +17,6 @@ const { Component, useEnv } = owl;
*/
export function useTranslationDialog() {
const dialog = useService("dialog");
const rpc = useService("rpc");
const env = useEnv();
async function openTranslationDialog({ record, fieldName, updateField }) {
@@ -43,22 +42,14 @@ export function useTranslationDialog() {
return;
}
}
const { resModel, resId, context } = record;
const result = await rpc("/web/dataset/call_button", {
model: "ir.translation",
method: "translate_fields",
args: [resModel, resId, fieldName],
kwargs: { context },
});
const { resModel, resId } = record;
dialog.add(TranslationDialog, {
domain: result.domain,
searchName: result.context.search_default_name,
fieldName: fieldName,
resId: resId,
resModel: resModel,
userLanguageValue: record.data[fieldName] || "",
isComingFromTranslationAlert: false,
isText: result.context.translation_type === "text",
showSource: result.context.translation_show_src,
updateField,
});
}
@@ -20,18 +20,22 @@ export class TranslationDialog extends Component {
onWillStart(async () => {
const languages = await loadLanguages(this.orm);
const translations = await this.loadTranslations(languages);
const [translations, context] = await this.loadTranslations(languages);
let id = 1;
translations.forEach((t) => (t.id = id++));
this.props.isText = context.translation_type === "text";
this.props.showSource = context.translation_show_source;
this.terms = translations.map((term) => {
const relatedLanguage = languages.find((l) => l[0] === term.lang);
if (!term.value && !this.props.showSource) {
term.value = term.src;
term.value = term.source;
}
return {
id: term.id,
lang: term.lang,
langName: relatedLanguage[1],
source: term.src,
source: term.source,
// 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
@@ -43,11 +47,7 @@ export class TranslationDialog extends Component {
: term.value || "",
};
});
this.terms.sort((a, b) =>
a.langName < b.langName || (a.langName === b.langName && a.source < b.source)
? -1
: 1
);
this.terms.sort((a, b) => a.langName.localeCompare(b.langName));
});
}
@@ -63,23 +63,37 @@ export class TranslationDialog extends Component {
* Load the translation terms for the installed language, for the current model and res_id
*/
async loadTranslations(languages) {
const domain = [...this.domain, ["lang", "in", languages.map((l) => l[0])]];
return this.orm.searchRead("ir.translation", domain, ["lang", "src", "value"]);
return this.orm.call(this.props.resModel, "get_field_translations", [
[this.props.resId],
this.props.fieldName,
]);
}
/**
* Save all the terms that have been updated
*/
async onSave() {
await Promise.all(
this.terms.map(async (term) => {
if (term.id in this.updatedTerms && term.value !== this.updatedTerms[term.id]) {
await this.orm.write("ir.translation", [term.id], {
value: this.updatedTerms[term.id],
});
const translations = {};
this.terms.map((term) => {
const updatedTermValue = this.updatedTerms[term.id];
if (term.id in this.updatedTerms && term.value !== updatedTermValue) {
if (this.props.showSource) {
if (!translations[term.lang]) {
translations[term.lang] = {};
}
translations[term.lang][term.source] = updatedTermValue;
} else {
translations[term.lang] = updatedTermValue;
}
})
);
}
});
await this.orm.call(this.props.resModel, "update_field_translations", [
[this.props.resId],
this.props.fieldName,
translations,
]);
// we might have to update the value of the field on the form
// view that opened the translation dialog
@@ -160,19 +160,6 @@ QUnit.module('Legacy basic_fields', {
position: "after",
}]
},
"ir.translation": {
fields: {
lang: {type: "char"},
value: {type: "char"},
res_id: {type: "integer"}
},
records: [{
id: 99,
res_id: 37,
value: '',
lang: 'en_US'
}]
},
};
target = getFixture();
@@ -1653,25 +1640,18 @@ QUnit.module('Legacy basic_fields', {
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: 'partnes,foo'},
});
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
assert.deepEqual(args.args, [[1],"foo"], "should translate the foo field of the record");
return Promise.resolve([
[{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}],
{translation_type: "char", translation_show_source: false},
]);
}
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: '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");
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
assert.deepEqual(args.args, [[1], "foo", {"en_US": "english value"}], "the new translation value should be written");
return Promise.resolve();
}
return this._super.apply(this, arguments);
@@ -1741,31 +1721,21 @@ QUnit.module('Legacy basic_fields', {
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/partner/get_field_translations") {
assert.deepEqual(args.args, [[1],"foo"], "should translate the foo field of the record");
return Promise.resolve([
[{lang: "en_US", source: "first paragraph", value: "first paragraph"},
{lang: "en_US", source: "second paragraph", value: "second paragraph"},
{lang: "fr_BE", source: "first paragraph", value: "premier paragraphe"},
{lang: "fr_BE", source: "second paragraph", value: "deuxième paragraphe"}],
{translation_type: "char", translation_show_source: 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");
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
assert.deepEqual(args.args, [[1], "foo", {"en_US": {"first paragraph": "first paragraph modified"}}], "the new translation value should be written");
return Promise.resolve();
}
return this._super.apply(this, arguments);
@@ -2694,15 +2664,15 @@ QUnit.module('Legacy basic_fields', {
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
assert.deepEqual(args.args, ["partner",1,"txt"], 'should call "call_button" route');
return Promise.resolve({
domain: [],
context: {search_default_name: 'partnes,foo'},
});
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
assert.deepEqual(args.args, [[1],"txt"], "should translate the txt field of the record");
return Promise.resolve([
[{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}],
{translation_type: "text", translation_show_source: false},
]);
}
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)"]]);
}
return this._super.apply(this, arguments);
},
@@ -140,19 +140,6 @@ QUnit.module('LegacyViews', {
{id: 14, display_name: "silver", color: 5},
]
},
"ir.translation": {
fields: {
lang_code: {type: "char"},
value: {type: "char"},
res_id: {type: "integer"}
},
records: [{
id: 99,
res_id: 12,
value: '',
lang_code: 'en_US'
}]
},
user: {
fields: {
name: {string: "Name", type: "char"},
@@ -7382,16 +7369,7 @@ QUnit.module('LegacyViews', {
});
QUnit.test('translation alerts preserved on reverse breadcrumb', async function (assert) {
assert.expect(2);
serverData.models['ir.translation'] = {
fields: {
name: { string: "name", type: "char" },
source: {string: "Source", type: "char"},
value: {string: "Value", type: "char"},
},
records: [],
};
assert.expect(1);
serverData.models.partner.fields.foo.translate = true;
@@ -7402,12 +7380,6 @@ QUnit.module('LegacyViews', {
'</sheet>' +
'</form>',
'partner,false,search': '<search></search>',
'ir.translation,false,list': '<tree>' +
'<field name="name"/>' +
'<field name="source"/>' +
'<field name="value"/>' +
'</tree>',
'ir.translation,false,search': '<search></search>',
};
serverData.actions = {
@@ -7418,14 +7390,6 @@ QUnit.module('LegacyViews', {
type: 'ir.actions.act_window',
views: [[false, 'form']],
},
2: {
id: 2,
name: 'Translate',
res_model: 'ir.translation',
type: 'ir.actions.act_window',
views: [[false, 'list']],
target: 'current',
}
};
const webClient = await createWebClient({ serverData });
@@ -7440,13 +7404,6 @@ QUnit.module('LegacyViews', {
assert.containsOnce(target, '.o_legacy_form_view .alert > div',
"should have a translation alert");
await doAction(webClient, 2);
await testUtils.dom.click($('.o_control_panel .breadcrumb a:first'));
await legacyExtraNextTick();
assert.containsOnce(target, '.o_legacy_form_view .alert > div',
"should have a translation alert");
});
QUnit.test('translate event correctly handled with multiple controllers', async function (assert) {
@@ -7485,16 +7442,16 @@ QUnit.module('LegacyViews', {
if (route === '/web/dataset/call_kw/product/get_formview_id') {
return Promise.resolve(false);
}
if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
assert.deepEqual(args.args, ["product",37,"name"], 'should call "call_button" route');
if (route === "/web/dataset/call_kw/product/get_field_translations") {
assert.deepEqual(args.args, [[37],"name"], "should translate the name field of the record");
nbTranslateCalls++;
return Promise.resolve({
domain: [],
context: {search_default_name: 'partnes,foo'},
});
return Promise.resolve([
[{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}],
{translation_type: "char", translation_show_source: false},
]);
}
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)"]]);
}
return this._super.apply(this, arguments);
},
@@ -123,31 +123,6 @@ QUnit.module('LegacyViews', {
{id: "2-20170808020000", name: "virtual"},
]
},
"ir.translation": {
fields: {
lang_code: {type: "char"},
src: {type: "char"},
value: {type: "char"},
res_id: {type: "integer"},
name: {type: "char"},
lang: {type: "char"},
},
records: [{
id: 99,
res_id: 1,
value: '',
lang_code: 'en_US',
lang: 'en_US',
name: 'foo,foo'
},{
id: 100,
res_id: 1,
value: '',
lang_code: 'fr_BE',
lang: 'fr_BE',
name: 'foo,foo'
}]
},
};
serverData = { models: this.data };
@@ -3620,11 +3595,11 @@ QUnit.module('LegacyViews', {
model: 'foo',
data: this.data,
mockRPC: function (route, args) {
if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
return Promise.resolve({
domain: [],
context: {search_default_name: 'foo,foo'},
});
if (route === "/web/dataset/call_kw/foo/get_field_translations") {
return Promise.resolve([
[{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}],
{translation_type: "char", translation_show_source: false},
]);
}
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([["en_US","English"], ["fr_BE", "Frenglish"]]);
@@ -123,21 +123,6 @@ QUnit.module("Fields", (hooks) => {
},
],
},
"ir.translation": {
fields: {
lang: { type: "char" },
value: { type: "char" },
res_id: { type: "integer" },
},
records: [
{
id: 99,
res_id: 37,
value: "",
lang: "en_US",
},
],
},
},
};
@@ -301,7 +286,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("char field translatable", async function (assert) {
assert.expect(13);
assert.expect(11);
serverData.models.partner.fields.foo.translate = true;
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
@@ -325,33 +310,25 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(route, { args, method, model }) {
if (route === "/web/dataset/call_button" && method === "translate_fields") {
assert.deepEqual(
args,
["partner", 1, "foo"],
'should call "call_button" route'
);
return Promise.resolve({
domain: [],
context: { search_default_name: "partnes,foo" },
});
}
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([
["en_US", "English"],
["fr_BE", "French (Belgium)"],
]);
}
if (method === "search_read" && model === "ir.translation") {
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
return Promise.resolve([
{ lang: "en_US", src: "yop", value: "yop", id: 42 },
{ lang: "fr_BE", src: "yop", value: "valeur français", id: 43 },
[
{ lang: "en_US", source: "yop", value: "yop" },
{ lang: "fr_BE", source: "yop", value: "valeur français" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
if (method === "write" && model === "ir.translation") {
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
assert.deepEqual(
args[1],
{ value: "english value" },
args[2],
{ en_US: "english value" },
"the new translation value should be written"
);
return Promise.resolve(null);
@@ -417,7 +394,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("html field translatable", async function (assert) {
assert.expect(6);
assert.expect(5);
serverData.models.partner.fields.foo.translate = true;
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
@@ -441,55 +418,48 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(route, { args, method, model }) {
if (route === "/web/dataset/call_button" && method === "translate_fields") {
assert.deepEqual(
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 (method === "search_read" && model === "ir.translation") {
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
return Promise.resolve([
{ lang: "en_US", src: "first paragraph", value: "first paragraph", id: 42 },
[
{
lang: "en_US",
source: "first paragraph",
value: "first paragraph",
},
{
lang: "en_US",
source: "second paragraph",
value: "second paragraph",
},
{
lang: "fr_BE",
source: "first paragraph",
value: "premier paragraphe",
},
{
lang: "fr_BE",
source: "second paragraph",
value: "deuxième paragraphe",
},
],
{
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,
translation_type: "char",
translation_show_source: true,
},
]);
}
if (method === "write" && model === "ir.translation") {
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
assert.deepEqual(
args[1],
{ value: "first paragraph modified" },
"Wrong update on translation"
args[2],
{ en_US: { "first paragraph": "first paragraph modified" } },
"the new translation value should be written"
);
return Promise.resolve(null);
}
@@ -49,21 +49,6 @@ QUnit.module("Fields", (hooks) => {
},
],
},
"ir.translation": {
fields: {
lang: { type: "char" },
value: { type: "char" },
res_id: { type: "integer" },
},
records: [
{
id: 99,
res_id: 37,
value: "",
lang: "en_US",
},
],
},
},
};
@@ -370,7 +355,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("text field translatable", async function (assert) {
assert.expect(4);
assert.expect(3);
serverData.models.partner.fields.txt.translate = true;
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
@@ -391,19 +376,20 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(route, { args, method }) {
if (route === "/web/dataset/call_button" && method === "translate_fields") {
assert.deepEqual(
args,
["partner", 1, "txt"],
`should call "call_button" route`
);
return Promise.resolve({
domain: [],
context: { search_default_name: "partnes,foo" },
});
}
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 (route === "/web/dataset/call_kw/partner/get_field_translations") {
return Promise.resolve([
[
{ lang: "en_US", source: "yop", value: "yop" },
{ lang: "fr_BE", source: "yop", value: "valeur français" },
],
{ translation_type: "text", translation_show_source: false },
]);
}
},
});
@@ -180,21 +180,6 @@ QUnit.module("Views", (hooks) => {
{ id: 14, display_name: "silver", color: 5 },
],
},
"ir.translation": {
fields: {
lang_code: { type: "char" },
value: { type: "char" },
res_id: { type: "integer" },
},
records: [
{
id: 99,
res_id: 12,
value: "",
lang_code: "en_US",
},
],
},
user: {
fields: {
name: { string: "Name", type: "char" },
@@ -8466,20 +8451,16 @@ QUnit.module("Views", (hooks) => {
["CUST2", "second custom"],
];
}
if (args.method === "translate_fields") {
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
assert.step(`translate args ${JSON.stringify(args.args)}`);
assert.step(`translate context ${JSON.stringify(args.kwargs.context)}`);
return {
domain: [["res_id", "=", args.args[1]]],
context: { search_default_name: "partner_type,foo" },
};
}
if (args.method === "search_read" && args.model === "ir.translation") {
assert.step(
`search_read translations args: ${JSON.stringify(
args.args
)} ; kwargs: ${JSON.stringify(args.kwargs)}`
);
return Promise.resolve([
[
{ lang: "CUST", source: "yop", value: "yop" },
{ lang: "CUST2", source: "yop", value: "valeur français" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
@@ -8487,9 +8468,8 @@ QUnit.module("Views", (hooks) => {
await click(target, ".o_field_translate.btn-link");
assert.verifySteps([
`translate args ["partner",1,"foo"]`,
`translate args [[1],"foo"]`,
`translate context {"lang":"en","uid":7,"tz":"taht"}`,
`search_read translations args: [] ; kwargs: {"domain":[["res_id","=",1],["name","=","partner_type,foo"],["lang","in",["CUST","CUST2"]]],"fields":["lang","src","value"],"context":{"lang":"en","uid":7,"tz":"taht"}}`,
]);
assert.containsOnce(target, ".modal");
@@ -8522,6 +8502,15 @@ QUnit.module("Views", (hooks) => {
["CUST2", "second custom"],
];
}
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
return Promise.resolve([
[
{ lang: "CUST", source: "yop", value: "yop" },
{ lang: "CUST2", source: "yop", value: "valeur français" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
@@ -8536,7 +8525,7 @@ QUnit.module("Views", (hooks) => {
await click(target, ".o_field_translate.btn-link");
await click(target.querySelectorAll(".modal-footer button")[0]); // save
assert.verifySteps(["create", "read", "translate_fields", "get_installed", "search_read"]);
assert.verifySteps(["create", "read", "get_installed", "get_field_translations"]);
assert.containsOnce(target, ".modal");
assert.strictEqual(target.querySelector(".modal-title").textContent, "Translate: foo");
});
@@ -8583,12 +8572,21 @@ QUnit.module("Views", (hooks) => {
["CUST2", "second custom"],
];
}
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
return Promise.resolve([
[
{ lang: "CUST", source: "yop", value: "yop" },
{ lang: "CUST2", source: "yop", value: "valeur français" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
assert.verifySteps(["get_views", "read"]);
await clickEdit(target);
await click(target, ".o_field_legacy_char .o_field_translate.btn-link");
assert.verifySteps(["translate_fields", "get_installed", "search_read"]);
assert.verifySteps(["get_installed", "get_field_translations"]);
assert.containsOnce(target, ".modal");
assert.strictEqual(target.querySelector(".modal-title").textContent, "Translate: foo");
});
@@ -8644,22 +8642,11 @@ QUnit.module("Views", (hooks) => {
});
QUnit.test("translation alerts preserved on reverse breadcrumb", async function (assert) {
serverData.models["ir.translation"] = {
fields: {
name: { string: "name", type: "char" },
source: { string: "Source", type: "char" },
value: { string: "Value", type: "char" },
},
records: [],
};
serverData.models.partner.fields.foo.translate = true;
serverData.views = {
"partner,false,form": `<form><sheet><field name="foo"/></sheet></form>`,
"partner,false,search": "<search></search>",
"ir.translation,false,list": `<tree><field name="name"/><field name="source"/><field name="value"/></tree>`,
"ir.translation,false,search": "<search></search>",
};
serverData.actions = {
@@ -8670,15 +8657,6 @@ QUnit.module("Views", (hooks) => {
type: "ir.actions.act_window",
views: [[false, "form"]],
},
2: {
id: 2,
name: "Translate",
res_model: "ir.translation",
type: "ir.actions.act_window",
views: [[false, "list"]],
target: "current",
flags: { search_view: true, action_buttons: true },
},
};
const webClient = await createWebClient({ serverData });
@@ -8692,17 +8670,12 @@ QUnit.module("Views", (hooks) => {
await clickSave(target);
assert.containsOnce(target, ".alert .o_field_translate", "should have a translation alert");
await doAction(webClient, 2);
await click(target.querySelector(".o_control_panel .breadcrumb a:nth-child(1)"));
assert.containsOnce(target, ".alert .o_field_translate", "should have a translation alert");
});
QUnit.test(
"translate event correctly handled with multiple controllers",
async function (assert) {
assert.expect(3);
assert.expect(2);
serverData.models.product.fields.name.translate = true;
serverData.models.partner.records[0].product_id = 37;
@@ -8742,23 +8715,21 @@ QUnit.module("Views", (hooks) => {
if (route === "/web/dataset/call_kw/product/get_formview_id") {
return false;
}
if (
route === "/web/dataset/call_button" &&
args.method === "translate_fields"
) {
assert.deepEqual(
args.args,
["product", 37, "name"],
'should call "call_button" route'
);
nbTranslateCalls++;
return {
domain: [],
context: { search_default_name: "partnes,foo" },
};
}
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return [["en_US"], ["fr_BE"]];
return [
["en_US", "English"],
["fr_BE", "French (Belgium)"],
];
}
if (route === "/web/dataset/call_kw/product/get_field_translations") {
nbTranslateCalls++;
return Promise.resolve([
[
{ lang: "en_US", source: "yop", value: "yop" },
{ lang: "fr_BE", source: "yop", value: "valeur français" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
@@ -197,34 +197,6 @@ QUnit.module("Views", (hooks) => {
},
records: [{ id: "2-20170808020000", name: "virtual" }],
},
"ir.translation": {
fields: {
lang_code: { type: "char" },
src: { type: "char" },
value: { type: "char" },
res_id: { type: "integer" },
name: { type: "char" },
lang: { type: "char" },
},
records: [
{
id: 99,
res_id: 1,
value: "",
lang_code: "en_US",
lang: "en_US",
name: "foo,foo",
},
{
id: 100,
res_id: 1,
value: "",
lang_code: "fr_BE",
lang: "fr_BE",
name: "foo,foo",
},
],
},
},
};
setupViewRegistries();
@@ -4096,18 +4068,21 @@ QUnit.module("Views", (hooks) => {
resModel: "foo",
serverData,
mockRPC(route, args) {
if (route === "/web/dataset/call_button" && args.method === "translate_fields") {
return Promise.resolve({
domain: [],
context: { search_default_name: "foo,foo" },
});
}
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([
["en_US", "English"],
["fr_BE", "Frenglish"],
]);
}
if (route === "/web/dataset/call_kw/foo/get_field_translations") {
return Promise.resolve([
[
{ lang: "en_US", source: "yop", value: "yop" },
{ lang: "fr_BE", source: "yop", value: "valeur français" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
arch: '<tree editable="top">' + '<field name="foo" required="1"/>' + "</tree>",
});
@@ -130,19 +130,6 @@ QUnit.module('web_editor', {}, function () {
body_arch: "<div class='field_body'>yep</div>",
}],
},
"ir.translation": {
fields: {
lang_code: {type: "char"},
value: {type: "char"},
res_id: {type: "integer"}
},
records: [{
id: 99,
res_id: 12,
value: '',
lang_code: 'en_US'
}]
},
});
testUtils.mock.patch(ajax, {
@@ -880,15 +867,18 @@ QUnit.module('web_editor', {}, function () {
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
assert.deepEqual(args.args, ['note.note', 1, 'body'], "should call 'call_button' route");
return Promise.resolve({
domain: [],
context: {search_default_name: 'partnes,foo'},
});
if (route === "/web/dataset/call_kw/note.note/get_field_translations") {
assert.deepEqual(args.args, [[1],"body"], "should translate the body field of the record");
return Promise.resolve([
[{lang: "en_US", source: "first paragraph", value: "first paragraph"},
{lang: "en_US", source: "second paragraph", value: "second paragraph"},
{lang: "fr_BE", source: "first paragraph", value: "premier paragraphe"},
{lang: "fr_BE", source: "second paragraph", value: "deuxième paragraphe"}],
{translation_type: "text", translation_show_source: true},
]);
}
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)"]]);
}
return this._super.apply(this, arguments);
},