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>

Reply via email to