Skip to content

Commit 8ede015

Browse files
committed
[WIP][ADD] translation module
1 parent f9085b2 commit 8ede015

18 files changed

+1152
-0
lines changed

addons/t9n/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

addons/t9n/__manifest__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "Translations",
3+
"version": "1.0",
4+
"category": "TODO: find the appropriate category",
5+
"description": "TODO: write a description of the module",
6+
"depends": ["base", "web"],
7+
"application": True,
8+
"assets": {
9+
"web.assets_backend": [
10+
"t9n/static/src/**/*",
11+
],
12+
},
13+
"data": [
14+
"data/t9n.language.csv",
15+
"security/ir.model.access.csv",
16+
"views/t9n_project_views.xml",
17+
"views/t9n_menu_views.xml",
18+
"views/t9n_language_views.xml",
19+
"views/t9n_resource_views.xml",
20+
"views/t9n_message_views.xml",
21+
],
22+
"license": "LGPL-3",
23+
}

addons/t9n/data/t9n.language.csv

Lines changed: 723 additions & 0 deletions
Large diffs are not rendered by default.

addons/t9n/models/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from . import language
2+
from . import message
3+
from . import project
4+
from . import resource
5+
from . import translation

addons/t9n/models/language.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from odoo import fields, models
2+
3+
4+
class Language(models.Model):
5+
_name = "t9n.language"
6+
_description = "Language"
7+
8+
name = fields.Char("Formal Name", required=True, readonly=True)
9+
code = fields.Char("Code", required=True, readonly=True)
10+
native_name = fields.Char("Native Name", readonly=True)
11+
direction = fields.Selection(
12+
required=True,
13+
selection=[
14+
("ltr", "left-to-right"),
15+
("rtl", "right-to-left"),
16+
],
17+
readonly=True,
18+
)
19+
20+
_sql_constraints = [
21+
("language_code_unique", "unique(code)", "The language code must be unique.")
22+
]

addons/t9n/models/message.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from odoo import api, fields, models
2+
3+
4+
class Message(models.Model):
5+
"""Models a localizable message, i.e. any textual content to be translated.
6+
Messages are retrieved from a Resource.
7+
A Message localized to a specific Language becomes a Translation.
8+
"""
9+
10+
_name = "t9n.message"
11+
_description = "Localizable message"
12+
13+
body = fields.Text(
14+
"Entry to be Translated",
15+
help="Text to Translate",
16+
)
17+
context = fields.Char(help="Text Context")
18+
translator_comments = fields.Text(
19+
help="Comments written by the translator/developer in the resource file.",
20+
)
21+
extracted_comments = fields.Text("Resource Comments")
22+
references = fields.Text(
23+
help="The full text that represents the references, one per line.",
24+
)
25+
resource_id = fields.Many2one(
26+
comodel_name="t9n.resource",
27+
help="The resource (typically a file) from which the entry is coming from.",
28+
ondelete="cascade",
29+
required=True,
30+
)
31+
translation_ids = fields.One2many(
32+
comodel_name="t9n.translation",
33+
inverse_name="source_id",
34+
string="Translations",
35+
)
36+
37+
_sql_constraints = [
38+
(
39+
"body_context_resource_unique",
40+
"UNIQUE(body, context, resource_id)",
41+
"The combination of a text to translate and its context must be unique within the same resource!",
42+
),
43+
]

addons/t9n/models/project.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from odoo import fields, models, api, _
2+
from odoo.exceptions import ValidationError
3+
4+
5+
class Project(models.Model):
6+
"""A project is a collection of Resources to be localized into a given set
7+
of Languages.
8+
"""
9+
10+
_name = "t9n.project"
11+
_description = "Translation project"
12+
13+
name = fields.Char("Project", required=True)
14+
src_lang_id = fields.Many2one(
15+
comodel_name="t9n.language",
16+
string="Source Language",
17+
help="The original language of the messages you want to translate.",
18+
)
19+
resource_ids = fields.One2many(
20+
comodel_name="t9n.resource",
21+
inverse_name="project_id",
22+
string="Resources",
23+
)
24+
target_lang_ids = fields.Many2many(
25+
comodel_name="t9n.language",
26+
string="Languages",
27+
help="The list of languages into which the project can be translated.",
28+
)
29+
30+
@api.constrains("src_lang_id", "target_lang_ids")
31+
def _check_source_and_target_languages(self):
32+
for record in self:
33+
if record.src_lang_id in record.target_lang_ids:
34+
raise ValidationError(_("A project's target languages must be different from its source language."))

