diff --git a/addons/t9n/__manifest__.py b/addons/t9n/__manifest__.py index 350c8f1faf193..36e9f7598368d 100644 --- a/addons/t9n/__manifest__.py +++ b/addons/t9n/__manifest__.py @@ -3,7 +3,7 @@ "version": "1.0", "category": "TODO: find the appropriate category", "description": "TODO: write a description of the module", - "depends": ["base", "web"], + "depends": ["base", "mail", "web"], "application": True, "assets": { "web.assets_backend": [ diff --git a/addons/t9n/models/language.py b/addons/t9n/models/language.py index 94f9647fdf086..a52624c21ef26 100644 --- a/addons/t9n/models/language.py +++ b/addons/t9n/models/language.py @@ -20,3 +20,12 @@ class Language(models.Model): _sql_constraints = [ ("language_code_unique", "unique(code)", "The language code must be unique.") ] + + def _format(self): + return [{ + "id": language.id, + "name": language.name, + "code": language.code, + "native_name": language.native_name, + "direction": language.direction, + } for language in self] diff --git a/addons/t9n/models/project.py b/addons/t9n/models/project.py index da6362199d23e..4c6e264c4bb3f 100644 --- a/addons/t9n/models/project.py +++ b/addons/t9n/models/project.py @@ -35,20 +35,22 @@ def _check_source_and_target_languages(self): @api.model def get_projects(self): - projects_records = self.search([]) - return [{ + return self.search([])._format() + + def _format(self): + return [ + { "id": record.id, "name": record.name, - "src_lang": { - "id": record.src_lang_id.id, - "name": record.src_lang_id.name if record.src_lang_id.name else "", - }, - "resources": [{ - "id": resource.id, - "file_name": resource.file_name, - } for resource in record.resource_ids], - "target_langs": [{ - "id": lang.id, - "name": lang.name, - } for lang in record.target_lang_ids], - } for record in projects_records] + "src_lang_id": record.src_lang_id._format()[0], + "resource_ids": [ + { + "id": resource.id, + "file_name": resource.file_name, + } + for resource in record.resource_ids + ], + "target_lang_ids": record.target_lang_ids._format(), + } + for record in self + ] diff --git a/addons/t9n/models/resource.py b/addons/t9n/models/resource.py index 576fa394fdbad..9a20717ddbbe0 100644 --- a/addons/t9n/models/resource.py +++ b/addons/t9n/models/resource.py @@ -92,3 +92,34 @@ def write(self, vals): + [Command.update(id, vals) for id, vals in to_update] ) return super().write(vals) + + @api.model + def get_resources(self, ids): + return self.browse(ids)._format() + + def _format(self): + return [ + { + "id": resource.id, + "file_name": resource.file_name, + "message_ids": [ + { + "id": msg.id, + "body": msg.body, + "translation_ids": [ + { + "id": translation.id, + "body": translation.body, + "lang_id": translation.lang_id.id, + } + for translation in msg.translation_ids + ], + } + for msg in resource.message_ids + ], + "project_id": { + "id": resource.project_id.id, + }, + } + for resource in self + ] diff --git a/addons/t9n/models/translation.py b/addons/t9n/models/translation.py index 4d511dd2dba5d..f4ad123244e70 100644 --- a/addons/t9n/models/translation.py +++ b/addons/t9n/models/translation.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class Translation(models.Model): @@ -18,3 +18,17 @@ class Translation(models.Model): string="Language", help="The language to which the translation translates the original message.", ) + + @api.model + def create_and_format(self, **kwargs): + return self.create(kwargs)._format() + + def _format(self): + return [{ + "id": translation.id, + "body": translation.body, + "source_id": { + "id": translation.source_id.id, + }, + "lang_id": translation.lang_id._format()[0], + } for translation in self] diff --git a/addons/t9n/static/src/core/app.js b/addons/t9n/static/src/core/app.js index f58178faf5a15..dd33d44002b67 100644 --- a/addons/t9n/static/src/core/app.js +++ b/addons/t9n/static/src/core/app.js @@ -1,11 +1,25 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; + import { ProjectList } from "@t9n/core/project_list"; +import { LanguageList } from "@t9n/core/language_list"; +import { ResourceList } from "@t9n/core/resource_list"; +import { TranslationEditor } from "@t9n/core/translation_editor"; + +import { useService } from "@web/core/utils/hooks"; /** * The "root", the "homepage" of the translation application. */ export class App extends Component { - static components = { ProjectList }; + static components = { LanguageList, ProjectList, ResourceList, TranslationEditor }; static props = {}; static template = "t9n.App"; + + setup() { + this.store = useState(useService("mail.store")); + } + + get activeView() { + return this.store.t9n.activeView; + } } diff --git a/addons/t9n/static/src/core/app.xml b/addons/t9n/static/src/core/app.xml index b792d771b8352..c47a40418474c 100644 --- a/addons/t9n/static/src/core/app.xml +++ b/addons/t9n/static/src/core/app.xml @@ -2,7 +2,10 @@ - + + + + diff --git a/addons/t9n/static/src/core/language_list.js b/addons/t9n/static/src/core/language_list.js new file mode 100644 index 0000000000000..b193f3b590a8f --- /dev/null +++ b/addons/t9n/static/src/core/language_list.js @@ -0,0 +1,55 @@ +import { Component, useState } from "@odoo/owl"; + +import { useService } from "@web/core/utils/hooks"; + +export class LanguageList extends Component { + static props = { languages: Array }; + static template = "t9n.LanguageList"; + + setup() { + this.action = useService("action"); + this.state = useState({ + filters: { + searchText: "", + }, + sorting: { + column: "name", + order: "asc", + }, + }); + this.store = useState(useService("mail.store")); + } + + get languages() { + const searchTerms = this.state.filters.searchText.trim().toUpperCase(); + const languages = searchTerms + ? this.props.languages.filter((l) => l.name.toUpperCase().includes(searchTerms)) + : [...this.props.languages]; + return languages.sort((l1, l2) => { + const l1Col = l1[this.state.sorting.column]; + const l2Col = l2[this.state.sorting.column]; + + if (l1Col < l2Col) { + return this.state.sorting.order === "asc" ? -1 : 1; + } + if (l1Col > l2Col) { + return this.state.sorting.order === "asc" ? 1 : -1; + } + return 0; + }); + } + + onClickColumnName(column) { + if (this.state.sorting.column === column) { + this.state.sorting.order = this.state.sorting.order === "asc" ? "desc" : "asc"; + } else { + this.state.sorting.column = column; + this.state.sorting.order = "asc"; + } + } + + onClickLanguage(language) { + this.store.t9n.activeView = "ResourceList"; + this.store.t9n.activeLanguage = language; + } +} diff --git a/addons/t9n/static/src/core/language_list.xml b/addons/t9n/static/src/core/language_list.xml new file mode 100644 index 0000000000000..d3662615452c8 --- /dev/null +++ b/addons/t9n/static/src/core/language_list.xml @@ -0,0 +1,35 @@ + + + +
+
+
+ +
+
+ + + + + + + + + + + + + + + +
Language Name + Native nameLocale
+ + + +
+
+
+
diff --git a/addons/t9n/static/src/core/message_form.js b/addons/t9n/static/src/core/message_form.js new file mode 100644 index 0000000000000..48ac00d8405df --- /dev/null +++ b/addons/t9n/static/src/core/message_form.js @@ -0,0 +1,55 @@ +import { Component, useState } from "@odoo/owl"; + +import { _t } from "@web/core/l10n/translation"; +import { useService } from "@web/core/utils/hooks"; + +export class MessageForm extends Component { + static props = {}; + static template = "t9n.MessageForm"; + + setup() { + this.state = useState({ + suggestedTranslationText: "", + }); + this.store = useState(useService("mail.store")); + this.orm = useService("orm"); + this.notification = useService("notification"); + } + + get message() { + return this.store.t9n.activeMessage; + } + + get translations() { + return this.message.translationsInCurrentLanguage; + } + + onClickClear() { + this.state.suggestedTranslationText = ""; + } + + async onClickCopy(ev) { + try { + await navigator.clipboard.writeText(this.message.body.trim()); + this.notification.add( + _t("Copied to clipboard!"), + { type: "info" } + ); + } catch (error) { + console.error("Error copying text:", error); + } + } + + async onClickSuggest() { + const data = await this.orm.call("t9n.translation", "create_and_format", [], + { + body: this.state.suggestedTranslationText.trim(), + source_id: this.store.t9n.activeMessage.id, + lang_id: this.store.t9n.activeLanguage.id, + }, + ); + + this.store["t9n.translation"].insert(data); + this.state.suggestedTranslationText = ""; + } +} diff --git a/addons/t9n/static/src/core/message_form.xml b/addons/t9n/static/src/core/message_form.xml new file mode 100644 index 0000000000000..0d4a92b1f5401 --- /dev/null +++ b/addons/t9n/static/src/core/message_form.xml @@ -0,0 +1,70 @@ + + + +
+
+ +
+
+
+

