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
« 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, TypedDict, cast
9from django import forms
10from django.utils.functional import classproperty
11from django.utils.translation import gettext_lazy
12from translate.storage.lisa import LISAfile
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
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
24class FieldKwargsDict(TypedDict, total=False):
25 min_value: int
26 max_value: int
27 min_length: int
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
40 @classmethod
41 def get_identifier(cls) -> str:
42 return cls.name
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())
48 def get_widget_attrs(self) -> dict:
49 field_classes = ["file-format-param-field"]
51 if self.field_class != forms.BooleanField:
52 # the default radio/checkbox input looks better than bootstrap one
53 field_classes.append("form-control")
55 return {
56 "label": self.label,
57 "fileformats": " ".join(self.file_formats),
58 "class": " ".join(field_classes),
59 }
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
70 def setup_store(self, store: TranslationStore, **file_format_params) -> None:
71 """Configure store with this file format parameters."""
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
83FILE_FORMATS_PARAMS: list[type[BaseFileFormatParam]] = []
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
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]
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}
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
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 )
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
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 )
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}
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"
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
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
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 )
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 )
214 def setup_store(self, store: TranslationStore, **file_format_params) -> None:
215 cast("pofile", store).wrapper.width = int(self.get_value(file_format_params))
218class BaseGettextFormatParam(BaseFileFormatParam):
219 file_formats = ("po",)
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
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
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
246class BaseYAMLFormatParam(BaseFileFormatParam):
247 file_formats = (
248 "yaml",
249 "ruby-yaml",
250 )
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}
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 )
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 ]
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 )
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"
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]
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
312 @classproperty
313 def file_formats(self) -> Sequence[str]:
314 from weblate.formats.models import FILE_FORMATS
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
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 )
329class BaseFlatXMLFormatParam(BaseFileFormatParam):
330 file_formats = ("flatxml",)
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}
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}
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}