Coverage for app/venv/lib/python3.14/site-packages/weblate/formats/multi.py: 35%

116 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 

5"""Translate Toolkit based file-format wrappers for multi string support.""" 

6 

7from __future__ import annotations 

8 

9from typing import TYPE_CHECKING, ClassVar 

10 

11from django.utils.functional import cached_property 

12from django.utils.translation import gettext_lazy 

13 

14from weblate.trans.util import get_string 

15 

16from .base import TranslationFormat, TranslationUnit 

17from .ttkit import CSVUtf8Format 

18 

19if TYPE_CHECKING: 19 ↛ 20line 19 didn't jump to line 20 because the condition on line 19 was never true

20 from translate.storage.base import TranslationStore 

21 from translate.storage.base import TranslationUnit as TranslateToolkitUnit 

22 

23 

24class MultiUnit(TranslationUnit): 

25 units: list[TranslationUnit] 

26 parent: MultiFormatMixin 

27 empty_unit_ok: ClassVar[bool] = True 

28 

29 def __init__( 

30 self, 

31 parent: MultiFormatMixin, 

32 unit: TranslationUnit, 

33 template: TranslationUnit | None = None, 

34 ) -> None: 

35 super().__init__(parent, None, template) 

36 self.units = [unit] 

37 

38 def merge(self, unit) -> None: 

39 self.units.append(unit) 

40 self._invalidate_target() 

41 

42 @cached_property 

43 def locations(self): 

44 return ", ".join(unit.locations for unit in self.units) 

45 

46 @cached_property 

47 def source(self): 

48 return get_string(unit.source for unit in self.units) 

49 

50 @cached_property 

51 def target(self): 

52 return get_string(unit.target for unit in self.units) 

53 

54 @cached_property 

55 def context(self): 

56 # Context should be same for all units 

57 return self.units[0].context 

58 

59 @cached_property 

60 def id_hash(self): 

61 # Hash should be same for all units 

62 return self.units[0].id_hash 

63 

64 @cached_property 

65 def notes(self): 

66 return "\n".join(unit.notes for unit in self.units if unit.notes) 

67 

68 def is_translated(self): 

69 return any(unit.is_translated() for unit in self.units) 

70 

71 def is_fuzzy(self, fallback=False): 

72 return any(unit.is_fuzzy(fallback) for unit in self.units) 

73 

74 def has_content(self): 

75 return any(unit.has_content() for unit in self.units) 

76 

77 def is_readonly(self): 

78 return any(unit.is_readonly() for unit in self.units) 

79 

80 def set_target(self, target: str | list[str]) -> None: 

81 """Set translation unit target.""" 

82 self._invalidate_target() 

83 

84 # Mare sure we have a list 

85 if isinstance(target, str): 

86 target = [target] 

87 

88 # Remove any extra units 

89 while len(target) < len(self.units): 

90 last = self.units.pop() 

91 self.parent.store.removeunit(last.unit) 

92 

93 # Add missing units 

94 while len(target) > len(self.units): 

95 new = self.parent.create_unit(self.context, self.units[0].source) 

96 self.units.append( 

97 self.parent.unit_class(self.parent, new, self.units[0].template) 

98 ) 

99 self.parent.store.addunit(new) 

100 

101 for i, value in enumerate(target): 

102 self.units[i].set_target(value) 

103 

104 def set_state(self, state) -> None: 

105 for unit in self.units: 

106 unit.set_state(state) 

107 

108 @cached_property 

109 def flags(self): 

110 flags = super().flags 

111 for unit in self.units: 

112 flags.merge(unit.flags) 

113 return flags 

114 

115 def has_unit(self) -> bool: 

116 return all(unit.has_unit() for unit in self.units) 

117 

118 def clone_template(self) -> None: 

119 for unit in self.units: 

120 if not unit.has_unit(): 

121 unit.clone_template() 

122 

123 def untranslate(self, language) -> None: 

124 for unit in self.units: 

125 unit.untranslate(language) 

126 

127 

128class MultiFormatMixin(TranslationFormat): 

129 has_multiple_strings: bool = True 

130 units: list[TranslateToolkitUnit] 

131 store: TranslationStore 

132 

133 def merge_multi(self, iterable): 

134 result: dict[int, MultiUnit] = {} 

135 for unit in iterable: 

136 id_hash = unit.id_hash 

137 if id_hash in result: 

138 result[id_hash].merge(unit) 

139 else: 

140 if not isinstance(unit, MultiUnit): 

141 unit = MultiUnit(unit.parent, unit, template=unit.template) 

142 result[id_hash] = unit 

143 return list(result.values()) 

144 

145 @cached_property 

146 def template_units(self): 

147 return self.merge_multi(super().template_units) 

148 

149 def _get_all_bilingual_units(self): 

150 return self.merge_multi(super()._get_all_bilingual_units()) 

151 

152 def _build_monolingual_unit(self, unit): 

153 try: 

154 matching = self._template_index[unit.id_hash] 

155 except KeyError: 

156 return MultiUnit(self, self.unit_class(self, None, unit.units[0].template)) 

157 matching_units = [unit.template for unit in matching.units] 

158 result = MultiUnit( 

159 self, self.unit_class(self, matching_units[0], unit.units[0].template) 

160 ) 

161 for extra in matching_units[1:]: 

162 result.merge(self.unit_class(self, extra, unit.units[0].template)) 

163 return result 

164 

165 def _get_all_monolingual_units(self): 

166 return self.merge_multi(super()._get_all_monolingual_units()) 

167 

168 def add_unit(self, unit: TranslationUnit) -> None: 

169 if isinstance(unit, MultiUnit): 

170 for child in unit.units: 

171 super().add_unit(child) 

172 else: 

173 super().add_unit(unit) 

174 

175 

176class MultiCSVUtf8Format(MultiFormatMixin, CSVUtf8Format): 

177 name = gettext_lazy("Multivalue CSV file (UTF-8)") 

178 format_id = "csv-multi-utf-8"