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

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7from typing import TYPE_CHECKING, ClassVar 

8 

9from django.utils.translation import gettext_lazy 

10from translate.storage.resx import RESXFile 

11 

12from weblate.addons.cleanup import BaseCleanupAddon 

13 

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 

18 

19 IndexType = dict[int, Unit] 

20 

21 

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 } 

34 

35 @staticmethod 

36 def build_index(storage) -> IndexType: 

37 return {unit.getid(): unit for unit in storage.units} 

38 

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 

46 

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 

54 

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. 

64 

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 

70 

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 

76 

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 

86 

87 if changed: 

88 storage.save() 

89 

90 @staticmethod 

91 def find_changes(index: IndexType, storage: RESXFile) -> set[int]: 

92 """Find changed string IDs in upstream repository.""" 

93 result = set() 

94 

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) 

101 

102 return result 

103 

104 def update_translations(self, component: Component, previous_head: str) -> None: 

105 index, intermediate = self.build_indexes(component) 

106 

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 )