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

87 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"""External file format specific behavior.""" 

6 

7from __future__ import annotations 

8 

9import os 

10from io import BytesIO, StringIO 

11from typing import TYPE_CHECKING, Any 

12from zipfile import BadZipFile 

13 

14from django.utils.translation import gettext_lazy 

15from translate.storage.csvl10n import csv 

16 

17from weblate.formats.helpers import CONTROLCHARS_TRANS, NamedBytesIO 

18from weblate.formats.ttkit import CSVUtf8Format 

19 

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

21 from collections.abc import Callable 

22 

23CSV_DIALECT = "unix" 

24 

25 

26class XlsxFormat(CSVUtf8Format): 

27 name = gettext_lazy("Excel Open XML") 

28 format_id = "xlsx" 

29 autoload = ("*.xlsx",) 

30 

31 def write_cell(self, worksheet, column: int, row: int, value: str): 

32 from openpyxl.cell.cell import TYPE_STRING 

33 

34 cell = worksheet.cell(column=column, row=row) 

35 cell.value = value 

36 # Set the data_type after value to override function auto-detection 

37 cell.data_type = TYPE_STRING 

38 return cell 

39 

40 def get_title(self, fallback: str = "Weblate"): 

41 from openpyxl.workbook.child import INVALID_TITLE_REGEX 

42 

43 title = self.store.targetlanguage 

44 if title is None: 

45 return fallback 

46 # Remove possible invalid characters 

47 title = INVALID_TITLE_REGEX.sub(title, "").strip() 

48 if not title: 

49 return fallback 

50 return title 

51 

52 def save_content(self, handle) -> None: 

53 from openpyxl import Workbook 

54 

55 workbook = Workbook() 

56 worksheet = workbook.active 

57 worksheet.title = self.get_title() 

58 fieldnames = self.store.fieldnames 

59 

60 # write headers 

61 for column, field in enumerate(fieldnames): 

62 self.write_cell(worksheet, column + 1, 1, field) 

63 

64 for row, unit in enumerate(self.store.units): 

65 data = unit.todict() 

66 

67 for column, field in enumerate(fieldnames): 

68 self.write_cell( 

69 worksheet, 

70 column + 1, 

71 row + 2, 

72 data[field].translate(CONTROLCHARS_TRANS), 

73 ) 

74 

75 workbook.save(handle) 

76 

77 @staticmethod 

78 def serialize(store): 

79 # store is CSV (csvfile) here 

80 output = BytesIO() 

81 XlsxFormat(store).save_content(output) 

82 return output.getvalue() 

83 

84 def parse_store(self, storefile): 

85 from openpyxl import load_workbook 

86 

87 # try to load the given file via openpyxl 

88 # catch at least the BadZipFile exception if an unsupported 

89 # file has been given 

90 try: 

91 workbook = load_workbook(filename=storefile) 

92 worksheet = workbook.active 

93 except BadZipFile: 

94 return None, None 

95 

96 output = StringIO() 

97 

98 writer = csv.writer(output, dialect=CSV_DIALECT) 

99 

100 # value can be None or blank stringfor cells having formatting only, 

101 # we need to ignore such columns as that would be treated like "" fields 

102 # later in the translate-toolkit 

103 fields = [cell.value for cell in next(worksheet.rows) if cell.value] 

104 for row in worksheet.rows: 

105 values = [cell.value for cell in row] 

106 values = values[: len(fields)] 

107 # Skip formatting only cells 

108 if not any(values): 

109 continue 

110 writer.writerow(values) 

111 

112 if isinstance(storefile, str): 

113 name = os.path.basename(storefile) + ".csv" 

114 else: 

115 name = os.path.basename(storefile.name) + ".csv" 

116 

117 # return the new csv as bytes 

118 content = output.getvalue().encode("utf-8") 

119 

120 # Load the file as CSV 

121 return super().parse_store(NamedBytesIO(name, content), dialect=CSV_DIALECT) 

122 

123 @staticmethod 

124 def mimetype() -> str: 

125 """Return most common mime type for format.""" 

126 return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" 

127 

128 @staticmethod 

129 def extension() -> str: 

130 """Return most common file extension for format.""" 

131 return "xlsx" 

132 

133 @classmethod 

134 def create_new_file( 

135 cls, 

136 filename: str, 

137 language: str, 

138 base: str, 

139 callback: Callable | None = None, 

140 file_format_params: dict[str, Any] | None = None, # noqa: ARG003 

141 ) -> None: 

142 """Handle creation of new translation file.""" 

143 if not base: 

144 msg = "Not supported" 

145 raise ValueError(msg) 

146 # Parse file 

147 store = cls(base) 

148 if callback: 

149 callback(store) 

150 store.untranslate_store(language) 

151 with open(filename, "wb") as handle: 

152 XlsxFormat(store.store).save_content(handle)