supervertaler 1.9.153__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.

Potentially problematic release.


This version of supervertaler might be problematic. Click here for more details.

Files changed (85) hide show
  1. Supervertaler.py +47886 -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 +1878 -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 +333 -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 +1172 -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.153.dist-info/METADATA +896 -0
  81. supervertaler-1.9.153.dist-info/RECORD +85 -0
  82. supervertaler-1.9.153.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.153.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1060 @@
1
+ """
2
+ Termbase Manager Module
3
+
4
+ Handles all termbase operations: creation, activation, term management, searching.
5
+ Uses 'termbase' terminology throughout (never 'glossary').
6
+
7
+ Termbases can be:
8
+ - Global (available to all projects)
9
+ - Project-specific (linked to particular project)
10
+
11
+ Activation system: termbases can be activated/deactivated per project.
12
+ """
13
+
14
+ import sqlite3
15
+ import json
16
+ from typing import List, Dict, Optional, Tuple
17
+ from datetime import datetime
18
+
19
+
20
+ class TermbaseManager:
21
+ """Manages termbase operations and term storage"""
22
+
23
+ def __init__(self, db_manager, log_callback=None):
24
+ """
25
+ Initialize termbase manager
26
+
27
+ Args:
28
+ db_manager: DatabaseManager instance
29
+ log_callback: Optional logging function
30
+ """
31
+ self.db_manager = db_manager
32
+ self.log = log_callback if log_callback else print
33
+
34
+ # ========================================================================
35
+ # TERMBASE MANAGEMENT
36
+ # ========================================================================
37
+
38
+ def create_termbase(self, name: str, source_lang: Optional[str] = None,
39
+ target_lang: Optional[str] = None, project_id: Optional[int] = None,
40
+ description: str = "", is_global: bool = True, is_project_termbase: bool = False) -> Optional[int]:
41
+ """
42
+ Create a new termbase
43
+
44
+ Args:
45
+ name: Termbase name
46
+ source_lang: Source language code (e.g., 'en', 'nl')
47
+ target_lang: Target language code
48
+ project_id: If set, termbase is project-specific; if None, it's global
49
+ description: Optional description
50
+ is_global: Whether this is a global termbase (available to all projects)
51
+ is_project_termbase: Whether this is the special project termbase (only one allowed per project)
52
+
53
+ Returns:
54
+ Termbase ID or None if failed
55
+ """
56
+ try:
57
+ cursor = self.db_manager.cursor
58
+ now = datetime.now().isoformat()
59
+
60
+ # If this is a project termbase, check if one already exists for this project
61
+ if is_project_termbase and project_id:
62
+ cursor.execute("""
63
+ SELECT id, name FROM termbases
64
+ WHERE project_id = ? AND is_project_termbase = 1
65
+ """, (project_id,))
66
+ existing = cursor.fetchone()
67
+ if existing:
68
+ self.log(f"✗ Project {project_id} already has a project termbase: {existing[1]}")
69
+ return None
70
+
71
+ cursor.execute("""
72
+ INSERT INTO termbases (name, source_lang, target_lang, project_id,
73
+ description, is_global, is_project_termbase, created_date, modified_date)
74
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
75
+ """, (name, source_lang, target_lang, project_id, description, is_global, is_project_termbase, now, now))
76
+
77
+ self.db_manager.connection.commit()
78
+ termbase_id = cursor.lastrowid
79
+ tb_type = "project termbase" if is_project_termbase else "termbase"
80
+ self.log(f"✓ Created {tb_type}: {name} (ID: {termbase_id})")
81
+ return termbase_id
82
+ except Exception as e:
83
+ self.log(f"✗ Error creating termbase: {e}")
84
+ return None
85
+
86
+ def get_all_termbases(self) -> List[Dict]:
87
+ """
88
+ Get all termbases (global and project-specific)
89
+
90
+ Returns:
91
+ List of termbase dictionaries with fields: id, name, source_lang, target_lang,
92
+ project_id, description, is_global, is_active, term_count, created_date, modified_date
93
+ """
94
+ try:
95
+ cursor = self.db_manager.cursor
96
+
97
+ cursor.execute("""
98
+ SELECT
99
+ t.id, t.name, t.source_lang, t.target_lang, t.project_id,
100
+ t.description, t.is_global, t.priority, t.is_project_termbase,
101
+ t.ranking, t.read_only, t.created_date, t.modified_date,
102
+ COUNT(gt.id) as term_count
103
+ FROM termbases t
104
+ LEFT JOIN termbase_terms gt ON CAST(t.id AS TEXT) = gt.termbase_id
105
+ GROUP BY t.id
106
+ ORDER BY t.is_project_termbase DESC, t.is_global DESC, t.name ASC
107
+ """)
108
+
109
+ termbases = []
110
+ for row in cursor.fetchall():
111
+ termbases.append({
112
+ 'id': row[0],
113
+ 'name': row[1],
114
+ 'source_lang': row[2],
115
+ 'target_lang': row[3],
116
+ 'project_id': row[4],
117
+ 'description': row[5],
118
+ 'is_global': row[6],
119
+ 'priority': row[7] or 50, # Default to 50 if NULL (legacy)
120
+ 'is_project_termbase': bool(row[8]),
121
+ 'ranking': row[9], # Termbase ranking
122
+ 'read_only': bool(row[10]) if row[10] is not None else True, # Default to read-only if NULL
123
+ 'created_date': row[11],
124
+ 'modified_date': row[12],
125
+ 'term_count': row[13] or 0
126
+ })
127
+
128
+ return termbases
129
+ except Exception as e:
130
+ self.log(f"✗ Error fetching termbases: {e}")
131
+ return []
132
+
133
+ def get_termbase(self, termbase_id: int) -> Optional[Dict]:
134
+ """Get single termbase by ID"""
135
+ try:
136
+ cursor = self.db_manager.cursor
137
+
138
+ cursor.execute("""
139
+ SELECT
140
+ t.id, t.name, t.source_lang, t.target_lang, t.project_id,
141
+ t.description, t.is_global, t.created_date, t.modified_date,
142
+ COUNT(gt.id) as term_count
143
+ FROM termbases t
144
+ LEFT JOIN termbase_terms gt ON t.id = gt.termbase_id
145
+ WHERE t.id = ?
146
+ GROUP BY t.id
147
+ """, (termbase_id,))
148
+
149
+ row = cursor.fetchone()
150
+ if row:
151
+ return {
152
+ 'id': row[0],
153
+ 'name': row[1],
154
+ 'source_lang': row[2],
155
+ 'target_lang': row[3],
156
+ 'project_id': row[4],
157
+ 'description': row[5],
158
+ 'is_global': row[6],
159
+ 'created_date': row[7],
160
+ 'modified_date': row[8],
161
+ 'term_count': row[9] or 0
162
+ }
163
+ return None
164
+ except Exception as e:
165
+ self.log(f"✗ Error fetching termbase: {e}")
166
+ return None
167
+
168
+ def delete_termbase(self, termbase_id: int) -> bool:
169
+ """
170
+ Delete termbase and all its terms
171
+
172
+ Args:
173
+ termbase_id: Termbase ID
174
+
175
+ Returns:
176
+ True if successful
177
+ """
178
+ try:
179
+ cursor = self.db_manager.cursor
180
+
181
+ # Delete terms first
182
+ cursor.execute("DELETE FROM termbase_terms WHERE termbase_id = ?", (termbase_id,))
183
+
184
+ # Delete termbase
185
+ cursor.execute("DELETE FROM termbases WHERE id = ?", (termbase_id,))
186
+
187
+ self.db_manager.connection.commit()
188
+ self.log(f"✓ Deleted termbase ID: {termbase_id}")
189
+ return True
190
+ except Exception as e:
191
+ self.log(f"✗ Error deleting termbase: {e}")
192
+ return False
193
+
194
+ def rename_termbase(self, termbase_id: int, new_name: str) -> bool:
195
+ """
196
+ Rename a termbase
197
+
198
+ Args:
199
+ termbase_id: Termbase ID
200
+ new_name: New name for the termbase
201
+
202
+ Returns:
203
+ True if successful
204
+ """
205
+ try:
206
+ if not new_name or not new_name.strip():
207
+ self.log(f"✗ Cannot rename termbase: empty name provided")
208
+ return False
209
+
210
+ new_name = new_name.strip()
211
+ cursor = self.db_manager.cursor
212
+ now = datetime.now().isoformat()
213
+
214
+ cursor.execute("""
215
+ UPDATE termbases
216
+ SET name = ?, modified_date = ?
217
+ WHERE id = ?
218
+ """, (new_name, now, termbase_id))
219
+
220
+ self.db_manager.connection.commit()
221
+ self.log(f"✓ Renamed termbase ID {termbase_id} to '{new_name}'")
222
+ return True
223
+ except Exception as e:
224
+ self.log(f"✗ Error renaming termbase: {e}")
225
+ return False
226
+
227
+ def get_active_termbases_for_project(self, project_id: int) -> List[Dict]:
228
+ """
229
+ Get all active termbases for a specific project
230
+
231
+ Args:
232
+ project_id: Project ID
233
+
234
+ Returns:
235
+ List of active termbase dictionaries
236
+ """
237
+ try:
238
+ cursor = self.db_manager.cursor
239
+
240
+ cursor.execute("""
241
+ SELECT
242
+ t.id, t.name, t.source_lang, t.target_lang, t.project_id,
243
+ t.description, t.is_global, t.created_date, t.modified_date,
244
+ t.ranking, t.is_project_termbase,
245
+ COUNT(gt.id) as term_count
246
+ FROM termbases t
247
+ LEFT JOIN termbase_terms gt ON t.id = gt.termbase_id
248
+ LEFT JOIN termbase_activation ta ON t.id = ta.termbase_id AND ta.project_id = ?
249
+ WHERE (t.is_global = 1 OR t.project_id = ?)
250
+ AND (ta.is_active = 1 OR ta.is_active IS NULL)
251
+ GROUP BY t.id
252
+ ORDER BY t.name ASC
253
+ """, (project_id, project_id))
254
+
255
+ termbases = []
256
+ for row in cursor.fetchall():
257
+ termbases.append({
258
+ 'id': row[0],
259
+ 'name': row[1],
260
+ 'source_lang': row[2],
261
+ 'target_lang': row[3],
262
+ 'project_id': row[4],
263
+ 'description': row[5],
264
+ 'is_global': row[6],
265
+ 'created_date': row[7],
266
+ 'modified_date': row[8],
267
+ 'ranking': row[9],
268
+ 'is_project_termbase': row[10],
269
+ 'term_count': row[11] or 0
270
+ })
271
+
272
+ return termbases
273
+ except Exception as e:
274
+ self.log(f"✗ Error fetching active termbases: {e}")
275
+ return []
276
+
277
+ # ========================================================================
278
+ # TERMBASE ACTIVATION
279
+ # ========================================================================
280
+
281
+ def is_termbase_active(self, termbase_id: int, project_id: int) -> bool:
282
+ """Check if termbase is active for a project"""
283
+ try:
284
+ cursor = self.db_manager.cursor
285
+
286
+ cursor.execute("""
287
+ SELECT is_active FROM termbase_activation
288
+ WHERE termbase_id = ? AND project_id = ?
289
+ """, (termbase_id, project_id))
290
+
291
+ result = cursor.fetchone()
292
+ if result:
293
+ return result[0] == 1
294
+
295
+ # If no record exists, termbases are active by default
296
+ return True
297
+ except Exception as e:
298
+ self.log(f"✗ Error checking termbase activation: {e}")
299
+ return True
300
+
301
+ def activate_termbase(self, termbase_id: int, project_id: int) -> bool:
302
+ """Activate termbase for project and assign ranking"""
303
+ try:
304
+ cursor = self.db_manager.cursor
305
+
306
+ self.log(f"🔵 ACTIVATE: termbase_id={termbase_id}, project_id={project_id}")
307
+
308
+ # Check if activation record already exists
309
+ cursor.execute("""
310
+ SELECT activated_date FROM termbase_activation
311
+ WHERE termbase_id = ? AND project_id = ?
312
+ """, (termbase_id, project_id))
313
+ existing = cursor.fetchone()
314
+
315
+ if existing:
316
+ # Preserve original activated_date when re-activating
317
+ # Check if priority is NULL and set default if needed
318
+ cursor.execute("""
319
+ SELECT priority FROM termbase_activation
320
+ WHERE termbase_id = ? AND project_id = ?
321
+ """, (termbase_id, project_id))
322
+ existing_priority = cursor.fetchone()[0]
323
+
324
+ if existing_priority is None:
325
+ # Priority is NULL - assign default priority
326
+ cursor.execute("""
327
+ SELECT COALESCE(MAX(priority), 0) FROM termbase_activation
328
+ WHERE project_id = ? AND is_active = 1
329
+ """, (project_id,))
330
+ max_priority = cursor.fetchone()[0]
331
+ default_priority = max_priority + 1
332
+
333
+ cursor.execute("""
334
+ UPDATE termbase_activation
335
+ SET is_active = 1, priority = ?
336
+ WHERE termbase_id = ? AND project_id = ?
337
+ """, (default_priority, termbase_id, project_id))
338
+ self.log(f" ✓ Updated activation record (preserved timestamp, set priority #{default_priority})")
339
+ else:
340
+ # Priority already exists - just update is_active
341
+ cursor.execute("""
342
+ UPDATE termbase_activation
343
+ SET is_active = 1
344
+ WHERE termbase_id = ? AND project_id = ?
345
+ """, (termbase_id, project_id))
346
+ self.log(f" ✓ Updated activation record (preserved timestamp and priority #{existing_priority})")
347
+ else:
348
+ # Create new activation record with default priority
349
+ # Default priority: Find highest existing priority and add 1
350
+ cursor.execute("""
351
+ SELECT COALESCE(MAX(priority), 0) FROM termbase_activation
352
+ WHERE project_id = ? AND is_active = 1
353
+ """, (project_id,))
354
+ max_priority = cursor.fetchone()[0]
355
+ default_priority = max_priority + 1
356
+
357
+ cursor.execute("""
358
+ INSERT INTO termbase_activation (termbase_id, project_id, is_active, priority)
359
+ VALUES (?, ?, 1, ?)
360
+ """, (termbase_id, project_id, default_priority))
361
+ self.log(f" ✓ Created new activation record with default priority #{default_priority}")
362
+
363
+ self.db_manager.connection.commit()
364
+ self.log(f"✓ Activated termbase {termbase_id} for project {project_id}")
365
+ return True
366
+ except Exception as e:
367
+ self.log(f"✗ Error activating termbase: {e}")
368
+ import traceback
369
+ self.log(f"Traceback: {traceback.format_exc()}")
370
+ return False
371
+
372
+ def deactivate_termbase(self, termbase_id: int, project_id: int) -> bool:
373
+ """Deactivate termbase for project and reassign rankings"""
374
+ try:
375
+ cursor = self.db_manager.cursor
376
+
377
+ self.log(f"🔴 DEACTIVATE: termbase_id={termbase_id}, project_id={project_id}")
378
+
379
+ cursor.execute("""
380
+ INSERT OR REPLACE INTO termbase_activation (termbase_id, project_id, is_active)
381
+ VALUES (?, ?, 0)
382
+ """, (termbase_id, project_id))
383
+
384
+ self.log(f" ✓ Inserted deactivation record")
385
+
386
+ # Note: Priority is preserved in termbase_activation table even when deactivated
387
+ # This way if user re-activates, the priority is remembered
388
+
389
+ self.db_manager.connection.commit()
390
+ self.log(f"✓ Deactivated termbase {termbase_id} for project {project_id}")
391
+ return True
392
+ except Exception as e:
393
+ self.log(f"✗ Error deactivating termbase: {e}")
394
+ import traceback
395
+ self.log(f"Traceback: {traceback.format_exc()}")
396
+ return False
397
+
398
+ def set_termbase_read_only(self, termbase_id: int, read_only: bool) -> bool:
399
+ """Set termbase read-only status (True = read-only, False = writable)"""
400
+ try:
401
+ cursor = self.db_manager.cursor
402
+ cursor.execute("""
403
+ UPDATE termbases SET read_only = ? WHERE id = ?
404
+ """, (1 if read_only else 0, termbase_id))
405
+ self.db_manager.connection.commit()
406
+ status = "read-only" if read_only else "writable"
407
+ self.log(f"✓ Set termbase {termbase_id} to {status}")
408
+ return True
409
+ except Exception as e:
410
+ self.log(f"✗ Error setting termbase read_only: {e}")
411
+ return False
412
+
413
+ def set_termbase_priority(self, termbase_id: int, project_id: int, priority: int) -> bool:
414
+ """
415
+ Set manual priority for a termbase in a specific project.
416
+ Multiple termbases can have the same priority.
417
+
418
+ Args:
419
+ termbase_id: Termbase ID
420
+ project_id: Project ID
421
+ priority: Priority level (1=highest, 2=second, etc.)
422
+
423
+ Returns:
424
+ True if successful
425
+ """
426
+ try:
427
+ cursor = self.db_manager.cursor
428
+
429
+ # Update priority in termbase_activation table
430
+ cursor.execute("""
431
+ UPDATE termbase_activation
432
+ SET priority = ?
433
+ WHERE termbase_id = ? AND project_id = ?
434
+ """, (priority, termbase_id, project_id))
435
+
436
+ if cursor.rowcount == 0:
437
+ self.log(f"⚠️ No activation record found for termbase {termbase_id}, project {project_id}")
438
+ return False
439
+
440
+ self.db_manager.connection.commit()
441
+ self.log(f"✓ Set termbase {termbase_id} priority to #{priority} for project {project_id}")
442
+ return True
443
+ except Exception as e:
444
+ self.log(f"✗ Error setting termbase priority: {e}")
445
+ return False
446
+
447
+ def get_termbase_priority(self, termbase_id: int, project_id: int) -> Optional[int]:
448
+ """Get priority for a termbase in a specific project"""
449
+ try:
450
+ cursor = self.db_manager.cursor
451
+ cursor.execute("""
452
+ SELECT priority FROM termbase_activation
453
+ WHERE termbase_id = ? AND project_id = ? AND is_active = 1
454
+ """, (termbase_id, project_id))
455
+ result = cursor.fetchone()
456
+ return result[0] if result else None
457
+ except Exception as e:
458
+ self.log(f"✗ Error getting termbase priority: {e}")
459
+ return None
460
+
461
+ def set_as_project_termbase(self, termbase_id: int, project_id: int) -> bool:
462
+ """
463
+ Set a termbase as the project termbase for a project.
464
+ Only one project termbase allowed per project - this will unset any existing one.
465
+ """
466
+ try:
467
+ cursor = self.db_manager.cursor
468
+
469
+ # First, unset any existing project termbase for this project
470
+ cursor.execute("""
471
+ UPDATE termbases
472
+ SET is_project_termbase = 0
473
+ WHERE project_id = ? AND is_project_termbase = 1
474
+ """, (project_id,))
475
+
476
+ # Then set the new one
477
+ cursor.execute("""
478
+ UPDATE termbases
479
+ SET is_project_termbase = 1
480
+ WHERE id = ?
481
+ """, (termbase_id,))
482
+
483
+ self.db_manager.connection.commit()
484
+ self.log(f"✓ Set termbase {termbase_id} as project termbase for project {project_id}")
485
+ return True
486
+ except Exception as e:
487
+ self.log(f"✗ Error setting project termbase: {e}")
488
+ return False
489
+
490
+ def get_active_termbase_ids(self, project_id: int) -> List[int]:
491
+ """
492
+ Get list of active termbase IDs for a project (for saving to project file)
493
+
494
+ Returns:
495
+ List of termbase IDs (not database IDs)
496
+ """
497
+ try:
498
+ cursor = self.db_manager.cursor
499
+ cursor.execute("""
500
+ SELECT t.id
501
+ FROM termbases t
502
+ INNER JOIN termbase_activation ta ON t.id = ta.termbase_id
503
+ WHERE ta.project_id = ? AND ta.is_active = 1
504
+ ORDER BY ta.activated_date ASC
505
+ """, (project_id,))
506
+
507
+ active_ids = [row[0] for row in cursor.fetchall()]
508
+ self.log(f"📋 Found {len(active_ids)} active termbases for project {project_id}: {active_ids}")
509
+ return active_ids
510
+ except Exception as e:
511
+ self.log(f"✗ Error getting active termbase IDs: {e}")
512
+ return []
513
+
514
+ def _reassign_rankings_for_project(self, project_id: int):
515
+ """
516
+ Reassign rankings to all activated termbases for a project.
517
+ Rankings are assigned sequentially (1, 2, 3, ...) based on termbase ID order.
518
+ Project termbases don't get rankings (they're always highlighted pink).
519
+ """
520
+ try:
521
+ cursor = self.db_manager.cursor
522
+
523
+ # Get all activated termbases for this project (excluding project termbases)
524
+ # Order by activation timestamp so first activated gets #1, second gets #2, etc.
525
+ cursor.execute("""
526
+ SELECT t.id
527
+ FROM termbases t
528
+ INNER JOIN termbase_activation ta ON t.id = ta.termbase_id
529
+ WHERE ta.project_id = ? AND ta.is_active = 1
530
+ AND (t.is_project_termbase = 0 OR t.is_project_termbase IS NULL)
531
+ ORDER BY ta.activated_date ASC
532
+ """, (project_id,))
533
+
534
+ activated_termbase_ids = [row[0] for row in cursor.fetchall()]
535
+
536
+ # Assign rankings sequentially
537
+ for rank, termbase_id in enumerate(activated_termbase_ids, start=1):
538
+ cursor.execute("""
539
+ UPDATE termbases SET ranking = ? WHERE id = ?
540
+ """, (rank, termbase_id))
541
+ self.log(f" ✓ Assigned ranking #{rank} to termbase ID {termbase_id}")
542
+
543
+ # Clear rankings for non-activated termbases
544
+ if activated_termbase_ids:
545
+ placeholders = ','.join('?' * len(activated_termbase_ids))
546
+ cursor.execute(f"""
547
+ UPDATE termbases SET ranking = NULL
548
+ WHERE id NOT IN ({placeholders})
549
+ """, activated_termbase_ids)
550
+ else:
551
+ cursor.execute("UPDATE termbases SET ranking = NULL")
552
+
553
+ # Commit the changes
554
+ self.db_manager.connection.commit()
555
+ self.log(f"✓ Assigned rankings to {len(activated_termbase_ids)} activated termbase(s) for project {project_id}")
556
+
557
+ except Exception as e:
558
+ self.log(f"✗ Error reassigning rankings: {e}")
559
+
560
+ def unset_project_termbase(self, termbase_id: int) -> bool:
561
+ """Remove project termbase designation from a termbase"""
562
+ try:
563
+ cursor = self.db_manager.cursor
564
+
565
+ cursor.execute("""
566
+ UPDATE termbases
567
+ SET is_project_termbase = 0
568
+ WHERE id = ?
569
+ """, (termbase_id,))
570
+
571
+ self.db_manager.connection.commit()
572
+ self.log(f"✓ Removed project termbase designation from termbase {termbase_id}")
573
+ return True
574
+ except Exception as e:
575
+ self.log(f"✗ Error unsetting project termbase: {e}")
576
+ return False
577
+
578
+ def get_project_termbase(self, project_id: int) -> Optional[Dict]:
579
+ """Get the project termbase for a specific project"""
580
+ try:
581
+ cursor = self.db_manager.cursor
582
+
583
+ cursor.execute("""
584
+ SELECT
585
+ t.id, t.name, t.source_lang, t.target_lang, t.project_id,
586
+ t.description, t.is_global, t.priority, t.is_project_termbase,
587
+ t.created_date, t.modified_date,
588
+ COUNT(gt.id) as term_count
589
+ FROM termbases t
590
+ LEFT JOIN termbase_terms gt ON CAST(t.id AS TEXT) = gt.termbase_id
591
+ WHERE t.project_id = ? AND t.is_project_termbase = 1
592
+ GROUP BY t.id
593
+ """, (project_id,))
594
+
595
+ row = cursor.fetchone()
596
+ if row:
597
+ return {
598
+ 'id': row[0],
599
+ 'name': row[1],
600
+ 'source_lang': row[2],
601
+ 'target_lang': row[3],
602
+ 'project_id': row[4],
603
+ 'description': row[5],
604
+ 'is_global': row[6],
605
+ 'priority': row[7] or 50,
606
+ 'is_project_termbase': bool(row[8]),
607
+ 'created_date': row[9],
608
+ 'modified_date': row[10],
609
+ 'term_count': row[11] or 0
610
+ }
611
+ return None
612
+ except Exception as e:
613
+ self.log(f"✗ Error getting project termbase: {e}")
614
+ return None
615
+
616
+ # ========================================================================
617
+ # TERM MANAGEMENT
618
+ # ========================================================================
619
+
620
+ def add_term(self, termbase_id: int, source_term: str, target_term: str,
621
+ priority: int = 99, domain: str = "", notes: str = "",
622
+ project: str = "", client: str = "",
623
+ forbidden: bool = False, source_lang: Optional[str] = None,
624
+ target_lang: Optional[str] = None, term_uuid: Optional[str] = None) -> Optional[int]:
625
+ """
626
+ Add a term to termbase
627
+
628
+ Args:
629
+ termbase_id: Termbase ID
630
+ source_term: Source language term
631
+ target_term: Target language term
632
+ priority: Priority (1=highest, 99=default)
633
+ domain: Domain/category
634
+ notes: Optional notes/definition
635
+ project: Optional project name
636
+ client: Optional client name
637
+ forbidden: Whether this is a forbidden term
638
+ source_lang: Source language code
639
+ target_lang: Target language code
640
+ term_uuid: Optional UUID for tracking term across imports/exports
641
+
642
+ Returns:
643
+ Term ID or None if failed (returns None if duplicate found)
644
+ """
645
+ try:
646
+ import uuid
647
+ cursor = self.db_manager.cursor
648
+
649
+ # Check for duplicate (case-insensitive check)
650
+ cursor.execute("""
651
+ SELECT id FROM termbase_terms
652
+ WHERE termbase_id = ?
653
+ AND LOWER(source_term) = LOWER(?)
654
+ AND LOWER(target_term) = LOWER(?)
655
+ """, (termbase_id, source_term, target_term))
656
+
657
+ existing = cursor.fetchone()
658
+ if existing:
659
+ self.log(f"⚠️ Duplicate term not added: {source_term} → {target_term} (already exists in termbase {termbase_id})")
660
+ return None
661
+
662
+ # Generate UUID if not provided
663
+ if not term_uuid:
664
+ term_uuid = str(uuid.uuid4())
665
+
666
+ cursor.execute("""
667
+ INSERT INTO termbase_terms
668
+ (termbase_id, source_term, target_term, priority, domain, notes,
669
+ project, client, forbidden, source_lang, target_lang, term_uuid)
670
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
671
+ """, (termbase_id, source_term, target_term, priority, domain, notes,
672
+ project, client, forbidden, source_lang, target_lang, term_uuid))
673
+
674
+ self.db_manager.connection.commit()
675
+ term_id = cursor.lastrowid
676
+ self.log(f"✓ Added term to termbase {termbase_id}: {source_term} → {target_term}")
677
+ return term_id
678
+ except Exception as e:
679
+ self.log(f"✗ Error adding term: {e}")
680
+ return None
681
+
682
+ def get_terms(self, termbase_id: int) -> List[Dict]:
683
+ """Get all terms in a termbase"""
684
+ try:
685
+ cursor = self.db_manager.cursor
686
+
687
+ cursor.execute("""
688
+ SELECT id, source_term, target_term, priority, domain, notes,
689
+ project, client, forbidden, term_uuid
690
+ FROM termbase_terms
691
+ WHERE termbase_id = ?
692
+ ORDER BY priority ASC, source_term ASC
693
+ """, (termbase_id,))
694
+
695
+ terms = []
696
+ for row in cursor.fetchall():
697
+ terms.append({
698
+ 'id': row[0],
699
+ 'source_term': row[1],
700
+ 'target_term': row[2],
701
+ 'priority': row[3],
702
+ 'domain': row[4],
703
+ 'notes': row[5],
704
+ 'project': row[6],
705
+ 'client': row[7],
706
+ 'forbidden': row[8],
707
+ 'term_uuid': row[9]
708
+ })
709
+
710
+ return terms
711
+ except Exception as e:
712
+ self.log(f"✗ Error fetching terms: {e}")
713
+ return []
714
+
715
+ def update_term(self, term_id: int, source_term: Optional[str] = None,
716
+ target_term: Optional[str] = None, priority: Optional[int] = None,
717
+ domain: Optional[str] = None, notes: Optional[str] = None,
718
+ project: Optional[str] = None, client: Optional[str] = None,
719
+ forbidden: Optional[bool] = None) -> bool:
720
+ """Update a term"""
721
+ try:
722
+ cursor = self.db_manager.cursor
723
+ updates = []
724
+ params = []
725
+
726
+ if source_term is not None:
727
+ updates.append("source_term = ?")
728
+ params.append(source_term)
729
+ if target_term is not None:
730
+ updates.append("target_term = ?")
731
+ params.append(target_term)
732
+ if priority is not None:
733
+ updates.append("priority = ?")
734
+ params.append(priority)
735
+ if domain is not None:
736
+ updates.append("domain = ?")
737
+ params.append(domain)
738
+ if notes is not None:
739
+ updates.append("notes = ?")
740
+ params.append(notes)
741
+ if project is not None:
742
+ updates.append("project = ?")
743
+ params.append(project)
744
+ if client is not None:
745
+ updates.append("client = ?")
746
+ params.append(client)
747
+ if forbidden is not None:
748
+ updates.append("forbidden = ?")
749
+ params.append(forbidden)
750
+
751
+ if not updates:
752
+ return False
753
+
754
+ params.append(term_id)
755
+ sql = f"UPDATE termbase_terms SET {', '.join(updates)} WHERE id = ?"
756
+ cursor.execute(sql, params)
757
+ self.db_manager.connection.commit()
758
+
759
+ self.log(f"✓ Updated term {term_id}")
760
+ return True
761
+ except Exception as e:
762
+ self.log(f"✗ Error updating term: {e}")
763
+ return False
764
+
765
+ def delete_term(self, term_id: int) -> bool:
766
+ """Delete a term"""
767
+ try:
768
+ cursor = self.db_manager.cursor
769
+ cursor.execute("DELETE FROM termbase_terms WHERE id = ?", (term_id,))
770
+ self.db_manager.connection.commit()
771
+ self.log(f"✓ Deleted term {term_id}")
772
+ return True
773
+ except Exception as e:
774
+ self.log(f"✗ Error deleting term: {e}")
775
+ return False
776
+
777
+ # ========================================================================
778
+ # SEARCH
779
+ # ========================================================================
780
+
781
+ def search_termbase(self, termbase_id: int, search_term: str,
782
+ search_source: bool = True, search_target: bool = True) -> List[Dict]:
783
+ """
784
+ Search within a termbase (searches main terms AND synonyms)
785
+
786
+ Args:
787
+ termbase_id: Termbase ID to search in
788
+ search_term: Term to search for
789
+ search_source: Search in source terms and source synonyms
790
+ search_target: Search in target terms and target synonyms
791
+
792
+ Returns:
793
+ List of matching terms (includes main term + synonyms as separate entries)
794
+ """
795
+ try:
796
+ cursor = self.db_manager.cursor
797
+
798
+ # Find matching term IDs (from main terms OR synonyms)
799
+ matching_term_ids = set()
800
+
801
+ if search_source:
802
+ # Search main source terms
803
+ cursor.execute("""
804
+ SELECT id FROM termbase_terms
805
+ WHERE termbase_id = ? AND source_term LIKE ?
806
+ """, (termbase_id, f"%{search_term}%"))
807
+ matching_term_ids.update(row[0] for row in cursor.fetchall())
808
+
809
+ # Search source synonyms
810
+ cursor.execute("""
811
+ SELECT term_id FROM termbase_synonyms
812
+ WHERE term_id IN (SELECT id FROM termbase_terms WHERE termbase_id = ?)
813
+ AND language = 'source' AND synonym_text LIKE ?
814
+ """, (termbase_id, f"%{search_term}%"))
815
+ matching_term_ids.update(row[0] for row in cursor.fetchall())
816
+
817
+ if search_target:
818
+ # Search main target terms
819
+ cursor.execute("""
820
+ SELECT id FROM termbase_terms
821
+ WHERE termbase_id = ? AND target_term LIKE ?
822
+ """, (termbase_id, f"%{search_term}%"))
823
+ matching_term_ids.update(row[0] for row in cursor.fetchall())
824
+
825
+ # Search target synonyms
826
+ cursor.execute("""
827
+ SELECT term_id FROM termbase_synonyms
828
+ WHERE term_id IN (SELECT id FROM termbase_terms WHERE termbase_id = ?)
829
+ AND language = 'target' AND synonym_text LIKE ?
830
+ """, (termbase_id, f"%{search_term}%"))
831
+ matching_term_ids.update(row[0] for row in cursor.fetchall())
832
+
833
+ if not matching_term_ids:
834
+ return []
835
+
836
+ # Get full details for matching terms
837
+ placeholders = ','.join('?' * len(matching_term_ids))
838
+ sql = f"""
839
+ SELECT id, source_term, target_term, priority, domain, definition, forbidden
840
+ FROM termbase_terms
841
+ WHERE id IN ({placeholders})
842
+ ORDER BY priority ASC, source_term ASC
843
+ """
844
+
845
+ cursor.execute(sql, list(matching_term_ids))
846
+
847
+ results = []
848
+ for row in cursor.fetchall():
849
+ term_id = row[0]
850
+
851
+ # Add main term
852
+ results.append({
853
+ 'id': term_id,
854
+ 'source_term': row[1],
855
+ 'target_term': row[2],
856
+ 'priority': row[3],
857
+ 'domain': row[4],
858
+ 'definition': row[5],
859
+ 'forbidden': row[6]
860
+ })
861
+
862
+ # Add target synonyms as separate entries (memoQ style)
863
+ # Synonyms are ordered by display_order (position 0 = main/preferred)
864
+ target_synonyms = self.get_synonyms(term_id, language='target')
865
+ for syn in target_synonyms:
866
+ results.append({
867
+ 'id': term_id, # Same term ID
868
+ 'source_term': row[1], # Same source
869
+ 'target_term': syn['synonym_text'], # Synonym as target
870
+ 'priority': row[3],
871
+ 'domain': row[4],
872
+ 'definition': row[5],
873
+ 'forbidden': syn['forbidden'] # Use synonym's forbidden flag
874
+ })
875
+
876
+ return results
877
+ except Exception as e:
878
+ self.log(f"✗ Error searching termbase: {e}")
879
+ return []
880
+
881
+ # ========================================================================
882
+ # SYNONYM MANAGEMENT
883
+ # ========================================================================
884
+
885
+ def add_synonym(self, term_id: int, synonym_text: str, language: str = 'target',
886
+ display_order: int = 0, forbidden: bool = False) -> bool:
887
+ """
888
+ Add a synonym to a term
889
+
890
+ Args:
891
+ term_id: Term ID to add synonym to
892
+ synonym_text: The synonym text
893
+ language: 'source' or 'target' (default: 'target')
894
+ display_order: Position in list (0 = main/top, higher = lower priority)
895
+ forbidden: Whether this synonym is forbidden
896
+
897
+ Returns:
898
+ True if successful, False otherwise
899
+ """
900
+ try:
901
+ cursor = self.db_manager.cursor
902
+ now = datetime.now().isoformat()
903
+
904
+ # Check if synonym already exists
905
+ cursor.execute("""
906
+ SELECT id FROM termbase_synonyms
907
+ WHERE term_id = ? AND synonym_text = ? AND language = ?
908
+ """, (term_id, synonym_text, language))
909
+
910
+ if cursor.fetchone():
911
+ self.log(f"✗ Synonym already exists: {synonym_text}")
912
+ return False
913
+
914
+ cursor.execute("""
915
+ INSERT INTO termbase_synonyms (term_id, synonym_text, language, display_order, forbidden, created_date, modified_date)
916
+ VALUES (?, ?, ?, ?, ?, ?, ?)
917
+ """, (term_id, synonym_text, language, display_order, 1 if forbidden else 0, now, now))
918
+
919
+ self.db_manager.connection.commit()
920
+ self.log(f"✓ Added synonym: {synonym_text}")
921
+ return True
922
+ except Exception as e:
923
+ self.log(f"✗ Error adding synonym: {e}")
924
+ return False
925
+
926
+ def get_synonyms(self, term_id: int, language: Optional[str] = None) -> List[Dict]:
927
+ """
928
+ Get synonyms for a term, ordered by display_order (position)
929
+
930
+ Args:
931
+ term_id: Term ID to get synonyms for
932
+ language: Optional filter - 'source', 'target', or None for both
933
+
934
+ Returns:
935
+ List of synonym dictionaries with fields: id, synonym_text, language, display_order, forbidden
936
+ """
937
+ try:
938
+ cursor = self.db_manager.cursor
939
+
940
+ if language:
941
+ cursor.execute("""
942
+ SELECT id, synonym_text, language, display_order, forbidden, created_date, modified_date
943
+ FROM termbase_synonyms
944
+ WHERE term_id = ? AND language = ?
945
+ ORDER BY display_order ASC, created_date ASC
946
+ """, (term_id, language))
947
+ else:
948
+ cursor.execute("""
949
+ SELECT id, synonym_text, language, display_order, forbidden, created_date, modified_date
950
+ FROM termbase_synonyms
951
+ WHERE term_id = ?
952
+ ORDER BY language DESC, display_order ASC, created_date ASC
953
+ """, (term_id,))
954
+
955
+ results = []
956
+ for row in cursor.fetchall():
957
+ results.append({
958
+ 'id': row[0],
959
+ 'synonym_text': row[1],
960
+ 'language': row[2],
961
+ 'display_order': row[3],
962
+ 'forbidden': bool(row[4]),
963
+ 'created_date': row[5],
964
+ 'modified_date': row[6]
965
+ })
966
+
967
+ return results
968
+ except Exception as e:
969
+ self.log(f"✗ Error getting synonyms: {e}")
970
+ return []
971
+
972
+ def update_synonym_order(self, synonym_id: int, new_order: int) -> bool:
973
+ """
974
+ Update the display order of a synonym
975
+
976
+ Args:
977
+ synonym_id: Synonym ID to update
978
+ new_order: New display order (0 = top/main)
979
+
980
+ Returns:
981
+ True if successful, False otherwise
982
+ """
983
+ try:
984
+ cursor = self.db_manager.cursor
985
+ now = datetime.now().isoformat()
986
+ cursor.execute("""
987
+ UPDATE termbase_synonyms
988
+ SET display_order = ?, modified_date = ?
989
+ WHERE id = ?
990
+ """, (new_order, now, synonym_id))
991
+ self.db_manager.connection.commit()
992
+ return True
993
+ except Exception as e:
994
+ self.log(f"✗ Error updating synonym order: {e}")
995
+ return False
996
+
997
+ def update_synonym_forbidden(self, synonym_id: int, forbidden: bool) -> bool:
998
+ """
999
+ Update the forbidden flag of a synonym
1000
+
1001
+ Args:
1002
+ synonym_id: Synonym ID to update
1003
+ forbidden: New forbidden status
1004
+
1005
+ Returns:
1006
+ True if successful, False otherwise
1007
+ """
1008
+ try:
1009
+ cursor = self.db_manager.cursor
1010
+ now = datetime.now().isoformat()
1011
+ cursor.execute("""
1012
+ UPDATE termbase_synonyms
1013
+ SET forbidden = ?, modified_date = ?
1014
+ WHERE id = ?
1015
+ """, (1 if forbidden else 0, now, synonym_id))
1016
+ self.db_manager.connection.commit()
1017
+ return True
1018
+ except Exception as e:
1019
+ self.log(f"✗ Error updating synonym forbidden status: {e}")
1020
+ return False
1021
+
1022
+ def reorder_synonyms(self, term_id: int, language: str, synonym_ids_in_order: List[int]) -> bool:
1023
+ """
1024
+ Reorder synonyms for a term
1025
+
1026
+ Args:
1027
+ term_id: Term ID
1028
+ language: 'source' or 'target'
1029
+ synonym_ids_in_order: List of synonym IDs in desired order
1030
+
1031
+ Returns:
1032
+ True if successful, False otherwise
1033
+ """
1034
+ try:
1035
+ for order, syn_id in enumerate(synonym_ids_in_order):
1036
+ self.update_synonym_order(syn_id, order)
1037
+ return True
1038
+ except Exception as e:
1039
+ self.log(f"✗ Error reordering synonyms: {e}")
1040
+ return False
1041
+
1042
+ def delete_synonym(self, synonym_id: int) -> bool:
1043
+ """
1044
+ Delete a synonym
1045
+
1046
+ Args:
1047
+ synonym_id: Synonym ID to delete
1048
+
1049
+ Returns:
1050
+ True if successful, False otherwise
1051
+ """
1052
+ try:
1053
+ cursor = self.db_manager.cursor
1054
+ cursor.execute("DELETE FROM termbase_synonyms WHERE id = ?", (synonym_id,))
1055
+ self.db_manager.connection.commit()
1056
+ self.log(f"✓ Deleted synonym {synonym_id}")
1057
+ return True
1058
+ except Exception as e:
1059
+ self.log(f"✗ Error deleting synonym: {e}")
1060
+ return False