Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/guide.py: 0%

199 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 

7import os 

8from typing import TYPE_CHECKING 

9 

10from django.conf import settings 

11from django.core.cache import cache 

12from django.template.loader import render_to_string 

13from django.urls import reverse 

14from django.utils.translation import gettext_lazy 

15 

16from weblate.addons.models import ADDONS 

17from weblate.trans.actions import ActionEvents 

18from weblate.utils.docs import get_doc_url 

19 

20if TYPE_CHECKING: 

21 from django_stubs_ext import StrOrPromise 

22 

23GUIDELINES = [] 

24 

25 

26def register(cls): 

27 GUIDELINES.append(cls) 

28 return cls 

29 

30 

31class Guideline: 

32 description: StrOrPromise = "" 

33 group = False 

34 url = "" 

35 anchor = "" 

36 hint = False 

37 

38 def __init__(self, component) -> None: 

39 self.component = component 

40 self.passed = self.is_passing() 

41 

42 def is_passing(self) -> bool: 

43 raise NotImplementedError 

44 

45 def is_relevant(self) -> bool: 

46 return True 

47 

48 def get_url(self): 

49 url = reverse(self.url, kwargs={"path": self.component.get_url_path()}) 

50 if self.anchor: 

51 url = f"{url}#{self.anchor}" 

52 return url 

53 

54 def get_doc_url(self, user=None) -> str: 

55 return "" 

56 

57 

58class Group(Guideline): 

59 group = True 

60 

61 def is_passing(self) -> bool: 

62 # Not used 

63 return False 

64 

65 def get_url(self) -> str: 

66 # Not used 

67 return "" 

68 

69 

70@register 

71class VCSGroup(Group): 

72 description = gettext_lazy("Version control integration") 

73 

74 def get_doc_url(self, user=None): 

75 return get_doc_url("vcs", user=user) 

76 

77 

78@register 

79class HookGuideline(Guideline): 

80 description = gettext_lazy( 

81 "Configure repository hooks for automated flow of updates to Weblate." 

82 ) 

83 url = "settings" 

84 anchor = "vcs" 

85 

86 def is_passing(self): 

87 return self.component.change_set.filter(action=ActionEvents.HOOK).exists() 

88 

89 def is_relevant(self) -> bool: 

90 return not self.component.is_repo_link 

91 

92 def get_url(self): 

93 return self.get_doc_url() 

94 

95 def get_doc_url(self, user=None): 

96 return get_doc_url("admin/continuous", "update-vcs", user=user) 

97 

98 

99@register 

100class PushGuideline(Guideline): 

101 description = gettext_lazy( 

102 "Configure push URL for automated flow of translations from Weblate." 

103 ) 

104 url = "settings" 

105 anchor = "vcs" 

106 

107 def is_passing(self): 

108 return self.component.can_push() 

109 

110 def get_doc_url(self, user=None): 

111 return get_doc_url("admin/continuous", "push-changes", user=user) 

112 

113 

114@register 

115class CommunityGroup(Group): 

116 description = gettext_lazy("Building community") 

117 

118 def get_doc_url(self, user=None): 

119 return get_doc_url("devel/community", user=user) 

120 

121 

122@register 

123class InstructionsGuideline(Guideline): 

124 description = gettext_lazy( 

125 "Define translation instructions to give translators a guideline." 

126 ) 

127 

128 def is_passing(self): 

129 return bool(self.component.project.instructions) 

130 

131 def get_url(self): 

132 return reverse( 

133 "settings", kwargs={"path": self.component.project.get_url_path()} 

134 ) 

135 

136 def get_doc_url(self, user=None): 

137 return get_doc_url("admin/projects", "project", user=user) 

138 

139 

140@register 

141class LicenseGuideline(Guideline): 

142 description = gettext_lazy( 

143 "Make your translations available under a libre license." 

144 ) 

145 url = "settings" 

146 anchor = "basic" 

147 

148 def is_relevant(self): 

149 return settings.LICENSE_REQUIRED 

150 

151 def is_passing(self): 

152 return self.component.libre_license 

153 

154 def get_doc_url(self, user=None) -> str: 

155 return "https://choosealicense.com/" 

156 

157 

158@register 

159class AlertGuideline(Guideline): 

160 description = gettext_lazy("Fix this component to clear its alerts.") 

161 url = "show" 

162 anchor = "alerts" 

163 

164 def is_passing(self) -> bool: 

165 return not self.component.all_active_alerts 

166 

167 def get_doc_url(self, user=None): 

168 return get_doc_url("devel/alerts", user=user) 

169 

170 

171@register 

172class ContextGroup(Group): 

173 description = gettext_lazy("Provide context to the translators") 

