Coverage for app/venv/lib/python3.14/site-packages/weblate/checks/source.py: 75%
78 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 re
8from collections import defaultdict
9from datetime import timedelta
10from typing import TYPE_CHECKING
12from django.db.models import Count, F, Q
13from django.utils import timezone
14from django.utils.html import format_html, format_html_join
15from django.utils.translation import gettext, gettext_lazy
17from weblate.checks.base import BatchCheckMixin, SourceCheck
18from weblate.utils.html import format_html_join_comma
19from weblate.utils.state import STATE_EMPTY, STATE_FUZZY
21if TYPE_CHECKING: 21 ↛ 22line 21 didn't jump to line 22 because the condition on line 21 was never true
22 from collections.abc import Iterable
24 from weblate.trans.models import Component, Unit
26# Matches (s) not followed by alphanumeric chars or at the end
27PLURAL_MATCH = re.compile(r"\w\(s\)(\W|\Z)")
30class OptionalPluralCheck(SourceCheck):
31 """Check for not used plural form."""
33 check_id = "optional_plural"
34 name = gettext_lazy("Unpluralised")
35 description = gettext_lazy(
36 "The string is used as plural, but not using plural forms."
37 )
39 def check_source_unit(self, sources: list[str], unit: Unit):
40 if len(sources) > 1: 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true
41 return False
42 return len(PLURAL_MATCH.findall(sources[0])) > 0
45class EllipsisCheck(SourceCheck):
46 """Check for using "..." instead of "…"."""
48 check_id = "ellipsis"
49 name = gettext_lazy("Ellipsis")
50 description = gettext_lazy(
51 "The string uses three dots (...) instead of an ellipsis character (…)."
52 )
54 def check_source_unit(self, sources: list[str], unit: Unit):
55 return "..." in sources[0]
58class MultipleFailingCheck(SourceCheck, BatchCheckMixin):
59 """Check whether there are more failing checks on this translation."""
61 check_id = "multiple_failures"
62 name = gettext_lazy("Multiple failing checks")
63 description = gettext_lazy(
64 "The translations in several languages have failing checks."
65 )
67 def get_related_checks(self, unit_ids: Iterable[int]):
68 from weblate.checks.models import Check
70 return Check.objects.filter(
71 Q(unit_id__in=unit_ids) | Q(unit__source_unit_id__in=unit_ids)
72 ).select_related("unit__translation")
74 def check_source_unit(self, sources: list[str], unit: Unit):
75 if unit.translation.component.batch_checks:
76 return self.handle_batch(unit, unit.translation.component)
77 return self._check_unit(unit)
79 def _check_unit(self, unit: Unit) -> bool:
80 related = (
81 self.get_related_checks([unit.pk])
82 .values_list("unit__translation", flat=True)
83 .distinct()
84 )
85 return related.count() >= 2
87 def check_component(self, component: Component) -> Iterable[Unit]:
88 from weblate.trans.models import Unit
90 unit_id_and_check_count = (
91 self.get_related_checks(
92 Unit.objects.filter(translation__component=component).values_list(
93 "pk", flat=True
94 )
95 )
96 .values_list("unit__source_unit_id")
97 .annotate(translation_count=Count("unit__translation_id", distinct=True))
98 .filter(translation_count__gte=2)
99 )
100 return (
101 Unit.objects.prefetch()
102 .prefetch_bulk()
103 .filter(
104 id__in=unit_id_and_check_count.values_list(
105 "unit__source_unit_id", flat=True
106 )
107 )
108 )
110 def get_description(self, check_obj):
111 unit_ids = check_obj.unit.unit_set.exclude(pk=check_obj.unit.id).values_list(
112 "pk", flat=True
113 )
114 related = self.get_related_checks(unit_ids).select_related(
115 "unit__translation__language"
116 )
117 if not related:
118 return super().get_description(check_obj)
120 checks = defaultdict(list)
122 for check in related:
123 checks[check.name].append(check)
125 return format_html(
126 "{}<dl>{}</dl>",
127 gettext("Following checks are failing:"),
128 format_html_join(
129 "\n",
130 "<dt>{}</dt><dd>{}</dd>",
131 (
132 (
133 check_list[0].get_name(),
134 format_html_join_comma(
135 '<a href="{}">{}</a>',
136 (
137 (
138 check.unit.get_absolute_url(),
139 str(check.unit.translation.language),
140 )
141 for check in check_list
142 ),
143 ),
144 )
145 for check_list in checks.values()
146 ),
147 ),
148 )
151class LongUntranslatedCheck(SourceCheck, BatchCheckMixin):
152 check_id = "long_untranslated"
153 name = gettext_lazy("Long untranslated")
154 description = gettext_lazy("The string has not been translated for a long time.")
156 def check_source_unit(self, sources: list[str], unit: Unit) -> bool:
157 component = unit.translation.component
158 if component.batch_checks:
159 return self.handle_batch(unit, component)
160 return self._check_unit(unit)
162 def _check_unit(self, unit: Unit) -> bool:
163 if unit.timestamp > timezone.now() - timedelta(days=90): 163 ↛ 166line 163 didn't jump to line 166 because the condition on line 163 was always true
164 return False
166 states = list(unit.unit_set.values_list("state", flat=True))
167 total = len(states)
168 not_translated = states.count(STATE_EMPTY) + states.count(STATE_FUZZY)
169 translated_percent = 100 * (total - not_translated) / total
170 return (
171 total
172 and 2 * translated_percent
173 < unit.translation.component.stats.translated_percent
174 )
176 def check_component(self, component: Component) -> Iterable[Unit]:
177 from weblate.trans.models import Unit
179 result = (
180 Unit.objects.filter(
181 translation__component=component,
182 source_unit__timestamp__lt=timezone.now() - timedelta(days=90),
183 )
184 .values_list("source_unit_id")
185 .annotate(
186 total=Count("state"),
187 not_translated=Count("state", filter=Q(state__lte=STATE_FUZZY)),
188 )
189 .filter(total__gt=0)
190 .annotate(
191 translated_percent=100 * (F("total") - F("not_translated")) / F("total")
192 )
193 ).filter(translated_percent__lt=component.stats.translated_percent / 2)
194 return (
195 Unit.objects.prefetch()
196 .prefetch_bulk()
197 .filter(id__in=result.values_list("source_unit_id", flat=True))
198 )