Coverage for app/venv/lib/python3.14/site-packages/weblate/addons/resx.py: 24%
65 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
7from typing import TYPE_CHECKING, ClassVar
9from django.utils.translation import gettext_lazy
10from translate.storage.resx import RESXFile
12from weblate.addons.cleanup import BaseCleanupAddon
14if TYPE_CHECKING: 14 ↛ 15line 14 didn't jump to line 15 because the condition on line 14 was never true
15 from weblate.addons.base import CompatDict
16 from weblate.formats.ttkit import RESXFormat
17 from weblate.trans.models import Component, Translation, Unit
19 IndexType = dict[int, Unit]
22class ResxUpdateAddon(BaseCleanupAddon):
23 name = "weblate.resx.update"
24 verbose = gettext_lazy("Update RESX files")
25 description = gettext_lazy(
26 "Update all translation files to match the monolingual upstream base file. "
27 "Unused strings are removed, and new ones added as copies of the source "
28 "string."
29 )
30 icon = "refresh.svg"
31 compat: ClassVar[CompatDict] = {
32 "file_format": {"resx"},
33 }
35 @staticmethod
36 def build_index(storage) -> IndexType:
37 return {unit.getid(): unit for unit in storage.units}
39 def build_indexes(self, component: Component) -> tuple[IndexType, IndexType]:
40 index = self.build_index(component.template_store.store)
41 if component.intermediate:
42 intermediate = self.build_index(component.intermediate_store.store)
43 else:
44 intermediate = {}
45 return index, intermediate
47 @staticmethod
48 def get_index(
49 index: IndexType, intermediate: IndexType, translation: Translation
50 ) -> IndexType:
51 if intermediate and translation.is_source:
52 return intermediate
53 return index
55 def update_resx(
56 self,
57 index: IndexType,
58 translation: Translation,
59 storage: RESXFormat,
60 changes: set[int],
61 ) -> None:
62 """
63 Filter obsolete units in RESX storage.
65 This removes the corresponding XML element and also adds newly added, and
66 changed units.
67 """
68 sindex = self.build_index(storage.store)
69 changed = False
71 # Add missing units
72 for unit in translation.component.template_store.store.units:
73 if unit.getid() not in sindex:
74 storage.store.addunit(unit, True)
75 changed = True
77 # Remove extra units and apply target changes
78 for unit in storage.store.units:
79 unitid = unit.getid()
80 if unitid not in index and storage.store.body is not None:
81 storage.store.body.remove(unit.xmlelement)
82 changed = True
83 if unitid in changes:
84 unit.target = index[unitid].target
85 changed = True
87 if changed:
88 storage.save()
90 @staticmethod
91 def find_changes(index: IndexType, storage: RESXFile) -> set[int]:
92 """Find changed string IDs in upstream repository."""
93 result = set()
95 for unit in storage.units:
96 unitid = unit.getid()
97 if unitid not in index:
98 continue
99 if unit.target != index[unitid].target:
100 result.add(unitid)
102 return result
104 def update_translations(self, component: Component, previous_head: str) -> None:
105 index, intermediate = self.build_indexes(component)
107 if previous_head:
108 content = component.repository.get_file(component.template, previous_head)
109 changes = self.find_changes(index, RESXFile.parsestring(content))
110 else:
111 # No previous revision, probably first commit
112 changes = set()
113 for translation in self.iterate_translations(component):
114 self.update_resx(
115 self.get_index(index, intermediate, translation),
116 translation,
117 translation.store,
118 changes,
119 )