174 

175 def get_doc_url(self, user=None): 

176 return get_doc_url("admin/translating", "additional", user=user) 

177 

178 

179@register 

180class ScreenshotGuideline(Guideline): 

181 description = gettext_lazy("Add screenshots to show where strings are being used.") 

182 url = "screenshots" 

183 

184 def is_passing(self): 

185 from weblate.screenshots.models import Screenshot 

186 

187 return Screenshot.objects.filter(translation__component=self.component).exists() 

188 

189 def get_doc_url(self, user=None): 

190 return get_doc_url("admin/translating", "screenshots", user=user) 

191 

192 

193@register 

194class FlagsGuideline(Guideline): 

195 description = gettext_lazy( 

196 "Use flags to indicate special strings in your translation." 

197 ) 

198 url = "settings" 

199 anchor = "show" 

200 

201 def is_passing(self): 

202 return ( 

203 bool(self.component.check_flags) 

204 or self.component.source_translation.unit_set.exclude( 

205 extra_flags="" 

206 ).exists() 

207 ) 

208 

209 def get_doc_url(self, user=None): 

210 return get_doc_url("admin/checks", "custom-checks", user=user) 

211 

212 

213@register 

214class SafeHTMLGuideline(Guideline): 

215 description = gettext_lazy( 

216 "Add safe-html flag to avoid dangerous HTML from translators for strings which are rendered as HTML." 

217 ) 

218 url = "settings" 

219 anchor = "show" 

220 hint = True 

221 

222 def is_relevant(self): 

223 cache_key = f"guide:safe-html:{self.component.id}" 

224 result = cache.get(cache_key) 

225 if result is not None: 

226 return result 

227 result = self.component.source_translation.unit_set.filter( 

228 source__contains="<a " 

229 ).exists() 

230 cache.set(cache_key, result, 86400) 

231 return result 

232 

233 def is_passing(self): 

234 return ( 

235 "safe-html" in self.component.check_flags 

236 or self.component.source_translation.unit_set.filter( 

237 extra_flags__contains="safe-html" 

238 ).exists() 

239 ) 

240 

241 def get_doc_url(self, user=None): 

242 return get_doc_url("user/checks", "check-safe-html", user=user) 

243 

244 

245@register 

246class AddonsGroup(Group): 

247 description = gettext_lazy("Workflow customization") 

248 

249 def get_doc_url(self, user=None): 

250 return get_doc_url("admin/addons", user=user) 

251 

252 

253class AddonGuideline(Guideline): 

254 addon = "" 

255 url = "addons" 

256 

257 def is_passing(self): 

258 return self.addon in self.component.addons_cache["__names__"] 

259 

260 def is_relevant(self): 

261 if self.addon not in ADDONS: 

262 return False 

263 addon = ADDONS[self.addon] 

264 return addon.can_install(self.component, None) 

265 

266 def get_doc_url(self, user=None): 

267 return get_doc_url( 

268 "admin/addons", ADDONS[self.addon].get_doc_anchor(), user=user 

269 ) 

270 

271 @property 

272 def description(self): 

273 return render_to_string( 

274 "trans/guide/addon.html", 

275 { 

276 "name": ADDONS[self.addon].verbose, 

277 "description": ADDONS[self.addon].description, 

278 }, 

279 ) 

280 

281 

282@register 

283class LanguageConsistencyGuideline(AddonGuideline): 

284 addon = "weblate.consistency.languages" 

285 hint = True 

286 

287 def is_relevant(self): 

288 if self.component.project.component_set.exclude(is_glossary=True).count() <= 1: 

289 return False 

290 return super().is_relevant() 

291 

292 

293@register 

294class LinguasGuideline(AddonGuideline): 

295 addon = "weblate.gettext.linguas" 

296 

297 

298@register 

299class ConfigureGuideline(AddonGuideline): 

300 addon = "weblate.gettext.configure" 

301 

302 

303@register 

304class CleanupGuideline(AddonGuideline): 

305 addon = "weblate.cleanup.generic" 

306 hint = True 

307 

308 

309@register 

310class GenerateMoGuideline(AddonGuideline): 

311 addon = "weblate.gettext.mo" 

312 

313 def is_relevant(self): 

314 if not super().is_relevant(): 

315 return False 

316 component = self.component 

317 translations = component.translation_set.exclude( 

318 pk=component.source_translation.id 

319 ) 

320 try: 

321 translation = translations[0] 

322 except IndexError: 

323 return False 

324 if not translation.filename.endswith(".po"): 

325 return False 

326 mofilename = translation.get_filename()[:-3] + ".mo" 

327 return os.path.exists(mofilename)