This commit is contained in:
2026-07-11 01:28:32 +02:00
parent 0a6176cf53
commit 8e01c7c8ec
99 changed files with 34739 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<div class="row position-relative pt-lg-3 px-lg-5 pb-lg-3 p-md-4 p-3 mx-auto d-flex align-items-center" style="color:#333333; font-weight:500; font-size:16px; width:95%">
<section class="oe_container">
<div class="col-md-12 text-center" style="margin-top: 10px;">
<a target="_blank" href="https://otoolkit.app">
<h2>Otoolkit</h2>
</a>
<p>
<a href="https://otoolkit.app" target="_blank">Website: otoolkit.app</a><br>
Email: <a href="mailto:contact@otoolkit.app">contact@otoolkit.app</a>
</p>
</div>
<div class="col-md-12 text-center" style="margin-top: 30px;">
<a target="_blank" href="https://otoolkit.app/apps/auto-translate-fields">
<h2>Auto Translate Fields</h2>
</a>
<p>The <strong>Auto Translate Fields</strong> module enhances the translation process in Odoo by adding a convenient "Translate" button to the translation popup of any translatable field. When clicked, it automatically translates the field content into all configured languages in the system, using the current user's language as the source. This feature simplifies and accelerates the workflow for multilingual environments, ensuring that translations are consistent and up-to-date across the platform. Ideal for businesses that operate in multiple languages, this module improves efficiency by reducing manual translation efforts.</p>
<p>Instructions on how to use this module can be found at <a href="https://otoolkit.app/apps/auto-translate-fields">https://otoolkit.app/apps/auto-translate-fields</a></p>
</div>
<div class="col-md-12 text-center" style="margin-top: 30px;text-align: left !important;">
<h2 style="text-align: center !important">
<b>How it works</b>
</h2>
<div class="row text-dark">
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/translate-field.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="Classic text input with auto translate in odoo">
</div>
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Auto Translate</b>
</h3>
<p class="mt-4">When you translate your fields, you'll find a new button called «Auto Translate». Click on it to translate your text into all the languages of your instance. The user's base language is used as the starting language.</p>
</div>
</div>
<hr class="d-lg-none">
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Also works with HTML fields</b>
</h3>
<p class="mt-4">Automatic translation also works with HTML fields. This saves time by avoiding line-by-line translation.</p>
</div>
</div>
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/markdown-input.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="Markdown input with auto translate in odoo">
</div>
</div>
</div>
<div class="col-md-12 text-center mt-5 p-4" style="background-color: #f8f9fa; border-radius: 15px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
<h3 style="color: rgb(124, 58, 237);">Have any questions?</h3>
<p class="mt-3" style="color: #333;">If you have any questions or need more information, feel free to visit our website or contact us directly.</p>
<p>
<span href="https://otoolkit.app" class="btn otoolkit-open-site" style="background-color: rgb(124, 58, 237); color: #fff; border-radius: 5px; padding: 10px 20px; text-decoration: none;">Visit Our Website</span>
<a href="mailto:contact@otoolkit.app" class="btn" style="background-color: #fff; color: rgb(124, 58, 237); border: 2px solid rgb(124, 58, 237); border-radius: 5px; padding: 10px 20px; text-decoration: none;">Email Us</a>
</p>
<a href="https://otoolkit.app">https://otoolkit.app</a>
</div>
</section>
</div>
</body>
</html>
@@ -0,0 +1,25 @@
/** @odoo-module **/
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
export class JsonPrettyField extends Component {
static template = xml`
<p style="white-space: pre-wrap; background: #f5f5f5; padding: 12px; border-radius: 6px;">
<t t-esc="this.prettyJson"/>
</p>
`;
static props = standardFieldProps;
get prettyJson() {
try {
const value = this.props.record.data[this.props.name];
return JSON.stringify(value ?? {}, null, 4);
} catch (e) {
return "Invalid JSON";
}
}
}
registry.category("fields").add("json_pretty", { component: JsonPrettyField });
@@ -0,0 +1,210 @@
/** @odoo-module **/
import { TranslationDialog } from "@web/views/fields/translation_dialog";
import { patch } from "@web/core/utils/patch";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { useState } from "@odoo/owl";
patch(TranslationDialog.prototype, {
setup() {
super.setup(...arguments);
this.orm = useService('orm');
this.notification = useService("notification");
this.action = useService("action");
this.state = useState({ translating: false, translatingTermIds: {} });
},
/**
* Find the best source term to translate from for a given target term.
* Picks a term in a different language with the same source text.
* Priority: any candidate with actual translated content > en_US > first
* candidate. en_US is deprioritized because Odoo populates en_US from the
* record's base value — which may itself be in another language (e.g. a
* record edited primarily in French) and would mislabel the source to GPT.
*/
_findSourceTerm(targetTerm) {
const candidates = this.terms.filter(
t => t.lang !== targetTerm.lang && t.source === targetTerm.source
);
if (candidates.length === 0) return null;
return candidates.find(t => (this.updatedTerms[t.id] ?? t.value ?? "").trim())
|| candidates.find(t => t.lang === "en_US")
|| candidates[0];
},
/**
* Handle translation error notifications.
* Returns true if an error was displayed (caller should stop).
*/
_handleTranslationNotification(result) {
if (!result[0]) {
if (result[2] === "credits") {
this.notification.add(result[1], {
type: "danger",
buttons: [{
onClick: () => window.open("https://osolutions.app/pricing"),
primary: false,
name: _t("Get tokens"),
}],
});
} else if (result[2] === "settings") {
this.notification.add(result[1], {
type: "danger",
buttons: [{
onClick: () => this.action.doAction({
type: "ir.actions.act_window",
res_model: "res.config.settings",
views: [[false, "form"]],
target: "current",
context: { module: "otoolkit_auth" },
}),
primary: false,
name: _t("Open Settings"),
}],
});
} else {
this.notification.add(result[1], { type: "danger" });
}
return true;
}
return false;
},
/**
* Apply translated terms to the dialog state.
* Returns the count of terms updated.
*/
_applyTranslationResults(translatedTerms) {
let count = 0;
for (const item of translatedTerms) {
if (item.translation) {
this.updatedTerms[item.id] = item.translation;
const target = this.terms.find(t => t.id === item.id);
if (target) {
target.value = item.translation;
count++;
}
}
}
return count;
},
/**
* Whether any translation (bulk or single-term) is currently running.
*/
get anyTranslating() {
if (this.state.translating) return true;
for (const id in this.state.translatingTermIds) {
if (this.state.translatingTermIds[id]) return true;
}
return false;
},
/**
* Bulk auto-translate all empty terms.
*/
async autoTranslate() {
this.state.translating = true;
try {
const result = await this._translateText(this.terms, this.updatedTerms, null, null);
if (this._handleTranslationNotification(result)) return;
if (result[1].length === 0) {
return this.notification.add(
_t("All translations are already filled."), { type: "info" }
);
}
const count = this._applyTranslationResults(result[1]);
this.render();
if (count > 0) {
this.notification.add(_t("Successfully translated"), { type: "success" });
}
} finally {
this.state.translating = false;
}
},
/**
* Translate a single empty term.
*/
async translateSingleTerm(targetTerm) {
const sourceTerm = this._findSourceTerm(targetTerm);
if (!sourceTerm) {
return this.notification.add(
_t("No other language available to translate from."), { type: "warning" }
);
}
this.state.translatingTermIds[targetTerm.id] = true;
try {
const result = await this._translateText(
[sourceTerm, targetTerm], this.updatedTerms,
sourceTerm.lang, targetTerm.lang,
);
if (this._handleTranslationNotification(result)) return;
if (result[1].length === 0) {
return this.notification.add(
_t("No translation returned for this term."), { type: "warning" }
);
}
// Apply only the target term's translation. The API also returns
// an "echo" entry for the source-language term (its `translation`
// is the source text itself) — applying it would overwrite the
// source row's displayed value, which is wrong when the source
// peer is an empty row whose displayed `value` is empty (HTML
// showSource case).
const targetOnly = result[1].filter(item => item.id === targetTerm.id);
const count = this._applyTranslationResults(targetOnly);
this.render();
if (count > 0) {
this.notification.add(_t("Successfully translated"), { type: "success" });
} else {
this.notification.add(
_t("No translation returned for this term."), { type: "warning" }
);
}
} finally {
delete this.state.translatingTermIds[targetTerm.id];
}
},
async _translateText(terms, updated_terms, source_lang_hint = null, target_lang_hint = null) {
return await this.orm.call(
"translation.dialog", "translate_text",
[terms, updated_terms, source_lang_hint, target_lang_hint],
);
},
async onSave() {
const translations = {};
this.terms.forEach((term) => {
const updatedTermValue = this.updatedTerms[term.id];
if (term.id in this.updatedTerms && updatedTermValue !== term.originalValue) {
if (this.props.showSource) {
if (!translations[term.lang]) {
translations[term.lang] = {};
}
const oldTermValue = term.originalValue ? term.originalValue : term.source;
translations[term.lang][oldTermValue] = updatedTermValue || term.source;
} else {
translations[term.lang] = updatedTermValue || false;
}
}
});
await this.orm.call(this.props.resModel, "update_field_translations", [
[this.props.resId],
this.props.fieldName,
translations,
]);
await this.props.onSave();
this.props.close();
},
});
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-inherit="web.TranslationDialog" t-inherit-mode="extension">
<!-- Make translation column a flex container for inline button -->
<xpath expr="//div[hasclass('translation')]" position="attributes">
<attribute name="class" add="d-flex align-items-start gap-1" separator=" "/>
</xpath>
<!-- Make textarea fill remaining space -->
<xpath expr="//div[hasclass('translation')]//textarea" position="attributes">
<attribute name="class" add="flex-grow-1" separator=" "/>
</xpath>
<!-- Make input fill remaining space -->
<xpath expr="//div[hasclass('translation')]//input[@type='text']" position="attributes">
<attribute name="class" add="flex-grow-1" separator=" "/>
</xpath>
<!-- Single-term translate button (shown only for empty terms) -->
<xpath expr="//div[hasclass('translation')]" position="inside">
<button t-if="!term.value"
class="btn btn-sm btn-outline-primary px-2 o_translate_single_btn flex-shrink-0"
t-on-click="() => this.translateSingleTerm(term)"
t-att-disabled="anyTranslating"
t-att-title="'Translate to ' + term.langName">
<t t-if="state.translatingTermIds[term.id]">
<i class="fa fa-spinner fa-spin"/>
</t>
<t t-else="">
<i class="fa fa-language"/>
</t>
</button>
</xpath>
<!-- Footer: Save, Discard, Auto Translate -->
<xpath expr="//t[@t-set-slot='footer']" position="replace">
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="onSave">
Save
</button>
<button class="btn btn-secondary" t-on-click="props.close">
Discard
</button>
<button type="button" class="btn btn-secondary ml-3" t-on-click="autoTranslate" t-att-disabled="anyTranslating">
<t t-if="state.translating">
<i class="fa fa-spinner fa-spin me-1"/>Translating...
</t>
<t t-else="">Auto Translate</t>
</button>
</t>
</xpath>
</t>
</templates>