diff --git a/addons/transifex/__init__.py b/addons/transifex/__init__.py
new file mode 100644
index 00000000000..dc5e6b693d1
--- /dev/null
+++ b/addons/transifex/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons/transifex/__manifest__.py b/addons/transifex/__manifest__.py
new file mode 100644
index 00000000000..2d56593f9a9
--- /dev/null
+++ b/addons/transifex/__manifest__.py
@@ -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',
+}
diff --git a/addons/transifex/data/transifex_data.xml b/addons/transifex/data/transifex_data.xml
new file mode 100644
index 00000000000..4ec36668c1a
--- /dev/null
+++ b/addons/transifex/data/transifex_data.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ transifex.project_url
+ https://www.transifex.com/odoo
+
+
+
+ Transifex: Reload code translations
+
+ code
+ model.reload()
+ 7
+ days
+ -1
+
+
+
+
diff --git a/addons/transifex/i18n/transifex.pot b/addons/transifex/i18n/transifex.pot
new file mode 100644
index 00000000000..5c47cb71971
--- /dev/null
+++ b/addons/transifex/i18n/transifex.pot
@@ -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 ""
diff --git a/addons/transifex/models/__init__.py b/addons/transifex/models/__init__.py
new file mode 100644
index 00000000000..4a641a2a50c
--- /dev/null
+++ b/addons/transifex/models/__init__.py
@@ -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
diff --git a/addons/transifex/models/models.py b/addons/transifex/models/models.py
new file mode 100644
index 00000000000..f110a314d61
--- /dev/null
+++ b/addons/transifex/models/models.py
@@ -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
diff --git a/addons/transifex/models/transifex_code_translation.py b/addons/transifex/models/transifex_code_translation.py
new file mode 100644
index 00000000000..37042f9d2ae
--- /dev/null
+++ b/addons/transifex/models/transifex_code_translation.py
@@ -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()
diff --git a/addons/transifex/models/transifex_translation.py b/addons/transifex/models/transifex_translation.py
new file mode 100644
index 00000000000..810ad0a70ac
--- /dev/null
+++ b/addons/transifex/models/transifex_translation.py
@@ -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}"
diff --git a/addons/transifex/security/ir.model.access.csv b/addons/transifex/security/ir.model.access.csv
new file mode 100644
index 00000000000..ae973ec18fb
--- /dev/null
+++ b/addons/transifex/security/ir.model.access.csv
@@ -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
diff --git a/addons/transifex/static/description/icon.png b/addons/transifex/static/description/icon.png
new file mode 100644
index 00000000000..9e6c9556004
Binary files /dev/null and b/addons/transifex/static/description/icon.png differ
diff --git a/addons/transifex/static/description/icon.svg b/addons/transifex/static/description/icon.svg
new file mode 100644
index 00000000000..a1c957930b5
--- /dev/null
+++ b/addons/transifex/static/description/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addons/transifex/static/src/views/fields/translation_dialog.xml b/addons/transifex/static/src/views/fields/translation_dialog.xml
new file mode 100644
index 00000000000..c7343f4bfa6
--- /dev/null
+++ b/addons/transifex/static/src/views/fields/translation_dialog.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/transifex/static/src/views/reload_code_translations_views.js b/addons/transifex/static/src/views/reload_code_translations_views.js
new file mode 100644
index 00000000000..ee4f474d958
--- /dev/null
+++ b/addons/transifex/static/src/views/reload_code_translations_views.js
@@ -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",
+});
diff --git a/addons/transifex/static/src/views/reload_code_translations_views.xml b/addons/transifex/static/src/views/reload_code_translations_views.xml
new file mode 100644
index 00000000000..b109b4b2d32
--- /dev/null
+++ b/addons/transifex/static/src/views/reload_code_translations_views.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/transifex/views/code_translation_views.xml b/addons/transifex/views/code_translation_views.xml
new file mode 100644
index 00000000000..b5ae3c7c5e5
--- /dev/null
+++ b/addons/transifex/views/code_translation_views.xml
@@ -0,0 +1,47 @@
+
+
+
+ transifex.code.translation.tree
+ transifex.code.translation
+
+
+
+
+
+
+
+
+
+
+
+
+ transifex.code.translation.view.search
+ transifex.code.translation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Transifex Code Translations
+
+
+ code
+ action = model._open_code_translations()
+
+
+
+
diff --git a/addons/web/static/src/views/fields/translation_dialog.js b/addons/web/static/src/views/fields/translation_dialog.js
index fde698b7141..c59cbf7c49a 100644
--- a/addons/web/static/src/views/fields/translation_dialog.js
+++ b/addons/web/static/src/views/fields/translation_dialog.js
@@ -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 &&
diff --git a/odoo/models.py b/odoo/models.py
index e6d0daee6a7..36f54d1338f 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -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
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index 2fee48869d8..05407f7ef46 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -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']