Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/file_format_params.py: 85%

210 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, TypedDict, cast 

8 

9from django import forms 

10from django.utils.functional import classproperty 

11from django.utils.translation import gettext_lazy 

12from translate.storage.lisa import LISAfile 

13 

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

15 from collections.abc import Sequence 

16 

17 from django_stubs_ext import StrOrPromise 

18 from translate.storage.base import TranslationStore 

19 from translate.storage.jsonl10n import JsonFile 

20 from translate.storage.pypo import pofile 

21 from translate.storage.yaml import YAMLFile 

22 

23 

24class FieldKwargsDict(TypedDict, total=False): 

25 min_value: int 

26 max_value: int 

27 min_length: int 

28 

29 

30class BaseFileFormatParam: 

31 name: str = "" 

32 file_formats: Sequence[str] = [] 

33 field_class: type[forms.Field] = forms.CharField 

34 label: StrOrPromise = "" 

35 default: str | int | bool 

36 field_kwargs: ClassVar[FieldKwargsDict] = {} 

37 choices: ClassVar[list[tuple[str | int, StrOrPromise]] | None] = None 

38 help_text: StrOrPromise | None = None 

39 

40 @classmethod 

41 def get_identifier(cls) -> str: 

42 return cls.name 

43 

44 def get_field(self) -> forms.Field: 

45 widget = self.field_class.widget(attrs=self.get_widget_attrs()) 

46 return self.field_class(widget=widget, **self.get_field_kwargs()) 

47 

48 def get_widget_attrs(self) -> dict: 

49 field_classes = ["file-format-param-field"] 

50 

51 if self.field_class != forms.BooleanField: 

52 # the default radio/checkbox input looks better than bootstrap one 

53 field_classes.append("form-control") 

54 

55 return { 

56 "label": self.label, 

57 "fileformats": " ".join(self.file_formats), 

58 "class": " ".join(field_classes), 

59 } 

60 

61 def get_field_kwargs(self) -> dict: 

62 kwargs = cast("dict", self.field_kwargs.copy()) 

63 kwargs.update({"required": False, "initial": self.default}) 

64 if self.choices is not None: 

65 kwargs["choices"] = self.choices 

66 if self.help_text: 

67 kwargs["help_text"] = self.help_text 

68 return kwargs 

69 

70 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

71 """Configure store with this file format parameters.""" 

72 

73 @classmethod 

74 def get_value(cls, file_format_params: dict): 

75 value = file_format_params.get(cls.name, cls.default) 

76 cast = type(cls.default) 

77 try: 

78 return cast(value) 

79 except (ValueError, TypeError): 

80 return cls.default 

81 

82 

83FILE_FORMATS_PARAMS: list[type[BaseFileFormatParam]] = [] 

84 

85 

86def register_file_format_param( 

87 param_class: type[BaseFileFormatParam], 

88) -> type[BaseFileFormatParam]: 

89 """Register a new file format parameter class.""" 

90 FILE_FORMATS_PARAMS.append(param_class) 

91 return param_class 

92 

93 

94def get_params_for_file_format(file_format: str) -> list[type[BaseFileFormatParam]]: 

95 """Get all registered file format parameters for a given file format.""" 

96 return [param for param in FILE_FORMATS_PARAMS if file_format in param.file_formats] 

97 

98 

99def get_default_params_for_file_format(file_format: str) -> dict[str, str | int | bool]: 

100 """Get default values for all registered file format parameters.""" 

101 params = get_params_for_file_format(file_format) 

102 return {param.name: param.default for param in params} 

103 

104 

105def strip_unused_file_format_params(file_format: str, file_format_params: dict) -> dict: 

106 """Clean file format parameters, removing those not applicable to the given file format.""" 

107 for param in FILE_FORMATS_PARAMS: 

108 if file_format not in param.file_formats: 

109 file_format_params.pop(param.name, None) 

110 return file_format_params 

111 

112 

113class JSONOutputCustomizationBaseParam(BaseFileFormatParam): 

114 file_formats = ( 

115 "json", 

116 "json-nested", 

117 "webextension", 

118 "i18next", 

119 "i18nextv4", 

120 "arb", 

121 "go-i18n-json", 

122 "go-i18n-json-v2", 

123 "formatjs", 

124 "gotext", 

125 ) 

126 

127 

128@register_file_format_param 

129class JSONOutputSortKeys(JSONOutputCustomizationBaseParam): 

