Coverage for app/venv/lib/python3.14/site-packages/weblate/checks/glossary.py: 33%
67 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 typing import TYPE_CHECKING
10from django.utils.html import escape, format_html
11from django.utils.translation import gettext, gettext_lazy
13from weblate.checks.base import TargetCheck
14from weblate.utils.csv import (
15 PROHIBITED_INITIAL_CHARS,
16 PROHIBITED_INITIAL_CHARS_FOR_DISPLAY,
17)
18from weblate.utils.html import format_html_join_comma
20if TYPE_CHECKING: 20 ↛ 21line 20 didn't jump to line 21 because the condition on line 20 was never true
21 from weblate.trans.models import Unit
24class GlossaryCheck(TargetCheck):
25 default_disabled = True
26 check_id = "check_glossary"
27 name = gettext_lazy("Does not follow glossary")
28 description = gettext_lazy(
29 "The translation does not follow terms defined in a glossary."
30 )
32 def check_single(self, source: str, target: str, unit: Unit):
33 from weblate.glossary.models import get_glossary_terms
35 forbidden = set()
36 mismatched = set()
37 matched = set()
38 boundary = r"\b" if unit.translation.language.uses_whitespace() else ""
39 for term in get_glossary_terms(unit, include_variants=False):
40 term_source = term.source
41 flags = term.all_flags
42 expected = term_source if "read-only" in flags else term.target
43 if "forbidden" in flags:
44 if re.search(
45 rf"{boundary}{re.escape(expected)}{boundary}", target, re.IGNORECASE
46 ):
47 forbidden.add(term_source)
48 else:
49 if term_source in matched:
50 continue
51 if re.search(
52 rf"{boundary}{re.escape(expected)}{boundary}", target, re.IGNORECASE
53 ):
54 mismatched.discard(term_source)
55 matched.add(term_source)
56 else:
57 mismatched.add(term_source)
59 return forbidden | mismatched
61 def get_description(self, check_obj):
62 unit = check_obj.unit
63 sources = unit.get_source_plurals()
64 targets = unit.get_target_plurals()
65 source = sources[0]
66 results = set()
67 # Check singular
68 result = self.check_single(source, targets[0], unit)
69 if result:
70 results.update(result)
71 # Do we have more to check?
72 if len(sources) > 1:
73 source = sources[1]
74 # Check plurals against plural from source
75 for target in targets[1:]:
76 result = self.check_single(source, target, unit)
77 if result:
78 results.update(result)
80 if not results:
81 return super().get_description(check_obj)
83 return format_html(
84 escape(
85 gettext("Following terms are not translated according to glossary: {}")
86 ),
87 format_html_join_comma("{}", ((term,) for term in sorted(results))),
88 )
91class ProhibitedInitialCharacterCheck(TargetCheck):
92 check_id = "prohibited_initial_character"
93 name = gettext_lazy("Prohibited initial character")
94 description = gettext_lazy("The string starts with a prohibited character in CSV.")
95 # Process readonly (source) strings
96 ignore_readonly = False
97 glossary = True
99 def should_skip(self, unit: Unit) -> bool:
100 if not unit.translation.component.is_glossary: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 return True
102 return super().should_skip(unit)
104 def check_single(self, source: str, target: str, unit: Unit) -> bool:
105 """Check if the source string starts with a prohibited character."""
106 return (target and target[0] in PROHIBITED_INITIAL_CHARS) or (
107 source and source[0] in PROHIBITED_INITIAL_CHARS
108 )
110 def get_description(self, check_obj) -> str:
111 """Return description of the check."""
112 return format_html(
113 escape(
114 gettext(
115 "The string starts with one or more of the following forbidden characters: {}"
116 )
117 ),
118 format_html_join_comma(
119 "<code>{}</code>", PROHIBITED_INITIAL_CHARS_FOR_DISPLAY
120 ),
121 )