Coverage for app/venv/lib/python3.14/site-packages/weblate/addons/autotranslate.py: 26%
75 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
5from __future__ import annotations
7from collections import defaultdict
8from typing import TYPE_CHECKING, ClassVar
10from django.conf import settings
11from django.utils import timezone
12from django.utils.translation import gettext_lazy
14from weblate.addons.base import BaseAddon
15from weblate.addons.events import AddonEvent
16from weblate.addons.forms import AutoAddonForm
17from weblate.trans.actions import ACTIONS_CONTENT, ActionEvents
18from weblate.trans.models import Component
19from weblate.trans.tasks import auto_translate, auto_translate_component
21if TYPE_CHECKING: 21 ↛ 22line 21 didn't jump to line 22 because the condition on line 21 was never true
22 from django.forms.boundfield import BoundField
23 from django_stubs_ext import StrOrPromise
25 from weblate.trans.models import Change
27SKIP_ACTIONS = {ActionEvents.AUTO, ActionEvents.ENFORCED_CHECK}
30class AutoTranslateAddon(BaseAddon):
31 events: ClassVar[set[AddonEvent]] = {
32 AddonEvent.EVENT_COMPONENT_UPDATE,
33 AddonEvent.EVENT_DAILY,
34 AddonEvent.EVENT_CHANGE,
35 }
36 name = "weblate.autotranslate.autotranslate"
37 verbose = gettext_lazy("Automatic translation")
38 description = gettext_lazy(
39 "Automatically translates strings using machine translation or "
40 "other components."
41 )
42 settings_form = AutoAddonForm
43 multiple = True
44 icon = "language.svg"
46 def component_update(
47 self, component: Component, activity_log_id: int | None = None
48 ) -> None:
49 self.trigger_autotranslate(component=component)
51 def trigger_autotranslate(
52 self,
53 *,
54 component: Component | None = None,
55 user_id: int | None = None,
56 translation_id: int | None = None,
57 unit_ids: list[int] | None = None,
58 ) -> None:
59 conf = self.instance.configuration
60 if component is None:
61 auto_translate.delay_on_commit(
62 mode=conf["mode"],
63 q=conf["q"],
64 auto_source=conf["auto_source"],
65 engines=conf["engines"],
66 threshold=conf["threshold"],
67 component=conf["component"],
68 user_id=user_id,
69 translation_id=translation_id,
70 unit_ids=unit_ids,
71 )
72 else:
73 auto_translate_component.delay_on_commit(
74 component.pk,
75 mode=conf["mode"],
76 q=conf["q"],
77 auto_source=conf["auto_source"],
78 engines=conf["engines"],
79 threshold=conf["threshold"],
80 component=conf["component"],
81 )
83 def daily(self, component: Component, activity_log_id: int | None = None) -> None:
84 # Translate every component less frequenctly to reduce load.
85 # The translation is anyway triggered on update, so it should
86 # not matter that much that we run this less often.
87 if settings.BACKGROUND_TASKS == "never":
88 return
89 today = timezone.now()
90 if settings.BACKGROUND_TASKS == "monthly" and component.id % 30 != today.day:
91 return
92 if (
93 settings.BACKGROUND_TASKS == "weekly"
94 and component.id % 7 != today.weekday()
95 ):
96 return
98 self.trigger_autotranslate(component=component)
100 def change_event(self, change: Change, activity_log_id: int | None = None) -> None:
101 units = []
102 if change.action in ACTIONS_CONTENT and change.action not in SKIP_ACTIONS:
103 if change.unit is not None:
104 units.append(change.unit)
105 elif change.action in {
106 ActionEvents.SCREENSHOT_UPLOADED,
107 ActionEvents.SCREENSHOT_ADDED,
108 }:
109 if change.screenshot is not None:
110 units.extend(change.screenshot.units.all())
111 elif change.action in {
112 ActionEvents.SUGGESTION,
113 ActionEvents.SUGGESTION_CLEANUP,
114 ActionEvents.SUGGESTION_DELETE,
115 }:
116 if change.suggestion is not None:
117 units.append(change.suggestion.unit)
118 elif (
119 change.action
120 in {
121 ActionEvents.COMMENT,
122 ActionEvents.COMMENT_RESOLVE,
123 ActionEvents.COMMENT_DELETE,
124 }
125 and change.comment is not None
126 ):
127 units.append(change.comment.unit)
129 all_units = set()
130 for unit in units:
131 all_units.add(unit)
132 # for source units, trigger auto-translation on all target units
133 if unit.is_source:
134 all_units.update(unit.unit_set.exclude(pk=unit.pk))
136 translation_with_unit_ids = defaultdict(list)
137 for unit in all_units:
138 if unit.labels.filter(name="Automatically translated").exists():
139 # Skip already automatically translated strings here to avoid repeated
140 # translating, for example with enforced checks.
141 continue
142 translation_with_unit_ids[unit.translation.id].append(unit.pk)
144 for translation_id, unit_ids in translation_with_unit_ids.items():
145 self.trigger_autotranslate(
146 user_id=change.user_id,
147 translation_id=translation_id,
148 unit_ids=unit_ids,
149 )
151 def show_setting_field(self, field: BoundField) -> bool:
152 auto_source = self.instance.configuration["auto_source"]
153 # Do not show UI hidden fields
154 if (auto_source == "mt" and field.name == "component") or (
155 auto_source == "others" and field.name in {"engines", "threshold"}
156 ):
157 return False
158 return not field.is_hidden and field.value()
160 def get_setting_value(self, field: BoundField) -> StrOrPromise:
161 if field.name == "component" and not hasattr(field.field, "choices"):
162 # Manually handle char field
163 return str(Component.objects.get(pk=field.value()))
164 return super().get_setting_value(field)