130 name = "json_sort_keys" 

131 label = gettext_lazy("Sort JSON keys") 

132 field_class = forms.BooleanField 

133 default = False 

134 

135 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

136 cast("JsonFile", store).dump_args["sort_keys"] = self.get_value( 

137 file_format_params 

138 ) 

139 

140 

141@register_file_format_param 

142class JSONOutputIndentation(JSONOutputCustomizationBaseParam): 

143 name = "json_indent" 

144 label = gettext_lazy("JSON indentation") 

145 field_class = forms.IntegerField 

146 default = 4 

147 field_kwargs: ClassVar[FieldKwargsDict] = {"min_value": 0} 

148 

149 

150@register_file_format_param 

151class JSONOutputIndentStyle(JSONOutputCustomizationBaseParam): 

152 name = "json_indent_style" 

153 label = gettext_lazy("JSON indentation style") 

154 field_class = forms.ChoiceField 

155 choices: ClassVar[list[tuple[str | int, StrOrPromise]] | None] = [ 

156 ("spaces", gettext_lazy("Spaces")), 

157 ("tabs", gettext_lazy("Tabs")), 

158 ] 

159 default = "spaces" 

160 

161 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

162 indent = JSONOutputIndentation.get_value(file_format_params) 

163 dump_args = cast("JsonFile", store).dump_args 

164 if self.get_value(file_format_params) == "tabs": 

165 dump_args["indent"] = "\t" * indent 

166 else: 

167 dump_args["indent"] = indent 

168 

169 

170@register_file_format_param 

171class JSONOutputCompactSeparators(JSONOutputCustomizationBaseParam): 

172 name = "json_use_compact_separators" 

173 label = gettext_lazy("Avoid spaces after separators") 

174 field_class = forms.BooleanField 

175 default = False 

176 

177 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

178 dump_args = cast("JsonFile", store).dump_args 

179 use_compact_separators = self.get_value(file_format_params) 

180 dump_args["separators"] = ( 

181 ",", 

182 ":" if use_compact_separators else ": ", 

183 ) 

184 

185 

186@register_file_format_param 

187class GettextPoLineWrap(BaseFileFormatParam): 

188 file_formats = ( 

189 "po", 

190 "po-mono", 

191 ) 

192 name = "po_line_wrap" 

193 label = gettext_lazy("Long lines wrapping") 

194 field_class = forms.ChoiceField 

195 choices: ClassVar[list[tuple[str | int, StrOrPromise]] | None] = [ 

196 ( 

197 77, 

198 gettext_lazy( 

199 "Wrap lines at 77 characters and at newlines (xgettext default)" 

200 ), 

201 ), 

202 ( 

203 65535, 

204 gettext_lazy("Only wrap lines at newlines (like 'xgettext --no-wrap')"), 

205 ), 

206 (-1, gettext_lazy("No line wrapping")), 

207 ] 

208 default = 77 

209 help_text = gettext_lazy( 

210 "By default, gettext wraps lines at 77 characters and at newlines. " 

211 "With the --no-wrap parameter, wrapping is only done at newlines." 

212 ) 

213 

214 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

215 cast("pofile", store).wrapper.width = int(self.get_value(file_format_params)) 

216 

217 

218class BaseGettextFormatParam(BaseFileFormatParam): 

219 file_formats = ("po",) 

220 

221 

222@register_file_format_param 

223class GettextKeepPreviousMsgids(BaseGettextFormatParam): 

224 name = "po_keep_previous" 

225 label = gettext_lazy("Keep previous msgids of translated strings") 

226 field_class = forms.BooleanField 

227 default = True 

228 

229 

230@register_file_format_param 

231class GettextNoLocation(BaseGettextFormatParam): 

232 name = "po_no_location" 

233 label = gettext_lazy("Do not include location information in the file") 

234 field_class = forms.BooleanField 

235 default = True 

236 

237 

238@register_file_format_param 

239class GettextFuzzyMatching(BaseGettextFormatParam): 

240 name = "po_fuzzy_matching" 

241 label = gettext_lazy("Use fuzzy matching") 

242 field_class = forms.BooleanField 

243 default = False 

244 

245 

246class BaseYAMLFormatParam(BaseFileFormatParam): 

247 file_formats = ( 

248 "yaml", 

249 "ruby-yaml", 

250 ) 

251 

252 

253@register_file_format_param 

