details: https://code.tryton.org/tryton/commit/b7e6867ab2a9
branch: default
user: Cédric Krier <[email protected]>
date: Thu Oct 08 23:57:20 2026 +0200
description:
Add scheduled task to synchronize local translations
Closes #15144
diffstat:
trytond/CHANGELOG | 1 +
trytond/doc/modules/ir/design/translation.inc.rst | 9 +
trytond/doc/topics/translation.rst | 28 +---
trytond/trytond/ir/cron.py | 1 +
trytond/trytond/ir/translation.py | 89 +++++++++----
trytond/trytond/ir/translation.xml | 39 +-----
trytond/trytond/ir/tryton.cfg | 2 -
trytond/trytond/ir/view/translation_clean_start_form.xml | 8 -
trytond/trytond/ir/view/translation_clean_succeed_form.xml | 7 -
9 files changed, 87 insertions(+), 97 deletions(-)
diffs (394 lines):
diff -r f3aef553d05b -r b7e6867ab2a9 trytond/CHANGELOG
--- a/trytond/CHANGELOG Fri Oct 09 00:41:07 2026 +0200
+++ b/trytond/CHANGELOG Thu Oct 08 23:57:20 2026 +0200
@@ -1,3 +1,4 @@
+* Add scheduled task to synchronize local translations
* Support creating and writing translated fields in multiple languages at once
* Support reading translated fields in any suffixed language
* Rename delete_many to deletemany in the FileStore
diff -r f3aef553d05b -r b7e6867ab2a9
trytond/doc/modules/ir/design/translation.inc.rst
--- a/trytond/doc/modules/ir/design/translation.inc.rst Fri Oct 09 00:41:07
2026 +0200
+++ b/trytond/doc/modules/ir/design/translation.inc.rst Thu Oct 08 23:57:20
2026 +0200
@@ -6,6 +6,15 @@
The *Translation* stores the translated strings for each `Language
<model-ir.translation>`.
+.. seealso::
+
+ Translations are found by opening the main menu item:
+
+ |Administration --> Localization --> Translations|__
+
+ .. |Administration --> Localization --> Translations| replace::
:menuselection:`Administration --> Localization --> Translations`
+ __ https://demo.tryton.org/model/ir.translation
+
Wizards
-------
diff -r f3aef553d05b -r b7e6867ab2a9 trytond/doc/topics/translation.rst
--- a/trytond/doc/topics/translation.rst Fri Oct 09 00:41:07 2026 +0200
+++ b/trytond/doc/topics/translation.rst Thu Oct 08 23:57:20 2026 +0200
@@ -21,32 +21,16 @@
a term is missing in one language, it will search on the parent languages.
Also when activate a children language, you must also activate all parents.
-Translation Wizards
-===================
-
-Set Translations
-----------------
+Translation task
+================
-The wizard adds new translations to the base language ``en``.
-
-Clean Translations
-------------------
-
-The wizard deletes obsolete translations from the database.
+A `scheduled task <model-ir.cron>` updates the local translations frequently.
-Synchronize Translations
-------------------------
-
-The wizard updates the translations of the selected language based on the
-translations of the :ref:`database language <config-database.language>`.
-It will also remove duplicate translations with its direct parent.
+The translation files for a module can be exported with this command:
-Export Translations
--------------------
+.. code-block:: console
-The wizard requires to select a language and a module and will export the
-translations for this selection into a PO-file.
-
+ $ trytond -c <config file> -d <database> --export-translations -m <module>
-l <language>
Override translations
=====================
diff -r f3aef553d05b -r b7e6867ab2a9 trytond/trytond/ir/cron.py
--- a/trytond/trytond/ir/cron.py Fri Oct 09 00:41:07 2026 +0200
+++ b/trytond/trytond/ir/cron.py Thu Oct 08 23:57:20 2026 +0200
@@ -99,6 +99,7 @@
('ir.cron.log|clean', "Clean Cron Logs"),
('ir.filestore.queue|remove', "Remove Deleted Binaries"),
('ir.model|refresh_materialized', "Refresh Materialized Models"),
+ ('ir.translation|sync_local', "Synchronize Local Translations"),
], "Method", required=True, states=_states)
method_string = method.translated('method')
diff -r f3aef553d05b -r b7e6867ab2a9 trytond/trytond/ir/translation.py
--- a/trytond/trytond/ir/translation.py Fri Oct 09 00:41:07 2026 +0200
+++ b/trytond/trytond/ir/translation.py Thu Oct 08 23:57:20 2026 +0200
@@ -1108,6 +1108,35 @@
else:
return
+ @classmethod
+ def sync_local(cls, languages=None):
+ pool = Pool()
+ Lang = pool.get('ir.lang')
+ TranslationSet = pool.get('ir.translation.set', type='wizard')
+ TranslationClean = pool.get('ir.translation.clean', type='wizard')
+ TranslationUpdate = pool.get('ir.translation.update', type='wizard')
+
+ if languages is None:
+ languages = Lang.search([('translatable', '=', True)])
+
+ with Transaction().set_context(localonly=True):
+ session_id, _, _ = TranslationSet.create()
+ TranslationSet.execute(session_id, {}, 'set_')
+ TranslationSet.delete(session_id)
+
+ session_id, _, _ = TranslationClean.create()
+ TranslationClean.execute(session_id, {}, 'clean')
+ TranslationClean.delete(session_id)
+
+ for language in languages:
+ session_id, _, _ = TranslationUpdate.create()
+ TranslationUpdate.execute(session_id, {
+ 'start': {
+ 'language': language.id,
+ },
+ }, 'update')
+ TranslationUpdate.delete(session_id)
+
class TranslationSetStart(ModelView):
__name__ = 'ir.translation.set.start'
@@ -1205,7 +1234,10 @@
if self.model == Report:
reports = self.records
elif not self.model or self.model.__name__ == 'ir.ui.menu':
- reports = Report.search([('translatable', '=', True)])
+ domain = [('translatable', '=', True)]
+ if Transaction().context.get('localonly', False):
+ domain.append(('module', '=', None))
+ reports = Report.search(domain)
else:
return
@@ -1365,36 +1397,21 @@
def transition_set_(self):
pool = Pool()
Config = pool.get('ir.configuration')
- default_lang = Config.get_language()
- if default_lang != INTERNAL_LANG:
- raise DefaultLanguageError(gettext(
- 'ir.msg_translation_set_default_lang'))
+ if not Transaction().context.get('localonly', False):
+ default_lang = Config.get_language()
+ if default_lang != INTERNAL_LANG:
+ raise DefaultLanguageError(gettext(
+ 'ir.msg_translation_set_default_lang'))
+ self.set_view()
self.set_report()
- self.set_view()
return 'succeed'
-class TranslationCleanStart(ModelView):
- __name__ = 'ir.translation.clean.start'
-
-
-class TranslationCleanSucceed(ModelView):
- __name__ = 'ir.translation.clean.succeed'
-
-
class TranslationClean(Wizard):
__name__ = 'ir.translation.clean'
+ start_state = 'clean'
- start = StateView('ir.translation.clean.start',
- 'ir.translation_clean_start_view_form', [
- Button('Cancel', 'end', 'tryton-cancel'),
- Button('Clean', 'clean', 'tryton-ok', default=True),
- ])
clean = StateTransition()
- succeed = StateView('ir.translation.clean.succeed',
- 'ir.translation_clean_succeed_view_form', [
- Button('OK', 'end', 'tryton-ok', default=True),
- ])
@staticmethod
def _clean_field(translation):
@@ -1529,7 +1546,10 @@
to_delete = []
records = defaultdict(lambda: defaultdict(set))
keys = set()
- translations = Translation.search([])
+ domain = []
+ if Transaction().context.get('localonly', False):
+ domain.append(('module', '=', None))
+ translations = Translation.search(domain)
for translation in translations:
if getattr(self, '_clean_%s' % translation.type)(translation):
to_delete.append(translation.id)
@@ -1562,7 +1582,7 @@
to_delete.remove(mdata.db_id)
Translation.delete(Translation.browse(to_delete))
- return 'succeed'
+ return 'end'
class TranslationUpdateStart(ModelView):
@@ -1626,6 +1646,10 @@
& translation.name.in_([v.model for v in views]))
else:
source_clause = Literal(True)
+ if Transaction().context.get('localonly', False):
+ local_clause = translation.module == Null
+ else:
+ local_clause = Literal(True)
default_lang = Config.get_language()
columns = [
@@ -1639,10 +1663,12 @@
cursor.execute(*(translation.select(*columns,
where=(translation.lang == default_lang)
& source_clause
+ & local_clause
& translation.type.in_(self._source_types))
- translation.select(*columns,
where=(translation.lang == lang)
& source_clause
+ & local_clause
& translation.type.in_(self._source_types))))
with Translation.bulk_create() as create:
create.extend(cursor)
@@ -1652,10 +1678,12 @@
cursor.execute(*(translation.select(*columns,
where=(translation.lang == parent_lang)
& source_clause
+ & local_clause
& translation.type.in_(self._source_types))
& translation.select(*columns,
where=(translation.lang == lang)
& source_clause
+ & local_clause
& translation.type.in_(self._source_types))))
for row in cursor:
cursor_update.execute(*translation.update(
@@ -1680,9 +1708,11 @@
translation.module.as_('module')]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == default_lang)
+ & local_clause
& translation.type.in_(self._ressource_types))
- translation.select(*columns,
where=(translation.lang == lang)
+ & local_clause
& translation.type.in_(self._ressource_types))))
with Translation.bulk_create() as create:
create.extend(cursor)
@@ -1691,9 +1721,11 @@
columns.append(translation.value)
cursor.execute(*(translation.select(*columns,
where=(translation.lang == parent_lang)
+ & local_clause
& translation.type.in_(self._ressource_types))
& translation.select(*columns,
where=(translation.lang == lang)
+ & local_clause
& translation.type.in_(self._ressource_types))))
for row in cursor:
cursor_update.execute(*translation.update(
@@ -1715,9 +1747,11 @@
]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == default_lang)
+ & local_clause
& translation.type.in_(self._updatable_types))
- translation.select(*columns,
where=(translation.lang == lang)
+ & local_clause
& translation.type.in_(self._updatable_types))))
for row in cursor:
cursor_update.execute(*translation.update(
@@ -1734,10 +1768,12 @@
translation.src.as_('src'),
Max(translation.value).as_('value'),
where=(translation.lang == lang)
+ & local_clause
& translation.src.in_(
translation.select(translation.src,
where=((translation.value == '')
| (translation.value == Null))
+ & local_clause
& (translation.lang == lang)
& (translation.src != '')
& (translation.src != Null)))
@@ -1758,7 +1794,8 @@
[translation.fuzzy],
[False],
where=((translation.value == '') | (translation.value == Null))
- & (translation.lang == lang)))
+ & (translation.lang == lang)
+ & local_clause))
action['pyson_domain'] = PYSONEncoder().encode([
('module', '!=', None),
diff -r f3aef553d05b -r b7e6867ab2a9 trytond/trytond/ir/translation.xml
--- a/trytond/trytond/ir/translation.xml Fri Oct 09 00:41:07 2026 +0200
+++ b/trytond/trytond/ir/translation.xml Thu Oct 08 23:57:20 2026 +0200
@@ -101,33 +101,11 @@
<field name="model">ir.ui.view,-1</field>
<field name="action" ref="act_translation_set"/>
</record>
- <menuitem
- parent="menu_localization"
- action="act_translation_set"
- sequence="90"
- id="menu_translation_set"/>
-
- <record model="ir.ui.view" id="translation_clean_start_view_form">
- <field name="model">ir.translation.clean.start</field>
- <field name="type">form</field>
- <field name="name">translation_clean_start_form</field>
- </record>
-
- <record model="ir.ui.view" id="translation_clean_succeed_view_form">
- <field name="model">ir.translation.clean.succeed</field>
- <field name="type">form</field>
- <field name="name">translation_clean_succeed_form</field>
- </record>
<record model="ir.action.wizard" id="act_translation_clean">
<field name="name">Clean Translations</field>
<field name="wiz_name">ir.translation.clean</field>
</record>
- <menuitem
- parent="menu_localization"
- action="act_translation_clean"
- sequence="91"
- id="menu_translation_clean"/>
<record model="ir.ui.view" id="translation_update_start_view_form">
<field name="model">ir.translation.update.start</field>
@@ -151,11 +129,6 @@
<field name="model">ir.ui.view,-1</field>
<field name="action" ref="act_translation_update"/>
</record>
- <menuitem
- parent="menu_localization"
- action="act_translation_update"
- sequence="92"
- id="menu_translation_update"/>
<record model="ir.ui.view" id="translation_export_start_view_form">
<field name="model">ir.translation.export.start</field>
@@ -173,10 +146,12 @@
<field name="name">Export Translations</field>
<field name="wiz_name">ir.translation.export</field>
</record>
- <menuitem
- parent="menu_localization"
- action="act_translation_export"
- sequence="93"
- id="menu_translation_export"/>
+ </data>
+ <data noupdate="1">
+ <record model="ir.cron" id="cron_translation_sync_local">
+ <field name="method">ir.translation|sync_local</field>
+ <field name="interval_number" eval="1"/>
+ <field name="interval_type">days</field>
+ </record>
</data>
</tryton>
diff -r f3aef553d05b -r b7e6867ab2a9 trytond/trytond/ir/tryton.cfg
--- a/trytond/trytond/ir/tryton.cfg Fri Oct 09 00:41:07 2026 +0200
+++ b/trytond/trytond/ir/tryton.cfg Thu Oct 08 23:57:20 2026 +0200
@@ -33,8 +33,6 @@
translation.Translation
translation.TranslationSetStart
translation.TranslationSetSucceed
- translation.TranslationCleanStart
- translation.TranslationCleanSucceed
translation.TranslationUpdateStart
translation.TranslationExportStart
translation.TranslationExportResult
diff -r f3aef553d05b -r b7e6867ab2a9
trytond/trytond/ir/view/translation_clean_start_form.xml
--- a/trytond/trytond/ir/view/translation_clean_start_form.xml Fri Oct 09
00:41:07 2026 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<form col="2">
- <image name="tryton-question" xexpand="0" xfill="0"/>
- <label string="Clean Translations?" id="clean"
- yalign="0.5" xalign="0.0" xexpand="1"/>
-</form>
diff -r f3aef553d05b -r b7e6867ab2a9
trytond/trytond/ir/view/translation_clean_succeed_form.xml
--- a/trytond/trytond/ir/view/translation_clean_succeed_form.xml Fri Oct
09 00:41:07 2026 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<form>
- <image name="tryton-info" xexpand="0" xfill="0"/>
- <label string="Translations cleaned successfully." id="succeed"
yalign="0.5" xalign="0.0" xexpand="1"/>
-</form>