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']