Coverage for app/venv/lib/python3.14/site-packages/weblate/addons/generate.py: 32%
160 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 typing import TYPE_CHECKING, ClassVar
9from django.db.models import F, Q
10from django.utils.translation import gettext_lazy
12from weblate.addons.base import BaseAddon
13from weblate.addons.events import AddonEvent
14from weblate.addons.forms import GenerateForm, PseudolocaleAddonForm
15from weblate.checks.flags import Flags
16from weblate.trans.actions import ActionEvents
17from weblate.trans.models import Translation
18from weblate.utils.errors import report_error
19from weblate.utils.render import render_template
20from weblate.utils.state import (
21 STATE_EMPTY,
22 STATE_FUZZY,
23 STATE_READONLY,
24 STATE_TRANSLATED,
25)
27if TYPE_CHECKING: 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true
28 from weblate.auth.models import User
29 from weblate.trans.models import Component, Unit
30 from weblate.utils.state import (
31 StringState,
32 )
35class GenerateFileAddon(BaseAddon):
36 events: ClassVar[set[AddonEvent]] = {
37 AddonEvent.EVENT_PRE_COMMIT,
38 }
39 name = "weblate.generate.generate"
40 verbose = gettext_lazy("Statistics generator")
41 description = gettext_lazy(
42 "Generates a file containing detailed info about the translation status."
43 )
44 settings_form = GenerateForm
45 multiple = True
46 icon = "poll.svg"
48 @classmethod
49 def can_install(cls, component: Component, user: User | None) -> bool:
50 if not component.translation_set.exists():
51 return False
52 return super().can_install(component, user)
54 def pre_commit(
55 self,
56 translation: Translation,
57 author: str,
58 store_hash: bool,
59 activity_log_id: int | None = None,
60 ) -> None:
61 filename = self.render_repo_filename(
62 self.instance.configuration["filename"], translation
63 )
64 if not filename:
65 return
66 content = render_template(
67 self.instance.configuration["template"], translation=translation
68 )
69 with open(filename, "w") as handle:
70 handle.write(content)
71 translation.addon_commit_files.append(filename)
74class LocaleGenerateAddonBase(BaseAddon):
75 events: ClassVar[set[AddonEvent]] = {
76 AddonEvent.EVENT_COMPONENT_UPDATE,
77 AddonEvent.EVENT_DAILY,
78 }
79 multiple = True
80 icon = "language.svg"
82 def fetch_strings(self, translation: Translation, query: Q) -> dict[int, Unit]:
83 return {
84 unit.source_unit_id: unit for unit in translation.unit_set.filter(query)
85 }
87 def generate_translation(
88 self,
89 source_translation: Translation,
90 target_translation: Translation,
91 query: Q,
92 *,
93 prefix: str = "",
94 suffix: str = "",
95 var_prefix: str = "",
96 var_suffix: str = "",
97 var_multiplier: float = 0.0,
98 target_state: StringState = STATE_TRANSLATED,
99 ) -> int:
100 updated = 0
101 sources = self.fetch_strings(source_translation, Q(state__gte=STATE_TRANSLATED))
102 targets = self.fetch_strings(target_translation, query)
103 for source_id, unit in targets.items():
104 if source_id not in sources:
105 continue
106 source_strings = sources[source_id].get_target_plurals(
107 target_translation.plural.number
108 )
109 last_string = ""
110 for i, source_string in enumerate(source_strings):
111 if source_string:
112 last_string = source_string
113 else:
114 source_strings[i] = last_string
115 new_strings = []
116 for source in source_strings:
117 multi = int(var_multiplier * len(source))
118 new_strings.append(
119 f"{prefix}{var_prefix * multi}{source}{var_suffix * multi}{suffix}"
120 )
121 target_strings = unit.get_target_plurals()
122 if new_strings != target_strings or unit.state < STATE_TRANSLATED:
123 unit.translate(
124 self.user,
125 new_strings,
126 target_state,
127 change_action=ActionEvents.AUTO,
128 propagate=False,
129 )
130 updated += 1
131 if updated > 0:
132 target_translation.invalidate_cache()
133 return updated
135 def daily(self, component: Component, activity_log_id: int | None = None) -> None:
136 raise NotImplementedError
138 def component_update(
139 self, component: Component, activity_log_id: int | None = None
140 ) -> None:
141 raise NotImplementedError
144class PseudolocaleAddon(LocaleGenerateAddonBase):
145 name = "weblate.generate.pseudolocale"
146 verbose = gettext_lazy("Pseudolocale generation")
147 description = gettext_lazy(
148 "Generates a translation by adding prefix and suffix "
149 "to source strings automatically."
150 )
151 settings_form = PseudolocaleAddonForm
152 user_name = "pseudolocale"
153 user_verbose = "Pseudolocale add-on"
155 def daily(self, component: Component, activity_log_id: int | None = None) -> None:
156 # Check all strings
157 query = Q(state__lte=STATE_TRANSLATED)
158 if self.instance.configuration.get("include_readonly", False):
159 query |= Q(state=STATE_READONLY)
160 self.do_update(component, query)
162 def component_update(
163 self, component: Component, activity_log_id: int | None = None
164 ) -> None:
165 # Update only untranslated strings
166 self.do_update(component, Q(state__lt=STATE_TRANSLATED))
168 def get_target_translation(self, component: Component) -> Translation:
169 return component.translation_set.get(pk=self.instance.configuration["target"])
171 def do_update(self, component: Component, query: Q) -> None:
172 try:
173 source_translation = component.translation_set.get(
174 pk=self.instance.configuration["source"]
175 )
176 target_translation = self.get_target_translation(component)
177 except Translation.DoesNotExist:
178 # Uninstall misconfigured add-on
179 report_error("add-on error", project=component.project)
180 self.instance.disable()
181 return
182 var_multiplier = self.instance.configuration.get("var_multiplier")
183 # As it is optional, it can be stored as None
184 if var_multiplier is None:
185 var_multiplier = 0.1
186 self.generate_translation(
187 source_translation,
188 target_translation,
189 prefix=self.instance.configuration.get("prefix", ""),
190 suffix=self.instance.configuration.get("suffix", ""),
191 var_prefix=self.instance.configuration.get("var_prefix", ""),
192 var_suffix=self.instance.configuration.get("var_suffix", ""),
193 var_multiplier=var_multiplier,
194 query=query,
195 )
197 def post_uninstall(self) -> None:
198 if self.instance.component:
199 try:
200 target_translation = self.get_target_translation(
201 self.instance.component
202 )
203 flags = Flags(target_translation.check_flags)
204 flags.remove("ignore-all-checks")
205 target_translation.check_flags = flags.format()
206 target_translation.save(update_fields=["check_flags"])
207 except Translation.DoesNotExist:
208 pass
209 super().post_uninstall()
211 def post_configure_run(self) -> None:
212 super().post_configure_run()
213 if self.instance.component:
214 try:
215 target_translation = self.get_target_translation(
216 self.instance.component
217 )
218 flags = Flags(target_translation.check_flags)
219 flags.merge("ignore-all-checks")
220 target_translation.check_flags = flags.format()
221 target_translation.save(update_fields=["check_flags"])
222 except Translation.DoesNotExist:
223 pass
226class PrefillAddon(LocaleGenerateAddonBase):
227 name = "weblate.generate.prefill"
228 verbose = gettext_lazy("Prefill translation with source")
229 description = gettext_lazy("Fills in translation strings with source string.")
230 user_name = "prefill"
231 user_verbose = "Prefill add-on"
233 def daily(self, component: Component, activity_log_id: int | None = None) -> None:
234 # Check all strings
235 self.do_update(component)
237 def component_update(
238 self, component: Component, activity_log_id: int | None = None
239 ) -> None:
240 # Update only untranslated strings
241 self.do_update(component)
243 def do_update(self, component: Component) -> None:
244 source_translation = component.source_translation
245 updated = 0
246 for translation in component.translation_set.prefetch():
247 if translation.is_source:
248 continue
249 updated += self.generate_translation(
250 source_translation,
251 translation,
252 target_state=STATE_FUZZY,
253 query=Q(state=STATE_EMPTY),
254 )
255 if updated:
256 # Commit generated changes to the files
257 component.commit_pending("add-on", None)
260class FillReadOnlyAddon(LocaleGenerateAddonBase):
261 name = "weblate.generate.fill_read_only"
262 verbose = gettext_lazy("Fill read-only strings with source")
263 description = gettext_lazy(
264 "Fills in translation of read-only strings with source string."
265 )
266 user_name = "fill"
267 user_verbose = "Fill read-only add-on"
269 def daily(self, component: Component, activity_log_id: int | None = None) -> None:
270 self.do_update(component)
272 def component_update(
273 self, component: Component, activity_log_id: int | None = None
274 ) -> None:
275 self.do_update(component)
277 def do_update(self, component: Component) -> None:
278 source_translation = component.source_translation
279 updated = 0
280 for translation in component.translation_set.prefetch():
281 if translation.is_source:
282 continue
283 updated += self.generate_translation(
284 source_translation,
285 translation,
286 target_state=STATE_READONLY,
287 query=Q(state=STATE_READONLY) & ~Q(target=F("source")),
288 )
289 if updated:
290 # Commit generated changes to the files
291 component.commit_pending("add-on", None)