+ + + TRANSLATOR COMMENT + + + + + +

+

+ + + RESOURCE COMMENT + + + + + +

+

+ + + CONTEXT + + + + +

  • Cras justo odio
  • + +

    +

    + + + REFERENCES + + + + + +

    +
    +
    + +
    +
    + + + +
    +
    +
    + +
    +
    diff --git a/addons/t9n/static/src/core/models/app_model.js b/addons/t9n/static/src/core/models/app_model.js new file mode 100644 index 0000000000000..7684942129815 --- /dev/null +++ b/addons/t9n/static/src/core/models/app_model.js @@ -0,0 +1,14 @@ +import { Record } from "@mail/core/common/record"; + +export class AppModel extends Record { + static name = "t9n.App"; + + activeProject = Record.one("t9n.project"); + activeLanguage = Record.one("t9n.language"); + activeResource = Record.one("t9n.resource"); + activeMessage = Record.one("t9n.message"); + /** @type {"ProjectList"|"LanguageList"|"ResourceList"|"TranslationEditor"} */ + activeView = "ProjectList"; +} + +AppModel.register(); diff --git a/addons/t9n/static/src/core/models/language_model.js b/addons/t9n/static/src/core/models/language_model.js new file mode 100644 index 0000000000000..469fc09d74b1a --- /dev/null +++ b/addons/t9n/static/src/core/models/language_model.js @@ -0,0 +1,13 @@ +import { Record } from "@mail/core/common/record"; + +export class Language extends Record { + static name = "t9n.language"; + static id = "id"; + + name; + code; + native_name; + direction; +} + +Language.register(); diff --git a/addons/t9n/static/src/core/models/message_model.js b/addons/t9n/static/src/core/models/message_model.js new file mode 100644 index 0000000000000..0055ef262dc76 --- /dev/null +++ b/addons/t9n/static/src/core/models/message_model.js @@ -0,0 +1,24 @@ +import { Record } from "@mail/core/common/record"; + +export class Message extends Record { + static name = "t9n.message"; + static id = "id"; + + body; + context; + translator_comments; + extracted_comments; + references; + resource_id = Record.one("t9n.resource"); + translation_ids = Record.many("t9n.translation", { + inverse: "source_id", + }); + translationsInCurrentLanguage = Record.many("t9n.translation", { + compute() { + const { activeLanguage } = this.store.t9n; + return this.translation_ids.filter(({ lang_id }) => lang_id.eq(activeLanguage)); + }, + }); +} + +Message.register(); diff --git a/addons/t9n/static/src/core/models/project_model.js b/addons/t9n/static/src/core/models/project_model.js new file mode 100644 index 0000000000000..4537ff1fa20b1 --- /dev/null +++ b/addons/t9n/static/src/core/models/project_model.js @@ -0,0 +1,29 @@ +import { Record } from "@mail/core/common/record"; + +import { formatList } from "@web/core/l10n/utils"; + +export class Project extends Record { + static name = "t9n.project"; + static id = "id"; + + /** @type {string} */ + name; + src_lang_id = Record.one("t9n.language"); + resource_ids = Record.many("t9n.resource"); + target_lang_ids = Record.many("t9n.language"); + + /** @type {string} */ + targetLanguages = Record.attr("", { + compute() { + return formatList(this.target_lang_ids.map(({ name }) => name)); + }, + }); + /** @type {number} */ + resourceCount = Record.attr(0, { + compute() { + return this.resource_ids.length; + }, + }); +} + +Project.register(); diff --git a/addons/t9n/static/src/core/models/resource_model.js b/addons/t9n/static/src/core/models/resource_model.js new file mode 100644 index 0000000000000..904ac867a529c --- /dev/null +++ b/addons/t9n/static/src/core/models/resource_model.js @@ -0,0 +1,12 @@ +import { Record } from "@mail/core/common/record"; + +export class Resource extends Record { + static name = "t9n.resource"; + static id = "id"; + + file_name; + message_ids = Record.many("t9n.message"); + project_id = Record.one("t9n.project"); +} + +Resource.register(); diff --git a/addons/t9n/static/src/core/models/store_service_patch.js b/addons/t9n/static/src/core/models/store_service_patch.js new file mode 100644 index 0000000000000..e86cbcb04a5b9 --- /dev/null +++ b/addons/t9n/static/src/core/models/store_service_patch.js @@ -0,0 +1,15 @@ +import { Record } from "@mail/core/common/record"; +import { Store } from "@mail/core/common/store_service"; + +import { patch } from "@web/core/utils/patch"; + +patch(Store.prototype, { + setup() { + super.setup(...arguments); + this.t9n = Record.one("t9n.App", { + compute() { + return {}; + }, + }); + }, +}); diff --git a/addons/t9n/static/src/core/models/translation_model.js b/addons/t9n/static/src/core/models/translation_model.js new file mode 100644 index 0000000000000..cd7fc1e582846 --- /dev/null +++ b/addons/t9n/static/src/core/models/translation_model.js @@ -0,0 +1,14 @@ +import { Record } from "@mail/core/common/record"; + +export class Translation extends Record { + static name = "t9n.translation"; + static id = "id"; + + body; + source_id = Record.one("t9n.message", { + inverse: "translation_ids", + }); + lang_id = Record.one("t9n.language"); +} + +Translation.register(); diff --git a/addons/t9n/static/src/core/project_list.js b/addons/t9n/static/src/core/project_list.js index 7959beb12b0c6..bf29841f38cf5 100644 --- a/addons/t9n/static/src/core/project_list.js +++ b/addons/t9n/static/src/core/project_list.js @@ -7,7 +7,6 @@ export class ProjectList extends Component { static template = "t9n.ProjectList"; setup() { - this.action = useService("action"); this.state = useState({ filters: { searchText: "", @@ -17,16 +16,21 @@ export class ProjectList extends Component { order: "asc", }, }); - this.store = useState(useService("t9n.store")); - this.store.fetchProjects(); + this.store = useState(useService("mail.store")); + this.fetchProjects(); + } + + async fetchProjects() { + const projects = await this.env.services.orm.call("t9n.project", "get_projects"); + this.store["t9n.project"].insert(projects); } get projects() { const searchTerms = this.state.filters.searchText.trim().toUpperCase(); + const allProjects = Object.values(this.store["t9n.project"].records); const projects = searchTerms - ? this.store.projects.filter((p) => p.name.toUpperCase().includes(searchTerms)) - : [...this.store.projects]; - + ? allProjects.filter((p) => p.name.toUpperCase().includes(searchTerms)) + : allProjects; projects.sort((p1, p2) => { let p1Col = p1[this.state.sorting.column]; let p2Col = p2[this.state.sorting.column]; @@ -56,13 +60,8 @@ export class ProjectList extends Component { } } - onClickProject(id) { - this.action.doAction({ - type: "ir.actions.act_window", - res_id: id, - res_model: "t9n.project", - views: [[false, "form"]], - target: "new", - }); + onClickProject(project) { + this.store.t9n.activeProject = project; + this.store.t9n.activeView = "LanguageList"; } } diff --git a/addons/t9n/static/src/core/project_list.xml b/addons/t9n/static/src/core/project_list.xml index 443f2ea61f47a..681c3733f26cb 100644 --- a/addons/t9n/static/src/core/project_list.xml +++ b/addons/t9n/static/src/core/project_list.xml @@ -18,15 +18,15 @@ - + - - - + + diff --git a/addons/t9n/static/src/core/project_model.js b/addons/t9n/static/src/core/project_model.js deleted file mode 100644 index 3cb421ba770b0..0000000000000 --- a/addons/t9n/static/src/core/project_model.js +++ /dev/null @@ -1,15 +0,0 @@ -import { formatList } from "@web/core/l10n/utils"; - -export class Project { - constructor(id, name, srcLang, targetLangs, resourceCount) { - this.id = id; - this.name = name; - this.srcLang = srcLang; - this.targetLangs = targetLangs; - this.resourceCount = resourceCount; - } - - get formattedTargetLanguages() { - return formatList(this.targetLangs.map(({ name }) => name)); - } -} diff --git a/addons/t9n/static/src/core/resource_list.js b/addons/t9n/static/src/core/resource_list.js new file mode 100644 index 0000000000000..c2d0da0cd1798 --- /dev/null +++ b/addons/t9n/static/src/core/resource_list.js @@ -0,0 +1,70 @@ +import { Component, useState } from "@odoo/owl"; + +import { useService } from "@web/core/utils/hooks"; + +export class ResourceList extends Component { + static props = { resources: Array }; + static template = "t9n.ResourceList"; + + setup() { + this.action = useService("action"); + this.state = useState({ + filters: { + searchText: "", + }, + sorting: { + column: "file_name", + order: "asc", + }, + }); + this.store = useState(useService("mail.store")); + this.fetchResources(); + } + + get resources() { + const searchTerms = this.state.filters.searchText.trim().toUpperCase(); + const resources = searchTerms + ? this.props.resources.filter((r) => r.file_name.toUpperCase().includes(searchTerms)) + : [...this.props.resources]; + + resources.sort((r1, r2) => { + let r1Col = r1[this.state.sorting.column]; + let r2Col = r2[this.state.sorting.column]; + + r1Col = r1Col.toLowerCase(); + r2Col = r2Col.toLowerCase(); + + if (r1Col < r2Col) { + return this.state.sorting.order === "asc" ? -1 : 1; + } + if (r1Col > r2Col) { + return this.state.sorting.order === "asc" ? 1 : -1; + } + return 0; + }); + return resources; + } + + async fetchResources() { + const resourceData = await this.env.services.orm.call( + "t9n.resource", + "get_resources", + [this.props.resources.map(({ id }) => id)], + ); + this.store["t9n.resource"].insert(resourceData); + } + + onClickColumnName(column) { + if (this.state.sorting.column === column) { + this.state.sorting.order = this.state.sorting.order === "asc" ? "desc" : "asc"; + } else { + this.state.sorting.column = column; + this.state.sorting.order = "asc"; + } + } + + onClickResource(resource) { + this.store.t9n.activeView = "TranslationEditor"; + this.store.t9n.activeResource = resource; + } +} diff --git a/addons/t9n/static/src/core/resource_list.xml b/addons/t9n/static/src/core/resource_list.xml new file mode 100644 index 0000000000000..e60f871d1fed7 --- /dev/null +++ b/addons/t9n/static/src/core/resource_list.xml @@ -0,0 +1,31 @@ + + + +
    +
    +
    + +
    +
    + + + + + + + + + + + + + +
    Resource +
    + +
    +
    +
    +
    diff --git a/addons/t9n/static/src/core/store.js b/addons/t9n/static/src/core/store.js deleted file mode 100644 index 766af9f1ae60c..0000000000000 --- a/addons/t9n/static/src/core/store.js +++ /dev/null @@ -1,30 +0,0 @@ -import { reactive } from "@odoo/owl"; - -import { Project } from "@t9n/core/project_model"; - -import { registry } from "@web/core/registry"; - -export class Store { - constructor(env, { orm }) { - this.env = env; - this.orm = orm; - this.projects = []; - return reactive(this); - } - - async fetchProjects() { - const projects = await this.orm.call("t9n.project", "get_projects"); - this.projects = projects.map((p) => { - return new Project(p.id, p.name, p.src_lang.name, p.target_langs, p.resources.length); - }); - } -} - -export const storeService = { - dependencies: ["orm"], - start(env, deps) { - return new Store(env, deps); - }, -}; - -registry.category("services").add("t9n.store", storeService); diff --git a/addons/t9n/static/src/core/translation_editor.js b/addons/t9n/static/src/core/translation_editor.js new file mode 100644 index 0000000000000..f272662ddd84f --- /dev/null +++ b/addons/t9n/static/src/core/translation_editor.js @@ -0,0 +1,23 @@ +import { useState, Component } from "@odoo/owl"; + +import { MessageForm } from "@t9n/core/message_form"; + +import { useService } from "@web/core/utils/hooks"; + +export class TranslationEditor extends Component { + static props = {}; + static components = { MessageForm }; + static template = "t9n.TranslationEditor"; + + setup() { + this.store = useState(useService("mail.store")); + } + + get messages() { + return this.store.t9n.activeResource.message_ids; + } + + onClickMessage(message) { + this.store.t9n.activeMessage = message; + } +} diff --git a/addons/t9n/static/src/core/translation_editor.xml b/addons/t9n/static/src/core/translation_editor.xml new file mode 100644 index 0000000000000..de32720e1ddef --- /dev/null +++ b/addons/t9n/static/src/core/translation_editor.xml @@ -0,0 +1,22 @@ + + + + +
    +
    +
    + + + + + +
    +
    +
    + +
    +
    + +
    + +
    diff --git a/addons/t9n/static/src/web/open_app_action.js b/addons/t9n/static/src/web/open_app_action.js index dc66682513b26..c3fcd89a1e351 100644 --- a/addons/t9n/static/src/web/open_app_action.js +++ b/addons/t9n/static/src/web/open_app_action.js @@ -1,9 +1,10 @@ -import { Component, xml } from "@odoo/owl"; +import { Component, xml, useState} from "@odoo/owl"; import { App } from "@t9n/core/app"; import { registry } from "@web/core/registry"; import { standardActionServiceProps } from "@web/webclient/actions/action_service"; +import { useService } from "@web/core/utils/hooks"; /** * Wraps the application root, allowing us to open the application as a result @@ -13,6 +14,14 @@ export class OpenApp extends Component { static components = { App }; static props = { ...standardActionServiceProps }; static template = xml``; + + setup() { + this.store = useState(useService("mail.store")); + this.store.t9n.activeView = 'ProjectList'; + this.store.t9n.activeLanguage = null; + this.store.t9n.activeResource = null; + this.store.t9n.activeMessage = null; + } } registry.category("actions").add("t9n.open_app", OpenApp); diff --git a/addons/t9n/views/t9n_resource_views.xml b/addons/t9n/views/t9n_resource_views.xml index 58d906ce9055f..593dbe60adc65 100644 --- a/addons/t9n/views/t9n_resource_views.xml +++ b/addons/t9n/views/t9n_resource_views.xml @@ -4,11 +4,11 @@ t9n.resource.form t9n.resource -
    + - +