supervertaler 1.9.163__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. Supervertaler.py +48473 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1911 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +932 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +329 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +252 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +351 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1176 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +715 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +659 -0
  76. modules/unified_prompt_manager_qt.py +3951 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.163.dist-info/METADATA +906 -0
  81. supervertaler-1.9.163.dist-info/RECORD +85 -0
  82. supervertaler-1.9.163.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.163.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.163.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.163.dist-info/top_level.txt +2 -0
@@ -0,0 +1,469 @@
1
+ """
2
+ Configuration Manager for Supervertaler
3
+ Handles user_data folder location, first-time setup, and configuration persistence.
4
+
5
+ Author: Michael Beijer
6
+ License: MIT
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import shutil
12
+ from pathlib import Path
13
+ from typing import Optional, Tuple
14
+
15
+
16
+ class ConfigManager:
17
+ """
18
+ Manages Supervertaler configuration and user_data paths.
19
+
20
+ MODES:
21
+ - Dev mode: .supervertaler.local exists → uses user_data_private/ folder (git-ignored)
22
+ - User mode: No .supervertaler.local → uses ~/.supervertaler_config.json to store path
23
+
24
+ Stores configuration in home directory as .supervertaler_config.json
25
+ Allows users to choose their own user_data folder location.
26
+ """
27
+
28
+ CONFIG_FILENAME = ".supervertaler_config.json"
29
+ DEFAULT_USER_DATA_FOLDER = "Supervertaler_Data"
30
+ DEV_MODE_FLAG = ".supervertaler.local"
31
+ API_KEYS_EXAMPLE_FILENAME = "api_keys.example.txt"
32
+ API_KEYS_FILENAME = "api_keys.txt"
33
+
34
+ # Folder structure that must exist in user_data directory
35
+ REQUIRED_FOLDERS = [
36
+ # Note: Old numbered folders (1_System_Prompts, 2_Domain_Prompts, etc.) are deprecated
37
+ # Migration moves them to unified Library structure
38
+ "prompt_library/domain_expertise",
39
+ "prompt_library/project_prompts",
40
+ "prompt_library/style_guides",
41
+ "resources/termbases",
42
+ "resources/tms",
43
+ "resources/non_translatables",
44
+ "resources/segmentation_rules",
45
+ "projects",
46
+ ]
47
+
48
+ def __init__(self):
49
+ """Initialize ConfigManager."""
50
+ self.dev_mode = self._is_dev_mode()
51
+ self.script_dir = os.path.dirname(os.path.abspath(__file__))
52
+ self.config_path = self._get_config_file_path()
53
+ self.config = self._load_config()
54
+
55
+ @staticmethod
56
+ def _is_dev_mode() -> bool:
57
+ """Check if running in dev mode (looking for .supervertaler.local flag)."""
58
+ script_dir = os.path.dirname(os.path.abspath(__file__))
59
+ repo_root = os.path.dirname(script_dir) # Go up one level from modules/
60
+ dev_flag_path = os.path.join(repo_root, ConfigManager.DEV_MODE_FLAG)
61
+ return os.path.exists(dev_flag_path)
62
+
63
+ def _get_config_file_path(self) -> str:
64
+ """
65
+ Get the full path to the config file.
66
+
67
+ Dev mode: No config file needed (uses user_data_private/)
68
+ User mode: ~/.supervertaler_config.json
69
+ """
70
+ if self.dev_mode:
71
+ return None # Dev mode doesn't use config file
72
+ home = str(Path.home())
73
+ return os.path.join(home, ConfigManager.CONFIG_FILENAME)
74
+
75
+ @staticmethod
76
+ def _get_default_user_data_path() -> str:
77
+ """Get the default suggested user_data path."""
78
+ home = str(Path.home())
79
+ return os.path.join(home, ConfigManager.DEFAULT_USER_DATA_FOLDER)
80
+
81
+ def _load_config(self) -> dict:
82
+ """Load configuration from file. Return empty dict if file doesn't exist."""
83
+ # Dev mode doesn't use config file
84
+ if self.dev_mode:
85
+ return {}
86
+
87
+ if os.path.exists(self.config_path):
88
+ try:
89
+ with open(self.config_path, 'r', encoding='utf-8') as f:
90
+ return json.load(f)
91
+ except (json.JSONDecodeError, IOError) as e:
92
+ print(f"[Config] Error loading config: {e}. Using defaults.")
93
+ return {}
94
+ return {}
95
+
96
+ def _save_config(self) -> bool:
97
+ """Save configuration to file. Return True if successful."""
98
+ # Dev mode doesn't use config file
99
+ if self.dev_mode:
100
+ return True
101
+
102
+ if self.config_path is None:
103
+ return False
104
+
105
+ try:
106
+ with open(self.config_path, 'w', encoding='utf-8') as f:
107
+ json.dump(self.config, f, indent=2, ensure_ascii=False)
108
+ return True
109
+ except IOError as e:
110
+ print(f"[Config] Error saving config: {e}")
111
+ return False
112
+
113
+ def is_first_launch(self) -> bool:
114
+ """
115
+ Check if this is the first launch (no user_data path set).
116
+
117
+ Dev mode: Always False (dev doesn't need first-launch wizard)
118
+ User mode: True if no path in config
119
+ """
120
+ if self.dev_mode:
121
+ return False
122
+ return 'user_data_path' not in self.config or not self.config['user_data_path']
123
+
124
+ def get_user_data_path(self) -> str:
125
+ """
126
+ Get the current user_data path.
127
+
128
+ Dev mode: Returns ./user_data_private/ (in repo root)
129
+ User mode: Returns configured path from ~/.supervertaler_config.json
130
+
131
+ If not configured, returns default suggestion (doesn't create it).
132
+ Use ensure_user_data_exists() to create the folder.
133
+ """
134
+ if self.dev_mode:
135
+ # Dev mode: use user_data_private folder
136
+ repo_root = os.path.dirname(self.script_dir)
137
+ return os.path.join(repo_root, "user_data_private")
138
+
139
+ # User mode: use configured path
140
+ if 'user_data_path' in self.config and self.config['user_data_path']:
141
+ return self.config['user_data_path']
142
+ return self._get_default_user_data_path()
143
+
144
+ def set_user_data_path(self, path: str) -> Tuple[bool, str]:
145
+ """
146
+ Set the user_data path and save configuration.
147
+
148
+ Args:
149
+ path: Full path to user_data folder
150
+
151
+ Returns:
152
+ Tuple of (success: bool, message: str)
153
+ """
154
+ # Validate path
155
+ is_valid, error_msg = self._validate_path(path)
156
+ if not is_valid:
157
+ return False, error_msg
158
+
159
+ # Normalize path
160
+ path = os.path.normpath(path)
161
+
162
+ # Save configuration
163
+ self.config['user_data_path'] = path
164
+ self.config['last_modified'] = str(Path.ctime(Path(self.config_path))) if os.path.exists(self.config_path) else None
165
+
166
+ if self._save_config():
167
+ return True, f"User data path set to: {path}"
168
+ else:
169
+ return False, "Failed to save configuration"
170
+
171
+ @staticmethod
172
+ def _validate_path(path: str) -> Tuple[bool, str]:
173
+ """
174
+ Validate that a path is suitable for user_data.
175
+
176
+ Returns:
177
+ Tuple of (is_valid: bool, error_message: str)
178
+ """
179
+ if not path or not isinstance(path, str):
180
+ return False, "Path must be a non-empty string"
181
+
182
+ try:
183
+ path_obj = Path(path)
184
+
185
+ # Try to create the path
186
+ path_obj.mkdir(parents=True, exist_ok=True)
187
+
188
+ # Check if writable
189
+ test_file = path_obj / ".supervertaler_test"
190
+ test_file.touch()
191
+ test_file.unlink()
192
+
193
+ return True, ""
194
+ except PermissionError:
195
+ return False, f"Permission denied: Cannot write to {path}"
196
+ except OSError as e:
197
+ return False, f"Invalid path: {e}"
198
+
199
+ def ensure_user_data_exists(self, user_data_path: Optional[str] = None) -> Tuple[bool, str]:
200
+ """
201
+ Ensure user_data folder exists with proper structure.
202
+
203
+ Creates all required subdirectories if they don't exist.
204
+ Also copies api_keys.example.txt → api_keys.txt if not present.
205
+
206
+ Args:
207
+ user_data_path: Optional specific path. If None, uses configured path.
208
+
209
+ Returns:
210
+ Tuple of (success: bool, message: str)
211
+ """
212
+ if user_data_path is None:
213
+ user_data_path = self.get_user_data_path()
214
+
215
+ try:
216
+ # Create root user_data folder
217
+ Path(user_data_path).mkdir(parents=True, exist_ok=True)
218
+
219
+ # Create all required subdirectories
220
+ for folder in self.REQUIRED_FOLDERS:
221
+ folder_path = os.path.join(user_data_path, folder)
222
+ Path(folder_path).mkdir(parents=True, exist_ok=True)
223
+
224
+ # Copy api_keys.example.txt if it exists and api_keys.txt doesn't
225
+ self._setup_api_keys(user_data_path)
226
+
227
+ return True, f"User data folder structure created at: {user_data_path}"
228
+ except Exception as e:
229
+ return False, f"Failed to create user_data structure: {e}"
230
+
231
+ def _setup_api_keys(self, user_data_path: str) -> Tuple[bool, str]:
232
+ """
233
+ Copy api_keys.example.txt to api_keys.txt in user_data folder.
234
+
235
+ Only creates if api_keys.txt doesn't already exist.
236
+ """
237
+ try:
238
+ # Get paths
239
+ repo_root = os.path.dirname(self.script_dir)
240
+ example_source = os.path.join(repo_root, self.API_KEYS_EXAMPLE_FILENAME)
241
+ api_keys_dest = os.path.join(user_data_path, self.API_KEYS_FILENAME)
242
+
243
+ # If api_keys.txt already exists, nothing to do
244
+ if os.path.exists(api_keys_dest):
245
+ return True, "api_keys.txt already exists"
246
+
247
+ # If example file exists, copy it
248
+ if os.path.exists(example_source):
249
+ shutil.copy2(example_source, api_keys_dest)
250
+ print(f"[Config] Created {api_keys_dest} from template")
251
+ return True, f"Created api_keys.txt from template"
252
+ else:
253
+ # Create empty api_keys.txt with instructions
254
+ with open(api_keys_dest, 'w', encoding='utf-8') as f:
255
+ f.write("# API Keys Configuration\n")
256
+ f.write("# Add your API keys here in the format: KEY_NAME=value\n")
257
+ f.write("# Example:\n")
258
+ f.write("# OPENAI_API_KEY=sk-...\n")
259
+ f.write("# ANTHROPIC_API_KEY=sk-ant-...\n\n")
260
+ print(f"[Config] Created empty {api_keys_dest} with instructions")
261
+ return True, "Created api_keys.txt with instructions"
262
+ except Exception as e:
263
+ print(f"[Config] Error setting up api_keys: {e}")
264
+ return False, f"Failed to setup api_keys.txt: {e}"
265
+
266
+ def get_subfolder_path(self, subfolder: str) -> str:
267
+ """
268
+ Get the full path to a subfolder in user_data.
269
+
270
+ Example:
271
+ config.get_subfolder_path('resources/tms')
272
+ -> '/home/user/Supervertaler/resources/tms'
273
+ """
274
+ user_data_path = self.get_user_data_path()
275
+ full_path = os.path.join(user_data_path, subfolder)
276
+
277
+ # Ensure subfolder exists
278
+ Path(full_path).mkdir(parents=True, exist_ok=True)
279
+
280
+ return full_path
281
+
282
+ def get_existing_user_data_folder(self) -> Optional[str]:
283
+ """
284
+ Detect if there's existing user_data in the script directory (from development).
285
+
286
+ Returns path if found, None otherwise.
287
+ """
288
+ script_dir = os.path.dirname(os.path.abspath(__file__))
289
+ old_user_data_path = os.path.join(script_dir, "user_data")
290
+
291
+ if os.path.exists(old_user_data_path) and os.path.isdir(old_user_data_path):
292
+ # Check if it has any content
293
+ if os.listdir(old_user_data_path):
294
+ return old_user_data_path
295
+
296
+ return None
297
+
298
+ def migrate_user_data(self, old_path: str, new_path: str) -> Tuple[bool, str]:
299
+ """
300
+ Migrate user_data from old location to new location.
301
+
302
+ Also handles migration of api_keys.txt if it exists in old location.
303
+
304
+ Args:
305
+ old_path: Current user_data location
306
+ new_path: New user_data location
307
+
308
+ Returns:
309
+ Tuple of (success: bool, message: str)
310
+ """
311
+ if not os.path.exists(old_path):
312
+ return False, f"Old path does not exist: {old_path}"
313
+
314
+ try:
315
+ # Ensure new location exists
316
+ Path(new_path).mkdir(parents=True, exist_ok=True)
317
+
318
+ # Move all items from old to new
319
+ files_moved = 0
320
+ for item in os.listdir(old_path):
321
+ old_item_path = os.path.join(old_path, item)
322
+ new_item_path = os.path.join(new_path, item)
323
+
324
+ # Skip if item already exists at destination
325
+ if os.path.exists(new_item_path):
326
+ print(f"[Migration] Skipping (exists): {item}")
327
+ continue
328
+
329
+ try:
330
+ if os.path.isdir(old_item_path):
331
+ shutil.copytree(old_item_path, new_item_path)
332
+ else:
333
+ shutil.copy2(old_item_path, new_item_path)
334
+ files_moved += 1
335
+ except Exception as e:
336
+ print(f"[Migration] Error moving {item}: {e}")
337
+ continue
338
+
339
+ return True, f"Migrated {files_moved} items from {old_path} to {new_path}"
340
+ except Exception as e:
341
+ return False, f"Migration failed: {e}"
342
+
343
+ def migrate_api_keys_from_installation(self, user_data_path: str) -> Tuple[bool, str]:
344
+ """
345
+ Migrate api_keys.txt from installation folder to user_data folder if it exists.
346
+
347
+ This handles migration for users upgrading from older versions.
348
+
349
+ Args:
350
+ user_data_path: Target user_data folder
351
+
352
+ Returns:
353
+ Tuple of (success: bool, message: str)
354
+ """
355
+ try:
356
+ repo_root = os.path.dirname(self.script_dir)
357
+ old_api_keys = os.path.join(repo_root, self.API_KEYS_FILENAME)
358
+ new_api_keys = os.path.join(user_data_path, self.API_KEYS_FILENAME)
359
+
360
+ # If old api_keys.txt exists and new one doesn't, move it
361
+ if os.path.exists(old_api_keys) and not os.path.exists(new_api_keys):
362
+ shutil.copy2(old_api_keys, new_api_keys)
363
+ print(f"[Migration] Migrated api_keys.txt to {new_api_keys}")
364
+ return True, f"Migrated api_keys.txt to user_data folder"
365
+
366
+ return True, "api_keys.txt migration not needed"
367
+ except Exception as e:
368
+ print(f"[Migration] Error migrating api_keys.txt: {e}")
369
+ return False, f"Failed to migrate api_keys.txt: {e}"
370
+
371
+ def validate_current_path(self) -> Tuple[bool, str]:
372
+ """
373
+ Validate that the currently configured path is still valid.
374
+
375
+ Returns:
376
+ Tuple of (is_valid: bool, error_message: str)
377
+ """
378
+ user_data_path = self.get_user_data_path()
379
+
380
+ # Check if path exists and is writable
381
+ if not os.path.exists(user_data_path):
382
+ return False, f"User data path no longer exists: {user_data_path}"
383
+
384
+ try:
385
+ # Try to write test file
386
+ test_file = os.path.join(user_data_path, ".supervertaler_test")
387
+ Path(test_file).touch()
388
+ Path(test_file).unlink()
389
+ return True, ""
390
+ except Exception as e:
391
+ return False, f"User data path is not writable: {e}"
392
+
393
+ def get_preferences_path(self) -> str:
394
+ """Get the path to the UI preferences file."""
395
+ user_data_path = self.get_user_data_path()
396
+ return os.path.join(user_data_path, 'ui_preferences.json')
397
+
398
+ def load_preferences(self) -> dict:
399
+ """Load UI preferences from file."""
400
+ prefs_path = self.get_preferences_path()
401
+ if os.path.exists(prefs_path):
402
+ try:
403
+ with open(prefs_path, 'r', encoding='utf-8') as f:
404
+ return json.load(f)
405
+ except (json.JSONDecodeError, IOError) as e:
406
+ print(f"[Config] Error loading preferences: {e}")
407
+ return {}
408
+
409
+ def save_preferences(self, preferences: dict) -> bool:
410
+ """Save UI preferences to file."""
411
+ prefs_path = self.get_preferences_path()
412
+ try:
413
+ # Ensure directory exists
414
+ os.makedirs(os.path.dirname(prefs_path), exist_ok=True)
415
+ with open(prefs_path, 'w', encoding='utf-8') as f:
416
+ json.dump(preferences, f, indent=2, ensure_ascii=False)
417
+ return True
418
+ except IOError as e:
419
+ print(f"[Config] Error saving preferences: {e}")
420
+ return False
421
+
422
+ def get_all_config_info(self) -> dict:
423
+ """Get all configuration information for debugging."""
424
+ return {
425
+ 'config_file': self.config_path,
426
+ 'user_data_path': self.get_user_data_path(),
427
+ 'is_first_launch': self.is_first_launch(),
428
+ 'config': self.config,
429
+ }
430
+
431
+ def get_last_directory(self) -> str:
432
+ """
433
+ Get the last directory used in file dialogs.
434
+ Returns empty string if no directory has been saved yet.
435
+ """
436
+ return self.config.get('last_directory', '')
437
+
438
+ def set_last_directory(self, directory: str) -> None:
439
+ """
440
+ Save the last directory used in file dialogs.
441
+
442
+ Args:
443
+ directory: Full path to the directory to remember
444
+ """
445
+ if directory and os.path.isdir(directory):
446
+ self.config['last_directory'] = os.path.normpath(directory)
447
+ self._save_config()
448
+
449
+ def update_last_directory_from_file(self, file_path: str) -> None:
450
+ """
451
+ Extract and save the directory from a file path.
452
+
453
+ Args:
454
+ file_path: Full path to a file
455
+ """
456
+ if file_path:
457
+ directory = os.path.dirname(file_path)
458
+ self.set_last_directory(directory)
459
+
460
+
461
+ # Convenience function for easy access
462
+ _config_manager = None
463
+
464
+ def get_config_manager() -> ConfigManager:
465
+ """Get or create the global ConfigManager instance."""
466
+ global _config_manager
467
+ if _config_manager is None:
468
+ _config_manager = ConfigManager()
469
+ return _config_manager