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,545 @@
1
+ """
2
+ Translation Memory Metadata Manager Module
3
+
4
+ Handles TM metadata operations: creation, activation, TM management.
5
+ Works alongside the existing translation_memory.py module which handles TM matching/searching.
6
+
7
+ TMs can be activated/deactivated per project (similar to termbases).
8
+ """
9
+
10
+ import sqlite3
11
+ from typing import List, Dict, Optional
12
+ from datetime import datetime
13
+
14
+
15
+ class TMMetadataManager:
16
+ """Manages translation memory metadata and activation"""
17
+
18
+ def __init__(self, db_manager, log_callback=None):
19
+ """
20
+ Initialize TM metadata manager
21
+
22
+ Args:
23
+ db_manager: DatabaseManager instance
24
+ log_callback: Optional logging function
25
+ """
26
+ self.db_manager = db_manager
27
+ self.log = log_callback if log_callback else print
28
+
29
+ # ========================================================================
30
+ # TM MANAGEMENT
31
+ # ========================================================================
32
+
33
+ def tm_id_exists(self, tm_id: str) -> bool:
34
+ """Check if a tm_id already exists in the database"""
35
+ try:
36
+ cursor = self.db_manager.cursor
37
+ cursor.execute("SELECT 1 FROM translation_memories WHERE tm_id = ?", (tm_id,))
38
+ return cursor.fetchone() is not None
39
+ except Exception:
40
+ return False
41
+
42
+ def get_unique_tm_id(self, base_tm_id: str) -> str:
43
+ """
44
+ Get a unique tm_id by appending a number suffix if needed.
45
+
46
+ Args:
47
+ base_tm_id: The desired tm_id base (e.g., 'my_tm')
48
+
49
+ Returns:
50
+ A unique tm_id (e.g., 'my_tm' or 'my_tm_2' if 'my_tm' exists)
51
+ """
52
+ if not self.tm_id_exists(base_tm_id):
53
+ return base_tm_id
54
+
55
+ # Find a unique suffix
56
+ suffix = 2
57
+ while True:
58
+ candidate = f"{base_tm_id}_{suffix}"
59
+ if not self.tm_id_exists(candidate):
60
+ return candidate
61
+ suffix += 1
62
+ if suffix > 1000: # Safety limit
63
+ import uuid
64
+ return f"{base_tm_id}_{uuid.uuid4().hex[:8]}"
65
+
66
+ def create_tm(self, name: str, tm_id: str, source_lang: Optional[str] = None,
67
+ target_lang: Optional[str] = None, description: str = "",
68
+ is_project_tm: bool = False, read_only: bool = False,
69
+ project_id: Optional[int] = None, auto_unique_id: bool = True) -> Optional[int]:
70
+ """
71
+ Create a new TM metadata entry
72
+
73
+ Args:
74
+ name: Display name for the TM (e.g., "ClientX_Medical_2024")
75
+ tm_id: Unique identifier used in translation_units.tm_id field
76
+ source_lang: Source language code (e.g., 'en', 'nl')
77
+ target_lang: Target language code
78
+ description: Optional description
79
+ is_project_tm: Whether this is the special project TM (only one per project)
80
+ read_only: Whether this TM should not be updated
81
+ project_id: Which project this TM belongs to (NULL = global)
82
+ auto_unique_id: If True, automatically make tm_id unique by appending suffix
83
+
84
+ Returns:
85
+ TM database ID or None if failed
86
+ """
87
+ try:
88
+ cursor = self.db_manager.cursor
89
+ now = datetime.now().isoformat()
90
+
91
+ # If this is a project TM, check if one already exists for this project
92
+ if is_project_tm and project_id:
93
+ cursor.execute("""
94
+ SELECT id, name FROM translation_memories
95
+ WHERE project_id = ? AND is_project_tm = 1
96
+ """, (project_id,))
97
+ existing = cursor.fetchone()
98
+ if existing:
99
+ self.log(f"✗ Project {project_id} already has a project TM: {existing[1]}")
100
+ return None
101
+
102
+ # Make tm_id unique if it already exists
103
+ if auto_unique_id and self.tm_id_exists(tm_id):
104
+ original_tm_id = tm_id
105
+ tm_id = self.get_unique_tm_id(tm_id)
106
+ self.log(f"ℹ️ TM ID '{original_tm_id}' already exists, using '{tm_id}' instead")
107
+
108
+ cursor.execute("""
109
+ INSERT INTO translation_memories
110
+ (name, tm_id, source_lang, target_lang, description, created_date, modified_date, entry_count,
111
+ is_project_tm, read_only, project_id)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)
113
+ """, (name, tm_id, source_lang, target_lang, description, now, now, is_project_tm, read_only, project_id))
114
+
115
+ self.db_manager.connection.commit()
116
+ db_id = cursor.lastrowid
117
+ tm_type = "project TM" if is_project_tm else "TM"
118
+ self.log(f"✓ Created {tm_type}: {name} (ID: {db_id}, tm_id: {tm_id})")
119
+ return db_id
120
+ except Exception as e:
121
+ self.log(f"✗ Error creating TM: {e}")
122
+ return None
123
+
124
+ def get_all_tms(self) -> List[Dict]:
125
+ """
126
+ Get all TMs with metadata
127
+
128
+ Returns:
129
+ List of TM dictionaries with fields: id, name, tm_id, source_lang, target_lang,
130
+ description, entry_count, created_date, modified_date, last_used,
131
+ is_project_tm, read_only, project_id
132
+ """
133
+ try:
134
+ cursor = self.db_manager.cursor
135
+
136
+ # Get TM metadata with actual entry counts from translation_units
137
+ cursor.execute("""
138
+ SELECT
139
+ tm.id, tm.name, tm.tm_id, tm.source_lang, tm.target_lang,
140
+ tm.description, tm.created_date, tm.modified_date, tm.last_used,
141
+ COUNT(tu.id) as actual_count,
142
+ tm.is_project_tm, tm.read_only, tm.project_id
143
+ FROM translation_memories tm
144
+ LEFT JOIN translation_units tu ON tm.tm_id = tu.tm_id
145
+ GROUP BY tm.id
146
+ ORDER BY tm.is_project_tm DESC, tm.name ASC
147
+ """)
148
+
149
+ tms = []
150
+ for row in cursor.fetchall():
151
+ tms.append({
152
+ 'id': row[0],
153
+ 'name': row[1],
154
+ 'tm_id': row[2],
155
+ 'source_lang': row[3],
156
+ 'target_lang': row[4],
157
+ 'description': row[5],
158
+ 'created_date': row[6],
159
+ 'modified_date': row[7],
160
+ 'last_used': row[8],
161
+ 'entry_count': row[9],
162
+ 'is_project_tm': bool(row[10]) if len(row) > 10 else False,
163
+ 'read_only': bool(row[11]) if len(row) > 11 else False,
164
+ 'project_id': row[12] if len(row) > 12 else None
165
+ })
166
+
167
+ return tms
168
+ except Exception as e:
169
+ self.log(f"✗ Error fetching TMs: {e}")
170
+ import traceback
171
+ traceback.print_exc()
172
+ return []
173
+
174
+ def get_tm(self, tm_db_id: int) -> Optional[Dict]:
175
+ """Get single TM by database ID"""
176
+ try:
177
+ cursor = self.db_manager.cursor
178
+
179
+ cursor.execute("""
180
+ SELECT
181
+ tm.id, tm.name, tm.tm_id, tm.source_lang, tm.target_lang,
182
+ tm.description, tm.created_date, tm.modified_date, tm.last_used,
183
+ COUNT(tu.id) as actual_count
184
+ FROM translation_memories tm
185
+ LEFT JOIN translation_units tu ON tm.tm_id = tu.tm_id
186
+ WHERE tm.id = ?
187
+ GROUP BY tm.id
188
+ """, (tm_db_id,))
189
+
190
+ row = cursor.fetchone()
191
+ if row:
192
+ return {
193
+ 'id': row[0],
194
+ 'name': row[1],
195
+ 'tm_id': row[2],
196
+ 'source_lang': row[3],
197
+ 'target_lang': row[4],
198
+ 'description': row[5],
199
+ 'created_date': row[6],
200
+ 'modified_date': row[7],
201
+ 'last_used': row[8],
202
+ 'entry_count': row[9] or 0
203
+ }
204
+ return None
205
+ except Exception as e:
206
+ self.log(f"✗ Error fetching TM: {e}")
207
+ return None
208
+
209
+ def update_tm(self, tm_db_id: int, name: Optional[str] = None,
210
+ description: Optional[str] = None, source_lang: Optional[str] = None,
211
+ target_lang: Optional[str] = None) -> bool:
212
+ """Update TM metadata"""
213
+ try:
214
+ cursor = self.db_manager.cursor
215
+ updates = []
216
+ values = []
217
+
218
+ if name is not None:
219
+ updates.append("name = ?")
220
+ values.append(name)
221
+ if description is not None:
222
+ updates.append("description = ?")
223
+ values.append(description)
224
+ if source_lang is not None:
225
+ updates.append("source_lang = ?")
226
+ values.append(source_lang)
227
+ if target_lang is not None:
228
+ updates.append("target_lang = ?")
229
+ values.append(target_lang)
230
+
231
+ if not updates:
232
+ return True
233
+
234
+ updates.append("modified_date = ?")
235
+ values.append(datetime.now().isoformat())
236
+ values.append(tm_db_id)
237
+
238
+ sql = f"UPDATE translation_memories SET {', '.join(updates)} WHERE id = ?"
239
+ cursor.execute(sql, values)
240
+ self.db_manager.connection.commit()
241
+
242
+ self.log(f"✓ Updated TM (ID: {tm_db_id})")
243
+ return True
244
+ except Exception as e:
245
+ self.log(f"✗ Error updating TM: {e}")
246
+ return False
247
+
248
+ def delete_tm(self, tm_db_id: int, delete_entries: bool = False) -> bool:
249
+ """
250
+ Delete TM metadata (and optionally its translation units)
251
+
252
+ Args:
253
+ tm_db_id: Database ID of the TM
254
+ delete_entries: If True, also delete all translation_units with this tm_id
255
+ """
256
+ try:
257
+ cursor = self.db_manager.cursor
258
+
259
+ # Get tm_id first
260
+ cursor.execute("SELECT tm_id FROM translation_memories WHERE id = ?", (tm_db_id,))
261
+ row = cursor.fetchone()
262
+ if not row:
263
+ self.log(f"✗ TM not found: {tm_db_id}")
264
+ return False
265
+
266
+ tm_id = row[0]
267
+
268
+ # Delete translation units if requested
269
+ if delete_entries:
270
+ cursor.execute("DELETE FROM translation_units WHERE tm_id = ?", (tm_id,))
271
+ self.log(f"✓ Deleted translation units for tm_id: {tm_id}")
272
+
273
+ # Delete TM metadata (this will cascade delete tm_activation entries)
274
+ cursor.execute("DELETE FROM translation_memories WHERE id = ?", (tm_db_id,))
275
+
276
+ self.db_manager.connection.commit()
277
+ self.log(f"✓ Deleted TM (ID: {tm_db_id})")
278
+ return True
279
+ except Exception as e:
280
+ self.log(f"✗ Error deleting TM: {e}")
281
+ return False
282
+
283
+ def update_entry_count(self, tm_id: str) -> bool:
284
+ """Update cached entry count for a TM"""
285
+ try:
286
+ cursor = self.db_manager.cursor
287
+
288
+ cursor.execute("""
289
+ UPDATE translation_memories
290
+ SET entry_count = (
291
+ SELECT COUNT(*) FROM translation_units WHERE tm_id = ?
292
+ ),
293
+ modified_date = ?
294
+ WHERE tm_id = ?
295
+ """, (tm_id, datetime.now().isoformat(), tm_id))
296
+
297
+ self.db_manager.connection.commit()
298
+ return True
299
+ except Exception as e:
300
+ self.log(f"✗ Error updating entry count: {e}")
301
+ return False
302
+
303
+ # ========================================================================
304
+ # TM ACTIVATION (per-project)
305
+ # ========================================================================
306
+
307
+ def activate_tm(self, tm_db_id: int, project_id: int) -> bool:
308
+ """Activate a TM for a specific project"""
309
+ try:
310
+ cursor = self.db_manager.cursor
311
+ now = datetime.now().isoformat()
312
+
313
+ cursor.execute("""
314
+ INSERT OR REPLACE INTO tm_activation (tm_id, project_id, is_active, activated_date)
315
+ VALUES (?, ?, 1, ?)
316
+ """, (tm_db_id, project_id, now))
317
+
318
+ self.db_manager.connection.commit()
319
+ self.log(f"✓ Activated TM {tm_db_id} for project {project_id}")
320
+ return True
321
+ except Exception as e:
322
+ self.log(f"✗ Error activating TM: {e}")
323
+ return False
324
+
325
+ def deactivate_tm(self, tm_db_id: int, project_id: int) -> bool:
326
+ """Deactivate a TM for a specific project"""
327
+ try:
328
+ cursor = self.db_manager.cursor
329
+
330
+ cursor.execute("""
331
+ UPDATE tm_activation
332
+ SET is_active = 0
333
+ WHERE tm_id = ? AND project_id = ?
334
+ """, (tm_db_id, project_id))
335
+
336
+ self.db_manager.connection.commit()
337
+ self.log(f"✓ Deactivated TM {tm_db_id} for project {project_id}")
338
+ return True
339
+ except Exception as e:
340
+ self.log(f"✗ Error deactivating TM: {e}")
341
+ return False
342
+
343
+ def is_tm_active(self, tm_db_id: int, project_id: Optional[int]) -> bool:
344
+ """Check if a TM is active for a project (or global when project_id=0)"""
345
+ if project_id is None:
346
+ return False # If None (not 0), default to inactive
347
+
348
+ try:
349
+ cursor = self.db_manager.cursor
350
+
351
+ cursor.execute("""
352
+ SELECT is_active FROM tm_activation
353
+ WHERE tm_id = ? AND project_id = ?
354
+ """, (tm_db_id, project_id))
355
+
356
+ row = cursor.fetchone()
357
+ if row:
358
+ return bool(row[0])
359
+
360
+ # If no activation record exists, TM is inactive by default
361
+ return False
362
+ except Exception as e:
363
+ self.log(f"✗ Error checking TM activation: {e}")
364
+ return False
365
+
366
+ def get_active_tm_ids(self, project_id: Optional[int]) -> List[str]:
367
+ """
368
+ Get list of active tm_id strings for a project
369
+
370
+ Returns:
371
+ List of tm_id strings that are active for the project
372
+ """
373
+ if project_id is None:
374
+ # No project - return all TMs
375
+ try:
376
+ cursor = self.db_manager.cursor
377
+ cursor.execute("SELECT tm_id FROM translation_memories")
378
+ return [row[0] for row in cursor.fetchall()]
379
+ except Exception as e:
380
+ self.log(f"✗ Error fetching all tm_ids: {e}")
381
+ return []
382
+
383
+ try:
384
+ cursor = self.db_manager.cursor
385
+
386
+ # Only return TMs that have been explicitly activated (is_active = 1)
387
+ cursor.execute("""
388
+ SELECT tm.tm_id
389
+ FROM translation_memories tm
390
+ INNER JOIN tm_activation ta ON tm.id = ta.tm_id
391
+ WHERE ta.project_id = ? AND ta.is_active = 1
392
+ """, (project_id,))
393
+
394
+ return [row[0] for row in cursor.fetchall()]
395
+ except Exception as e:
396
+ self.log(f"✗ Error fetching active tm_ids: {e}")
397
+ return []
398
+
399
+ # ========================================================================
400
+ # PROJECT TM MANAGEMENT (similar to termbases)
401
+ # ========================================================================
402
+
403
+ def set_as_project_tm(self, tm_db_id: int, project_id: int) -> bool:
404
+ """
405
+ Set a TM as the project TM for a specific project.
406
+ Only one TM can be the project TM per project (automatically unsets others).
407
+ """
408
+ try:
409
+ cursor = self.db_manager.cursor
410
+
411
+ # First, unset any existing project TM for this project
412
+ cursor.execute("""
413
+ UPDATE translation_memories
414
+ SET is_project_tm = 0
415
+ WHERE project_id = ? AND is_project_tm = 1
416
+ """, (project_id,))
417
+
418
+ # Then set the new one
419
+ cursor.execute("""
420
+ UPDATE translation_memories
421
+ SET is_project_tm = 1, project_id = ?
422
+ WHERE id = ?
423
+ """, (project_id, tm_db_id))
424
+
425
+ self.db_manager.connection.commit()
426
+ self.log(f"✓ Set TM {tm_db_id} as project TM for project {project_id}")
427
+ return True
428
+ except Exception as e:
429
+ self.log(f"✗ Error setting project TM: {e}")
430
+ return False
431
+
432
+ def unset_project_tm(self, tm_db_id: int) -> bool:
433
+ """Unset a TM as project TM"""
434
+ try:
435
+ cursor = self.db_manager.cursor
436
+
437
+ cursor.execute("""
438
+ UPDATE translation_memories
439
+ SET is_project_tm = 0
440
+ WHERE id = ?
441
+ """, (tm_db_id,))
442
+
443
+ self.db_manager.connection.commit()
444
+ self.log(f"✓ Unset TM {tm_db_id} as project TM")
445
+ return True
446
+ except Exception as e:
447
+ self.log(f"✗ Error unsetting project TM: {e}")
448
+ return False
449
+
450
+ def get_project_tm(self, project_id: int) -> Optional[Dict]:
451
+ """Get the project TM for a specific project"""
452
+ try:
453
+ cursor = self.db_manager.cursor
454
+
455
+ cursor.execute("""
456
+ SELECT
457
+ tm.id, tm.name, tm.tm_id, tm.source_lang, tm.target_lang,
458
+ tm.description, tm.created_date, tm.modified_date, tm.last_used,
459
+ COUNT(tu.id) as actual_count,
460
+ tm.is_project_tm, tm.read_only, tm.project_id
461
+ FROM translation_memories tm
462
+ LEFT JOIN translation_units tu ON tm.tm_id = tu.tm_id
463
+ WHERE tm.project_id = ? AND tm.is_project_tm = 1
464
+ GROUP BY tm.id
465
+ """, (project_id,))
466
+
467
+ row = cursor.fetchone()
468
+ if row:
469
+ return {
470
+ 'id': row[0],
471
+ 'name': row[1],
472
+ 'tm_id': row[2],
473
+ 'source_lang': row[3],
474
+ 'target_lang': row[4],
475
+ 'description': row[5],
476
+ 'created_date': row[6],
477
+ 'modified_date': row[7],
478
+ 'last_used': row[8],
479
+ 'entry_count': row[9],
480
+ 'is_project_tm': bool(row[10]),
481
+ 'read_only': bool(row[11]),
482
+ 'project_id': row[12]
483
+ }
484
+ return None
485
+ except Exception as e:
486
+ self.log(f"✗ Error fetching project TM: {e}")
487
+ return None
488
+
489
+ def get_tm_by_tm_id(self, tm_id: str) -> Optional[Dict]:
490
+ """Get TM by its tm_id string"""
491
+ try:
492
+ cursor = self.db_manager.cursor
493
+
494
+ cursor.execute("""
495
+ SELECT
496
+ tm.id, tm.name, tm.tm_id, tm.source_lang, tm.target_lang,
497
+ tm.description, tm.created_date, tm.modified_date, tm.last_used,
498
+ COUNT(tu.id) as actual_count,
499
+ tm.is_project_tm, tm.read_only, tm.project_id
500
+ FROM translation_memories tm
501
+ LEFT JOIN translation_units tu ON tm.tm_id = tu.tm_id
502
+ WHERE tm.tm_id = ?
503
+ GROUP BY tm.id
504
+ """, (tm_id,))
505
+
506
+ row = cursor.fetchone()
507
+ if row:
508
+ return {
509
+ 'id': row[0],
510
+ 'name': row[1],
511
+ 'tm_id': row[2],
512
+ 'source_lang': row[3],
513
+ 'target_lang': row[4],
514
+ 'description': row[5],
515
+ 'created_date': row[6],
516
+ 'modified_date': row[7],
517
+ 'last_used': row[8],
518
+ 'entry_count': row[9],
519
+ 'is_project_tm': bool(row[10]) if len(row) > 10 else False,
520
+ 'read_only': bool(row[11]) if len(row) > 11 else False,
521
+ 'project_id': row[12] if len(row) > 12 else None
522
+ }
523
+ return None
524
+ except Exception as e:
525
+ self.log(f"✗ Error fetching TM by tm_id: {e}")
526
+ return None
527
+
528
+ def set_read_only(self, tm_db_id: int, read_only: bool) -> bool:
529
+ """Set whether a TM is read-only (cannot be updated)"""
530
+ try:
531
+ cursor = self.db_manager.cursor
532
+
533
+ cursor.execute("""
534
+ UPDATE translation_memories
535
+ SET read_only = ?
536
+ WHERE id = ?
537
+ """, (read_only, tm_db_id))
538
+
539
+ self.db_manager.connection.commit()
540
+ status = "read-only" if read_only else "writable"
541
+ self.log(f"✓ Set TM {tm_db_id} as {status}")
542
+ return True
543
+ except Exception as e:
544
+ self.log(f"✗ Error setting read-only status: {e}")
545
+ return False