Coverage for app/venv/lib/python3.14/site-packages/weblate/addons/properties.py: 21%
89 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
5"""
6Properties cleanup addon.
8This is reimplementation of
9https://github.com/freeplane/freeplane/blob/1.4.x/freeplane_ant/
10src/main/java/org/freeplane/ant/FormatTranslation.java
11"""
13from __future__ import annotations
15import re
16from typing import TYPE_CHECKING, ClassVar
18from django.utils.translation import gettext_lazy
20from weblate.addons.base import BaseAddon
21from weblate.addons.events import AddonEvent
22from weblate.addons.forms import PropertiesSortAddonForm
24if TYPE_CHECKING: 24 ↛ 25line 24 didn't jump to line 25 because the condition on line 24 was never true
25 from weblate.addons.base import CompatDict
27SPLITTER = re.compile(r"\s*=\s*")
28UNICODE = re.compile(r"\\[uU][0-9a-fA-F]{4}")
31def sort_key(line: str, case_sensitive: bool) -> str:
32 """Sort key for properties."""
33 prefix = SPLITTER.split(line, 1)[0]
34 if case_sensitive:
35 return prefix
36 return prefix.lower()
39def unicode_format(match) -> str:
40 """
41 Format unicode characters.
43 Callback for re.sub.
44 """
45 return f"\\u{match.group(0)[2:].upper()}"
48def fix_newlines(lines) -> None:
49 """Convert newlines to unix."""
50 for i, line in enumerate(lines):
51 if line.endswith("\r\n"):
52 lines[i] = line[:-2] + "\n"
53 elif line.endswith("\r"):
54 lines[i] = line[:-1] + "\n"
57def format_unicode(lines) -> None:
58 """Format unicode characters."""
59 for i, line in enumerate(lines):
60 if UNICODE.findall(line) is None:
61 continue
62 lines[i] = UNICODE.sub(unicode_format, line)
65def value_quality(value) -> int:
66 """Calculate value quality."""
67 if not value:
68 return 0
69 if "[translate me]" in value:
70 return 1
71 if "[auto]" in value:
72 return 2
73 return 3
76def filter_lines(lines: list[str]) -> list[str]:
77 """Filter comments, empty lines and duplicate strings."""
78 result: list[str] = []
79 lastkey = None
80 lastvalue = None
82 for line in lines:
83 # Skip comments and blank lines
84 if line[0] == "#" or not line.strip():
85 continue
86 parts = SPLITTER.split(line, 1)
88 # Missing = or empty key
89 if len(parts) != 2 or not parts[0]:
90 continue
92 key, value = parts
93 # Strip trailing \n in value
94 value = value[:-1]
96 # Empty translation
97 if value in {"", "[auto]", "[translate me]"}:
98 continue
100 # Check for duplicate key
101 if key == lastkey:
102 # Skip duplicate
103 if value == lastvalue:
104 continue
106 quality = value_quality(value)
107 lastquality = value_quality(lastvalue)
109 if quality > lastquality:
110 # Replace lower quality with new one
111 result.pop()
112 elif lastquality > quality or quality < 4:
113 # Drop lower quality one
114 continue
116 result.append(line)
117 lastkey = key
118 lastvalue = value
120 return result
123def format_file(filename: str, case_sensitive: bool) -> bool:
124 """Format single properties file."""
125 with open(filename) as handle:
126 lines = handle.readlines()
128 result = sorted(lines, key=lambda line: sort_key(line, case_sensitive))
130 fix_newlines(result)
131 format_unicode(result)
132 result = filter_lines(result)
134 if lines != result:
135 with open(filename, "w") as handle:
136 handle.writelines(result)
137 return True
138 return False
141class PropertiesSortAddon(BaseAddon):
142 events: ClassVar[set[AddonEvent]] = {
143 AddonEvent.EVENT_PRE_COMMIT,
144 }
145 name = "weblate.properties.sort"
146 verbose = gettext_lazy("Format the Java properties file")
147 description = gettext_lazy("Formats and sorts the Java properties file.")
148 compat: ClassVar[CompatDict] = {
149 "file_format": {"properties-utf8", "properties", "gwt"},
150 }
151 icon = "sort-alphabetical.svg"
152 settings_form = PropertiesSortAddonForm
154 def pre_commit(
155 self,
156 translation,
157 author: str,
158 store_hash: bool,
159 activity_log_id: int | None = None,
160 ) -> None:
161 case_sensitive = self.instance.configuration.get("case_sensitive", False)
162 changed = format_file(translation.get_filename(), case_sensitive)
163 if changed and store_hash:
164 translation.store_hash()