Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC
This commit is contained in:
BIN
translation_helper/static/description/icon.png
Normal file
BIN
translation_helper/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
13
translation_helper/static/src/env.js
Normal file
13
translation_helper/static/src/env.js
Normal file
@ -0,0 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
import * as envModule from "@web/env";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
|
||||
const originalMakeEnv = envModule.makeEnv;
|
||||
|
||||
patch(envModule, {
|
||||
async makeEnv() {
|
||||
const env = await originalMakeEnv(...arguments);
|
||||
env.translate = odoo.translate;
|
||||
return env;
|
||||
},
|
||||
});
|
||||
54
translation_helper/static/src/field_translation.js
Normal file
54
translation_helper/static/src/field_translation.js
Normal file
@ -0,0 +1,54 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import {Dropdown} from "@web/core/dropdown/dropdown";
|
||||
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
|
||||
import {FormLabel} from "@web/views/form/form_label";
|
||||
import {TranslationDialog} from "@web/views/fields/translation_dialog";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {browser} from "@web/core/browser/browser";
|
||||
|
||||
patch(FormLabel, {
|
||||
components: {
|
||||
...FormLabel.components,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
},
|
||||
});
|
||||
|
||||
patch(FormLabel.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.hasTranslation = this.getHasTranslation();
|
||||
},
|
||||
getHasTranslation() {
|
||||
return Boolean(odoo.translate);
|
||||
},
|
||||
|
||||
async onClickTranslate(attributeName) {
|
||||
const modelId = await this.env.services.orm.searchRead("ir.model", [["model", "=", this.props.record.resModel]], ["id"]);
|
||||
const fieldRecordId = await this.env.services.orm.searchRead(
|
||||
"ir.model.fields",
|
||||
[
|
||||
["model_id", "=", modelId[0].id],
|
||||
["name", "=", this.props.fieldName],
|
||||
],
|
||||
["id"]
|
||||
);
|
||||
|
||||
this.env.services.dialog.add(TranslationDialog, {
|
||||
fieldName: attributeName,
|
||||
resId: fieldRecordId[0].id,
|
||||
resModel: "ir.model.fields",
|
||||
userLanguageValue: "",
|
||||
userUserCurrentLang: true,
|
||||
isComingFromTranslationAlert: false,
|
||||
onSave: async () => {
|
||||
this.env.bus.trigger("CLEAR-CACHES");
|
||||
// Full page reload — preserves current URL including ?translate=1
|
||||
browser.location.reload();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
FormLabel.template = "translation_link_generation.FormLabel";
|
||||
20
translation_helper/static/src/field_translation.xml
Normal file
20
translation_helper/static/src/field_translation.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="translation_link_generation.FormLabel" t-inherit="web.FormLabel">
|
||||
<xpath expr="//sup" position="after">
|
||||
<Dropdown t-if="hasTranslation">
|
||||
<button type="button" class="btn btn-link btn-primary py-0 px-1">
|
||||
<i class="fa fa-language" />
|
||||
</button>
|
||||
<t t-set-slot="content">
|
||||
<t t-if="tooltipHelp">
|
||||
<DropdownItem onSelected="() => this.onClickTranslate('help')">Help</DropdownItem>
|
||||
</t>
|
||||
<DropdownItem onSelected="() => this.onClickTranslate('field_description')">String</DropdownItem>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
96
translation_helper/static/src/fields/translation_dialog.js
Normal file
96
translation_helper/static/src/fields/translation_dialog.js
Normal file
@ -0,0 +1,96 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import {_t, loadLanguages} from "@web/core/l10n/translation";
|
||||
import {jsToPyLocale} from "@web/core/l10n/utils";
|
||||
import {TranslationDialog} from "@web/views/fields/translation_dialog";
|
||||
import {user} from "@web/core/user";
|
||||
import {onWillStart} from "@odoo/owl";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
|
||||
// Add userUserCurrentLang prop that doesn't exist in Odoo 19's TranslationDialog
|
||||
TranslationDialog.props = {
|
||||
...TranslationDialog.props,
|
||||
userUserCurrentLang: {type: Boolean, optional: true},
|
||||
};
|
||||
|
||||
patch(TranslationDialog.prototype, {
|
||||
setup() {
|
||||
this.title = _t("Translate: %s", this.props.fieldName);
|
||||
this.user = user;
|
||||
this.orm = useService("orm");
|
||||
this.terms = [];
|
||||
this.updatedTerms = {};
|
||||
this.translateIsCallable = false;
|
||||
|
||||
onWillStart(async () => {
|
||||
const allLanguages = await loadLanguages(this.orm);
|
||||
const languages = this.props.userUserCurrentLang
|
||||
? allLanguages.filter((l) => l[0] === jsToPyLocale(user.lang))
|
||||
: allLanguages;
|
||||
|
||||
const [translations, context] = await this.loadTranslations(languages);
|
||||
this.translateIsCallable = Boolean(context.translation_show_source);
|
||||
|
||||
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);
|
||||
const termInfo = {
|
||||
...term,
|
||||
langName: relatedLanguage ? relatedLanguage[1] : term.lang,
|
||||
value: term.value || "",
|
||||
};
|
||||
if (
|
||||
term.lang === jsToPyLocale(user.lang) &&
|
||||
!this.props.showSource &&
|
||||
!this.props.isComingFromTranslationAlert &&
|
||||
this.props.userLanguageValue
|
||||
) {
|
||||
this.updatedTerms[term.id] = this.props.userLanguageValue;
|
||||
termInfo.value = this.props.userLanguageValue;
|
||||
}
|
||||
return termInfo;
|
||||
});
|
||||
this.terms.sort((a, b) => a.langName.localeCompare(b.langName));
|
||||
});
|
||||
},
|
||||
|
||||
async loadTranslations(languages) {
|
||||
const langs = languages.map((l) => l[0]);
|
||||
return this.orm.call(
|
||||
this.props.resModel,
|
||||
"get_field_translations",
|
||||
[[this.props.resId], this.props.fieldName, langs]
|
||||
);
|
||||
},
|
||||
|
||||
async onSave() {
|
||||
const translations = {};
|
||||
this.terms.forEach((term) => {
|
||||
const updatedTermValue = this.updatedTerms[term.id];
|
||||
if (term.id in this.updatedTerms && term.value !== updatedTermValue) {
|
||||
if (this.translateIsCallable) {
|
||||
if (!translations[term.lang]) {
|
||||
translations[term.lang] = {};
|
||||
}
|
||||
const oldTermValue = term.value || term.source;
|
||||
translations[term.lang][oldTermValue] = updatedTermValue || term.source;
|
||||
translations[term.lang].source = 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();
|
||||
},
|
||||
});
|
||||
1
translation_helper/static/src/images/translate.svg
Normal file
1
translation_helper/static/src/images/translate.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 5h12m-6-2v2m1.0482 9.5c-1.52737-1.5822-2.76747-3.4435-3.63633-5.5m6.08813 9h7m-8.5 3 5-10 5 10m-8.2489-16c-.968 5.7702-4.68141 10.6095-9.7511 13.129" stroke="#4a5568" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
53
translation_helper/static/src/menus/dropdown.js
Normal file
53
translation_helper/static/src/menus/dropdown.js
Normal file
@ -0,0 +1,53 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
// In Odoo 19, Dropdown uses an inline xml`` template (no QWeb template named web.Dropdown).
|
||||
// MenuTranslationButton is injected into DropdownItem via JS patch + XML template inheritance
|
||||
// of web.DropdownItem (NOT web.NavBar.DropdownItem which is unused as a component template).
|
||||
// NavBar is also patched to support translate buttons on section buttons with children.
|
||||
|
||||
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
|
||||
import {NavBar} from "@web/webclient/navbar/navbar";
|
||||
import {MenuTranslationButton} from "./menu_translation_button";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
|
||||
// Patch DropdownItem — for leaf menu items (no children)
|
||||
patch(DropdownItem, {
|
||||
components: {
|
||||
...DropdownItem.components,
|
||||
MenuTranslationButton,
|
||||
},
|
||||
});
|
||||
|
||||
patch(DropdownItem.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.hasTranslation = this.getHasTranslation();
|
||||
},
|
||||
|
||||
onClick(ev) {
|
||||
if (ev.target.classList.contains("translate-middle-y")) {
|
||||
return;
|
||||
}
|
||||
super.onClick(ev);
|
||||
},
|
||||
|
||||
getHasTranslation() {
|
||||
return Boolean(odoo.translate);
|
||||
},
|
||||
});
|
||||
|
||||
// Patch NavBar — for section buttons with children (Dropdown-based)
|
||||
patch(NavBar, {
|
||||
components: {
|
||||
...NavBar.components,
|
||||
MenuTranslationButton,
|
||||
},
|
||||
});
|
||||
|
||||
patch(NavBar.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
// expose to template via __translate__ variable
|
||||
this.__translate__ = Boolean(odoo.translate);
|
||||
},
|
||||
});
|
||||
18
translation_helper/static/src/menus/dropdown.xml
Normal file
18
translation_helper/static/src/menus/dropdown.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Patch SectionsMenu: add translate button inside Dropdown-based section buttons (items with children) -->
|
||||
<t t-name="translation_helper.NavBar.SectionsMenu" t-inherit="web.NavBar.SectionsMenu" t-inherit-mode="extension">
|
||||
<xpath expr="//button[@t-att-data-menu-xmlid]/span" position="after">
|
||||
<MenuTranslationButton t-if="__translate__" menuXmlId="section.xmlid" />
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<!-- Patch MenuSlot: add translate button after group headers (items with children inside dropdown) -->
|
||||
<t t-name="translation_helper.NavBar.SectionsMenu.Dropdown.MenuSlot" t-inherit="web.NavBar.SectionsMenu.Dropdown.MenuSlot" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('dropdown-menu_group')]" position="after">
|
||||
<MenuTranslationButton t-if="__translate__ and item.xmlid" menuXmlId="item.xmlid" />
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
10
translation_helper/static/src/menus/dropdown_item.xml
Normal file
10
translation_helper/static/src/menus/dropdown_item.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="translation_helper.DropdownItem" t-inherit="web.DropdownItem" t-inherit-mode="extension">
|
||||
<xpath expr="//t[@t-slot='default']" position="after">
|
||||
<MenuTranslationButton t-if="hasTranslation and props.attrs and props.attrs['data-menu-xmlid']" menuXmlId="props.attrs['data-menu-xmlid']" />
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@ -0,0 +1,92 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {Component, useState} from "@odoo/owl";
|
||||
import {useOwnedDialogs, useService} from "@web/core/utils/hooks";
|
||||
import {TranslationDialog} from "@web/views/fields/translation_dialog";
|
||||
|
||||
/**
|
||||
* @param {Object} env - OWL environment from the component (this.env)
|
||||
* @returns {Function} openTranslationDialog
|
||||
*/
|
||||
export function useTranslationDialog(env) {
|
||||
const addDialog = useOwnedDialogs();
|
||||
|
||||
async function openTranslationDialog({name, id}) {
|
||||
addDialog(TranslationDialog, {
|
||||
fieldName: "name",
|
||||
resId: id,
|
||||
resModel: "ir.ui.menu",
|
||||
userLanguageValue: name || "",
|
||||
isComingFromTranslationAlert: false,
|
||||
userUserCurrentLang: true,
|
||||
onSave: async () => {
|
||||
env.bus.trigger("CLEAR-CACHES");
|
||||
await env.services.action.doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "reload",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return openTranslationDialog;
|
||||
}
|
||||
|
||||
export class MenuTranslationButton extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.state = useState({record: {}});
|
||||
this.translationDialog = useTranslationDialog(this.env);
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
const xmlId = this.props.menuXmlId;
|
||||
const [module, name] = xmlId.split(".");
|
||||
if (!module || !name) {
|
||||
console.error("Неверный формат menuXmlId");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [menuIdRecord] = await this.orm.searchRead(
|
||||
"ir.model.data",
|
||||
[
|
||||
["model", "=", "ir.ui.menu"],
|
||||
["name", "=", name],
|
||||
["module", "=", module],
|
||||
],
|
||||
["res_id"]
|
||||
);
|
||||
|
||||
if (!menuIdRecord) {
|
||||
console.error("Меню не найдено по XML ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const menuId = menuIdRecord.res_id;
|
||||
const [menuRecord] = await this.orm.searchRead("ir.ui.menu", [["id", "=", menuId]], ["id", "name", "action"]);
|
||||
|
||||
if (!menuRecord) {
|
||||
console.error("Запись меню не найдена");
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.record = menuRecord;
|
||||
|
||||
this.translationDialog({
|
||||
name: menuRecord.name,
|
||||
id: menuRecord.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка при получении меню:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuTranslationButton.template = "translation_helper.MenuTranslationButton";
|
||||
MenuTranslationButton.props = {
|
||||
menuXmlId: {
|
||||
type: String,
|
||||
optional: false,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="translation_helper.MenuTranslationButton">
|
||||
<i class="fa fa-language translate-middle-y" style="margin-left: 5px;" aria-hidden="true" t-on-click.prevent.stop="onClick" />
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
14
translation_helper/static/src/redefine_view_hook.js
Normal file
14
translation_helper/static/src/redefine_view_hook.js
Normal file
@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
import * as viewHookModule from "@web/views/view_hook";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useComponent } from "@odoo/owl";
|
||||
import { useTranslateCategory } from "@translation_helper/translate/translate_context";
|
||||
|
||||
const originalUseSetupView = viewHookModule.useSetupView;
|
||||
|
||||
patch(viewHookModule, {
|
||||
useSetupView(params) {
|
||||
useTranslateCategory("view", { component: useComponent() });
|
||||
return originalUseSetupView(params);
|
||||
},
|
||||
});
|
||||
12
translation_helper/static/src/response_from_wizard.js
Normal file
12
translation_helper/static/src/response_from_wizard.js
Normal file
@ -0,0 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
async function handleResponseFromWizard(env) {
|
||||
const actionService = env.services.action;
|
||||
env.bus.trigger("CLEAR-CACHES");
|
||||
await actionService.doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "soft_reload",
|
||||
});
|
||||
}
|
||||
registry.category("actions").add("translation_helper.response_from_wizard", handleResponseFromWizard);
|
||||
@ -0,0 +1,16 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import {ResConfigDevTool} from "@web/webclient/settings_form_view/widgets/res_config_dev_tool";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {router} from "@web/core/browser/router";
|
||||
|
||||
patch(ResConfigDevTool.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.isTranslate = Boolean(odoo.translate);
|
||||
},
|
||||
|
||||
activateTranslate(value) {
|
||||
router.pushState({translate: value}, {reload: true});
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="res_config_dev_tool" t-inherit-mode="extension">
|
||||
<xpath expr="//div[@id='developer_tool']" position="after">
|
||||
<div id="translator_tool">
|
||||
<SettingsBlock title.translate="Translator Tools">
|
||||
<Setting addLabel="false">
|
||||
<a t-if="!isTranslate" class="d-block" t-on-click.prevent="() => this.activateTranslate(1)" href="#">Activate the translate mode</a>
|
||||
<a t-if="isTranslate" class="d-block" t-on-click.prevent="() => this.activateTranslate(0)" href="#">Deactivate the translate mode</a>
|
||||
</Setting>
|
||||
</SettingsBlock>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
4
translation_helper/static/src/translate/__init__.js
Normal file
4
translation_helper/static/src/translate/__init__.js
Normal file
@ -0,0 +1,4 @@
|
||||
/** @odoo-module **/
|
||||
export * from "./translate_context";
|
||||
export { TranslateMenu } from "./translate_menu";
|
||||
export { TranslateMenuItems } from "./translate_menu_items";
|
||||
6
translation_helper/static/src/translate/debug_menu.scss
Normal file
6
translation_helper/static/src/translate/debug_menu.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.o_dialog {
|
||||
.o_translate_manager .dropdown-toggle {
|
||||
padding: 0 4px;
|
||||
margin: 2px 10px 2px 0;
|
||||
}
|
||||
}
|
||||
57
translation_helper/static/src/translate/translate_contex.js
Normal file
57
translation_helper/static/src/translate/translate_contex.js
Normal file
@ -0,0 +1,57 @@
|
||||
/** @odoo-module **/
|
||||
import { useEffect, useEnv, useSubEnv } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const translateRegistry = registry.category("translate");
|
||||
const translateContextSymbol = Symbol("translateContext");
|
||||
|
||||
class TranslateContext {
|
||||
constructor(env, defaultCategories = []) {
|
||||
this.env = env;
|
||||
this.categories = new Map(defaultCategories.map(cat => [cat, [{}]]));
|
||||
}
|
||||
|
||||
activateCategory(category, context) {
|
||||
const contexts = this.categories.get(category) || new Set();
|
||||
contexts.add(context);
|
||||
this.categories.set(category, contexts);
|
||||
return () => {
|
||||
contexts.delete(context);
|
||||
if (!contexts.size) this.categories.delete(category);
|
||||
};
|
||||
}
|
||||
|
||||
async getItems() {
|
||||
return [...this.categories.entries()]
|
||||
.flatMap(([category, contexts]) =>
|
||||
translateRegistry
|
||||
.category(category)
|
||||
.getAll()
|
||||
.map(factory => factory({ env: this.env, ...Object.assign({}, ...contexts) }))
|
||||
)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => (a.sequence || 1000) - (b.sequence || 1000));
|
||||
}
|
||||
}
|
||||
|
||||
export function createTranslateContext(env, { categories = [] } = {}) {
|
||||
return { [translateContextSymbol]: new TranslateContext(env, categories) };
|
||||
}
|
||||
|
||||
export function useOwnTranslateContext({ categories = [] } = {}) {
|
||||
useSubEnv(createTranslateContext(useEnv(), { categories }));
|
||||
}
|
||||
|
||||
export function useEnvTranslateContext() {
|
||||
const translateContext = useEnv()[translateContextSymbol];
|
||||
if (!translateContext) throw new Error("No translate context in environment");
|
||||
return translateContext;
|
||||
}
|
||||
|
||||
export function useTranslateCategory(category, context = {}) {
|
||||
const env = useEnv();
|
||||
if (env.translate) {
|
||||
const translateContext = useEnvTranslateContext();
|
||||
useEffect(() => translateContext.activateCategory(category, context), () => []);
|
||||
}
|
||||
}
|
||||
89
translation_helper/static/src/translate/translate_context.js
Normal file
89
translation_helper/static/src/translate/translate_context.js
Normal file
@ -0,0 +1,89 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {useEffect, useEnv, useSubEnv} from "@odoo/owl";
|
||||
import {memoize} from "@web/core/utils/functions";
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
const translateRegistry = registry.category("translate");
|
||||
|
||||
const getAccessRights = memoize(async function getAccessRights(orm) {
|
||||
const rightsToCheck = {
|
||||
"ir.ui.view": "write",
|
||||
"ir.rule": "read",
|
||||
"ir.model.access": "read",
|
||||
};
|
||||
const proms = Object.entries(rightsToCheck).map(([model, operation]) => {
|
||||
return orm.call(model, "check_access_rights", [], {
|
||||
operation,
|
||||
raise_exception: false,
|
||||
});
|
||||
});
|
||||
const [canEditView, canSeeRecordRules, canSeeModelAccess] = await Promise.all(proms);
|
||||
const accessRights = {canEditView, canSeeRecordRules, canSeeModelAccess};
|
||||
return accessRights;
|
||||
});
|
||||
|
||||
class TranslateContext {
|
||||
constructor(env, defaultCategories) {
|
||||
this.orm = env.services.orm;
|
||||
this.categories = new Map(defaultCategories.map((cat) => [cat, new Set()]));
|
||||
}
|
||||
|
||||
activateCategory(category, context) {
|
||||
const contexts = this.categories.get(category) || new Set();
|
||||
contexts.add(context);
|
||||
this.categories.set(category, contexts);
|
||||
|
||||
return () => {
|
||||
contexts.delete(context);
|
||||
if (contexts.size === 0) {
|
||||
this.categories.delete(category);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getItems(env) {
|
||||
const accessRights = await getAccessRights(this.orm);
|
||||
return [...this.categories.entries()]
|
||||
.flatMap(([category, contexts]) => {
|
||||
return translateRegistry
|
||||
.category(category)
|
||||
.getAll()
|
||||
.map((factory) => factory(Object.assign({env, accessRights}, ...contexts)));
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((x, y) => {
|
||||
const xSeq = x.sequence || 1000;
|
||||
const ySeq = y.sequence || 1000;
|
||||
return xSeq - ySeq;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const translateContextSymbol = Symbol("translateContext");
|
||||
export function createTranslateContext(env, {categories = []} = {}) {
|
||||
return {[translateContextSymbol]: new TranslateContext(env, categories)};
|
||||
}
|
||||
|
||||
export function useOwnTranslateContext({categories = []} = {}) {
|
||||
useSubEnv(createTranslateContext(useEnv(), {categories}));
|
||||
}
|
||||
|
||||
export function useEnvTranslateContext() {
|
||||
const translateContext = useEnv()[translateContextSymbol];
|
||||
if (!translateContext) {
|
||||
throw new Error("There is no translate context available in the current environment.");
|
||||
}
|
||||
return translateContext;
|
||||
}
|
||||
|
||||
export function useTranslateCategory(category, context = {}) {
|
||||
const env = useEnv();
|
||||
if (env.translate) {
|
||||
const translateContext = useEnvTranslateContext();
|
||||
useEffect(
|
||||
() => translateContext.activateCategory(category, context),
|
||||
() => []
|
||||
);
|
||||
}
|
||||
}
|
||||
82
translation_helper/static/src/translate/translate_items.js
Normal file
82
translation_helper/static/src/translate/translate_items.js
Normal file
@ -0,0 +1,82 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import {Component} from "@odoo/owl";
|
||||
import {Dialog} from "@web/core/dialog/dialog";
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
import {editModelTranslate} from "@translation_helper/translate/translate_utils";
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
const translateRegistry = registry.category("translate");
|
||||
|
||||
function viewSeparator() {
|
||||
return {type: "separator", sequence: 300};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Get view
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
class GetViewDialog extends Component {
|
||||
setup() {
|
||||
this.title = _t("Get View");
|
||||
}
|
||||
}
|
||||
GetViewDialog.template = "web.TranslateMenu.GetViewDialog";
|
||||
GetViewDialog.components = {Dialog};
|
||||
GetViewDialog.props = {
|
||||
arch: {type: Element},
|
||||
close: {type: Function},
|
||||
};
|
||||
|
||||
// // ------------------------------------------------------------------------------
|
||||
// // Edit View
|
||||
// // ------------------------------------------------------------------------------
|
||||
|
||||
export function editView({accessRights, component, env}) {
|
||||
if (!accessRights.canEditView) {
|
||||
return null;
|
||||
}
|
||||
const {viewId, viewType: type} = component.env.config || {};
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
const displayName = type[0].toUpperCase() + type.slice(1);
|
||||
const description = _t("Translate View: ") + displayName;
|
||||
return {
|
||||
type: "item",
|
||||
description,
|
||||
callback: () => {
|
||||
editModelTranslate(env, description, "ir.ui.view", viewId, "soft_reload");
|
||||
},
|
||||
sequence: 350,
|
||||
};
|
||||
}
|
||||
|
||||
translateRegistry.category("view").add("editView", editView);
|
||||
|
||||
translateRegistry.category("view").add("viewSeparator", viewSeparator);
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Edit SearchView
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
export function editSearchView({accessRights, component, env}) {
|
||||
if (!accessRights.canEditView) {
|
||||
return null;
|
||||
}
|
||||
const {searchViewId} = component.props.info || {};
|
||||
if (searchViewId === undefined) {
|
||||
return null;
|
||||
}
|
||||
const description = _t("Translate SearchView");
|
||||
return {
|
||||
type: "item",
|
||||
description,
|
||||
callback: () => {
|
||||
editModelTranslate(env, description, "ir.ui.view", searchViewId, "reload");
|
||||
},
|
||||
sequence: 350,
|
||||
};
|
||||
}
|
||||
|
||||
translateRegistry.category("view").add("editSearchView", editSearchView);
|
||||
61
translation_helper/static/src/translate/translate_menu.js
Normal file
61
translation_helper/static/src/translate/translate_menu.js
Normal file
@ -0,0 +1,61 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {Dropdown} from "@web/core/dropdown/dropdown";
|
||||
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
|
||||
import {TranslateMenuBasic} from "@translation_helper/translate/translate_menu_basic";
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
import {useCommand} from "@web/core/commands/command_hook";
|
||||
import {useEnvTranslateContext} from "./translate_context";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
|
||||
export class TranslateMenu extends TranslateMenuBasic {
|
||||
setup() {
|
||||
super.setup();
|
||||
const translateContext = useEnvTranslateContext();
|
||||
this.command = useService("command");
|
||||
useCommand(
|
||||
_t("Translate tools..."),
|
||||
async () => {
|
||||
const items = await translateContext.getItems(this.env);
|
||||
let index = 0;
|
||||
const defaultCategories = items.filter((item) => item.type === "separator").map(() => (index += 1));
|
||||
const provider = {
|
||||
async provide() {
|
||||
const categories = [...defaultCategories];
|
||||
let category = categories.shift();
|
||||
const result = [];
|
||||
items.forEach((item) => {
|
||||
if (item.type === "item") {
|
||||
result.push({
|
||||
name: item.description.toString(),
|
||||
action: item.callback,
|
||||
category,
|
||||
});
|
||||
} else if (item.type === "separator") {
|
||||
category = categories.shift();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
};
|
||||
const configByNamespace = {
|
||||
default: {
|
||||
categories: defaultCategories,
|
||||
emptyMessage: _t("No translate command found"),
|
||||
placeholder: _t("Choose a translate command..."),
|
||||
},
|
||||
};
|
||||
const commandPaletteConfig = {
|
||||
configByNamespace,
|
||||
providers: [provider],
|
||||
};
|
||||
return commandPaletteConfig;
|
||||
},
|
||||
{
|
||||
category: "translate",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
TranslateMenu.components = {Dropdown, DropdownItem};
|
||||
TranslateMenu.props = {};
|
||||
26
translation_helper/static/src/translate/translate_menu.xml
Normal file
26
translation_helper/static/src/translate/translate_menu.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.TranslateMenu">
|
||||
<Dropdown
|
||||
class="'o_translate_manager'"
|
||||
beforeOpen="getElements"
|
||||
position="'bottom-end'"
|
||||
menuClass="env.inDialog ? 'btn btn-link' : ''"
|
||||
>
|
||||
<button type="button" class="o-dropdown--narrow btn btn-link">
|
||||
<i class="fa fa-language" role="img" aria-label="Open translate tools" />
|
||||
</button>
|
||||
<t t-set-slot="content">
|
||||
<t t-foreach="elements" t-as="element" t-key="element_index">
|
||||
<DropdownItem t-if="element.type == 'item'" onSelected="element.callback" t-att-attrs="element.href ? {href: element.href} : undefined">
|
||||
<t t-esc="element.description" />
|
||||
</DropdownItem>
|
||||
<div t-if="element.type == 'separator'" role="separator" class="dropdown-divider" />
|
||||
<t t-if="element.type == 'component'" t-component="element.Component" t-props="element.props" />
|
||||
</t>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {Component} from "@odoo/owl";
|
||||
import {Dropdown} from "@web/core/dropdown/dropdown";
|
||||
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
|
||||
import {useEnvTranslateContext} from "./translate_context";
|
||||
|
||||
export class TranslateMenuBasic extends Component {
|
||||
setup() {
|
||||
const translateContext = useEnvTranslateContext();
|
||||
// Needs to be bound to this for use in template
|
||||
this.getElements = async () => {
|
||||
this.elements = await translateContext.getItems(this.env);
|
||||
};
|
||||
}
|
||||
}
|
||||
TranslateMenuBasic.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
};
|
||||
TranslateMenuBasic.template = "web.TranslateMenu";
|
||||
@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
import {browser} from "@web/core/browser/browser";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {routeToUrl} from "@web/core/browser/router";
|
||||
|
||||
function leaveTranslateMode({env}) {
|
||||
return {
|
||||
type: "item",
|
||||
description: _t("Leave the Translate Tools"),
|
||||
callback: () => {
|
||||
const route = env.services.router.current;
|
||||
route.search.translate = "";
|
||||
browser.location.href = browser.location.origin + routeToUrl(route);
|
||||
},
|
||||
sequence: 450,
|
||||
};
|
||||
}
|
||||
|
||||
registry.category("translate").category("default").add("leaveTranslateMode", leaveTranslateMode);
|
||||
119
translation_helper/static/src/translate/translate_menu_items.xml
Normal file
119
translation_helper/static/src/translate/translate_menu_items.xml
Normal file
@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.TranslateMenu.SetDefaultDialog">
|
||||
<Dialog title="title">
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td>
|
||||
<label for="formview_default_fields" class="oe_label oe_align_right">
|
||||
Default:
|
||||
</label>
|
||||
</td>
|
||||
<td class="oe_form_required">
|
||||
<select id="formview_default_fields" class="o_input" t-model="state.fieldToSet">
|
||||
<option value="" />
|
||||
<option t-foreach="defaultFields" t-as="field" t-att-value="field.name" t-key="field.name">
|
||||
<t t-esc="field.string" /> = <t t-esc="field.displayed" />
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="conditions.length">
|
||||
<td>
|
||||
<label for="formview_default_conditions" class="oe_label oe_align_right">
|
||||
Condition:
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="formview_default_conditions" class="o_input" t-model="state.condition">
|
||||
<option value="" />
|
||||
<option t-foreach="conditions" t-as="cond" t-att-value="cond.name + '=' + cond.value" t-key="cond.name">
|
||||
<t t-esc="cond.string" />=<t t-esc="cond.displayed" />
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="radio" id="formview_default_self" value="self" name="scope" t-model="state.scope" />
|
||||
<label for="formview_default_self" class="oe_label" style="display: inline;">
|
||||
Only you
|
||||
</label>
|
||||
<br />
|
||||
<input type="radio" id="formview_default_all" value="all" name="scope" t-model="state.scope" />
|
||||
<label for="formview_default_all" class="oe_label" style="display: inline;">
|
||||
All users
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-secondary" t-on-click="props.close">Close</button>
|
||||
<button class="btn btn-secondary" t-on-click="saveDefault">Save default</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
<t t-name="web.TranslateMenu.GetMetadataDialog">
|
||||
<Dialog title="title">
|
||||
<table class="table table-sm table-striped">
|
||||
<tr>
|
||||
<th>ID:</th>
|
||||
<td><t t-esc="state.id" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>XML ID:</th>
|
||||
<td>
|
||||
<t t-if='state.xmlids.length > 1'>
|
||||
<t t-foreach="state.xmlids" t-as="imd" t-key="imd['xmlid']">
|
||||
<div
|
||||
t-att-class='"p-0 " + (imd["xmlid"] === state.xmlid ? "fw-bolder " : "") + (imd["noupdate"] === true ? "fst-italic " : "")'
|
||||
t-esc="imd['xmlid']"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="state.xmlid" t-esc="state.xmlid" />
|
||||
<t t-else="">
|
||||
/ <a t-on-click="onClickCreateXmlid"> (create)</a>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>No Update:</th>
|
||||
<td>
|
||||
<t t-esc="state.noupdate" />
|
||||
<t t-if="state.xmlid">
|
||||
<a t-on-click="toggleNoupdate"> (change)</a>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creation User:</th>
|
||||
<td><t t-esc="state.creator" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creation Date:</th>
|
||||
<td><t t-esc="state.createDate" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Latest Modification by:</th>
|
||||
<td><t t-esc="state.lastModifiedBy" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Latest Modification Date:</th>
|
||||
<td><t t-esc="state.writeDate" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
<t t-name="web.TranslateMenu.GetViewDialog">
|
||||
<Dialog title="title">
|
||||
<pre t-esc="props.arch.outerHTML" />
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-primary o-default-button" t-on-click="() => props.close()">Close</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
21
translation_helper/static/src/translate/translate_utils.js
Normal file
21
translation_helper/static/src/translate/translate_utils.js
Normal file
@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {TranslationDialog} from "@web/views/fields/translation_dialog";
|
||||
|
||||
export function editModelTranslate(env, title, model, id, reloadType) {
|
||||
env.services.dialog.add(TranslationDialog, {
|
||||
fieldName: "arch_db",
|
||||
resId: id,
|
||||
resModel: model,
|
||||
userLanguageValue: env.services.user.lang,
|
||||
userUserCurrentLang: true,
|
||||
isComingFromTranslationAlert: false,
|
||||
onSave: async () => {
|
||||
env.bus.trigger("CLEAR-CACHES");
|
||||
await env.services.action.doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: reloadType,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
23
translation_helper/static/src/webclient.js
Normal file
23
translation_helper/static/src/webclient.js
Normal file
@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {TranslateMenu} from "@translation_helper/translate/translate_menu";
|
||||
import {WebClient} from "@web/webclient/webclient";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {useOwnTranslateContext} from "@translation_helper/translate/translate_context";
|
||||
|
||||
patch(WebClient.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
useOwnTranslateContext({categories: ["default"]});
|
||||
if (this.env.translate) {
|
||||
registry.category("systray").add(
|
||||
"web.translate_mode_menu",
|
||||
{
|
||||
Component: TranslateMenu,
|
||||
},
|
||||
{sequence: 200}
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user