supervertaler 1.9.132__py3-none-any.whl → 1.9.145__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.
Supervertaler.py CHANGED
@@ -3,8 +3,8 @@ Supervertaler
3
3
  =============
4
4
  The Ultimate Translation Workbench.
5
5
  Modern PyQt6 interface with specialised modules to handle any problem.
6
- Version: 1.9.123 (QuickMenu now supports generic AI tasks)
7
- Release Date: January 19, 2026
6
+ Version: 1.9.136 (Glossary matching fix for punctuation)
7
+ Release Date: January 20, 2026
8
8
  Framework: PyQt6
9
9
 
10
10
  This is the modern edition of Supervertaler using PyQt6 framework.
@@ -34,9 +34,9 @@ License: MIT
34
34
  """
35
35
 
36
36
  # Version Information.
37
- __version__ = "1.9.132"
37
+ __version__ = "1.9.145"
38
38
  __phase__ = "0.9"
39
- __release_date__ = "2026-01-19"
39
+ __release_date__ = "2026-01-20"
40
40
  __edition__ = "Qt"
41
41
 
42
42
  import sys
@@ -59,6 +59,24 @@ def get_resource_path(relative_path: str) -> Path:
59
59
  # Running in development
60
60
  base_path = Path(__file__).parent
61
61
  return base_path / relative_path
62
+
63
+
64
+ def get_user_data_path() -> Path:
65
+ """
66
+ Get the path to user data directory.
67
+
68
+ In frozen builds (EXE): 'user_data' folder next to the EXE
69
+ In development: 'user_data' folder next to the script
70
+
71
+ The build process copies user_data directly next to the EXE for easy access.
72
+ """
73
+ if getattr(sys, 'frozen', False):
74
+ # Frozen build: user_data is next to the EXE (copied by build script)
75
+ return Path(sys.executable).parent / "user_data"
76
+ else:
77
+ # Development: user_data next to script
78
+ return Path(__file__).parent / "user_data"
79
+
62
80
  import threading
63
81
  import time # For delays in Superlookup
64
82
  import re
@@ -126,7 +144,7 @@ try:
126
144
  QScrollArea, QSizePolicy, QSlider, QToolButton, QAbstractItemView
127
145
  )
128
146
  from PyQt6.QtCore import Qt, QSize, QTimer, pyqtSignal, QObject, QUrl
129
- from PyQt6.QtGui import QFont, QAction, QKeySequence, QIcon, QTextOption, QColor, QDesktopServices, QTextCharFormat, QTextCursor, QBrush, QSyntaxHighlighter, QPalette, QTextBlockFormat, QCursor
147
+ from PyQt6.QtGui import QFont, QAction, QKeySequence, QIcon, QTextOption, QColor, QDesktopServices, QTextCharFormat, QTextCursor, QBrush, QSyntaxHighlighter, QPalette, QTextBlockFormat, QCursor, QFontMetrics
130
148
  from PyQt6.QtWidgets import QStyleOptionViewItem, QStyle
131
149
  from PyQt6.QtCore import QRectF
132
150
  from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@@ -5146,9 +5164,26 @@ class SupervertalerQt(QMainWindow):
5146
5164
  # Superlookup detached window
5147
5165
  self.lookup_detached_window = None
5148
5166
 
5149
- # Database Manager for Termbases
5167
+ # ============================================================================
5168
+ # USER DATA PATH INITIALIZATION
5169
+ # ============================================================================
5170
+ # Set up user data paths - handles both development and frozen (EXE) builds
5171
+ # In frozen builds, user_data is copied directly next to the EXE by the build script.
5150
5172
  from modules.database_manager import DatabaseManager
5151
- self.user_data_path = Path("user_data_private" if ENABLE_PRIVATE_FEATURES else "user_data")
5173
+
5174
+ if ENABLE_PRIVATE_FEATURES:
5175
+ # Developer mode: use private folder (git-ignored)
5176
+ self.user_data_path = Path(__file__).parent / "user_data_private"
5177
+ else:
5178
+ # Normal mode: use the helper function
5179
+ self.user_data_path = get_user_data_path()
5180
+
5181
+ # Ensure user_data directory exists (creates empty folder if missing)
5182
+ self.user_data_path.mkdir(parents=True, exist_ok=True)
5183
+
5184
+ print(f"[Data Paths] User data: {self.user_data_path}")
5185
+
5186
+ # Database Manager for Termbases
5152
5187
  self.db_manager = DatabaseManager(
5153
5188
  db_path=str(self.user_data_path / "Translation_Resources" / "supervertaler.db"),
5154
5189
  log_callback=self.log
@@ -5331,21 +5366,11 @@ class SupervertalerQt(QMainWindow):
5331
5366
  self.settings_tabs.setCurrentIndex(i)
5332
5367
  break
5333
5368
 
5334
- # Save the preference to ui_preferences.json (where load_general_settings reads from)
5369
+ # Save the preference to general_settings.json (where load_general_settings reads from)
5335
5370
  if dont_show_checkbox.isChecked():
5336
- prefs_file = self.user_data_path / "ui_preferences.json"
5337
- prefs = {}
5338
- if prefs_file.exists():
5339
- try:
5340
- with open(prefs_file, 'r') as f:
5341
- prefs = json.load(f)
5342
- except:
5343
- pass
5344
- if 'general_settings' not in prefs:
5345
- prefs['general_settings'] = {}
5346
- prefs['general_settings']['first_run_completed'] = True
5347
- with open(prefs_file, 'w', encoding='utf-8') as f:
5348
- json.dump(prefs, f, indent=2)
5371
+ settings = self.load_general_settings()
5372
+ settings['first_run_completed'] = True
5373
+ self.save_general_settings(settings)
5349
5374
  self.log("✅ First-run welcome completed (won't show again)")
5350
5375
  else:
5351
5376
  self.log("✅ First-run welcome shown (will show again next time)")
@@ -10488,6 +10513,111 @@ class SupervertalerQt(QMainWindow):
10488
10513
  f"Error testing segmentation:\n\n{e}"
10489
10514
  )
10490
10515
 
10516
+ def _refresh_termbase_display_for_current_segment(self):
10517
+ """Refresh only termbase/glossary display for the current segment.
10518
+
10519
+ This is a targeted refresh that does NOT trigger a TM search.
10520
+ Use this after adding a term to a glossary to update the display
10521
+ without the overhead of re-searching translation memories.
10522
+ """
10523
+ current_row = self.table.currentRow()
10524
+ if current_row < 0 or not self.current_project:
10525
+ return
10526
+
10527
+ # Get the segment from the grid (use ID from cell, not row index)
10528
+ id_item = self.table.item(current_row, 0)
10529
+ if not id_item:
10530
+ return
10531
+
10532
+ try:
10533
+ segment_id = int(id_item.text())
10534
+ except (ValueError, AttributeError):
10535
+ return
10536
+
10537
+ segment = next((seg for seg in self.current_project.segments if seg.id == segment_id), None)
10538
+ if not segment:
10539
+ return
10540
+
10541
+ # Clear only the termbase cache for this segment (NOT the TM cache)
10542
+ with self.termbase_cache_lock:
10543
+ if segment.id in self.termbase_cache:
10544
+ del self.termbase_cache[segment.id]
10545
+ self.log(f"🗑️ Cleared termbase cache for segment {segment.id}")
10546
+
10547
+ # Search for fresh termbase matches
10548
+ termbase_matches = self.find_termbase_matches_in_source(segment.source)
10549
+
10550
+ # Update termbase cache
10551
+ with self.termbase_cache_lock:
10552
+ self.termbase_cache[segment.id] = termbase_matches
10553
+
10554
+ # Update TermView widget
10555
+ if hasattr(self, 'termview_widget') and self.termview_widget:
10556
+ try:
10557
+ # Convert termbase matches to list format for termview
10558
+ # Note: find_termbase_matches_in_source returns dict with 'source' and 'translation' keys
10559
+ termbase_list = [
10560
+ {
10561
+ 'source_term': match.get('source', ''), # 'source' not 'source_term'
10562
+ 'target_term': match.get('translation', ''), # 'translation' not 'target_term'
10563
+ 'termbase_name': match.get('termbase_name', ''),
10564
+ 'ranking': match.get('ranking', 99),
10565
+ 'is_project_termbase': match.get('is_project_termbase', False),
10566
+ 'term_id': match.get('term_id'),
10567
+ 'termbase_id': match.get('termbase_id'),
10568
+ 'notes': match.get('notes', '')
10569
+ }
10570
+ for match in termbase_matches.values() if isinstance(match, dict)
10571
+ ] if isinstance(termbase_matches, dict) else []
10572
+
10573
+ # Get NT matches
10574
+ nt_matches = self.find_nt_matches_in_source(segment.source)
10575
+ self.termview_widget.update_with_matches(segment.source, termbase_list, nt_matches)
10576
+ except Exception as e:
10577
+ self.log(f"Error updating termview: {e}")
10578
+
10579
+ # Update Translation Results panel termbase section (without touching TM)
10580
+ if hasattr(self, 'results_panels'):
10581
+ for panel in self.results_panels:
10582
+ try:
10583
+ # Get the existing matches from the panel and update only Termbases
10584
+ if hasattr(panel, 'current_matches') and panel.current_matches:
10585
+ # Convert termbase matches to TranslationMatch format
10586
+ # Note: find_termbase_matches_in_source returns 'source' and 'translation' keys
10587
+ from modules.translation_results_panel import TranslationMatch
10588
+ tb_matches = []
10589
+ for match_data in termbase_matches.values():
10590
+ if isinstance(match_data, dict):
10591
+ tb_match = TranslationMatch(
10592
+ source=match_data.get('source', ''), # 'source' not 'source_term'
10593
+ target=match_data.get('translation', ''), # 'translation' not 'target_term'
10594
+ relevance=100.0,
10595
+ match_type='Termbase',
10596
+ provider=match_data.get('termbase_name', 'Glossary'),
10597
+ metadata={
10598
+ 'termbase_name': match_data.get('termbase_name', ''),
10599
+ 'ranking': match_data.get('ranking', 99),
10600
+ 'is_project_termbase': match_data.get('is_project_termbase', False),
10601
+ 'term_id': match_data.get('term_id'),
10602
+ 'termbase_id': match_data.get('termbase_id'),
10603
+ 'notes': match_data.get('notes', '')
10604
+ }
10605
+ )
10606
+ tb_matches.append(tb_match)
10607
+
10608
+ # Update just the Termbases section
10609
+ panel.current_matches['Termbases'] = tb_matches
10610
+ panel.set_matches(panel.current_matches)
10611
+ except Exception as e:
10612
+ self.log(f"Error updating results panel termbases: {e}")
10613
+
10614
+ # Re-highlight termbase matches in the source cell
10615
+ source_widget = self.table.cellWidget(current_row, 2)
10616
+ if source_widget and hasattr(source_widget, 'rehighlight'):
10617
+ source_widget.rehighlight()
10618
+
10619
+ self.log(f"🔄 Refreshed termbase display for segment {segment.id} (TM untouched)")
10620
+
10491
10621
  def add_term_pair_to_termbase(self, source_text: str, target_text: str):
10492
10622
  """Add a term pair to active termbase(s) with metadata dialog"""
10493
10623
  # Check if we have a current project
@@ -10628,26 +10758,8 @@ class SupervertalerQt(QMainWindow):
10628
10758
 
10629
10759
  QMessageBox.information(self, "Term Added", f"Successfully added term pair to {success_count} glossary(s):\\n\\nSource: {source_text}\\nTarget: {target_text}\\n\\nDomain: {metadata['domain'] or '(none)'}")
10630
10760
 
10631
- # Refresh translation results to show new termbase match immediately
10632
- current_row = self.table.currentRow()
10633
- if current_row >= 0 and current_row < len(self.current_project.segments):
10634
- segment = self.current_project.segments[current_row]
10635
-
10636
- # Clear BOTH caches for this segment to force refresh
10637
- with self.translation_matches_cache_lock:
10638
- if segment.id in self.translation_matches_cache:
10639
- del self.translation_matches_cache[segment.id]
10640
- self.log(f"🗑️ Cleared translation matches cache for segment {segment.id}")
10641
-
10642
- with self.termbase_cache_lock:
10643
- if segment.id in self.termbase_cache:
10644
- del self.termbase_cache[segment.id]
10645
- self.log(f"🗑️ Cleared termbase cache for segment {segment.id}")
10646
-
10647
- # Trigger lookup refresh by simulating segment change
10648
- self._last_selected_row = -1 # Reset to force refresh
10649
- self.on_cell_selected(current_row, self.table.currentColumn(), -1, -1)
10650
- self.log(f"🔄 Triggered refresh for segment {segment.id}")
10761
+ # Refresh termbase display (NOT TM - that would be wasteful)
10762
+ self._refresh_termbase_display_for_current_segment()
10651
10763
 
10652
10764
  # IMPORTANT: Refresh the termbase list UI if it's currently open to update term counts
10653
10765
  # Find the termbase tab and call its refresh function
@@ -10767,23 +10879,8 @@ class SupervertalerQt(QMainWindow):
10767
10879
  else:
10768
10880
  self.statusBar().showMessage(f"✓ Added: {source_text} → {target_text} (to {success_count} termbases)", 3000)
10769
10881
 
10770
- # Refresh translation results to show new termbase match immediately
10771
- current_row = self.table.currentRow()
10772
- if current_row >= 0 and current_row < len(self.current_project.segments):
10773
- segment = self.current_project.segments[current_row]
10774
-
10775
- # Clear BOTH caches for this segment to force refresh
10776
- with self.translation_matches_cache_lock:
10777
- if segment.id in self.translation_matches_cache:
10778
- del self.translation_matches_cache[segment.id]
10779
-
10780
- with self.termbase_cache_lock:
10781
- if segment.id in self.termbase_cache:
10782
- del self.termbase_cache[segment.id]
10783
-
10784
- # Trigger lookup refresh by simulating segment change
10785
- self._last_selected_row = -1 # Reset to force refresh
10786
- self.on_cell_selected(current_row, self.table.currentColumn(), -1, -1)
10882
+ # Refresh termbase display (NOT TM - that would be wasteful)
10883
+ self._refresh_termbase_display_for_current_segment()
10787
10884
 
10788
10885
  # Refresh termbase list UI if open
10789
10886
  if hasattr(self, 'termbase_tab_refresh_callback') and self.termbase_tab_refresh_callback:
@@ -10902,17 +10999,8 @@ class SupervertalerQt(QMainWindow):
10902
10999
  self.log(f"✓ Added to '{target_termbase['name']}': {source_text} → {target_text}")
10903
11000
  self.statusBar().showMessage(f"✓ Added to '{target_termbase['name']}': {source_text} → {target_text}", 3000)
10904
11001
 
10905
- # Refresh caches and display
10906
- if current_row < len(self.current_project.segments):
10907
- segment = self.current_project.segments[current_row]
10908
- with self.translation_matches_cache_lock:
10909
- if segment.id in self.translation_matches_cache:
10910
- del self.translation_matches_cache[segment.id]
10911
- with self.termbase_cache_lock:
10912
- if segment.id in self.termbase_cache:
10913
- del self.termbase_cache[segment.id]
10914
- self._last_selected_row = -1
10915
- self.on_cell_selected(current_row, self.table.currentColumn(), -1, -1)
11002
+ # Refresh termbase display (NOT TM - that would be wasteful)
11003
+ self._refresh_termbase_display_for_current_segment()
10916
11004
 
10917
11005
  if hasattr(self, 'termbase_tab_refresh_callback') and self.termbase_tab_refresh_callback:
10918
11006
  self.termbase_tab_refresh_callback()
@@ -14484,6 +14572,21 @@ class SupervertalerQt(QMainWindow):
14484
14572
  sound_event_combos['glossary_term_error'] = make_sound_combo(default_term_error)
14485
14573
  event_rows.addRow("Glossary add error:", sound_event_combos['glossary_term_error'])
14486
14574
 
14575
+ # Segment confirmed (Ctrl+Enter)
14576
+ default_segment_confirmed = sound_effects_map.get('segment_confirmed', 'none')
14577
+ sound_event_combos['segment_confirmed'] = make_sound_combo(default_segment_confirmed)
14578
+ event_rows.addRow("Segment confirmed:", sound_event_combos['segment_confirmed'])
14579
+
14580
+ # 100% TM match found
14581
+ default_tm_100_match = sound_effects_map.get('tm_100_percent_match', 'none')
14582
+ sound_event_combos['tm_100_percent_match'] = make_sound_combo(default_tm_100_match)
14583
+ event_rows.addRow("100% TM match alert:", sound_event_combos['tm_100_percent_match'])
14584
+
14585
+ # Fuzzy TM match found (< 100%)
14586
+ default_tm_fuzzy_match = sound_effects_map.get('tm_fuzzy_match', 'none')
14587
+ sound_event_combos['tm_fuzzy_match'] = make_sound_combo(default_tm_fuzzy_match)
14588
+ event_rows.addRow("Fuzzy TM match found:", sound_event_combos['tm_fuzzy_match'])
14589
+
14487
14590
  sound_layout.addLayout(event_rows)
14488
14591
 
14489
14592
  sound_note = QLabel("💡 Uses Windows system beeps (no audio files).")
@@ -16139,7 +16242,7 @@ class SupervertalerQt(QMainWindow):
16139
16242
  # Update status bar indicator (always visible when active)
16140
16243
  if hasattr(self, 'alwayson_indicator_label'):
16141
16244
  if status == "listening" or status == "waiting":
16142
- self.alwayson_indicator_label.setText("🎤 VOICE ON")
16245
+ self.alwayson_indicator_label.setText("🎤 VOICE COMMANDS ON")
16143
16246
  self.alwayson_indicator_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #2E7D32; background-color: #C8E6C9; padding: 2px 6px; border-radius: 3px;")
16144
16247
  self.alwayson_indicator_label.setToolTip("Always-on voice listening ACTIVE\nClick to stop")
16145
16248
  self.alwayson_indicator_label.show()
@@ -16159,7 +16262,7 @@ class SupervertalerQt(QMainWindow):
16159
16262
  # Update grid toolbar button (if exists)
16160
16263
  if hasattr(self, 'grid_alwayson_btn'):
16161
16264
  if status == "listening" or status == "waiting":
16162
- self.grid_alwayson_btn.setText("🎧 Voice ON")
16265
+ self.grid_alwayson_btn.setText("🎧 Voice Commands ON")
16163
16266
  self.grid_alwayson_btn.setChecked(True)
16164
16267
  self.grid_alwayson_btn.setStyleSheet("""