addons/t9n/models/resource.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import base64
2+
3+
import polib
4+
5+
from odoo import Command, _, api, fields, models
6+
from odoo.exceptions import UserError, ValidationError
7+
from odoo.tools import format_list
8+
9+
from odoo import _, api, Command, fields, models
10+
from odoo.exceptions import UserError, ValidationError
11+
from odoo.tools import format_list
12+
13+
class Resource(models.Model):
14+
_name = "t9n.resource"
15+
_description = "Resource file"
16+
17+
file_name = fields.Char()
18+
file = fields.Binary("Resource File", store=False)
19+
message_ids = fields.One2many(
20+
comodel_name="t9n.message",
21+
inverse_name="resource_id",
22+
string="Entries to translate",
23+
)
24+
project_id = fields.Many2one(
25+
comodel_name="t9n.project",
26+
)
27+
28+
_sql_constraints = [
29+
(
30+
"file_name_project_id_unique",
31+
"unique(file_name, project_id)",
32+
"A file with the same name already exists in the same project!",
33+
),
34+
]
35+
36+
def _decode_resource_file(self, resource_file):
37+
try:
38+
file_content = base64.b64decode(resource_file).decode()
39+
po_obj = polib.pofile(file_content)
40+
except (IOError, UnicodeDecodeError):
41+
po_obj = []
42+
return [
43+
{
44+
"body": entry.msgid,
45+
"context": entry.msgctxt,
46+
"translator_comments": entry.tcomment,
47+
"extracted_comments": entry.comment,
48+
"references": "\n".join([fpath + (lineno and f":{lineno}") for fpath, lineno in entry.occurrences]),
49+
}
50+
for entry in po_obj
51+
]
52+
53+
@api.model_create_multi
54+
def create(self, vals_list):
55+
broken_files = []
56+
for vals in vals_list:
57+
if not vals.get("file"):
58+
raise ValidationError(_("A resource file is required to create a resource."))
59+
po_obj = self._decode_resource_file(vals["file"])
60+
del vals["file"]
61+
if not po_obj:
62+
broken_files.append(vals["file_name"])
63+
continue
64+
vals["message_ids"] = [Command.create(message) for message in po_obj]
65+
if broken_files:
66+
raise UserError(
67+
_(
68+
"Resource files must be valid .pot files. The following files are ill-formatted or empty: %(file_names)s",
69+
file_names=format_list(self.env, broken_files),
70+
),
71+
)
72+
return super().create(vals_list)
73+
74+
def write(self, vals):
75+
self.ensure_one()
76+
if "file" not in vals:
77+
return super().write(vals)
78+
po_obj = self._decode_resource_file(vals["file"])
79+
del vals["file"]
80+
if not po_obj:
81+
raise UserError(
82+
_("The files: %(file_name)s should be a .po file with a valid syntax.", file_name=vals["file_name"]),
83+
)
84+
current_msgs_by_tuple = {(msg.body, msg.context): msg for msg in self.message_ids}
85+
new_msgs_by_tuple = {(msg["body"], msg["context"]): msg for msg in po_obj}
86+
to_create = [msg_val for key, msg_val in new_msgs_by_tuple.items() if key not in current_msgs_by_tuple]
87+
to_unlink = {msg.id for key, msg in current_msgs_by_tuple.items() if key not in new_msgs_by_tuple}
88+
to_update = [
89+
(current_msgs_by_tuple[key].id, new_msgs_by_tuple[key])
90+
for key in set(current_msgs_by_tuple) & set(new_msgs_by_tuple)
91+
]
92+
vals["message_ids"] = (
93+
[Command.create(vals) for vals in to_create]
94+
+ [Command.unlink(id) for id in to_unlink]
95+
+ [Command.update(id, vals) for id, vals in to_update]
96+
)
97+
return super().write(vals)

