[ADD] transifex: add transifex module to help translation

model/model_terms translations:
A transifex link if available is displayed after the language in the translation
dialog to help translators to contribute their translations. The tooltip message
for mouse hovering is 'Contribute'

code translations:
Setting -> Translations -> Application Terms -> Transifex Code Translations

In order to reuse the list view and search bar, model transifex.code.translation
is created to store all code translations.
This model is
1. readonly
2. reloaded on demand/by cron(7 days) to avoid increasing the Odoo restart time
3. updated for new installed modules/languages while opening the list view
4. shared to all users without duplicated translations

closes odoo/odoo#111685

X-original-commit: 7b30badb52e2a948c41f4340a01f6118818c6711
Signed-off-by: Raphael Collet <rco@odoo.com>
Signed-off-by: Wang Chong (cwg) <cwg@odoo.com>
This commit is contained in:
Chong Wang (cwg)
2023-02-03 06:05:51 +01:00
parent db4934be91
commit 6346f52acf
18 changed files with 484 additions and 15 deletions
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+37
View File
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Transifex integration',
'version': '1.0',
'summary': 'Add a link to edit a translation in Transifex',
'category': 'Hidden/Tools',
'description':
"""
Transifex integration
=====================
This module will add a link to the Transifex project in the translation view.
The purpose of this module is to speed up translations of the main modules.
To work, Odoo uses Transifex configuration files `.tx/config` to detect the
project source. Custom modules will not be translated (as not published on
the main Transifex project).
The language the user tries to translate must be activated on the Transifex
project.
""",
'data': [
'data/transifex_data.xml',
'views/code_translation_views.xml',
'security/ir.model.access.csv'
],
'assets': {
'web.assets_backend': [
'transifex/static/src/views/fields/translation_dialog.xml',
'transifex/static/src/views/*.js',
'transifex/static/src/views/*.xml',
],
},
'depends': ['base', 'web'],
'license': 'LGPL-3',
}
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="transifex_project_url" model="ir.config_parameter">
<field name="key">transifex.project_url</field>
<field name="value">https://www.transifex.com/odoo</field>
</record>
<record id="transifex_code_translation_reload" model="ir.cron">
<field name="name">Transifex: Reload code translations</field>
<field name="model_id" ref="model_transifex_code_translation"/>
<field name="state">code</field>
<field name="code">model.reload()</field>
<field name='interval_number'>7</field>
<field name='interval_type'>days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
</record>
</odoo>
+134
View File
@@ -0,0 +1,134 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * transifex
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-23 14:34+0000\n"
"PO-Revision-Date: 2023-01-23 14:34+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: transifex
#: model:ir.model,name:transifex.model_base
msgid "Base"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation__source
msgid "Code"
msgstr ""
#. module: transifex
#: model:ir.model,name:transifex.model_transifex_code_translation
msgid "Code Translation"
msgstr ""
#. module: transifex
#. odoo-javascript
#: code:addons/transifex/static/src/views/fields/translation_dialog.xml:0
#: model_terms:ir.ui.view,arch_db:transifex.transifex_code_translation_tree_view
#, python-format
msgid "Contribute"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation__display_name
msgid "Display Name"
msgstr ""
#. module: transifex
#: model_terms:ir.ui.view,arch_db:transifex.transifex_code_translation_view_search
msgid "Group By"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation__id
msgid "ID"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation__lang
msgid "Language"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation____last_update
msgid "Last Modified on"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation__module
#: model_terms:ir.ui.view,arch_db:transifex.transifex_code_translation_view_search
msgid "Module"
msgstr ""
#. module: transifex
#: model:ir.model.fields,help:transifex.field_transifex_code_translation__module
msgid "Module this term belongs to"
msgstr ""
#. module: transifex
#: model_terms:ir.ui.view,arch_db:transifex.transifex_code_translation_view_search
msgid "Not Translated"
msgstr ""
#. module: transifex
#: model:ir.model.fields,help:transifex.field_transifex_code_translation__transifex_url
msgid "Propose a modification in the official version of Odoo"
msgstr ""
#. module: transifex
#. odoo-javascript
#: code:addons/transifex/static/src/views/reload_code_translations_views.xml:0
#, python-format
msgid "Reload"
msgstr ""
#. module: transifex
#: model_terms:ir.ui.view,arch_db:transifex.transifex_code_translation_view_search
msgid "Search Code Translations"
msgstr ""
#. module: transifex
#: model_terms:ir.ui.view,arch_db:transifex.transifex_code_translation_tree_view
msgid "Transifex"
msgstr ""
#. module: transifex
#: model_terms:ir.ui.view,arch_db:transifex.transifex_code_translation_tree_view
msgid "Transifex Code Translation"
msgstr ""
#. module: transifex
#: model:ir.actions.server,name:transifex.action_code_translations
#: model:ir.ui.menu,name:transifex.menu_transifex_code_translations
msgid "Transifex Code Translations"
msgstr ""
#. module: transifex
#: model:ir.model,name:transifex.model_transifex_translation
msgid "Transifex Translation"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation__transifex_url
msgid "Transifex URL"
msgstr ""
#. module: transifex
#: model:ir.actions.server,name:transifex.transifex_code_translation_reload_ir_actions_server
#: model:ir.cron,cron_name:transifex.transifex_code_translation_reload
msgid "Transifex: Reload code translations"
msgstr ""
#. module: transifex
#: model:ir.model.fields,field_description:transifex.field_transifex_code_translation__value
msgid "Translation Value"
msgstr ""
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import transifex_code_translation
from . import transifex_translation
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class BaseModel(models.AbstractModel):
_inherit = 'base'
def get_field_translations(self, field_name, langs=None):
""" get model/model_term translations for records with transifex url
:param str field_name: field name
:param list langs: languages
:return: (translations, context) where
translations: list of dicts like [{"lang": lang, "source": source_term, "value": value_term,
"module": module, "transifexURL": transifex_url}]
context: {"translation_type": "text"/"char", "translation_show_source": True/False}
"""
translations, context = super().get_field_translations(field_name, langs=langs)
external_id = self.get_external_id().get(self.id)
if not external_id:
return translations, context
module = external_id.split('.')[0]
if module not in self.pool._init_modules:
return translations, context
for translation in translations:
translation['module'] = module
self.env['transifex.translation']._update_transifex_url(translations)
return translations, context
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import psycopg2
from odoo import api, models, fields
from odoo.tools.translate import CodeTranslations
class TransifexCodeTranslation(models.Model):
_name = "transifex.code.translation"
_description = "Code Translation"
_log_access = False
source = fields.Text(string='Code')
value = fields.Text(string='Translation Value')
module = fields.Char(help="Module this term belongs to")
lang = fields.Selection(selection='_get_languages', string='Language', validate=False)
transifex_url = fields.Char("Transifex URL", compute='_compute_transifex_url',
help="Propose a modification in the official version of Odoo")
def _get_languages(self):
return self.env['res.lang'].get_installed()
def _compute_transifex_url(self):
self.transifex_url = False
self.env['transifex.translation']._update_transifex_url(self)
def _load_code_translations(self, module_names=None, langs=None):
try:
# the table lock promises translations for a (module, language) will only be created once
self.env.cr.execute(f'LOCK TABLE {self._table} IN EXCLUSIVE MODE NOWAIT')
if module_names is None:
module_names = self.env['ir.module.module'].search([('state', '=', 'installed')]).mapped('name')
if langs is None:
langs = [lang for lang, _ in self._get_languages() if lang != 'en_US']
self.env.cr.execute(f'SELECT DISTINCT module, lang FROM {self._table}')
loaded_code_translations = set(self.env.cr.fetchall())
create_value_list = [
{
'source': src,
'value': value,
'module': module_name,
'lang': lang,
}
for module_name in module_names
for lang in langs
if (module_name, lang) not in loaded_code_translations
for src, value in CodeTranslations._get_code_translations(module_name, lang, lambda x: True).items()
]
self.sudo().create(create_value_list)
except psycopg2.errors.LockNotAvailable:
return False
return True
def _open_code_translations(self):
self._load_code_translations()
return {
'name': 'Code Translations',
'type': 'ir.actions.act_window',
'res_model': 'transifex.code.translation',
'view_mode': 'list',
}
@api.model
def reload(self):
self.env.cr.execute(f'DELETE FROM {self._table}')
return self._load_code_translations()
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug.urls
from configparser import ConfigParser
from os import pardir
from os.path import isfile, join as opj
import odoo
from odoo import models, tools
class TransifexTranslation(models.AbstractModel):
_name = "transifex.translation"
_description = "Transifex Translation"
@tools.ormcache()
def _get_transifex_projects(self):
""" get the transifex project name for each module
.tx/config files contains the project reference
first section is [main], after '[odoo-16.sale]'
:rtype: dict
:return: {module_name: tx_project_name}
"""
tx_config_file = ConfigParser()
projects = {}
for addon_path in odoo.addons.__path__:
for tx_path in (
opj(addon_path, '.tx', 'config'),
opj(addon_path, pardir, '.tx', 'config'),
):
if isfile(tx_path):
tx_config_file.read(tx_path)
# tx_config_file.sections(): ['main', 'odoo-16.sale', ...]
projects.update(sec.split('.')[::-1] for sec in tx_config_file.sections()[1:])
return projects
def _update_transifex_url(self, translations):
""" Update translations' Transifex URL
:param translations: the translations to update, may be a recordset or a list of dicts.
The elements of `translations` must have the fields/keys 'source', 'module', 'lang',
and the field/key 'transifex_url' is updated on them.
"""
# e.g. 'https://www.transifex.com/odoo/'
base_url = self.env['ir.config_parameter'].sudo().get_param('transifex.project_url')
if not base_url:
return
base_url = base_url.rstrip('/')
res_langs = self.env['res.lang'].search([])
lang_to_iso = {l.code: l.iso_code for l in res_langs}
if not lang_to_iso:
return
projects = self._get_transifex_projects()
if not projects:
return
for translation in translations:
if not translation['source'] or translation['lang'] == 'en_US':
continue
lang_iso = lang_to_iso.get(translation['lang'])
if not lang_iso:
continue
project = projects.get(translation['module'])
if not project:
continue
# e.g. https://www.transifex.com/odoo/odoo-16/translate/#fr_FR/sale/42?q=text:'Sale+Order'
# 42 is an arbitrary number to satisfy the transifex URL format
source = werkzeug.urls.url_quote_plus(translation['source'][:50].replace("\n", "").replace("'", "\\'"))
source = f"'{source}'" if "+" in source else source
translation['transifex_url'] = f"{base_url}/{project}/translate/#{lang_iso}/{translation['module']}/42?q=text%3A{source}"
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_transifex_code_translation,transifex.code.translation,model_transifex_code_translation,base.group_system,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_transifex_code_translation transifex.code.translation model_transifex_code_translation base.group_system 1 0 0 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="98.616%"><stop offset="0%" stop-color="#797C79"/><stop offset="100%" stop-color="#545554"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M4 69c-2 0-4-1-4-4V36.095L18.936 13l5.025 8H30.5L41 28l7.5-7-5.564 10.127L55.577 49 36 69H4z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" fill-rule="nonzero" d="M19.077 15h5.5v8h4v5h-4v18h4v5H23.54c-3.026 0-4.513-1.667-4.462-5V28h-4.452v-5h4.452v-8zm11.5 36l9-14-9.5-14h7l5.5 9 6-9h7l-9.5 14 9.5 14h-7l-5.5-8.445-5.5 8.445h-7zm-15.54 2.642H14V53h2.833v.642h-1.037v2.832h-.76v-2.832zM17 53h1.07l.809 2.389h.01L19.652 53h1.07v3.474h-.711v-2.462h-.01l-.847 2.462h-.586l-.848-2.438h-.01v2.438H17V53z" opacity=".3"/><path fill="#FFF" fill-rule="nonzero" d="M19.077 13h5.5v8h4v5h-4v18h4v5H23.54c-3.026 0-4.513-1.667-4.462-5V26h-4.452v-5h4.452v-8zm11.5 36l9-14-9.5-14h7l5.5 9 6-9h7l-9.5 14 9.5 14h-7l-5.5-8.445-5.5 8.445h-7zm-15.54 2.642H14V51h2.833v.642h-1.037v2.832h-.76v-2.832zM17 51h1.07l.809 2.389h.01L19.652 51h1.07v3.474h-.711v-2.462h-.01l-.847 2.462h-.586l-.848-2.438h-.01v2.438H17V51z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-inherit="web.TranslationDialog" t-inherit-mode="extension">
<xpath expr="//div[hasclass('row')]/div/t" position="after">
<a t-if="term.transifex_url" t-attf-href="#{term.transifex_url}" title="Contribute" target="_blank"> <i class="fa fa-globe"/></a>
</xpath>
</t>
</templates>
@@ -0,0 +1,25 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { ListController } from "@web/views/list/list_controller";
import { listView } from "@web/views/list/list_view";
import { browser } from "@web/core/browser/browser";
import { useService } from "@web/core/utils/hooks";
export class TransifexCodeTranslationListController extends ListController {
setup() {
super.setup();
this.orm = useService("orm");
}
async onClickReloadCodeTranslations() {
await this.orm.call("transifex.code.translation", "reload", [], {});
browser.location.reload();
}
}
registry.category("views").add("transifex_code_translation_tree", {
...listView,
Controller: TransifexCodeTranslationListController,
buttonTemplate: "transifex.CodeTranslationListView.Buttons",
});
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="transifex.CodeTranslationListView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<!-- Before the export button -->
<xpath expr="//t[contains(@t-if, 'isExportEnable')]" position="before">
<button type="button" class="btn btn-primary" t-on-click="onClickReloadCodeTranslations">Reload</button>
</xpath>
</t>
</templates>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="transifex_code_translation_tree_view" model="ir.ui.view">
<field name="name">transifex.code.translation.tree</field>
<field name="model">transifex.code.translation</field>
<field name="arch" type="xml">
<tree string="Transifex Code Translation" js_class="transifex_code_translation_tree">
<field name="source"/>
<field name="value"/>
<field name="module"/>
<field name="lang"/>
<field name="transifex_url"
widget="url"
text="Contribute"
string="Transifex" />
</tree>
</field>
</record>
<record id="transifex_code_translation_view_search" model="ir.ui.view">
<field name="name">transifex.code.translation.view.search</field>
<field name="model">transifex.code.translation</field>
<field name="arch" type="xml">
<search string="Search Code Translations">
<field name="module"/>
<field name="lang"/>
<field name="source"/>
<field name="value"/>
<separator/>
<filter string="Not Translated" name="not_translated" domain="[('value', '=', '')]"/>
<group string="Group By">
<filter string="Module" name="group_by_module" context="{'group_by': 'module'}"/>
</group>
</search>
</field>
</record>
<record id="action_code_translations" model="ir.actions.server">
<field name="name">Transifex Code Translations</field>
<field name="model_id" ref="transifex.model_transifex_code_translation"/>
<field name="groups_id" eval="[(4, ref('base.group_system'))]"/>
<field name="state">code</field>
<field name="code">action = model._open_code_translations()</field>
</record>
<menuitem action="action_code_translations" id="menu_transifex_code_translations" parent="base.menu_translation_app"/>
</odoo>
@@ -29,13 +29,8 @@ export class TranslationDialog extends Component {
this.terms = translations.map((term) => {
const relatedLanguage = languages.find((l) => l[0] === term.lang);
return {
id: term.id,
lang: term.lang,
...term,
langName: relatedLanguage[1],
source: term.source,
// we set the translation value coming from the database, except for the language
// the user is currently utilizing. Then we set the translation value coming
// from the value of the field in the form
value:
term.lang === this.user.lang &&
!this.props.showSource &&
+4 -7
View File
@@ -3070,10 +3070,9 @@ class BaseModel(metaclass=MetaModel):
:param str field_name: field name
:param list langs: languages
:return: list of dicts like [{"lang": lang, "source": source_term, "value": value_term}]
In the UI, translation_dialog.js
for model: val_en will be shown as the translation
for model term: val_en will be shown as the src
:return: (translations, context) where
translations: list of dicts like [{"lang": lang, "source": source_term, "value": value_term}]
context: {"translation_type": "text"/"char", "translation_show_source": True/False}
"""
self.ensure_one()
field = self._fields[field_name]
@@ -3098,9 +3097,7 @@ class BaseModel(metaclass=MetaModel):
for lang, term_lang in translations.items()]
context = {}
context['translation_type'] = 'text' if field.type in ['text', 'html'] else 'char'
context['translation_show_source'] = False
if callable(field.translate):
context['translation_show_source'] = True
context['translation_show_source'] = callable(field.translate)
return translations, context
+2 -2
View File
@@ -37,7 +37,7 @@ PYTHON_TRANSLATION_COMMENT = 'odoo-python'
# translation used for javascript code in web client
JAVASCRIPT_TRANSLATION_COMMENT = 'odoo-javascript'
# used to notify web client that these translations should be loaded in the UI
# depreciated comment since Odoo 16.0
# deprecated comment since Odoo 16.0
WEB_TRANSLATION_COMMENT = "openerp-web"
SKIPPED_ELEMENTS = ('script', 'style', 'title')
@@ -1516,7 +1516,7 @@ class CodeTranslations:
def filter_func(row):
# In the pot files with new translations, a code translation should have either
# PYTHON_TRANSLATION_COMMENT or JAVASCRIPT_TRANSLATION_COMMENT for comments.
# If a comment has neither the above comments, the pot file uses the depreciated
# If a comment has neither the above comments, the pot file uses the deprecated
# comments. And all code translations are stored as python translations.
return row.get('value') and (
PYTHON_TRANSLATION_COMMENT in row['comments']