supervertaler 1.9.153__py3-none-any.whl → 1.9.185__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.

@@ -172,7 +172,7 @@ class TermBlock(QWidget):
172
172
  # Get theme colors
173
173
  is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
174
174
  separator_color = "#555555" if is_dark else "#CCCCCC"
175
- source_text_color = "#E0E0E0" if is_dark else "#333"
175
+ source_text_color = "#FFFFFF" if is_dark else "#333"
176
176
  no_match_color = "#666666" if is_dark else "#ddd"
177
177
  no_match_bg = "#2A2A2A" if is_dark else "#F5F5F5"
178
178
 
@@ -224,10 +224,17 @@ class TermBlock(QWidget):
224
224
  if self.translations:
225
225
  target_text = primary_translation.get('target_term', primary_translation.get('target', ''))
226
226
  termbase_name = primary_translation.get('termbase_name', '')
227
-
228
- # Background color based on termbase type
229
- bg_color = "#FFE5F0" if self.is_effective_project else "#D6EBFF" # Pink for project, light blue for regular
230
- hover_color = "#FFD0E8" if self.is_effective_project else "#BBDEFB" # Slightly darker on hover
227
+
228
+ # Background color based on termbase type (theme-aware)
229
+ is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
230
+ if is_dark:
231
+ # Dark mode: darker backgrounds
232
+ bg_color = "#4A2D3A" if self.is_effective_project else "#2D3E4A" # Dark pink/blue
233
+ hover_color = "#5A3D4A" if self.is_effective_project else "#3D4E5A" # Lighter on hover
234
+ else:
235
+ # Light mode: original colors
236
+ bg_color = "#FFE5F0" if self.is_effective_project else "#D6EBFF" # Pink for project, light blue for regular
237
+ hover_color = "#FFD0E8" if self.is_effective_project else "#BBDEFB" # Slightly darker on hover
231
238
 
232
239
  # Create horizontal layout for target + shortcut badge
233
240
  # Apply background to container so it covers both text and badge
@@ -251,9 +258,11 @@ class TermBlock(QWidget):
251
258
  target_font.setBold(self.font_bold)
252
259
  target_label.setFont(target_font)
253
260
  target_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