254class YAMLOutputIndentation(BaseYAMLFormatParam): 

255 name = "yaml_indent" 

256 label = gettext_lazy("YAML indentation") 

257 field_class = forms.IntegerField 

258 default = 2 

259 field_kwargs: ClassVar[FieldKwargsDict] = {"min_value": 1, "max_value": 10} 

260 

261 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

262 cast("YAMLFile", store).dump_args["indent"] = int( # type: ignore[assignment] 

263 self.get_value(file_format_params) 

264 ) 

265 

266 

267@register_file_format_param 

268class YAMLLineWrap(BaseYAMLFormatParam): 

269 name = "yaml_line_wrap" 

270 label = gettext_lazy("Long lines wrapping") 

271 field_class = forms.ChoiceField 

272 default = 80 

273 choices: ClassVar[list[tuple[str | int, StrOrPromise]] | None] = [ 

274 (80, gettext_lazy("Wrap lines at 80 chars")), 

275 (100, gettext_lazy("Wrap lines at 100 chars")), 

276 (120, gettext_lazy("Wrap lines at 120 chars")), 

277 (180, gettext_lazy("Wrap lines at 180 chars")), 

278 (65535, gettext_lazy("No line wrapping")), 

279 ] 

280 

281 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

282 cast("YAMLFile", store).dump_args["width"] = int( # type: ignore[assignment] 

283 self.get_value(file_format_params) 

284 ) 

285 

286 

287@register_file_format_param 

288class YAMLLineBreak(BaseYAMLFormatParam): 

289 name = "yaml_line_break" 

290 label = gettext_lazy("Line breaks") 

291 field_class = forms.ChoiceField 

292 choices: ClassVar[list[tuple[str | int, StrOrPromise]] | None] = [ 

293 ("dos", gettext_lazy("DOS (\\r\\n)")), 

294 ("unix", gettext_lazy("UNIX (\\n)")), 

295 ("mac", gettext_lazy("MAC (\\r)")), 

296 ] 

297 default = "unix" 

298 

299 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

300 breaks = {"dos": "\r\n", "mac": "\r", "unix": "\n"} 

301 line_break = self.get_value(file_format_params) 

302 cast("YAMLFile", store).dump_args["line_break"] = breaks[line_break] # type: ignore[assignment] 

303 

304 

305@register_file_format_param 

306class XMLClosingTags(BaseFileFormatParam): 

307 name = "xml_closing_tags" 

308 label = gettext_lazy("Include closing tag for blank XML tags") 

309 field_class = forms.BooleanField 

310 default = False 

311 

312 @classproperty 

313 def file_formats(self) -> Sequence[str]: 

314 from weblate.formats.models import FILE_FORMATS 

315 

316 result = [] 

317 for file_format, format_class in FILE_FORMATS.items(): 

318 store_class = format_class.get_class() 

319 if store_class and issubclass(store_class, LISAfile): 

320 result.append(file_format) 

321 return result 

322 

323 def setup_store(self, store: TranslationStore, **file_format_params) -> None: 

324 cast("LISAfile", store).XMLSelfClosingTags = not self.get_value( 

325 file_format_params 

326 ) 

327 

328 

329class BaseFlatXMLFormatParam(BaseFileFormatParam): 

330 file_formats = ("flatxml",) 

331 

332 

333@register_file_format_param 

334class FlatXMLRootName(BaseFlatXMLFormatParam): 

335 name = "flatxml_root_name" 

336 label = gettext_lazy("FlatXML Root name") 

337 field_class = forms.CharField 

338 default = "root" 

339 field_kwargs: ClassVar[FieldKwargsDict] = {"min_length": 1} 

340 

341 

342@register_file_format_param 

343class FlatXMLValueName(BaseFlatXMLFormatParam): 

344 name = "flatxml_value_name" 

345 label = gettext_lazy("FlatXML value name") 

346 field_class = forms.CharField 

347 default = "str" 

348 field_kwargs: ClassVar[FieldKwargsDict] = {"min_length": 1} 

349 

350 

351@register_file_format_param 

352class FlatXMLKeyName(BaseFlatXMLFormatParam): 

353 name = "flatxml_key_name" 

354 label = gettext_lazy("FlatXML key name") 

355 field_class = forms.CharField 

356 default = "key" 

357 field_kwargs: ClassVar[FieldKwargsDict] = {"min_length": 1}