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 name |
+ Locale |
+
+
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+
+
+
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
-