261
+ # Theme-aware text color
262
+ target_text_color = "#B0C4DE" if is_dark else "#0052A3" # Light blue in dark mode
254
263
  target_label.setStyleSheet(f"""
255
264
  QLabel {{
256
- color: #0052A3;
265
+ color: {target_text_color};
257
266
  padding: 0px;
258
267
  background-color: transparent;
259
268
  border: none;
@@ -312,11 +321,12 @@ class TermBlock(QWidget):
312
321
  if len(self.translations) > 1:
313
322
  count_label = QLabel(f"+{len(self.translations) - 1}")
314
323
  count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
315
- count_label.setStyleSheet("""
316
- QLabel {
317
- color: #999;
324
+ count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
325
+ count_label.setStyleSheet(f"""
326
+ QLabel {{
327
+ color: {count_color};
318
328
  font-size: 7px;
319
- }
329
+ }}
320
330
  """)
321
331
  layout.addWidget(count_label)
322
332
  return
@@ -336,10 +346,13 @@ class TermBlock(QWidget):
336
346
  badge_label = QLabel(badge_text)
337
347
  badge_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
338
348
  badge_label.setFixedSize(badge_width, 14)
349
+ # Theme-aware badge colors
350
+ badge_bg = "#4A90E2" if is_dark else "#1976D2" # Lighter blue in dark mode
351
+ badge_text_color = "#FFFFFF" if is_dark else "white"
339
352
  badge_label.setStyleSheet(f"""
340
353
  QLabel {{
341
- background-color: #1976D2;
342
- color: white;
354
+ background-color: {badge_bg};
355
+ color: {badge_text_color};
343
356
  font-size: 9px;
344
357
  font-weight: bold;
345
358
  border-radius: 7px;
@@ -352,16 +365,17 @@ class TermBlock(QWidget):
352
365
  target_layout.addWidget(badge_label)
353
366
 
354
367
  layout.addWidget(target_container)
355
-
368
+
356
369
  # Show count if multiple translations - very compact
357
370
  if len(self.translations) > 1:
358
371
  count_label = QLabel(f"+{len(self.translations) - 1}")
359
372
  count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
360
- count_label.setStyleSheet("""
361
- QLabel {
362
- color: #999;
373
+ count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
374
+ count_label.setStyleSheet(f"""
375
+ QLabel {{
376
+ color: {count_color};
363
377
  font-size: 7px;
364
- }
378
+ }}
365
379
  """)
366
380
  layout.addWidget(count_label)
367
381
  else:
@@ -439,7 +453,7 @@ class NTBlock(QWidget):
439
453
 
440
454
  # Get theme colors
441
455
  is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
442
- source_text_color = "#E0E0E0" if is_dark else "#5D4E37"
456
+ source_text_color = "#FFFFFF" if is_dark else "#5D4E37"
443
457
 
444
458
  # Pastel yellow border for non-translatables
445
459
  border_color = "#E6C200" # Darker yellow for border
@@ -515,6 +529,9 @@ class TermviewWidget(QWidget):
515
529
  self.current_target_lang = None
516
530
  self.current_project_id = None # Store project ID for termbase priority lookup
517
531
 
532
+ # Debug mode - disable verbose tokenization logging by default (performance)
533
+ self.debug_tokenize = False
534
+
518
535
  # Default font settings (will be updated from main app settings)
519
536
  self.current_font_family = "Segoe UI"
520
537
  self.current_font_size = 10
@@ -634,6 +651,17 @@ class TermviewWidget(QWidget):
634
651
  is_dark = theme.name == "Dark"
635
652
  info_label_color = "#909090" if is_dark else info_text
636
653
  self.info_label.setStyleSheet(f"color: {info_label_color}; font-size: 10px; padding: 5px;")
654
+
655
+ # Refresh term blocks to pick up new theme colors
656
+ if hasattr(self, '_last_termbase_matches') and hasattr(self, '_last_nt_matches') and hasattr(self, 'current_source'):
657
+ # Re-render with stored matches to apply new theme colors
658
+ if self.current_source:
659
+ self.update_with_matches(
660
+ self.current_source,
661
+ self._last_termbase_matches or [],
662
+ self._last_nt_matches,
663
+ self._status_hint if hasattr(self, '_status_hint') else None
664
+ )
637
665
 
638
666
  def set_font_settings(self, font_family: str = "Segoe UI", font_size: int = 10, bold: bool = False):
639
667
  """Update font settings for Termview
@@ -691,27 +719,31 @@ class TermviewWidget(QWidget):
691
719
  font.setBold(self.current_font_bold)
692
720
  block.source_label.setFont(font)
693
721
 
694
- def update_with_matches(self, source_text: str, termbase_matches: List[Dict], nt_matches: List[Dict] = None):
722
+ def update_with_matches(self, source_text: str, termbase_matches: List[Dict], nt_matches: List[Dict] = None, status_hint: str = None):
695
723
  """
696
724
  Update the termview display with pre-computed termbase and NT matches
697
-
725
+
698
726
  RYS-STYLE DISPLAY: Show source text as tokens with translations underneath
699
-
727
+
700
728
  Args:
701
729
  source_text: Source segment text
702
730
  termbase_matches: List of termbase match dicts from Translation Results
703
731
  nt_matches: Optional list of NT match dicts with 'text', 'start', 'end', 'list_name' keys
732
+ status_hint: Optional hint about why there might be no matches (e.g., 'no_termbases_activated', 'wrong_language')
704
733
  """
705
734
  self.current_source = source_text
706
-
735
+ # Store matches for theme refresh
736
+ self._last_termbase_matches = termbase_matches
737
+ self._last_nt_matches = nt_matches
738
+
707
739
  # Clear existing blocks and shortcut mappings
708
740
  self.clear_terms()
709
741
  self.shortcut_terms = {} # Reset shortcut mappings
710
-
742
+
711
743
  if not source_text or not source_text.strip():
712
744
  self.info_label.setText("No segment selected")
713
745
  return
714
-
746
+
715
747
  # Strip HTML/XML tags from source text for display in TermView
716
748
  # This handles CAT tool tags like <b>, </b>, <i>, </i>, <u>, </u>, <bi>, <sub>, <sup>, <li-o>, <li-b>
717
749
  # as well as memoQ tags {1}, [2}, {3], Trados tags <1>, </1>, and Déjà Vu tags {00001}
@@ -722,17 +754,17 @@ class TermviewWidget(QWidget):
722
754
  display_text = re.sub(r'\[[^\[\]]*\}', '', display_text) # Opening: [anything}
723
755
  display_text = re.sub(r'\{[^\{\}]*\]', '', display_text) # Closing: {anything]
724
756
  display_text = display_text.strip()
725
-
757
+
726
758
  # If stripping tags leaves nothing, fall back to original
727
759
  if not display_text:
728
760
  display_text = source_text
729
-
761
+
730
762
  has_termbase = termbase_matches and len(termbase_matches) > 0
731
763
  has_nt = nt_matches and len(nt_matches) > 0
732
-
733
- if not has_termbase and not has_nt:
734
- self.info_label.setText("No terminology or NT matches for this segment")
735
- return
764
+
765
+ # Store status hint for info label (will be set at the end)
766
+ self._status_hint = status_hint
767
+ self._has_any_matches = has_termbase or has_nt
736
768
 
737
769
  # Convert termbase matches to dict for easy lookup: {source_term.lower(): [translations]}
738
770
  matches_dict = {}
@@ -820,7 +852,6 @@ class TermviewWidget(QWidget):
820
852
 
821
853
  # Check if this is a non-translatable
822
854
  if lookup_key in nt_dict:
823
- # Create NT block
824
855
  nt_block = NTBlock(token, nt_dict[lookup_key], self, theme_manager=self.theme_manager,
825
856
  font_size=self.current_font_size, font_family=self.current_font_family,
826
857
  font_bold=self.current_font_bold)
@@ -865,11 +896,18 @@ class TermviewWidget(QWidget):
865
896
  info_parts.append(f"{blocks_with_translations} terms")
866
897
  if blocks_with_nt > 0:
867
898
  info_parts.append(f"{blocks_with_nt} NTs")
868
-
899
+
869
900
  if info_parts:
870
901
  self.info_label.setText(f"✓ Found {', '.join(info_parts)} in {len(tokens)} words")
871
902
  else:
872
- self.info_label.setText(f"No matches in {len(tokens)} words")
903
+ # Show appropriate message based on status hint when no matches
904
+ status_hint = getattr(self, '_status_hint', None)
905
+ if status_hint == 'no_termbases_activated':
906
+ self.info_label.setText(f"No glossaries activated ({len(tokens)} words)")
907
+ elif status_hint == 'wrong_language':
908
+ self.info_label.setText(f"Glossaries don't match language pair ({len(tokens)} words)")
909
+ else:
910
+ self.info_label.setText(f"No matches in {len(tokens)} words")
873
911
 
874
912
  def get_all_termbase_matches(self, text: str) -> Dict[str, List[Dict]]:
875
913
  """
@@ -996,9 +1034,9 @@ class TermviewWidget(QWidget):
996
1034
  Returns:
997
1035
  List of tokens (words/phrases/numbers), with multi-word terms kept together
998
1036
  """
999
- # DEBUG: Log multi-word terms we're looking for
1037
+ # DEBUG: Log multi-word terms we're looking for (only if debug_tokenize enabled)
1000
1038
  multi_word_terms = [k for k in matches.keys() if ' ' in k]
1001
- if multi_word_terms:
1039
+ if multi_word_terms and self.debug_tokenize:
1002
1040
  self.log(f"🔍 Tokenize: Looking for {len(multi_word_terms)} multi-word terms:")
1003
1041
  for term in sorted(multi_word_terms, key=len, reverse=True)[:3]:
1004
1042
  self.log(f" - '{term}'")
@@ -1023,11 +1061,12 @@ class TermviewWidget(QWidget):
1023
1061
  else:
1024
1062
  pattern = r'\b' + term_escaped + r'\b'
1025
1063
 
1026
- # DEBUG: Check if multi-word term is found
1064
+ # DEBUG: Check if multi-word term is found (only if debug_tokenize enabled)
1027
1065
  found = re.search(pattern, text_lower)
1028
- self.log(f"🔍 Tokenize: Pattern '{pattern}' for '{term}' → {'FOUND' if found else 'NOT FOUND'}")
1029
- if found:
1030
- self.log(f" Match at position {found.span()}: '{text[found.start():found.end()]}'")
1066
+ if self.debug_tokenize:
1067
+ self.log(f"🔍 Tokenize: Pattern '{pattern}' for '{term}' → {'FOUND' if found else 'NOT FOUND'}")
1068
+ if found:
1069
+ self.log(f" Match at position {found.span()}: '{text[found.start():found.end()]}'")
1031
1070
 
1032
1071
  # Find all matches using regex
1033
1072
  for match in re.finditer(pattern, text_lower):
@@ -1040,10 +1079,11 @@ class TermviewWidget(QWidget):
1040
1079
  original_term = text[pos:pos + len(term)]
1041
1080
  tokens_with_positions.append((pos, len(term), original_term))
1042
1081
  used_positions.update(term_positions)
1043
- self.log(f" ✅ Added multi-word token: '{original_term}' covering positions {pos}-{pos+len(term)}")
1082
+ if self.debug_tokenize:
1083
+ self.log(f" ✅ Added multi-word token: '{original_term}' covering positions {pos}-{pos+len(term)}")
1044
1084
 
1045
- # DEBUG: Log used_positions after first pass
1046
- if ' ' in sorted(matches.keys(), key=len, reverse=True)[0]:
1085
+ # DEBUG: Log used_positions after first pass (only if debug_tokenize enabled)
1086
+ if matches and ' ' in sorted(matches.keys(), key=len, reverse=True)[0] and self.debug_tokenize:
1047
1087
  self.log(f"🔍 After first pass: {len(used_positions)} positions marked as used")
1048
1088
  self.log(f" Used positions: {sorted(list(used_positions))[:20]}...")
1049
1089
 
modules/theme_manager.py CHANGED
@@ -212,7 +212,10 @@ class ThemeManager:
212
212
  self.themes_file = user_data_path / "themes.json"
213
213
  self.current_theme: Theme = self.PREDEFINED_THEMES["Light (Default)"]
214
214
  self.custom_themes: Dict[str, Theme] = {}
215
-
215
+
216
+ # Global UI font scale (50-200%, default 100%)
217
+ self.font_scale: int = 100
218
+
216
219
  # Load custom themes
217
220
  self.load_custom_themes()
218
221
 
@@ -289,14 +292,48 @@ class ThemeManager:
289
292
  def apply_theme(self, app: QApplication):
290
293
  """
291
294
  Apply current theme to application
292
-
295
+
293
296
  Args:
294
297
  app: QApplication instance
295
298
  """
296
299
  theme = self.current_theme
297
-
300
+
301
+ # Calculate scaled font sizes based on font_scale (default 100%)
302
+ base_font_size = int(10 * self.font_scale / 100) # Base: 10pt at 100%
303
+ small_font_size = max(7, int(9 * self.font_scale / 100)) # Small text (status bar)
304
+
305
+ # Font scaling rules (only applied if scale != 100%)
306
+ font_rules = ""
307
+ if self.font_scale != 100:
308
+ font_rules = f"""
309
+ /* Global font scaling ({self.font_scale}%) */
310
+ QWidget {{ font-size: {base_font_size}pt; }}
311
+ QMenuBar {{ font-size: {base_font_size}pt; }}
312
+ QMenuBar::item {{ font-size: {base_font_size}pt; }}
313
+ QMenu {{ font-size: {base_font_size}pt; }}
314
+ QMenu::item {{ font-size: {base_font_size}pt; }}
315
+ QStatusBar {{ font-size: {small_font_size}pt; }}
316
+ QTabBar::tab {{ font-size: {base_font_size}pt; }}
317
+ QToolBar {{ font-size: {base_font_size}pt; }}
318
+ QLabel {{ font-size: {base_font_size}pt; }}
319
+ QCheckBox {{ font-size: {base_font_size}pt; }}
320
+ QRadioButton {{ font-size: {base_font_size}pt; }}
321
+ QComboBox {{ font-size: {base_font_size}pt; }}
322
+ QSpinBox {{ font-size: {base_font_size}pt; }}
323
+ QDoubleSpinBox {{ font-size: {base_font_size}pt; }}
324
+ QLineEdit {{ font-size: {base_font_size}pt; }}
325
+ QPushButton {{ font-size: {base_font_size}pt; }}
326
+ QGroupBox {{ font-size: {base_font_size}pt; }}
327
+ QGroupBox::title {{ font-size: {base_font_size}pt; }}
328
+ QTextEdit {{ font-size: {base_font_size}pt; }}
329
+ QPlainTextEdit {{ font-size: {base_font_size}pt; }}
330
+ QListWidget {{ font-size: {base_font_size}pt; }}
331
+ QTreeWidget {{ font-size: {base_font_size}pt; }}
332
+ QHeaderView::section {{ font-size: {base_font_size}pt; }}
333
+ """
334
+
298
335
  # Create and apply stylesheet - COLORS ONLY, preserves native sizes/spacing
299
- stylesheet = f"""
336
+ stylesheet = font_rules + f"""
300
337
  /* Main window background */
301
338
  QMainWindow, QWidget {{
302
339
  background-color: {theme.window_bg};
@@ -344,19 +344,22 @@ class TMMetadataManager:
344
344
  """Check if a TM is active for a project (or global when project_id=0)"""
345
345
  if project_id is None:
346
346
  return False # If None (not 0), default to inactive
347
-
347
+
348
348
  try:
349
349
  cursor = self.db_manager.cursor
350
-
350
+
351
+ # Check if TM is active for this project OR globally (project_id=0)
351
352
  cursor.execute("""
352
- SELECT is_active FROM tm_activation
353
- WHERE tm_id = ? AND project_id = ?
353
+ SELECT is_active FROM tm_activation
354
+ WHERE tm_id = ? AND (project_id = ? OR project_id = 0)
355
+ ORDER BY project_id DESC
354
356
  """, (tm_db_id, project_id))
355
-
356
- row = cursor.fetchone()
357
- if row:
358
- return bool(row[0])
359
-
357
+
358
+ # Return True if any activation is active (project-specific takes priority due to ORDER BY)
359
+ for row in cursor.fetchall():
360
+ if bool(row[0]):
361
+ return True
362
+
360
363
  # If no activation record exists, TM is inactive by default
361
364
  return False
362
365
  except Exception as e:
@@ -382,20 +385,63 @@ class TMMetadataManager:
382
385
 
383
386
  try:
384
387
  cursor = self.db_manager.cursor
385
-
388
+
386
389
  # Only return TMs that have been explicitly activated (is_active = 1)
390
+ # Include both project-specific activations AND global activations (project_id=0)
387
391
  cursor.execute("""
388
- SELECT tm.tm_id
392
+ SELECT DISTINCT tm.tm_id
389
393
  FROM translation_memories tm
390
394
  INNER JOIN tm_activation ta ON tm.id = ta.tm_id
391
- WHERE ta.project_id = ? AND ta.is_active = 1
395
+ WHERE (ta.project_id = ? OR ta.project_id = 0) AND ta.is_active = 1
392
396
  """, (project_id,))
393
-
397
+
394
398
  return [row[0] for row in cursor.fetchall()]
395
399
  except Exception as e:
396
400
  self.log(f"✗ Error fetching active tm_ids: {e}")
397
401
  return []
398
402
 
403
+ def get_writable_tm_ids(self, project_id: Optional[int]) -> List[str]:
404
+ """
405
+ Get list of writable tm_id strings for a project.
406
+
407
+ Returns TMs where:
408
+ - The TM has an activation record for this project AND
409
+ - read_only = 0 (Write checkbox is enabled)
410
+
411
+ This is used for SAVING segments to TM, separate from get_active_tm_ids()
412
+ which is used for READING/matching from TM.
413
+
414
+ Returns:
415
+ List of tm_id strings that are writable for the project
416
+ """
417
+ if project_id is None:
418
+ # No project - return all writable TMs
419
+ try:
420
+ cursor = self.db_manager.cursor
421
+ cursor.execute("SELECT tm_id FROM translation_memories WHERE read_only = 0")
422
+ return [row[0] for row in cursor.fetchall()]
423
+ except Exception as e:
424
+ self.log(f"✗ Error fetching all writable tm_ids: {e}")
425
+ return []
426
+
427
+ try:
428
+ cursor = self.db_manager.cursor
429
+
430
+ # Return TMs where Write checkbox is enabled (read_only = 0)
431
+ # AND the TM has an activation record for this project OR for global (project_id=0)
432
+ # This ensures TMs created when no project was loaded still work
433
+ cursor.execute("""
434
+ SELECT DISTINCT tm.tm_id
435
+ FROM translation_memories tm
436
+ INNER JOIN tm_activation ta ON tm.id = ta.tm_id
437
+ WHERE (ta.project_id = ? OR ta.project_id = 0) AND tm.read_only = 0
438
+ """, (project_id,))
439
+
440
+ return [row[0] for row in cursor.fetchall()]
441
+ except Exception as e:
442
+ self.log(f"✗ Error fetching writable tm_ids: {e}")
443
+ return []
444
+
399
445
  # ========================================================================
400
446
  # PROJECT TM MANAGEMENT (similar to termbases)
401
447
  # ========================================================================
@@ -123,7 +123,7 @@ class TMDatabase:
123
123
  if source_lang and target_lang:
124
124
  self.set_tm_languages(source_lang, target_lang)
125
125
 
126
- # Global fuzzy threshold
126
+ # Global fuzzy threshold (75% minimum similarity for fuzzy matches)
127
127
  self.fuzzy_threshold = 0.75
128
128
 
129
129
  # TM metadata cache (populated from database as needed)
@@ -205,20 +205,14 @@ class TMDatabase:
205
205
  Returns:
206
206
  List of match dictionaries sorted by similarity
207
207
  """
208
- print(f"[DEBUG] TMDatabase.search_all: source='{source[:50]}...', tm_ids={tm_ids}")
209
-
210
208
  # Determine which TMs to search
211
209
  # If tm_ids is None or empty, search ALL TMs (don't filter by tm_id)
212
210
  if tm_ids is None and enabled_only:
213
211
  tm_ids = [tm_id for tm_id, meta in self.tm_metadata.items() if meta.get('enabled', True)]
214
- print(f"[DEBUG] TMDatabase.search_all: No tm_ids provided, using from metadata: {tm_ids}")
215
-
212
+
216
213
  # If tm_ids is still empty, set to None to search ALL TMs
217
214
  if tm_ids is not None and len(tm_ids) == 0:
218
215
  tm_ids = None
219
- print(f"[DEBUG] TMDatabase.search_all: Empty tm_ids, setting to None to search ALL")
220
-
221
- print(f"[DEBUG] TMDatabase.search_all: Final tm_ids to search: {tm_ids}")
222
216
 
223
217
  # First try exact match
224
218
  exact_match = self.db.get_exact_match(
@@ -227,8 +221,7 @@ class TMDatabase:
227
221
  source_lang=self.source_lang,
228
222
  target_lang=self.target_lang
229
223
  )
230
- print(f"[DEBUG] TMDatabase.search_all: Exact match result: {exact_match}")
231
-
224
+
232
225
  if exact_match:
233
226
  # Format as match dictionary
234
227
  return [{
@@ -241,7 +234,6 @@ class TMDatabase:
241
234
  }]
242
235
 
243
236
  # Try fuzzy matches
244
- print(f"[DEBUG] TMDatabase.search_all: Calling fuzzy search with source_lang={self.source_lang}, target_lang={self.target_lang}")
245
237
  fuzzy_matches = self.db.search_fuzzy_matches(
246
238
  source=source,
247
239
  tm_ids=tm_ids,
@@ -250,8 +242,7 @@ class TMDatabase:
250
242
  source_lang=self.source_lang,
251
243
  target_lang=self.target_lang
252
244
  )
253
- print(f"[DEBUG] TMDatabase.search_all: Fuzzy search returned {len(fuzzy_matches)} matches")
254
-
245
+
255
246
  # Format matches for UI
256
247
  formatted_matches = []
257
248
  for match in fuzzy_matches:
@@ -1676,13 +1676,6 @@ class TranslationResultsPanel(QWidget):
1676
1676
  Args:
1677
1677
  matches_dict: Dict with keys like "NT", "MT", "TM", "Termbases"
1678
1678
  """
1679
- print(f"🎯 TranslationResultsPanel.set_matches() called with matches_dict keys: {list(matches_dict.keys())}")
1680
- for match_type, matches in matches_dict.items():
1681
- print(f" {match_type}: {len(matches)} matches")
1682
- if match_type == "Termbases" and matches:
1683
- for i, match in enumerate(matches[:2]): # Show first 2 for debugging
1684
- print(f" [{i}] {match.source} → {match.target}")
1685
-
1686
1679
  # Ensure CompactMatchItem has current theme_manager
1687
1680
  if self.theme_manager:
1688
1681
  CompactMatchItem.theme_manager = self.theme_manager
@@ -367,7 +367,7 @@ class UnifiedPromptLibrary:
367
367
 
368
368
  self.active_primary_prompt = self.prompts[relative_path]['content']
369
369
  self.active_primary_prompt_path = relative_path
370
- self.log(f"✓ Set primary prompt: {self.prompts[relative_path].get('name', relative_path)}")
370
+ self.log(f"✓ Set custom prompt: {self.prompts[relative_path].get('name', relative_path)}")
371
371
  return True
372
372
 
373
373
  def set_external_primary_prompt(self, file_path: str) -> Tuple[bool, str]:
@@ -399,7 +399,7 @@ class UnifiedPromptLibrary:
399
399
  self.active_primary_prompt = content
400
400
  self.active_primary_prompt_path = f"[EXTERNAL] {file_path}"
401
401
 
402
- self.log(f"✓ Set external primary prompt: {display_name}")
402
+ self.log(f"✓ Set external custom prompt: {display_name}")
403
403
  return True, display_name
404
404
 
405
405
  def attach_prompt(self, relative_path: str) -> bool:
@@ -1566,9 +1566,9 @@ class UnifiedPromptManagerQt:
1566
1566
 
1567
1567
  layout.addWidget(mode_frame)
1568
1568
 
1569
- # Primary Prompt
1569
+ # Custom Prompt
1570
1570
  primary_layout = QHBoxLayout()
1571
- primary_label = QLabel("Primary Prompt ⭐:")
1571
+ primary_label = QLabel("Custom Prompt ⭐:")
1572
1572
  primary_label.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold))
1573
1573
  primary_layout.addWidget(primary_label)
1574
1574
 
@@ -2210,8 +2210,8 @@ class UnifiedPromptManagerQt:
2210
2210
  if data['type'] == 'prompt':
2211
2211
  path = data['path']
2212
2212
 
2213
- # Set as primary
2214
- action_primary = menu.addAction("⭐ Set as Primary Prompt")
2213
+ # Set as custom prompt
2214
+ action_primary = menu.addAction("⭐ Set as Custom Prompt")
2215
2215
  action_primary.triggered.connect(lambda: self._set_primary_prompt(path))
2216
2216
 
2217
2217
  # Attach/detach
@@ -2372,8 +2372,20 @@ class UnifiedPromptManagerQt:
2372
2372
  else:
2373
2373
  # Name unchanged, just update in place
2374
2374
  if self.library.save_prompt(path, prompt_data):
2375
+ # Refresh active prompts if this prompt is currently active or attached
2376
+ # This ensures "Preview Combined" shows the updated content immediately
2377
+ if self.library.active_primary_prompt_path == path:
2378
+ # Update cached primary prompt content
2379
+ self.library.active_primary_prompt = self.library.prompts[path]['content']
2380
+
2381
+ if path in self.library.attached_prompt_paths:
2382
+ # Update cached attached prompt content
2383
+ idx = self.library.attached_prompt_paths.index(path)
2384
+ self.library.attached_prompts[idx] = self.library.prompts[path]['content']
2385
+
2375
2386
  QMessageBox.information(self.main_widget, "Saved", "Prompt updated successfully!")
2376
2387
  self._refresh_tree()
2388
+ self._update_attached_list() # Refresh attached list to show updated names
2377
2389
  else:
2378
2390
  QMessageBox.warning(self.main_widget, "Error", "Failed to save prompt")
2379
2391
  else:
@@ -2493,7 +2505,7 @@ class UnifiedPromptManagerQt:
2493
2505
  self.library.active_primary_prompt_path = None
2494
2506
  self.primary_prompt_label.setText("[None selected]")
2495
2507
  self.primary_prompt_label.setStyleSheet("color: #999;")
2496
- self.log_message("✓ Cleared primary prompt")
2508
+ self.log_message("✓ Cleared custom prompt")
2497
2509
 
2498
2510
  def _load_external_primary_prompt(self):
2499
2511
  """Load an external prompt file (not in library) as primary"""
@@ -2787,7 +2799,7 @@ class UnifiedPromptManagerQt:
2787
2799
  composition_parts.append(f"📏 Total prompt length: {len(combined):,} characters")
2788
2800
 
2789
2801
  if self.library.active_primary_prompt:
2790
- composition_parts.append(f"✓ Primary prompt attached")
2802
+ composition_parts.append(f"✓ Custom prompt attached")
2791
2803
 
2792
2804
  if self.library.attached_prompts:
2793
2805
  composition_parts.append(f"✓ {len(self.library.attached_prompts)} additional prompt(s) attached")
@@ -2993,49 +3005,66 @@ If the text refers to figures (e.g., 'Figure 1A'), relevant images may be provid
2993
3005
 
2994
3006
  # === Prompt Composition (for translation) ===
2995
3007
 
2996
- def build_final_prompt(self, source_text: str, source_lang: str, target_lang: str, mode: str = None) -> str:
3008
+ def build_final_prompt(self, source_text: str, source_lang: str, target_lang: str,
3009
+ mode: str = None, glossary_terms: list = None) -> str:
2997
3010
  """
2998
3011
  Build final prompt for translation using 2-layer architecture:
2999
3012
  1. System Prompt (auto-selected by mode)
3000
3013
  2. Combined prompts from library (primary + attached)
3001
-
3014
+ 3. Glossary terms (optional, injected before translation delimiter)
3015
+
3002
3016
  Args:
3003
3017
  source_text: Text to translate
3004
3018
  source_lang: Source language
3005
3019
  target_lang: Target language
3006
3020
  mode: Override mode (if None, uses self.current_mode)
3007
-
3021
+ glossary_terms: Optional list of term dicts with 'source_term' and 'target_term' keys
3022
+
3008
3023
  Returns:
3009
3024
  Complete prompt ready for LLM
3010
3025
  """
3011
3026
  if mode is None:
3012
3027
  mode = self.current_mode
3013
-
3028
+
3014
3029
  # Layer 1: System Prompt
3015
3030
  system_template = self.get_system_template(mode)
3016
-
3031
+
3017
3032
  # Replace placeholders in system prompt
3018
3033
  system_template = system_template.replace("{{SOURCE_LANGUAGE}}", source_lang)
3019
3034
  system_template = system_template.replace("{{TARGET_LANGUAGE}}", target_lang)
3020
3035
  system_template = system_template.replace("{{SOURCE_TEXT}}", source_text)
3021
-
3036
+
3022
3037
  # Layer 2: Library prompts (primary + attached)
3023
3038
  library_prompts = ""
3024
-
3039
+
3025
3040
  if self.library.active_primary_prompt:
3026
- library_prompts += "\n\n# PRIMARY INSTRUCTIONS\n\n"
3041
+ library_prompts += "\n\n# CUSTOM PROMPT\n\n"
3027
3042
  library_prompts += self.library.active_primary_prompt
3028
-
3043
+
3029
3044
  for attached_content in self.library.attached_prompts:
3030
3045
  library_prompts += "\n\n# ADDITIONAL INSTRUCTIONS\n\n"
3031
3046
  library_prompts += attached_content
3032
-
3047
+
3033
3048
  # Combine
3034
3049
  final_prompt = system_template + library_prompts
3035
-
3050
+
3051
+ # Glossary injection (if terms provided)
3052
+ if glossary_terms:
3053
+ final_prompt += "\n\n# GLOSSARY\n\n"
3054
+ final_prompt += "Use these approved terms in your translation:\n\n"
3055
+ for term in glossary_terms:
3056
+ source_term = term.get('source_term', '')
3057
+ target_term = term.get('target_term', '')
3058
+ if source_term and target_term:
3059
+ # Mark forbidden terms
3060
+ if term.get('forbidden'):
3061
+ final_prompt += f"- {source_term} → ⚠️ DO NOT USE: {target_term}\n"
3062
+ else:
3063
+ final_prompt += f"- {source_term} → {target_term}\n"
3064
+
3036
3065
  # Add translation delimiter
3037
3066
  final_prompt += "\n\n**YOUR TRANSLATION (provide ONLY the translated text, no numbering or labels):**\n"
3038
-
3067
+
3039
3068
  return final_prompt
3040
3069
 
3041
3070
  # ============================================================================