Coverage for src / ezqt_app / cli / create_qm_files.py: 53.00%
82 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-26 07:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-26 07:07 +0000
1# ///////////////////////////////////////////////////////////////
2# CLI.CREATE_QM_FILES - Translation file converter (.ts -> .qm)
3# Project: ezqt_app
4# ///////////////////////////////////////////////////////////////
6"""Convert Qt translation source files (.ts) to binary format (.qm)."""
8from __future__ import annotations
10# ///////////////////////////////////////////////////////////////
11# IMPORTS
12# ///////////////////////////////////////////////////////////////
13# Standard library imports
14import importlib.resources
15import struct
16import xml.etree.ElementTree as ET # noqa: S314
17from pathlib import Path
19# Local imports
20from ..utils.diagnostics import warn_tech
21from ..utils.printer import get_printer
24# ///////////////////////////////////////////////////////////////
25# FUNCTIONS
26# ///////////////////////////////////////////////////////////////
27def extract_translations_from_ts(ts_file_path: Path) -> dict[str, str]:
28 """Parse a Qt .ts XML file and return source→translation mapping.
30 Args:
31 ts_file_path: Path to the .ts file to parse.
33 Returns:
34 Dict mapping source strings to their translations.
36 Raises:
37 FileNotFoundError: If the .ts file does not exist.
38 ET.ParseError: If the XML is malformed.
39 """
40 if not ts_file_path.exists():
41 raise FileNotFoundError(f"Translation file not found: {ts_file_path}")
43 root = ET.parse(ts_file_path).getroot() # noqa: S314
44 translations: dict[str, str] = {}
46 for message in root.findall(".//message"):
47 source = message.find("source")
48 translation = message.find("translation")
49 if ( 49 ↛ 46line 49 didn't jump to line 46 because the condition on line 49 was always true
50 source is not None
51 and translation is not None
52 and source.text
53 and translation.text
54 ):
55 translations[source.text] = translation.text
57 return translations
60def create_proper_qm_from_ts(ts_file_path: Path, qm_file_path: Path) -> bool:
61 """Convert a .ts file to .qm binary format.
63 Args:
64 ts_file_path: Path to the source .ts file.
65 qm_file_path: Path where the .qm file will be written.
67 Returns:
68 True on success, False on error.
69 """
70 printer = get_printer()
71 printer.info(f"Converting {ts_file_path.name} to {qm_file_path.name}...")
73 try:
74 translations = extract_translations_from_ts(ts_file_path)
75 create_qt_qm_file(qm_file_path, translations)
76 printer.success(f"{len(translations)} translations converted")
77 return True
79 except Exception as e:
80 warn_tech(
81 "cli.qm.convert_failed",
82 "Conversion from .ts to .qm failed",
83 error=e,
84 )
85 printer.error(f"Error during conversion: {e}")
86 return False
89def create_qt_qm_file(qm_file_path: Path, translations: dict) -> None:
90 """Write translations to a .qm file in Qt binary format.
92 Args:
93 qm_file_path: Output file path.
94 translations: Dict mapping source strings to translated strings.
95 """
96 with open(qm_file_path, "wb") as f:
97 # Qt .qm magic number
98 f.write(b"qm\x00\x00")
99 # Version (4 bytes little-endian)
100 f.write(struct.pack("<I", 0x01))
101 # Number of translations
102 f.write(struct.pack("<I", len(translations)))
104 for source, translation in translations.items():
105 source_bytes = source.encode("utf-8")
106 translation_bytes = translation.encode("utf-8")
107 f.write(struct.pack("<I", len(source_bytes)))
108 f.write(source_bytes)
109 f.write(struct.pack("<I", len(translation_bytes)))
110 f.write(translation_bytes)
112 # Checksum (optional)
113 f.write(struct.pack("<I", 0))
116def _get_package_translations_dir() -> Path | None:
117 """Locate the bundled translations directory via importlib.resources."""
118 try:
119 pkg = importlib.resources.files("ezqt_app")
120 candidate = Path(str(pkg.joinpath("resources").joinpath("translations")))
121 return candidate if candidate.exists() else None
122 except Exception:
123 warn_tech(
124 "cli.qm.package_translations_resolution_failed",
125 "Unable to resolve bundled translations directory, using fallback",
126 )
127 fallback = Path(__file__).parent.parent / "resources" / "translations"
128 return fallback if fallback.exists() else None
131def main() -> None:
132 """Entry point: convert all .ts files found in the translations directory."""
133 printer = get_printer()
134 printer.section("Creating .qm files in the correct Qt format")
136 # Priority 1: user project translations directory
137 current_project_translations = Path.cwd() / "bin" / "translations"
138 # Priority 2: installed package translations
139 package_translations = _get_package_translations_dir()
141 if current_project_translations.exists():
142 translations_dir = current_project_translations
143 printer.info(f"Using project directory: {translations_dir}")
144 elif package_translations is not None:
145 translations_dir = package_translations
146 printer.info(f"Using package directory (fallback): {translations_dir}")
147 else:
148 printer.error("No translations directory found")
149 printer.verbose_msg(f" Project: {current_project_translations}")
150 printer.info("Make sure you are in an EzQt_App project or run 'ezqt init'")
151 return
153 ts_files = list(translations_dir.glob("*.ts"))
155 if not ts_files:
156 printer.error("No .ts files found")
157 return
159 printer.info(f".ts files found: {len(ts_files)}")
161 for ts_file in ts_files:
162 qm_file = ts_file.with_suffix(".qm")
163 if create_proper_qm_from_ts(ts_file, qm_file):
164 printer.success(f"{qm_file.name} created")
165 else:
166 printer.error(f"Failed to create {qm_file.name}")
168 printer.success("Process completed!")
169 printer.info("Next steps:")
170 printer.verbose_msg(" 1. Test the new .qm files")
171 printer.verbose_msg(" 2. If it still doesn't work, use the .ts files")
174# ///////////////////////////////////////////////////////////////
175# PUBLIC API
176# ///////////////////////////////////////////////////////////////
177__all__ = [
178 "extract_translations_from_ts",
179 "create_proper_qm_from_ts",
180 "create_qt_qm_file",
181 "main",
182]
184if __name__ == "__main__":
185 main()