addons/t9n/models/translation.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from odoo import fields, models
2+
3+
4+
class Translation(models.Model):
5+
_name = "t9n.translation"
6+
_description = "Message translated into a language"
7+
8+
body = fields.Text(
9+
help="The actual content of the translation.",
10+
)
11+
source_id = fields.Many2one(
12+
comodel_name="t9n.message",
13+
string="Source message",
14+
help="The original text, the source of the translation.",
15+
)
16+
lang_id = fields.Many2one(
17+
comodel_name="t9n.language",
18+
string="Language",
19+
help="The language to which the translation translates the original message.",
20+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2+
access_t9n_project_system,t9n.project.system,t9n.model_t9n_project,base.group_system,1,1,1,1
3+
access_t9n_language_system,t9n.language.system,t9n.model_t9n_language,base.group_system,1,1,1,1
4+
access_t9n_message_system,t9n.message.system,t9n.model_t9n_message,base.group_system,1,1,1,1
5+
access_t9n_resource_system,t9n.resource.system,t9n.model_t9n_resource,base.group_system,1,1,1,1
6+
access_t9n_translation_system,t9n.translation.system,t9n.model_t9n_translation,base.group_system,1,1,1,1

addons/t9n/static/src/core/app.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Component } from "@odoo/owl";
2+
3+
/**
4+
* The "root", the "homepage" of the translation application.
5+
*/
6+
export class App extends Component {
7+
static components = {};
8+
static props = {};
9+
static template = "t9n.App";
10+
}

addons/t9n/static/src/core/app.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="t9n.App">
5+
Hello World!
6+
</t>
7+
8+
</templates>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Component, xml } from "@odoo/owl";
2+
3+
import { App } from "@t9n/core/app";
4+
5+
import { registry } from "@web/core/registry";
6+
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
7+
8+
/**
9+
* Wraps the application root, allowing us to open the application as a result
10+
* of a call to the "t9n.open_app" client action.
11+
*/
12+
export class OpenApp extends Component {
13+
static components = { App };
14+
static props = { ...standardActionServiceProps };
15+
static template = xml`<App/>`;
16+
}
17+
18+
registry.category("actions").add("t9n.open_app", OpenApp);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<record id="t9n.language_view_list" model="ir.ui.view">
4+
<field name="name">t9n.language.list</field>
5+
<field name="model">t9n.language</field>
6+
<field name="arch" type="xml">
7+
<tree create="false" edit="false">
8+
<field name="code"/>
9+
<field name="name"/>
10+
<field name="native_name"/>
11+
</tree>
12+
</field>
13+
</record>
14+
<record id="t9n.language_view_search" model="ir.ui.view">
15+
<field name="name">t9n.language.search</field>
16+
<field name="model">t9n.language</field>
17+
<field name="arch" type="xml">
18+
<search>
19+
<field name="name" string="All Languages" filter_domain="[ '|', '|', ('name', 'ilike', self), ('native_name', 'ilike', self), ('code', 'ilike', self)]"/>
20+
<field name="name"/>
21+
<field name="native_name"/>
22+
<field name="code"/>
23+
</search>
24+
</field>
25+
</record>
26+
</odoo>

addons/t9n/views/t9n_menu_views.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<record id="t9n.open_app" model="ir.actions.client">
4+
<field name="name">Translate</field>
5+
<field name="tag">t9n.open_app</field>
6+
<field name="target">main</field>
7+
</record>
8+
<menuitem id="t9n.menu_root" name="Translations" action="t9n.open_app"/>
9+
<menuitem id="t9n.project_menu" name="Projects" action="t9n.project_action" parent="t9n.menu_root"/>
10+
</odoo>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<record id="t9n.message_view_list" model="ir.ui.view">
4+
<field name="name">t9n.message.list</field>
5+
<field name="model">t9n.message</field>
6+
<field name="arch" type="xml">
7+
<tree create="false">
8+
<field name="body"/>
9+
</tree>
10+
</field>
11+
</record>
12+
13+
<record id="t9n.message_view_form" model="ir.ui.view">
14+
<field name="name">t9n.message.form</field>
15+
<field name="model">t9n.message</field>
16+
<field name="arch" type="xml">
17+
<form>
18+
<sheet>
19+
<field name="body"/>
20+
<group>
21+
<field name="extracted_comments"/>
22+
<field name="translator_comments"/>
23+
</group>
24+
<group>
25+
<field name="context"/>
26+
</group>
27+
<notebook>
28+
<page string="References">
29+
<field name="references"/>
30+
</page>
31+
<page string="Translations">
32+
<field name="translation_ids"/>
33+
</page>
34+
</notebook>
35+
</sheet>
36+
</form>
37+
</field>
38+
</record>
39+
</odoo>

0 commit comments

Comments
 (0)