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
« 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
7import os
8from typing import TYPE_CHECKING
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
16from weblate.addons.models import ADDONS
17from weblate.trans.actions import ActionEvents
18from weblate.utils.docs import get_doc_url
20if TYPE_CHECKING:
21 from django_stubs_ext import StrOrPromise
23GUIDELINES = []
26def register(cls):
27 GUIDELINES.append(cls)
28 return cls
31class Guideline:
32 description: StrOrPromise = ""
33 group = False
34 url = ""
35 anchor = ""
36 hint = False
38 def __init__(self, component) -> None:
39 self.component = component
40 self.passed = self.is_passing()
42 def is_passing(self) -> bool:
43 raise NotImplementedError
45 def is_relevant(self) -> bool:
46 return True
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
54 def get_doc_url(self, user=None) -> str:
55 return ""
58class Group(Guideline):
59 group = True
61 def is_passing(self) -> bool:
62 # Not used
63 return False
65 def get_url(self) -> str:
66 # Not used
67 return ""
70@register
71class VCSGroup(Group):
72 description = gettext_lazy("Version control integration")
74 def get_doc_url(self, user=None):
75 return get_doc_url("vcs", user=user)
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"
86 def is_passing(self):
87 return self.component.change_set.filter(action=ActionEvents.HOOK).exists()
89 def is_relevant(self) -> bool:
90 return not self.component.is_repo_link
92 def get_url(self):
93 return self.get_doc_url()
95 def get_doc_url(self, user=None):
96 return get_doc_url("admin/continuous", "update-vcs", user=user)
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"
107 def is_passing(self):
108 return self.component.can_push()
110 def get_doc_url(self, user=None):
111 return get_doc_url("admin/continuous", "push-changes", user=user)
114@register
115class CommunityGroup(Group):
116 description = gettext_lazy("Building community")
118 def get_doc_url(self, user=None):
119 return get_doc_url("devel/community", user=user)
122@register
123class InstructionsGuideline(Guideline):
124 description = gettext_lazy(
125 "Define translation instructions to give translators a guideline."
126 )
128 def is_passing(self):
129 return bool(self.component.project.instructions)
131 def get_url(self):
132 return reverse(
133 "settings", kwargs={"path": self.component.project.get_url_path()}
134 )
136 def get_doc_url(self, user=None):
137 return get_doc_url("admin/projects", "project", user=user)
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"
148 def is_relevant(self):
149 return settings.LICENSE_REQUIRED
151 def is_passing(self):
152 return self.component.libre_license
154 def get_doc_url(self, user=None) -> str:
155 return "https://choosealicense.com/"
158@register
159class AlertGuideline(Guideline):
160 description = gettext_lazy("Fix this component to clear its alerts.")
161 url = "show"
162 anchor = "alerts"
164 def is_passing(self) -> bool:
165 return not self.component.all_active_alerts
167 def get_doc_url(self, user=None):
168 return get_doc_url("devel/alerts", user=user)
171@register
172class ContextGroup(Group):
173 description = gettext_lazy("Provide context to the translators")
175 def get_doc_url(self, user=None):
176 return get_doc_url("admin/translating", "additional", user=user)
179@register
180class ScreenshotGuideline(Guideline):
181 description = gettext_lazy("Add screenshots to show where strings are being used.")
182 url = "screenshots"
184 def is_passing(self):
185 from weblate.screenshots.models import Screenshot
187 return Screenshot.objects.filter(translation__component=self.component).exists()
189 def get_doc_url(self, user=None):
190 return get_doc_url("admin/translating", "screenshots", user=user)
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"
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 )
209 def get_doc_url(self, user=None):
210 return get_doc_url("admin/checks", "custom-checks", user=user)
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
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
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 )
241 def get_doc_url(self, user=None):
242 return get_doc_url("user/checks", "check-safe-html", user=user)
245@register
246class AddonsGroup(Group):
247 description = gettext_lazy("Workflow customization")
249 def get_doc_url(self, user=None):
250 return get_doc_url("admin/addons", user=user)
253class AddonGuideline(Guideline):
254 addon = ""
255 url = "addons"
257 def is_passing(self):
258 return self.addon in self.component.addons_cache["__names__"]
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)
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 )
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 )
282@register
283class LanguageConsistencyGuideline(AddonGuideline):
284 addon = "weblate.consistency.languages"
285 hint = True
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()
293@register
294class LinguasGuideline(AddonGuideline):
295 addon = "weblate.gettext.linguas"
298@register
299class ConfigureGuideline(AddonGuideline):
300 addon = "weblate.gettext.configure"
303@register
304class CleanupGuideline(AddonGuideline):
305 addon = "weblate.cleanup.generic"
306 hint = True
309@register
310class GenerateMoGuideline(AddonGuideline):
311 addon = "weblate.gettext.mo"
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)