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

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7from typing import TYPE_CHECKING, ClassVar 

8 

9from django.db.models import F, Q 

10from django.utils.translation import gettext_lazy 

11 

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) 

26 

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 ) 

33 

34 

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" 

47 

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) 

53 

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) 

72 

73 

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" 

81 

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 } 

86 

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 

134 

135 def daily(self, component: Component, activity_log_id: int | None = None) -> None: 

136 raise NotImplementedError 

137 

138 def component_update( 

139 self, component: Component, activity_log_id: int | None = None 

140 ) -> None: 

141 raise NotImplementedError 

142 

143 

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" 

154 

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) 

161 

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)) 

167 

168 def get_target_translation(self, component: Component) -> Translation: 

169 return component.translation_set.get(pk=self.instance.configuration["target"]) 

170 

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 ) 

196 

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() 

210 

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 

224 

225 

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" 

232 

233 def daily(self, component: Component, activity_log_id: int | None = None) -> None: 

234 # Check all strings 

235 self.do_update(component) 

236 

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) 

242 

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) 

258 

259 

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" 

268 

269 def daily(self, component: Component, activity_log_id: int | None = None) -> None: 

270 self.do_update(component) 

271 

272 def component_update( 

273 self, component: Component, activity_log_id: int | None = None 

274 ) -> None: 

275 self.do_update(component) 

276 

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)