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

1# /////////////////////////////////////////////////////////////// 

2# CLI.CREATE_QM_FILES - Translation file converter (.ts -> .qm) 

3# Project: ezqt_app 

4# /////////////////////////////////////////////////////////////// 

5 

6"""Convert Qt translation source files (.ts) to binary format (.qm).""" 

7 

8from __future__ import annotations 

9 

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 

18 

19# Local imports 

20from ..utils.diagnostics import warn_tech 

21from ..utils.printer import get_printer 

22 

23 

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. 

29 

30 Args: 

31 ts_file_path: Path to the .ts file to parse. 

32 

33 Returns: 

34 Dict mapping source strings to their translations. 

35 

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}") 

42 

43 root = ET.parse(ts_file_path).getroot() # noqa: S314 

44 translations: dict[str, str] = {} 

45 

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 

56 

57 return translations 

58 

59 

60def create_proper_qm_from_ts(ts_file_path: Path, qm_file_path: Path) -> bool: 

61 """Convert a .ts file to .qm binary format. 

62 

63 Args: 

64 ts_file_path: Path to the source .ts file. 

65 qm_file_path: Path where the .qm file will be written. 

66 

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}...") 

72 

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 

78 

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 

87 

88 

89def create_qt_qm_file(qm_file_path: Path, translations: dict) -> None: 

90 """Write translations to a .qm file in Qt binary format. 

91 

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))) 

103 

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) 

111 

112 # Checksum (optional) 

113 f.write(struct.pack("<I", 0)) 

114 

115 

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 

129 

130 

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") 

135 

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() 

140 

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 

152 

153 ts_files = list(translations_dir.glob("*.ts")) 

154 

155 if not ts_files: 

156 printer.error("No .ts files found") 

157 return 

158 

159 printer.info(f".ts files found: {len(ts_files)}") 

160 

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}") 

167 

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") 

172 

173 

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] 

183 

184if __name__ == "__main__": 

185 main()