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
« 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
5"""External file format specific behavior."""
7from __future__ import annotations
9import os
10from io import BytesIO, StringIO
11from typing import TYPE_CHECKING, Any
12from zipfile import BadZipFile
14from django.utils.translation import gettext_lazy
15from translate.storage.csvl10n import csv
17from weblate.formats.helpers import CONTROLCHARS_TRANS, NamedBytesIO
18from weblate.formats.ttkit import CSVUtf8Format
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
23CSV_DIALECT = "unix"
26class XlsxFormat(CSVUtf8Format):
27 name = gettext_lazy("Excel Open XML")
28 format_id = "xlsx"
29 autoload = ("*.xlsx",)
31 def write_cell(self, worksheet, column: int, row: int, value: str):
32 from openpyxl.cell.cell import TYPE_STRING
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
40 def get_title(self, fallback: str = "Weblate"):
41 from openpyxl.workbook.child import INVALID_TITLE_REGEX
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
52 def save_content(self, handle) -> None:
53 from openpyxl import Workbook
55 workbook = Workbook()
56 worksheet = workbook.active
57 worksheet.title = self.get_title()
58 fieldnames = self.store.fieldnames
60 # write headers
61 for column, field in enumerate(fieldnames):
62 self.write_cell(worksheet, column + 1, 1, field)
64 for row, unit in enumerate(self.store.units):
65 data = unit.todict()
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 )
75 workbook.save(handle)
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()
84 def parse_store(self, storefile):
85 from openpyxl import load_workbook
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
96 output = StringIO()
98 writer = csv.writer(output, dialect=CSV_DIALECT)
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)
112 if isinstance(storefile, str):
113 name = os.path.basename(storefile) + ".csv"
114 else:
115 name = os.path.basename(storefile.name) + ".csv"
117 # return the new csv as bytes
118 content = output.getvalue().encode("utf-8")
120 # Load the file as CSV
121 return super().parse_store(NamedBytesIO(name, content), dialect=CSV_DIALECT)
123 @staticmethod
124 def mimetype() -> str:
125 """Return most common mime type for format."""
126 return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
128 @staticmethod
129 def extension() -> str:
130 """Return most common file extension for format."""
131 return "xlsx"
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)