Skip to content

Commit 75e9a38

Browse files
committed
[WIP][ADD] translation module
1 parent f9085b2 commit 75e9a38

18 files changed

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

0 commit comments

Comments
 (0)