16165
16268
  QPushButton {
@@ -16202,7 +16305,7 @@ class SupervertalerQt(QMainWindow):
16202
16305
  }
16203
16306
  """)
16204
16307
  else: # stopped or other
16205
- self.grid_alwayson_btn.setText("🎧 Voice OFF")
16308
+ self.grid_alwayson_btn.setText("🎧 Voice Commands OFF")
16206
16309
  self.grid_alwayson_btn.setChecked(False)
16207
16310
  self.grid_alwayson_btn.setStyleSheet("""
16208
16311
  QPushButton {
@@ -18264,14 +18367,14 @@ class SupervertalerQt(QMainWindow):
18264
18367
  preview_prompt_btn.clicked.connect(self._preview_combined_prompt_from_grid)
18265
18368
  toolbar_layout.addWidget(preview_prompt_btn)
18266
18369
 
18267
- dictate_btn = QPushButton("🎤 Dictate")
18370
+ dictate_btn = QPushButton("🎤 Dictation")
18268
18371
  dictate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
18269
18372
  dictate_btn.clicked.connect(self.start_voice_dictation)
18270
18373
  dictate_btn.setToolTip("Start/stop voice dictation (F9)")
18271
18374
  toolbar_layout.addWidget(dictate_btn)
18272
18375
 
18273
18376
  # Always-On Voice toggle button
18274
- alwayson_btn = QPushButton("🎧 Voice OFF")
18377
+ alwayson_btn = QPushButton("🎧 Voice Commands OFF")
18275
18378
  alwayson_btn.setCheckable(True)
18276
18379
  alwayson_btn.setChecked(False)
18277
18380
  alwayson_btn.setStyleSheet("""
@@ -18542,9 +18645,11 @@ class SupervertalerQt(QMainWindow):
18542
18645
  def _apply_pagination_to_grid(self):
18543
18646
  """Show/hide rows based on current page, optionally constrained by active filters.
18544
18647
 
18545
- IMPORTANT: QTableWidget has only one hidden flag per row. Pagination and filtering
18546
- must be combined into a single visibility decision; otherwise switching pages can
18547
- accidentally unhide rows that filters intended to hide.
18648
+ IMPORTANT: When a text filter is active (Filter Source/Target boxes), we show ALL
18649
+ matching rows across the entire document, ignoring pagination. This ensures users
18650
+ can find content regardless of which page they're on.
18651
+
18652
+ When no filter is active, normal pagination applies.
18548
18653
  """
18549
18654
  if not self.current_project or not self.current_project.segments:
18550
18655
  return
@@ -18556,28 +18661,38 @@ class SupervertalerQt(QMainWindow):
18556
18661
  if not hasattr(self, 'grid_page_size'):
18557
18662
  self.grid_page_size = 50
18558
18663
 
18559
- # Calculate which rows should be visible
18560
- if self.grid_page_size >= 999999:
18561
- # "All" mode - show everything
18562
- start_row = 0
18563
- end_row = total_segments
18564
- else:
18565
- start_row = self.grid_current_page * self.grid_page_size
18566
- end_row = min(start_row + self.grid_page_size, total_segments)
18567
-
18568
18664
  # If a text filter (Filter Source/Target) is active, it will populate an allowlist
18569
18665
  # of row indices that are allowed to be visible.
18570
18666
  filter_allowlist = getattr(self, '_active_text_filter_rows', None)
18571
-
18572
- # Batch show/hide for performance
18573
- self.table.setUpdatesEnabled(False)
18574
- try:
18575
- for row in range(total_segments):
18576
- in_page = start_row <= row < end_row
18577
- allowed_by_filter = True if filter_allowlist is None else (row in filter_allowlist)
18578
- self.table.setRowHidden(row, not (in_page and allowed_by_filter))
18579
- finally:
18580
- self.table.setUpdatesEnabled(True)
18667
+
18668
+ # When a filter is active, show ALL matching rows (ignore pagination)
18669
+ # When no filter is active, apply normal pagination
18670
+ if filter_allowlist is not None:
18671
+ # Filter mode: show all matching rows across the entire document
18672
+ self.table.setUpdatesEnabled(False)
18673
+ try:
18674
+ for row in range(total_segments):
18675
+ self.table.setRowHidden(row, row not in filter_allowlist)
18676
+ finally:
18677
+ self.table.setUpdatesEnabled(True)
18678
+ else:
18679
+ # Normal pagination mode
18680
+ if self.grid_page_size >= 999999:
18681
+ # "All" mode - show everything
18682
+ start_row = 0
18683
+ end_row = total_segments
18684
+ else:
18685
+ start_row = self.grid_current_page * self.grid_page_size
18686
+ end_row = min(start_row + self.grid_page_size, total_segments)
18687
+
18688
+ # Batch show/hide for performance
18689
+ self.table.setUpdatesEnabled(False)
18690
+ try:
18691
+ for row in range(total_segments):
18692
+ in_page = start_row <= row < end_row
18693
+ self.table.setRowHidden(row, not in_page)
18694
+ finally:
18695
+ self.table.setUpdatesEnabled(True)
18581
18696
 
18582
18697
  # Update pagination UI
18583
18698
  self._update_pagination_ui()
@@ -18860,11 +18975,12 @@ class SupervertalerQt(QMainWindow):
18860
18975
  header.setStretchLastSection(False) # Don't auto-stretch last section (we use Stretch mode for Source/Target)
18861
18976
 
18862
18977
  # Set initial column widths - give Source and Target equal space
18863
- self.table.setColumnWidth(0, 40) # ID - compact, fits up to 3 digits comfortably
18978
+ # ID column width will be auto-adjusted by _update_segment_column_width() after segments load
18979
+ self.table.setColumnWidth(0, 35) # ID - temporary, auto-adjusts to fit content
18864
18980
  self.table.setColumnWidth(1, 40) # Type - narrower
18865
18981
  self.table.setColumnWidth(2, 400) # Source
18866
18982
  self.table.setColumnWidth(3, 400) # Target
18867
- self.table.setColumnWidth(4, 70) # Status
18983
+ self.table.setColumnWidth(4, 60) # Status - compact
18868
18984
 
18869
18985
  # Enable word wrap in cells (both display and edit mode)
18870
18986
  self.table.setWordWrap(True)
@@ -19162,7 +19278,7 @@ class SupervertalerQt(QMainWindow):
19162
19278
  clear_btn.clicked.connect(self.clear_tab_target)
19163
19279
 
19164
19280
  # Voice dictation button
19165
- dictate_btn = QPushButton("🎤 Dictate (F9)")
19281
+ dictate_btn = QPushButton("🎤 Dictation (F9)")
19166
19282
  dictate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
19167
19283
  dictate_btn.clicked.connect(self.start_voice_dictation)
19168
19284
  dictate_btn.setToolTip("Click or press F9 to start/stop voice dictation")
@@ -26998,33 +27114,65 @@ class SupervertalerQt(QMainWindow):
26998
27114
  self.set_compare_panel_matches(segment_id, current_source, tm_matches, mt_matches)
26999
27115
 
27000
27116
  def _set_compare_panel_text_with_diff(self, text_edit: QTextEdit, current: str, tm_source: str):
27001
- """Set text with diff highlighting (deletions in red, additions in green)"""
27117
+ """
27118
+ Set text with diff highlighting using memoQ-style track changes view:
27119
+ - Normal text: identical to current
27120
+ - Red underline: text added in current (insertion)
27121
+ - Red strikethrough: text removed from TM (deletion)
27122
+
27123
+ This displays the TM source annotated with tracked changes.
27124
+ """
27002
27125
  import difflib
27003
27126
 
27004
27127
  text_edit.clear()
27005
27128
  cursor = text_edit.textCursor()
27006
27129
 
27007
- # Create formatters
27130
+ # Create formatters - memoQ track changes style
27008
27131
  normal_format = QTextCharFormat()
27132
+
27133
+ # Red strikethrough for deletions (text in TM but not in current)
27009
27134
  delete_format = QTextCharFormat()
27010
- delete_format.setBackground(QColor("#ffcccc")) # Light red for deletions
27011
- delete_format.setForeground(QColor("#cc0000"))
27135
+ delete_format.setForeground(QColor("#cc0000")) # Red text
27136
+ delete_format.setFontStrikeOut(True)
27137
+
27138
+ # Red underline for insertions (text in current but not in TM)
27139
+ add_format = QTextCharFormat()
27140
+ add_format.setForeground(QColor("#cc0000")) # Red text
27141
+ add_format.setFontUnderline(True)
27142
+
27143
+ # Use SequenceMatcher at word level for better readability
27144
+ current_words = current.split()
27145
+ tm_words = tm_source.split()
27012
27146
 
27013
- # Use SequenceMatcher to find differences
27014
- matcher = difflib.SequenceMatcher(None, current, tm_source)
27147
+ matcher = difflib.SequenceMatcher(None, current_words, tm_words)
27015
27148
 
27149
+ result_parts = []
27016
27150
  for tag, i1, i2, j1, j2 in matcher.get_opcodes():
27017
27151
  if tag == 'equal':
27018
- cursor.insertText(tm_source[j1:j2], normal_format)
27152
+ # Same in both - show normally
27153
+ result_parts.append(('normal', ' '.join(tm_words[j1:j2])))
27019
27154
  elif tag == 'replace':
27020
- # Show the TM text (what's different from current)
27021
- cursor.insertText(tm_source[j1:j2], delete_format)
27155
+ # Different text - show TM version strikethrough, then current version underlined
27156
+ result_parts.append(('delete', ' '.join(tm_words[j1:j2])))
27157
+ result_parts.append(('add', ' '.join(current_words[i1:i2])))
27022
27158
  elif tag == 'insert':
27023
- # Text in TM but not in current
27024
- cursor.insertText(tm_source[j1:j2], delete_format)
27159
+ # Text in TM but not in current - strikethrough (it was "deleted" from current's perspective)
27160
+ result_parts.append(('delete', ' '.join(tm_words[j1:j2])))
27025
27161
  elif tag == 'delete':
27026
- # Text in current but not in TM - don't show in TM source box
27027
- pass
27162
+ # Text in current but not in TM - underline (it was "inserted" in current)
27163
+ result_parts.append(('add', ' '.join(current_words[i1:i2])))
27164
+
27165
+ # Render the parts with proper spacing
27166
+ for i, (part_type, text) in enumerate(result_parts):
27167
+ if i > 0:
27168
+ cursor.insertText(' ', normal_format) # Space between parts
27169
+
27170
+ if part_type == 'normal':
27171
+ cursor.insertText(text, normal_format)
27172
+ elif part_type == 'delete':
27173
+ cursor.insertText(text, delete_format)
27174
+ elif part_type == 'add':
27175
+ cursor.insertText(text, add_format)
27028
27176
 
27029
27177
  text_edit.setTextCursor(cursor)
27030
27178
 
@@ -27642,6 +27790,37 @@ class SupervertalerQt(QMainWindow):
27642
27790
  target_widget = self.table.cellWidget(row, 3)
27643
27791
  if target_widget and isinstance(target_widget, EditableGridTextEditor):
27644
27792
  target_widget.setFont(font)
27793
+
27794
+ # Adjust segment number column width based on font size
27795
+ self._update_segment_column_width()
27796
+
27797
+ def _update_segment_column_width(self):
27798
+ """Adjust segment number column width to fit the largest segment number.
27799
+
27800
+ Uses font metrics to calculate exact width needed for the highest segment number.
27801
+ """
27802
+ if not hasattr(self, 'table') or not self.table:
27803
+ return
27804
+
27805
+ # Get the highest segment number we need to display
27806
+ max_segment = self.table.rowCount()
27807
+ if max_segment == 0:
27808
+ max_segment = 1
27809
+
27810
+ # Use font metrics to calculate exact width needed
27811
+ font = QFont(self.default_font_family, self.default_font_size)
27812
+ fm = QFontMetrics(font)
27813
+
27814
+ # Measure the width of the largest number (as string)
27815
+ text_width = fm.horizontalAdvance(str(max_segment))
27816
+
27817
+ # Add padding (10px on each side = 20px total)
27818
+ new_width = text_width + 20
27819
+
27820
+ # Ensure minimum width for very small numbers
27821
+ new_width = max(30, new_width)
27822
+
27823
+ self.table.setColumnWidth(0, new_width)
27645
27824
 
27646
27825
  def set_font_family(self, family_name: str):
27647
27826
  """Set font family from menu"""
@@ -28599,12 +28778,22 @@ class SupervertalerQt(QMainWindow):
28599
28778
  if best_match:
28600
28779
  self.log(f"✨ CACHE: Auto-inserting 100% TM match into segment {segment.id} at row {current_row}")
28601
28780
  self._auto_insert_tm_match(segment, best_match.target, current_row)
28781
+ # Play 100% TM match alert sound
28782
+ self._play_sound_effect('tm_100_percent_match')
28602
28783
  else:
28603
28784
  relevances = [(tm.relevance, type(tm.relevance).__name__) for tm in cached_matches.get("TM", [])]
28604
28785
  self.log(f"⚠️ CACHE: No 100% match found. All relevances: {relevances}")
28605
28786
  else:
28606
28787
  self.log(f"⚠️ CACHE: Target not empty ('{segment.target}') - skipping auto-insert")
28607
28788
 
28789
+ # 🔊 Play fuzzy match sound if fuzzy matches found from cache (but not 100%)
28790
+ if tm_count > 0:
28791
+ tm_matches = cached_matches.get("TM", [])
28792
+ has_100_match = any(float(tm.relevance) >= 99.5 for tm in tm_matches)
28793
+ has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in tm_matches)
28794
+ if has_fuzzy_match and not has_100_match:
28795
+ self._play_sound_effect('tm_fuzzy_match')
28796
+
28608
28797
  # Skip the slow lookup below, we already have everything
28609
28798
  # Continue to prefetch trigger at the end
28610
28799
  matches_dict = cached_matches # Set for later use
@@ -28634,29 +28823,29 @@ class SupervertalerQt(QMainWindow):
28634
28823
  # Store in cache for future access (thread-safe) - EVEN IF EMPTY
28635
28824
  with self.termbase_cache_lock:
28636
28825
  self.termbase_cache[segment_id] = stored_matches
28637
-
28638
- # Update Termview with the newly cached matches
28639
- if stored_matches and hasattr(self, 'termview_widget') and self.current_project:
28640
- try:
28641
- # Convert dict format to list format
28642
- termbase_matches = [
28643
- {
28644
- 'source_term': match_data.get('source', ''),
28645
- 'target_term': match_data.get('translation', ''),
28646
- 'termbase_name': match_data.get('termbase_name', ''),
28647
- 'ranking': match_data.get('ranking', 99),
28648
- 'is_project_termbase': match_data.get('is_project_termbase', False),
28649
- 'term_id': match_data.get('term_id'),
28650
- 'termbase_id': match_data.get('termbase_id'),
28651
- 'notes': match_data.get('notes', '')
28652
- }
28653
- for match_data in stored_matches.values()
28654
- ]
28655
- # Also get NT matches
28656
- nt_matches = self.find_nt_matches_in_source(segment.source)
28657
- self.termview_widget.update_with_matches(segment.source, termbase_matches, nt_matches)
28658
- except Exception as e:
28659
- self.log(f"Error refreshing termview: {e}")
28826
+
28827
+ # CRITICAL FIX: Always update Termview (even with empty results) - show "No matches" state
28828
+ if hasattr(self, 'termview_widget') and self.current_project:
28829
+ try:
28830
+ # Convert dict format to list format
28831
+ termbase_matches = [
28832
+ {
28833
+ 'source_term': match_data.get('source', ''),
28834
+ 'target_term': match_data.get('translation', ''),
28835
+ 'termbase_name': match_data.get('termbase_name', ''),
28836
+ 'ranking': match_data.get('ranking', 99),
28837
+ 'is_project_termbase': match_data.get('is_project_termbase', False),
28838
+ 'term_id': match_data.get('term_id'),
28839
+ 'termbase_id': match_data.get('termbase_id'),
28840
+ 'notes': match_data.get('notes', '')
28841
+ }
28842
+ for match_data in stored_matches.values()
28843
+ ] if stored_matches else []
28844
+ # Also get NT matches
28845
+ nt_matches = self.find_nt_matches_in_source(segment.source)
28846
+ self.termview_widget.update_with_matches(segment.source, termbase_matches, nt_matches)
28847
+ except Exception as e:
28848
+ self.log(f"Error refreshing termview: {e}")
28660
28849
 
28661
28850
  # Store in widget for backwards compatibility
28662
28851
  if source_widget and hasattr(source_widget, 'termbase_matches'):
@@ -31168,8 +31357,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
31168
31357
 
31169
31358
  # Comprehensive set of quote and punctuation characters to strip
31170
31359
  # Includes: straight quotes, curly quotes (left/right), German quotes, guillemets, single quotes
31360
+ # ALSO includes parentheses, brackets, and braces for terms like "(typisch)" or "[example]"
31171
31361
  # Using explicit characters to avoid encoding issues
31172
- PUNCT_CHARS = '.,!?;:\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A'
31362
+ PUNCT_CHARS = '.,!?;:\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A()[]{}'
31173
31363
 
31174
31364
  for word in words:
31175
31365
  # Remove punctuation including quotes (preserve internal punctuation like "gew.%")
@@ -31184,13 +31374,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
31184
31374
  # Get project ID for termbase priority lookup
31185
31375
  project_id = self.current_project.id if self.current_project and hasattr(self.current_project, 'id') else None
31186
31376
 
31187
- termbase_results = self.db_manager.search_termbases(
31188
- clean_word,
31189
- source_lang=source_lang_code,
31190
- target_lang=target_lang_code,
31191
- project_id=project_id,
31192
- min_length=2
31193
- )
31377
+ # CRITICAL FIX: Search for BOTH the cleaned word AND the original word with punctuation
31378
+ # This allows glossary entries like "ca." to match source text "ca." where
31379
+ # the tokenized/cleaned version is "ca" but the glossary entry has the period
31380
+ words_to_search = [clean_word]
31381
+ # Also try searching with trailing punctuation (for entries like "ca.", "gew.%")
31382
+ original_word_stripped_leading = word.lstrip(PUNCT_CHARS)
31383
+ if original_word_stripped_leading != clean_word and len(original_word_stripped_leading) >= 2:
31384
+ words_to_search.append(original_word_stripped_leading)
31385
+
31386
+ termbase_results = []
31387
+ for search_word in words_to_search:
31388
+ results = self.db_manager.search_termbases(
31389
+ search_word,
31390
+ source_lang=source_lang_code,
31391
+ target_lang=target_lang_code,
31392
+ project_id=project_id,
31393
+ min_length=2
31394
+ )
31395
+ if results:
31396
+ termbase_results.extend(results)
31194
31397
 
31195
31398
  if termbase_results:
31196
31399
  for result in termbase_results:
@@ -35650,6 +35853,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35650
35853
 
35651
35854
  self.update_progress_stats()
35652
35855
 
35856
+ # Play sound effect for segment confirmation
35857
+ self._play_sound_effect('segment_confirmed')
35858
+
35653
35859
  if segment.target and segment.target.strip():
35654
35860
  try:
35655
35861
  self.save_segment_to_activated_tms(segment.source, segment.target)
@@ -35857,17 +36063,15 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35857
36063
  )
35858
36064
 
35859
36065
  if reply == QMessageBox.StandardButton.Yes:
35860
- if self.db_manager:
36066
+ if self.termbase_mgr:
35861
36067
  try:
35862
- conn = self.db_manager.get_connection()
35863
- cursor = conn.cursor()
35864
- cursor.execute("DELETE FROM termbase_terms WHERE id = ?", (term_id,))
35865
- conn.commit()
35866
-
35867
- self.log(f"✓ Deleted glossary entry: {source_term} → {target_term}")
35868
-
35869
- # Refresh termview and translation results
35870
- self._refresh_current_segment_matches()
36068
+ if self.termbase_mgr.delete_term(term_id):
36069
+ self.log(f"✓ Deleted glossary entry: {source_term} → {target_term}")
36070
+
36071
+ # Refresh termview and translation results
36072
+ self._refresh_current_segment_matches()
36073
+ else:
36074
+ self.log(f"✗ Failed to delete glossary entry")
35871
36075
  except Exception as e:
35872
36076
  self.log(f"✗ Error deleting glossary entry from database: {e}")
35873
36077
  except Exception as e:
@@ -35876,23 +36080,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35876
36080
  def _refresh_current_segment_matches(self):
35877
36081
  """Refresh termbase matches for the current segment (after edit/delete)"""
35878
36082
  try:
35879
- current_row = self.table.currentRow()
35880
- if current_row >= 0 and self.current_project and self.current_project.segments:
35881
- # Get the actual segment index
35882
- segment_idx = current_row
35883
- if hasattr(self, 'current_page') and hasattr(self, 'items_per_page'):
35884
- segment_idx = (self.current_page - 1) * self.items_per_page + current_row
35885
-
35886
- if 0 <= segment_idx < len(self.current_project.segments):
35887
- segment = self.current_project.segments[segment_idx]
35888
-
35889
- # Clear termbase cache for this segment
35890
- segment_id = id(segment)
35891
- if hasattr(self, 'termbase_cache') and segment_id in self.termbase_cache:
35892
- del self.termbase_cache[segment_id]
35893
-
35894
- # Trigger refresh by re-selecting the cell
35895
- self.on_cell_selected(current_row, 2)
36083
+ # Use the targeted refresh method that doesn't trigger TM search
36084
+ self._refresh_termbase_display_for_current_segment()
35896
36085
  except Exception as e:
35897
36086
  self.log(f"✗ Error refreshing segment matches: {e}")
35898
36087
 
@@ -35999,41 +36188,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35999
36188
  except Exception as e:
36000
36189
  self.log(f"Error updating tabbed panel: {e}")
36001
36190
 
36002
- # Update Termview widget - pass termbase matches from Translation Results
36003
- if hasattr(self, 'termview_widget') and self.current_project:
36004
- try:
36005
- # Get termbase matches from the segment's cached data if available
36006
- termbase_matches = []
36007
- if hasattr(self, 'termbase_cache') and segment_id in self.termbase_cache:
36008
- cached_matches = self.termbase_cache[segment_id]
36009
-
36010
- # Convert dict format to list format expected by Termview
36011
- if isinstance(cached_matches, dict):
36012
- termbase_matches = [
36013
- {
36014
- 'source_term': source_key, # Use dict key as source term
36015
- 'target_term': match_data.get('translation', ''),
36016
- 'termbase_name': match_data.get('termbase_name', ''),
36017
- 'ranking': match_data.get('ranking', 99),
36018
- 'is_project_termbase': match_data.get('is_project_termbase', False),
36019
- 'target_synonyms': match_data.get('target_synonyms', []),
36020
- 'term_id': match_data.get('term_id'),
36021
- 'termbase_id': match_data.get('termbase_id')
36022
- }
36023
- for source_key, match_data in cached_matches.items()
36024
- ]
36025
- else:
36026
- # Already in list format (shouldn't happen, but handle it)
36027
- termbase_matches = cached_matches
36028
-
36029
- # Also get NT matches
36030
- nt_matches = self.find_nt_matches_in_source(source_text)
36031
-
36032
- self.termview_widget.update_with_matches(source_text, termbase_matches, nt_matches)
36033
- except Exception as e:
36034
- self.log(f"Error updating termview: {e}")
36035
- import traceback
36036
- self.log(f"Traceback: {traceback.format_exc()}")
36191
+ # NOTE: Termview is updated AFTER termbase search completes in _on_cell_selected_full
36192
+ # Do NOT update Termview here - the cache may not be populated yet
36037
36193
 
36038
36194
  # ========================================================================
36039
36195
  # UTILITY
@@ -40447,6 +40603,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
40447
40603
 
40448
40604
  if best_match:
40449
40605
  self._auto_insert_tm_match(segment, best_match.target, None) # Let function find row
40606
+ # Play 100% TM match alert sound
40607
+ self._play_sound_effect('tm_100_percent_match')
40608
+
40609
+ # 🔊 Play fuzzy match sound if fuzzy matches found (but not 100%)
40610
+ has_100_match = any(float(tm.relevance) >= 99.5 for tm in matches_dict["TM"])
40611
+ has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in matches_dict["TM"])
40612
+ if has_fuzzy_match and not has_100_match:
40613
+ self._play_sound_effect('tm_fuzzy_match')
40450
40614
  except Exception as e:
40451
40615
  self.log(f"Error in delayed TM search: {e}")
40452
40616
 
@@ -1312,6 +1312,10 @@ class DatabaseManager:
1312
1312
  # Note: termbase_id is stored as TEXT in termbase_terms but INTEGER in termbases
1313
1313
  # Use CAST to ensure proper comparison
1314
1314
  # IMPORTANT: Join with termbase_activation to get the ACTUAL priority for this project
1315
+ # CRITICAL FIX: Also match when search_term starts with the glossary term
1316
+ # This handles cases like searching for "ca." when glossary has "ca."
1317
+ # AND searching for "ca" when glossary has "ca."
1318
+ # We also strip trailing punctuation from glossary terms for comparison
1315
1319
  query = """
1316
1320
  SELECT
1317
1321
  t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
@@ -1329,19 +1333,30 @@ class DatabaseManager:
1329
1333
  LOWER(t.source_term) = LOWER(?) OR
1330
1334
  LOWER(t.source_term) LIKE LOWER(?) OR
1331
1335
  LOWER(t.source_term) LIKE LOWER(?) OR
1332
- LOWER(t.source_term) LIKE LOWER(?)
1336
+ LOWER(t.source_term) LIKE LOWER(?) OR
1337
+ LOWER(RTRIM(t.source_term, '.!?,;:')) = LOWER(?) OR
1338
+ LOWER(?) LIKE LOWER(t.source_term) || '%' OR
1339
+ LOWER(?) = LOWER(RTRIM(t.source_term, '.!?,;:'))
1333
1340
  )
1334
1341
  AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
1335
1342
  """
1336
- # Exact match, word at start, word at end, word in middle
1337
- # Use LOWER() for case-insensitive matching (handles "Edelmetalen" = "edelmetalen")
1338
- # IMPORTANT: project_id must be first param for the LEFT JOIN ta.project_id = ? above
1343
+ # Matching patterns:
1344
+ # 1. Exact match: source_term = search_term
1345
+ # 2. Glossary term starts with search: source_term LIKE "search_term %"
1346
+ # 3. Glossary term ends with search: source_term LIKE "% search_term"
1347
+ # 4. Glossary term contains search: source_term LIKE "% search_term %"
1348
+ # 5. Glossary term (stripped) = search_term: RTRIM(source_term) = search_term (handles "ca." = "ca")
1349
+ # 6. Search starts with glossary term: search_term LIKE source_term || '%'
1350
+ # 7. Search = glossary term stripped: search_term = RTRIM(source_term)
1339
1351
  params = [
1340
1352
  project_id if project_id else 0, # Use 0 if no project (won't match any activation records)
1341
1353
  search_term,
1342
1354
  f"{search_term} %",
1343
1355
  f"% {search_term}",
1344
- f"% {search_term} %"
1356
+ f"% {search_term} %",
1357
+ search_term, # For RTRIM comparison
1358
+ search_term, # For reverse LIKE
1359
+ search_term # For reverse RTRIM comparison
1345
1360
  ]
1346
1361
 
1347
1362
  # Language filters - if term has no language, use termbase language for filtering
@@ -750,7 +750,10 @@ class TermviewWidget(QWidget):
750
750
  if not source_term or not target_term:
751
751
  continue
752
752
 
753
- key = source_term.lower()
753
+ # Strip punctuation from key to match lookup normalization
754
+ # This ensures "ca." in glossary matches "ca." token stripped to "ca"
755
+ PUNCT_CHARS_FOR_KEY = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A()[]'
756
+ key = source_term.lower().strip(PUNCT_CHARS_FOR_KEY)
754
757
  if key not in matches_dict:
755
758
  matches_dict[key] = []
756
759
 
@@ -803,7 +806,8 @@ class TermviewWidget(QWidget):
803
806
 
804
807
  # Comprehensive set of quote and punctuation characters to strip
805
808
  # Using Unicode escapes to avoid encoding issues
806
- PUNCT_CHARS = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A'
809
+ # Include brackets for terms like "(typisch)" to match "typisch"
810
+ PUNCT_CHARS = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A()[]'
807
811
 
808
812
  # Track which terms have already been assigned shortcuts (avoid duplicates)
809
813
  assigned_shortcuts = set()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supervertaler
3
- Version: 1.9.132
3
+ Version: 1.9.145
4
4
  Summary: Professional AI-powered translation workbench with multi-LLM support, glossary system, TM, spellcheck, voice commands, and PyQt6 interface. Batteries included (core).
5
5
  Home-page: https://supervertaler.com
6
6
  Author: Michael Beijer
@@ -71,7 +71,7 @@ Dynamic: home-page
71
71
  Dynamic: license-file
72
72
  Dynamic: requires-python
73
73
 
74
- # 🚀 Supervertaler v1.9.128
74
+ # 🚀 Supervertaler v1.9.145
75
75
 
76
76
  [![PyPI version](https://badge.fury.io/py/supervertaler.svg)](https://pypi.org/project/Supervertaler/)
77
77
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
@@ -79,7 +79,23 @@ Dynamic: requires-python
79
79
 
80
80
  AI-enhanced CAT tool with multi-LLM support (GPT-4, Claude, Gemini, Ollama), innovative Superlookup concordance system offering access to multiple terminology sources (TMs, glossaries, web resources, etc.), and seamless CAT tool integration (memoQ, Trados, CafeTran, Phrase).
81
81
 
82
- **Current Version:** v1.9.132 (January 20, 2026)
82
+ **Current Version:** v1.9.145 (January 20, 2026)
83
+
84
+ ### FIXED in v1.9.140 - 🐛 Glossary Add No Longer Triggers TM Search
85
+
86
+ **Performance Fix ([#118](https://github.com/michaelbeijer/Supervertaler/issues/118)):** Adding a term to a glossary was unnecessarily triggering a full TM search. Now uses targeted refresh that only updates glossary display - TM results stay cached.
87
+
88
+ **Also:** Renamed "Voice OFF" → "Voice Commands OFF" and "Dictate" → "Dictation" for clarity.
89
+
90
+ ### FIXED in v1.9.138 - 🏷️ Termview Punctuated Terms & Auto-Sizing Columns
91
+
92
+ **Termview Fix:** Glossary terms with punctuation (like "ca." or "(typisch)") now correctly appear in the Termview pane. Previously they were found but not displayed due to a key normalization mismatch.
93
+
94
+ **Grid UX:** Segment number column now auto-sizes based on font size and segment count - no more truncated numbers!
95
+
96
+ ### FIXED in v1.9.137 - 🔧 Termview Race Condition
97
+
98
+ **Glossary terms now appear immediately:** Fixed timing bug where Termview showed "No glossary matches" until you pressed F5. Now updates correctly when navigating segments.
83
99
 
84
100
  ### ENHANCED in v1.9.128 - 📝 Placeholders Tab Layout Optimization
85
101
 
@@ -1,4 +1,4 @@
1
- Supervertaler.py,sha256=9qsQbd5hEWk77Ai__NsBxX8hVkwa9f6sJG2knb-Otic,2143966
1
+ Supervertaler.py,sha256=B2j743pISRdPk_qIPGeSw3Atfw3Ojs-STlmw5EkmD74,2152003
2
2
  modules/__init__.py,sha256=G58XleS-EJ2sX4Kehm-3N2m618_W2Es0Kg8CW_eBG7g,327
3
3
  modules/ai_actions.py,sha256=i5MJcM-7Y6CAvKUwxmxrVHeoZAVtAP7aRDdWM5KLkO0,33877
4
4
  modules/ai_attachment_manager.py,sha256=mA5ISI22qN9mH3DQFF4gOTciDyBt5xVR7sHTkgkTIlw,11361
@@ -6,7 +6,7 @@ modules/ai_file_viewer_dialog.py,sha256=lKKqUUlOEVgHmmu6aRxqH7P6ds-7dRLk4ltDyjCw
6
6
  modules/autofingers_engine.py,sha256=eJ7tBi7YJvTToe5hYTfnyGXB-qme_cHrOPZibaoR2Xw,17061
7
7
  modules/cafetran_docx_handler.py,sha256=_F7Jh0WPVaDnMhdxEsVSXuD1fN9r-S_V6i0gr86Pdfc,14076
8
8
  modules/config_manager.py,sha256=kurQ56_o4IzxvKkIKB6YFg69nti01LWWoyVt_JlCWkU,17971
9
- modules/database_manager.py,sha256=qpTiNzahrt6kTLplxB2lE-dxBxdPeeY2k6VmdwYdDxU,72181
9
+ modules/database_manager.py,sha256=kVZilnvjHkThMwVgqTnQ6-OL2Sbi8R6g8_iYOzSV5ts,73166
10
10
  modules/database_migrations.py,sha256=Y1onFsLDV_6vzJLOpNy3WCZDohBZ2jc4prM-g2_RwLE,14085
11
11
  modules/dejavurtf_handler.py,sha256=8NZPPYtHga40SZCypHjPoJPmZTvm9rD-eEUUab7mjtg,28156
12
12
  modules/document_analyzer.py,sha256=t1rVvqLaTcpQTEja228C7zZnh8dXshK4wA9t1E9aGVk,19524
@@ -59,7 +59,7 @@ modules/term_extractor.py,sha256=qPvKNCVXFTGEGwXNvvC0cfCmdb5c3WhzE38EOgKdKUI,112
59
59
  modules/termbase_entry_editor.py,sha256=iWO9CgLjMomGAqBXDsGAX7TFJvDOp2s_taS4gBL1rZY,35818
60
60
  modules/termbase_import_export.py,sha256=16IAY04IS_rgt0GH5UOUzUI5NoqAli4JMfMquxmFBm0,23552
61
61
  modules/termbase_manager.py,sha256=-PlGF6fIA7KYCteoQ8FZ_0SQZNRRBFAtLimHPbmhQ6w,44544
62
- modules/termview_widget.py,sha256=KNzgQ7dEFW5ANcbCsUPHJ268MYwBmV5MQG-9GQJoobY,52745
62
+ modules/termview_widget.py,sha256=Imflzotv1raLH-tL4l3MquCL2Pwb56oLIfds4hg7A20,53125
63
63
  modules/theme_manager.py,sha256=EOI_5pM2bXAadw08bbl92TLN-w28lbw4Zi1E8vQ-kM0,16694
64
64
  modules/tm_editor_dialog.py,sha256=AzGwq4QW641uFJdF8DljLTRRp4FLoYX3Pe4rlTjQWNg,3517
65
65
  modules/tm_manager_qt.py,sha256=h2bvXkRuboHf_RRz9-5FX35GVRlpXgRDWeXyj1QWtPs,54406
@@ -77,9 +77,9 @@ modules/unified_prompt_manager_qt.py,sha256=kpR-Tk1xmVyJGHnZdHm6NbGQPTZ1jB8t4tI_
77
77
  modules/voice_commands.py,sha256=iBb-gjWxRMLhFH7-InSRjYJz1EIDBNA2Pog8V7TtJaY,38516
78
78
  modules/voice_dictation.py,sha256=QmitXfkG-vRt5hIQATjphHdhXfqmwhzcQcbXB6aRzIg,16386
79
79
  modules/voice_dictation_lite.py,sha256=jorY0BmWE-8VczbtGrWwt1zbnOctMoSlWOsQrcufBcc,9423
80
- supervertaler-1.9.132.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
81
- supervertaler-1.9.132.dist-info/METADATA,sha256=3TgEBK08En64IJMCwc9ea5VUYzJ3vJ4_mpiaOmgCKjs,42458
82
- supervertaler-1.9.132.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
- supervertaler-1.9.132.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
84
- supervertaler-1.9.132.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
85
- supervertaler-1.9.132.dist-info/RECORD,,
80
+ supervertaler-1.9.145.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
81
+ supervertaler-1.9.145.dist-info/METADATA,sha256=sJ3uoEIMo4xK3XreAPZVbdwq0a_1HKPBwV0gn24-DFU,43528
82
+ supervertaler-1.9.145.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
+ supervertaler-1.9.145.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
84
+ supervertaler-1.9.145.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
85
+ supervertaler-1.9.145.dist-info/RECORD,,