supervertaler 1.9.180__py3-none-any.whl → 1.9.183__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
@@ -32,9 +32,9 @@ License: MIT
32
32
  """
33
33
 
34
34
  # Version Information.
35
- __version__ = "1.9.180"
35
+ __version__ = "1.9.183"
36
36
  __phase__ = "0.9"
37
- __release_date__ = "2026-01-28"
37
+ __release_date__ = "2026-01-31"
38
38
  __edition__ = "Qt"
39
39
 
40
40
  import sys
@@ -1659,16 +1659,12 @@ class ReadOnlyGridTextEditor(QTextEdit):
1659
1659
  matches_dict: Dictionary of {term: {'translation': str, 'priority': int}} or {term: str}
1660
1660
  """
1661
1661
  from PyQt6.QtGui import QTextCursor, QTextCharFormat, QColor, QFont
1662
-
1663
- print(f"[HIGHLIGHT DEBUG] highlight_termbase_matches called with {len(matches_dict) if matches_dict else 0} matches")
1664
-
1662
+
1665
1663
  # Get the document and create a cursor
1666
1664
  doc = self.document()
1667
1665
  text = self.toPlainText()
1668
1666
  text_lower = text.lower()
1669
1667
 
1670
- print(f"[HIGHLIGHT DEBUG] Widget text length: {len(text)}, text preview: {text[:60]}...")
1671
-
1672
1668
  # IMPORTANT: Always clear all previous formatting first to prevent inconsistent highlighting
1673
1669
  cursor = QTextCursor(doc)
1674
1670
  cursor.select(QTextCursor.SelectionType.Document)
@@ -1677,7 +1673,6 @@ class ReadOnlyGridTextEditor(QTextEdit):
1677
1673
 
1678
1674
  # If no matches, we're done (highlighting has been cleared)
1679
1675
  if not matches_dict:
1680
- print(f"[HIGHLIGHT DEBUG] No matches, returning after clear")
1681
1676
  return
1682
1677
 
1683
1678
  # Get highlight style from main window settings
@@ -1695,9 +1690,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
1695
1690
  dotted_color = settings.get('termbase_dotted_color', '#808080')
1696
1691
  break
1697
1692
  parent = parent.parent() if hasattr(parent, 'parent') else None
1698
-
1699
- print(f"[HIGHLIGHT DEBUG] Using style: {highlight_style}")
1700
-
1693
+
1701
1694
  # Sort matches by source term length (longest first) to avoid partial matches
1702
1695
  # Since dict keys are now term_ids, we need to extract source terms first
1703
1696
  term_entries = []
@@ -1706,11 +1699,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
1706
1699
  source_term = match_info.get('source', '')
1707
1700
  if source_term:
1708
1701
  term_entries.append((source_term, term_id, match_info))
1709
-
1710
- print(f"[HIGHLIGHT DEBUG] Built {len(term_entries)} term entries from matches")
1711
- if term_entries:
1712
- print(f"[HIGHLIGHT DEBUG] First few terms to search: {[t[0] for t in term_entries[:3]]}")
1713
-
1702
+
1714
1703
  # Sort by source term length (longest first)
1715
1704
  term_entries.sort(key=lambda x: len(x[0]), reverse=True)
1716
1705
 
@@ -1835,8 +1824,6 @@ class ReadOnlyGridTextEditor(QTextEdit):
1835
1824
  highlighted_ranges.append((idx, end_idx))
1836
1825
 
1837
1826
  start = end_idx
1838
-
1839
- print(f"[HIGHLIGHT DEBUG] Applied formatting to {found_count} term occurrences in text")
1840
1827
 
1841
1828
  def highlight_non_translatables(self, nt_matches: list, highlighted_ranges: list = None):
1842
1829
  """
@@ -6224,6 +6211,12 @@ class SupervertalerQt(QMainWindow):
6224
6211
  self.termbase_cache_lock = threading.Lock() # Thread-safe cache access
6225
6212
  self.termbase_batch_worker_thread = None # Background worker thread
6226
6213
  self.termbase_batch_stop_event = threading.Event() # Signal to stop background worker
6214
+
6215
+ # In-memory termbase index for instant lookups (v1.9.182)
6216
+ # Loaded once on project load, contains ALL terms from activated termbases
6217
+ # Structure: list of term dicts with pre-compiled regex patterns
6218
+ self.termbase_index = []
6219
+ self.termbase_index_lock = threading.Lock()
6227
6220
 
6228
6221
  # TM/MT/LLM prefetch cache for instant segment switching (like memoQ)
6229
6222
  # Maps segment ID → {"TM": [...], "MT": [...], "LLM": [...]}
@@ -6237,9 +6230,9 @@ class SupervertalerQt(QMainWindow):
6237
6230
  self.idle_prefetch_timer = None # QTimer for triggering prefetch after typing pause
6238
6231
  self.idle_prefetch_delay_ms = 1500 # Start prefetch 1.5s after user stops typing
6239
6232
 
6240
- # 🧪 EXPERIMENTAL: Cache kill switch for performance testing
6233
+ # Cache kill switch for performance testing
6241
6234
  # When True, all caches are bypassed - direct lookups every time
6242
- self.disable_all_caches = True
6235
+ self.disable_all_caches = False # v1.9.183: Default to False (caches ENABLED)
6243
6236
 
6244
6237
  # Undo/Redo stack for grid edits
6245
6238
  self.undo_stack = [] # List of (segment_id, old_target, new_target, old_status, new_status)
@@ -10335,12 +10328,9 @@ class SupervertalerQt(QMainWindow):
10335
10328
 
10336
10329
  # Superdocs removed (online GitBook will be used instead)
10337
10330
 
10338
- print("[DEBUG] About to create SuperlookupTab...")
10339
10331
  lookup_tab = SuperlookupTab(self, user_data_path=self.user_data_path)
10340
- print("[DEBUG] SuperlookupTab created successfully")
10341
10332
  self.lookup_tab = lookup_tab # Store reference for later use
10342
10333
  modules_tabs.addTab(lookup_tab, "🔍 Superlookup")
10343
- print("[DEBUG] Superlookup tab added to modules_tabs")
10344
10334
 
10345
10335
  # Supervoice - Voice Commands & Dictation
10346
10336
  supervoice_tab = self._create_voice_dictation_settings_tab()
@@ -12202,27 +12192,130 @@ class SupervertalerQt(QMainWindow):
12202
12192
  f"Error testing segmentation:\n\n{e}"
12203
12193
  )
12204
12194
 
12205
- def _update_both_termviews(self, source_text, termbase_list, nt_matches):
12195
+ def _update_both_termviews(self, source_text, termbase_list, nt_matches, status_hint=None):
12206
12196
  """Update all three Termview instances with the same data.
12207
-
12197
+
12208
12198
  Termview locations:
12209
12199
  1. Under grid (collapsible via View menu)
12210
12200
  2. Match Panel tab (top section)
12201
+
12202
+ Args:
12203
+ source_text: The source text for the current segment
12204
+ termbase_list: List of termbase match dictionaries
12205
+ nt_matches: List of NT (Never Translate) matches
12206
+ status_hint: Optional hint for display when no matches:
12207
+ 'no_termbases_activated' - no glossaries activated for project
12208
+ 'wrong_language' - activated glossaries don't match project language
12211
12209
  """
12212
12210
  # Update left Termview (under grid)
12213
12211
  if hasattr(self, 'termview_widget') and self.termview_widget:
12214
12212
  try:
12215
- self.termview_widget.update_with_matches(source_text, termbase_list, nt_matches)
12213
+ self.termview_widget.update_with_matches(source_text, termbase_list, nt_matches, status_hint)
12216
12214
  except Exception as e:
12217
12215
  self.log(f"Error updating left termview: {e}")
12218
-
12216
+
12219
12217
  # Update Match Panel Termview
12220
12218
  if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
12221
12219
  try:
12222
- self.termview_widget_match.update_with_matches(source_text, termbase_list, nt_matches)
12220
+ self.termview_widget_match.update_with_matches(source_text, termbase_list, nt_matches, status_hint)
12223
12221
  except Exception as e:
12224
12222
  self.log(f"Error updating Match Panel termview: {e}")
12225
-
12223
+
12224
+ def _update_termview_for_segment(self, segment):
12225
+ """Explicitly update termview for a segment (v1.9.182).
12226
+
12227
+ This is called directly from Ctrl+Enter navigation to ensure
12228
+ the termview updates immediately, bypassing the deferred timer approach.
12229
+ """
12230
+ if not segment or not hasattr(self, 'termview_widget'):
12231
+ return
12232
+
12233
+ try:
12234
+ # Use in-memory index for fast lookup
12235
+ stored_matches = self.find_termbase_matches_in_source(segment.source)
12236
+
12237
+ # Convert dict format to list format for termview
12238
+ termbase_matches = [
12239
+ {
12240
+ 'source_term': match_data.get('source', ''),
12241
+ 'target_term': match_data.get('translation', ''),
12242
+ 'termbase_name': match_data.get('termbase_name', ''),
12243
+ 'ranking': match_data.get('ranking', 99),
12244
+ 'is_project_termbase': match_data.get('is_project_termbase', False),
12245
+ 'term_id': match_data.get('term_id'),
12246
+ 'termbase_id': match_data.get('termbase_id'),
12247
+ 'notes': match_data.get('notes', '')
12248
+ }
12249
+ for match_data in stored_matches.values()
12250
+ ] if stored_matches else []
12251
+
12252
+ # Get NT matches
12253
+ nt_matches = self.find_nt_matches_in_source(segment.source)
12254
+
12255
+ # Get status hint
12256
+ status_hint = self._get_termbase_status_hint()
12257
+
12258
+ # Update both Termview widgets
12259
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
12260
+
12261
+ except Exception as e:
12262
+ self.log(f"Error in _update_termview_for_segment: {e}")
12263
+
12264
+ def _get_termbase_status_hint(self) -> str:
12265
+ """Check termbase activation status and return appropriate hint.
12266
+
12267
+ Returns:
12268
+ 'no_termbases_activated' - if no glossaries are activated for this project
12269
+ 'wrong_language' - if activated glossaries don't match project language pair
12270
+ None - if everything is correctly configured
12271
+ """
12272
+ if not self.current_project:
12273
+ return None
12274
+
12275
+ project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
12276
+ if not project_id:
12277
+ return None
12278
+
12279
+ # Check if termbase manager is available
12280
+ if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
12281
+ return None
12282
+
12283
+ try:
12284
+ # Get active termbase IDs for this project
12285
+ active_tb_ids = self.termbase_mgr.get_active_termbase_ids(project_id)
12286
+
12287
+ # Check if no termbases are activated
12288
+ if not active_tb_ids or len(active_tb_ids) == 0:
12289
+ return 'no_termbases_activated'
12290
+
12291
+ # Check if any activated termbases match the project's language pair
12292
+ project_source = (self.current_project.source_lang or '').lower()
12293
+ project_target = (self.current_project.target_lang or '').lower()
12294
+
12295
+ # Get all termbases and check language pairs
12296
+ all_termbases = self.termbase_mgr.get_all_termbases()
12297
+ has_matching_language = False
12298
+
12299
+ for tb in all_termbases:
12300
+ if tb['id'] in active_tb_ids:
12301
+ tb_source = (tb.get('source_lang') or '').lower()
12302
+ tb_target = (tb.get('target_lang') or '').lower()
12303
+ # Match if: no language set, or languages match (bidirectional)
12304
+ if (not tb_source and not tb_target) or \
12305
+ (tb_source == project_source and tb_target == project_target) or \
12306
+ (tb_source == project_target and tb_target == project_source):
12307
+ has_matching_language = True
12308
+ break
12309
+
12310
+ if not has_matching_language:
12311
+ return 'wrong_language'
12312
+
12313
+ return None # All good
12314
+
12315
+ except Exception as e:
12316
+ self.log(f"Error checking termbase status: {e}")
12317
+ return None
12318
+
12226
12319
  def _refresh_termbase_display_for_current_segment(self):
12227
12320
  """Refresh only termbase/glossary display for the current segment.
12228
12321
 
@@ -12282,9 +12375,12 @@ class SupervertalerQt(QMainWindow):
12282
12375
 
12283
12376
  # Get NT matches
12284
12377
  nt_matches = self.find_nt_matches_in_source(segment.source)
12285
-
12378
+
12379
+ # Get status hint for termbase activation
12380
+ status_hint = self._get_termbase_status_hint()
12381
+
12286
12382
  # Update both Termview widgets (left and right)
12287
- self._update_both_termviews(segment.source, termbase_list, nt_matches)
12383
+ self._update_both_termviews(segment.source, termbase_list, nt_matches, status_hint)
12288
12384
  except Exception as e:
12289
12385
  self.log(f"Error updating termview: {e}")
12290
12386
 
@@ -12752,6 +12848,39 @@ class SupervertalerQt(QMainWindow):
12752
12848
  # Use term_id as key to avoid duplicates
12753
12849
  self.termbase_cache[segment_id][term_id] = new_match
12754
12850
  self.log(f"⚡ Added term directly to cache (instant update)")
12851
+
12852
+ # v1.9.182: Also add to in-memory termbase index for future lookups
12853
+ import re
12854
+ source_lower = source_text.lower().strip()
12855
+ try:
12856
+ if any(c in source_lower for c in '.%,/-'):
12857
+ pattern = re.compile(r'(?<!\w)' + re.escape(source_lower) + r'(?!\w)')
12858
+ else:
12859
+ pattern = re.compile(r'\b' + re.escape(source_lower) + r'\b')
12860
+ except re.error:
12861
+ pattern = None
12862
+
12863
+ index_entry = {
12864
+ 'term_id': term_id,
12865
+ 'source_term': source_text,
12866
+ 'source_term_lower': source_lower,
12867
+ 'target_term': target_text,
12868
+ 'termbase_id': target_termbase['id'],
12869
+ 'priority': 99,
12870
+ 'domain': '',
12871
+ 'notes': '',
12872
+ 'project': '',
12873
+ 'client': '',
12874
+ 'forbidden': False,
12875
+ 'is_project_termbase': False,
12876
+ 'termbase_name': target_termbase['name'],
12877
+ 'ranking': glossary_rank,
12878
+ 'pattern': pattern,
12879
+ }
12880
+ with self.termbase_index_lock:
12881
+ self.termbase_index.append(index_entry)
12882
+ # Re-sort by length (longest first) for proper phrase matching
12883
+ self.termbase_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
12755
12884
 
12756
12885
  # Update TermView widget with the new term
12757
12886
  if hasattr(self, 'termview_widget') and self.termview_widget:
@@ -12776,9 +12905,12 @@ class SupervertalerQt(QMainWindow):
12776
12905
 
12777
12906
  # Get NT matches
12778
12907
  nt_matches = self.find_nt_matches_in_source(segment.source)
12779
-
12908
+
12909
+ # Get status hint (although after adding a term, it should be fine)
12910
+ status_hint = self._get_termbase_status_hint()
12911
+
12780
12912
  # Update both Termview widgets (left and right)
12781
- self._update_both_termviews(segment.source, termbase_list, nt_matches)
12913
+ self._update_both_termviews(segment.source, termbase_list, nt_matches, status_hint)
12782
12914
  self.log(f"✅ Both TermView widgets updated instantly with new term")
12783
12915
 
12784
12916
  # Update source cell highlighting with updated cache
@@ -13634,15 +13766,16 @@ class SupervertalerQt(QMainWindow):
13634
13766
  # Use 0 (global) when no project is loaded - allows Superlookup to work
13635
13767
  curr_proj = self.current_project if hasattr(self, 'current_project') else None
13636
13768
  curr_proj_id = curr_proj.id if (curr_proj and hasattr(curr_proj, 'id')) else 0 # 0 = global
13637
-
13769
+
13638
13770
  if checked:
13639
13771
  termbase_mgr.activate_termbase(tb_id, curr_proj_id)
13640
13772
  else:
13641
13773
  termbase_mgr.deactivate_termbase(tb_id, curr_proj_id)
13642
-
13643
- # Clear cache and refresh
13774
+
13775
+ # Clear cache and rebuild in-memory index (v1.9.182)
13644
13776
  with self.termbase_cache_lock:
13645
13777
  self.termbase_cache.clear()
13778
+ self._build_termbase_index() # Rebuild index with new activation state
13646
13779
  refresh_termbase_list()
13647
13780
 
13648
13781
  read_checkbox.toggled.connect(on_read_toggle)
@@ -16971,7 +17104,7 @@ class SupervertalerQt(QMainWindow):
16971
17104
 
16972
17105
  # Cache kill switch
16973
17106
  disable_cache_cb = CheckmarkCheckBox("Disable ALL caches (direct lookups every time)")
16974
- disable_cache_cb.setChecked(general_settings.get('disable_all_caches', True))
17107
+ disable_cache_cb.setChecked(general_settings.get('disable_all_caches', False))
16975
17108
  disable_cache_cb.setToolTip(
16976
17109
  "When enabled, ALL caching is bypassed:\n"
16977
17110
  "• Termbase cache\n"
@@ -19615,7 +19748,7 @@ class SupervertalerQt(QMainWindow):
19615
19748
  'results_compare_font_size': 9,
19616
19749
  'autohotkey_path': ahk_path_edit.text().strip() if ahk_path_edit is not None else existing_settings.get('autohotkey_path', ''),
19617
19750
  'enable_sound_effects': sound_effects_cb.isChecked() if sound_effects_cb is not None else existing_settings.get('enable_sound_effects', False),
19618
- 'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', True)
19751
+ 'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', False)
19619
19752
  }
19620
19753
 
19621
19754
  # Keep a fast-access instance value
@@ -21558,36 +21691,35 @@ class SupervertalerQt(QMainWindow):
21558
21691
 
21559
21692
  # Source language
21560
21693
  source_lang_combo = QComboBox()
21561
- common_langs = [
21562
- ("English", "en"),
21563
- ("Dutch", "nl"),
21564
- ("German", "de"),
21565
- ("French", "fr"),
21566
- ("Spanish", "es"),
21567
- ("Italian", "it"),
21568
- ("Portuguese", "pt"),
21569
- ("Russian", "ru"),
21570
- ("Chinese", "zh"),
21571
- ("Japanese", "ja"),
21694
+ # Full language list matching Settings → Language Pair (with ISO 639-1 codes)
21695
+ available_langs = [
21696
+ ("Afrikaans", "af"), ("Albanian", "sq"), ("Arabic", "ar"), ("Armenian", "hy"),
21697
+ ("Basque", "eu"), ("Bengali", "bn"), ("Bulgarian", "bg"), ("Catalan", "ca"),
21698
+ ("Chinese (Simplified)", "zh-CN"), ("Chinese (Traditional)", "zh-TW"),
21699
+ ("Croatian", "hr"), ("Czech", "cs"), ("Danish", "da"), ("Dutch", "nl"),
21700
+ ("English", "en"), ("Estonian", "et"), ("Finnish", "fi"), ("French", "fr"),
21701
+ ("Galician", "gl"), ("Georgian", "ka"), ("German", "de"), ("Greek", "el"),
21702
+ ("Hebrew", "he"), ("Hindi", "hi"), ("Hungarian", "hu"), ("Icelandic", "is"),
21703
+ ("Indonesian", "id"), ("Irish", "ga"), ("Italian", "it"), ("Japanese", "ja"),
21704
+ ("Korean", "ko"), ("Latvian", "lv"), ("Lithuanian", "lt"), ("Macedonian", "mk"),
21705
+ ("Malay", "ms"), ("Norwegian", "no"), ("Persian", "fa"), ("Polish", "pl"),
21706
+ ("Portuguese", "pt"), ("Romanian", "ro"), ("Russian", "ru"), ("Serbian", "sr"),
21707
+ ("Slovak", "sk"), ("Slovenian", "sl"), ("Spanish", "es"), ("Swahili", "sw"),
21708
+ ("Swedish", "sv"), ("Thai", "th"), ("Turkish", "tr"), ("Ukrainian", "uk"),
21709
+ ("Urdu", "ur"), ("Vietnamese", "vi"), ("Welsh", "cy"),
21572
21710
  ]
21573
- for lang_name, lang_code in common_langs:
21711
+ for lang_name, lang_code in available_langs:
21574
21712
  source_lang_combo.addItem(lang_name, lang_code)
21575
21713
  settings_layout.addRow("Source Language:", source_lang_combo)
21576
-
21714
+
21577
21715
  # Target language
21578
21716
  target_lang_combo = QComboBox()
21579
- for lang_name, lang_code in common_langs:
21717
+ for lang_name, lang_code in available_langs:
21580
21718
  target_lang_combo.addItem(lang_name, lang_code)
21581
-
21582
- # Set defaults based on global language settings (if in common_langs)
21583
- try:
21584
- for lang_name, lang_code in common_langs:
21585
- if lang_name == self.source_language:
21586
- source_lang_combo.setCurrentText(lang_name)
21587
- if lang_name == self.target_language:
21588
- target_lang_combo.setCurrentText(lang_name)
21589
- except:
21590
- target_lang_combo.setCurrentIndex(1) # Fallback to Dutch
21719
+
21720
+ # Set defaults based on global language settings
21721
+ source_lang_combo.setCurrentText(self.source_language)
21722
+ target_lang_combo.setCurrentText(self.target_language)
21591
21723
 
21592
21724
  settings_layout.addRow("Target Language:", target_lang_combo)
21593
21725
 
@@ -21709,7 +21841,11 @@ class SupervertalerQt(QMainWindow):
21709
21841
  target_lang=target_lang,
21710
21842
  segments=[]
21711
21843
  )
21712
-
21844
+
21845
+ # Sync global language settings with new project languages
21846
+ self.source_language = source_lang
21847
+ self.target_language = target_lang
21848
+
21713
21849
  # Process source text if provided
21714
21850
  source_text = text_input.toPlainText().strip()
21715
21851
  if source_text:
@@ -21819,7 +21955,13 @@ class SupervertalerQt(QMainWindow):
21819
21955
  self.current_project = Project.from_dict(data)
21820
21956
  self.project_file_path = file_path
21821
21957
  self.project_modified = False
21822
-
21958
+
21959
+ # Sync global language settings with project languages
21960
+ if self.current_project.source_lang:
21961
+ self.source_language = self.current_project.source_lang
21962
+ if self.current_project.target_lang:
21963
+ self.target_language = self.current_project.target_lang
21964
+
21823
21965
  # Restore prompt settings if they exist (unified library)
21824
21966
  if hasattr(self.current_project, 'prompt_settings') and self.current_project.prompt_settings:
21825
21967
  prompt_settings = self.current_project.prompt_settings
@@ -22174,7 +22316,154 @@ class SupervertalerQt(QMainWindow):
22174
22316
  except Exception as e:
22175
22317
  QMessageBox.critical(self, "Error", f"Failed to load project:\n{str(e)}")
22176
22318
  self.log(f"✗ Error loading project: {e}")
22177
-
22319
+
22320
+ def _build_termbase_index(self):
22321
+ """
22322
+ Build in-memory index of ALL terms from activated termbases (v1.9.182).
22323
+
22324
+ This is called ONCE on project load and replaces thousands of per-word
22325
+ database queries with a single bulk load + fast in-memory lookups.
22326
+
22327
+ Performance: Reduces 349-segment termbase search from 365 seconds to <1 second.
22328
+ """
22329
+ import re
22330
+ import time
22331
+ start_time = time.time()
22332
+
22333
+ if not self.current_project or not hasattr(self, 'db_manager') or not self.db_manager:
22334
+ return
22335
+
22336
+ project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
22337
+
22338
+ # Query ALL terms from activated termbases in ONE query
22339
+ # This replaces ~17,500 individual queries (349 segments × 50 words each)
22340
+ query = """
22341
+ SELECT
22342
+ t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
22343
+ t.domain, t.notes, t.project, t.client, t.forbidden,
22344
+ tb.is_project_termbase, tb.name as termbase_name,
22345
+ COALESCE(ta.priority, tb.ranking) as ranking
22346
+ FROM termbase_terms t
22347
+ LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
22348
+ LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id
22349
+ AND ta.project_id = ? AND ta.is_active = 1
22350
+ WHERE (ta.is_active = 1 OR tb.is_project_termbase = 1)
22351
+ """
22352
+
22353
+ new_index = []
22354
+ try:
22355
+ self.db_manager.cursor.execute(query, [project_id or 0])
22356
+ rows = self.db_manager.cursor.fetchall()
22357
+
22358
+ for row in rows:
22359
+ source_term = row[1] # source_term
22360
+ if not source_term:
22361
+ continue
22362
+
22363
+ source_term_lower = source_term.lower().strip()
22364
+ if len(source_term_lower) < 2:
22365
+ continue
22366
+
22367
+ # Pre-compile regex pattern for word-boundary matching
22368
+ # This avoids recompiling the same pattern thousands of times
22369
+ try:
22370
+ # Handle terms with punctuation differently
22371
+ if any(c in source_term_lower for c in '.%,/-'):
22372
+ pattern = re.compile(r'(?<!\w)' + re.escape(source_term_lower) + r'(?!\w)')
22373
+ else:
22374
+ pattern = re.compile(r'\b' + re.escape(source_term_lower) + r'\b')
22375
+ except re.error:
22376
+ # If regex fails, use simple substring matching
22377
+ pattern = None
22378
+
22379
+ new_index.append({
22380
+ 'term_id': row[0],
22381
+ 'source_term': source_term,
22382
+ 'source_term_lower': source_term_lower,
22383
+ 'target_term': row[2],
22384
+ 'termbase_id': row[3],
22385
+ 'priority': row[4],
22386
+ 'domain': row[5],
22387
+ 'notes': row[6],
22388
+ 'project': row[7],
22389
+ 'client': row[8],
22390
+ 'forbidden': row[9],
22391
+ 'is_project_termbase': row[10],
22392
+ 'termbase_name': row[11],
22393
+ 'ranking': row[12],
22394
+ 'pattern': pattern, # Pre-compiled regex
22395
+ })
22396
+
22397
+ # Sort by term length (longest first) for better phrase matching
22398
+ new_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
22399
+
22400
+ # Thread-safe update of the index
22401
+ with self.termbase_index_lock:
22402
+ self.termbase_index = new_index
22403
+
22404
+ elapsed = time.time() - start_time
22405
+ self.log(f"✅ Built termbase index: {len(new_index)} terms in {elapsed:.2f}s")
22406
+
22407
+ except Exception as e:
22408
+ self.log(f"❌ Failed to build termbase index: {e}")
22409
+ import traceback
22410
+ self.log(traceback.format_exc())
22411
+
22412
+ def _search_termbase_in_memory(self, source_text: str) -> dict:
22413
+ """
22414
+ Search termbase using in-memory index (v1.9.182).
22415
+
22416
+ This replaces _search_termbases_thread_safe() for batch operations.
22417
+ Instead of N database queries (one per word), we do:
22418
+ - 1 pass through the index (typically ~1000 terms)
22419
+ - Fast string 'in' check + pre-compiled regex validation
22420
+
22421
+ Performance: <1ms per segment vs 1+ second per segment.
22422
+ """
22423
+ if not source_text:
22424
+ return {}
22425
+
22426
+ with self.termbase_index_lock:
22427
+ if not self.termbase_index:
22428
+ return {}
22429
+ index = self.termbase_index # Local reference for thread safety
22430
+
22431
+ source_lower = source_text.lower()
22432
+ matches = {}
22433
+
22434
+ for term in index:
22435
+ term_lower = term['source_term_lower']
22436
+
22437
+ # Quick substring check first (very fast, implemented in C)
22438
+ if term_lower not in source_lower:
22439
+ continue
22440
+
22441
+ # Word boundary validation using pre-compiled pattern
22442
+ pattern = term.get('pattern')
22443
+ if pattern:
22444
+ if not pattern.search(source_lower):
22445
+ continue
22446
+
22447
+ # Term matches! Add to results
22448
+ term_id = term['term_id']
22449
+ matches[term_id] = {
22450
+ 'source': term['source_term'],
22451
+ 'translation': term['target_term'],
22452
+ 'term_id': term_id,
22453
+ 'termbase_id': term['termbase_id'],
22454
+ 'termbase_name': term['termbase_name'],
22455
+ 'priority': term['priority'],
22456
+ 'ranking': term['ranking'],
22457
+ 'is_project_termbase': term['is_project_termbase'],
22458
+ 'forbidden': term['forbidden'],
22459
+ 'domain': term['domain'],
22460
+ 'notes': term['notes'],
22461
+ 'project': term['project'],
22462
+ 'client': term['client'],
22463
+ }
22464
+
22465
+ return matches
22466
+
22178
22467
  def _start_termbase_batch_worker(self):
22179
22468
  """
22180
22469
  Start background thread to batch-process termbase matches for all segments.
@@ -22182,21 +22471,25 @@ class SupervertalerQt(QMainWindow):
22182
22471
  """
22183
22472
  if not self.current_project or len(self.current_project.segments) == 0:
22184
22473
  return
22185
-
22474
+
22475
+ # Build in-memory termbase index FIRST (v1.9.182)
22476
+ # This is the key optimization: load all terms once, then do fast in-memory lookups
22477
+ self._build_termbase_index()
22478
+
22186
22479
  # 🧪 EXPERIMENTAL: Skip batch worker if cache kill switch is enabled
22187
22480
  if getattr(self, 'disable_all_caches', False):
22188
22481
  self.log("🧪 Termbase batch worker SKIPPED (caches disabled)")
22189
22482
  return
22190
-
22483
+
22191
22484
  # Stop any existing worker thread
22192
22485
  self.termbase_batch_stop_event.set()
22193
22486
  if self.termbase_batch_worker_thread and self.termbase_batch_worker_thread.is_alive():
22194
22487
  self.log("⏹️ Stopping existing termbase batch worker...")
22195
22488
  self.termbase_batch_worker_thread.join(timeout=2)
22196
-
22489
+
22197
22490
  # Reset stop event for new worker
22198
22491
  self.termbase_batch_stop_event.clear()
22199
-
22492
+
22200
22493
  # Start new background worker thread
22201
22494
  segment_count = len(self.current_project.segments)
22202
22495
  self.log(f"🔄 Starting background termbase batch processor for {segment_count} segments...")
@@ -22212,96 +22505,60 @@ class SupervertalerQt(QMainWindow):
22212
22505
  """
22213
22506
  Background worker thread: process all segments and populate termbase cache.
22214
22507
  Runs in separate thread to not block UI.
22215
-
22216
- IMPORTANT: Creates its own database connection to avoid SQLite threading errors.
22508
+
22509
+ v1.9.182: Now uses in-memory termbase index for 1000x faster lookups.
22510
+ Old approach: 365 seconds for 349 segments (1 second/segment)
22511
+ New approach: <1 second for 349 segments (<3ms/segment)
22217
22512
  """
22218
22513
  if not segments:
22219
22514
  return
22220
-
22221
- # Create a separate database connection for this thread
22222
- # SQLite connections are thread-local and cannot be shared across threads
22223
- import sqlite3
22224
- try:
22225
- thread_db_connection = sqlite3.connect(self.db_manager.db_path)
22226
- thread_db_connection.row_factory = sqlite3.Row
22227
- thread_db_cursor = thread_db_connection.cursor()
22228
- except Exception as e:
22229
- self.log(f"❌ Failed to create database connection in batch worker: {e}")
22230
- return
22231
-
22515
+
22232
22516
  try:
22233
22517
  processed = 0
22234
22518
  cached = 0
22519
+ with_matches = 0
22235
22520
  start_time = time.time()
22236
-
22521
+
22237
22522
  for segment in segments:
22238
22523
  # Check if stop event was signaled (user closed project or started new one)
22239
22524
  if self.termbase_batch_stop_event.is_set():
22240
22525
  self.log(f"⏹️ Termbase batch worker stopped by user (processed {processed} segments)")
22241
22526
  break
22242
-
22527
+
22243
22528
  segment_id = segment.id
22244
-
22529
+
22245
22530
  # Skip if already in cache (thread-safe check)
22246
22531
  with self.termbase_cache_lock:
22247
22532
  if segment_id in self.termbase_cache:
22248
22533
  cached += 1
22249
22534
  continue
22250
-
22251
- # Search termbase for this segment using thread-local database connection
22535
+
22536
+ # v1.9.182: Use in-memory index for instant lookup (no database queries!)
22252
22537
  try:
22253
- # Manually query the database using thread-local connection
22254
- # Pass project_id to filter by activated termbases only
22255
- current_project_id = self.current_project.id if (self.current_project and hasattr(self.current_project, 'id')) else None
22256
- matches = self._search_termbases_thread_safe(
22257
- segment.source,
22258
- thread_db_cursor,
22259
- source_lang=self.current_project.source_lang if self.current_project else None,
22260
- target_lang=self.current_project.target_lang if self.current_project else None,
22261
- project_id=current_project_id
22262
- )
22263
-
22538
+ matches = self._search_termbase_in_memory(segment.source)
22539
+
22540
+ # Store in cache (thread-safe) - even empty results to avoid re-lookup
22541
+ with self.termbase_cache_lock:
22542
+ self.termbase_cache[segment_id] = matches
22543
+
22544
+ processed += 1
22264
22545
  if matches:
22265
- # Store in cache (thread-safe)
22266
- with self.termbase_cache_lock:
22267
- self.termbase_cache[segment_id] = matches
22268
-
22269
- processed += 1
22270
-
22271
- # Log progress every 100 segments
22272
- if processed % 100 == 0:
22273
- elapsed = time.time() - start_time
22274
- rate = processed / elapsed if elapsed > 0 else 0
22275
- remaining = len(segments) - processed
22276
- eta_seconds = remaining / rate if rate > 0 else 0
22277
- self.log(f"📊 Batch progress: {processed}/{len(segments)} cached " +
22278
- f"({rate:.1f} seg/sec, ETA: {int(eta_seconds)}s)")
22279
-
22546
+ with_matches += 1
22547
+
22280
22548
  except Exception as e:
22281
22549
  self.log(f"❌ Error processing segment {segment_id} in batch worker: {e}")
22282
22550
  continue
22283
-
22284
- # Small delay to prevent CPU saturation (let UI thread work)
22285
- time.sleep(0.001) # 1ms delay between segments
22286
-
22551
+
22287
22552
  elapsed = time.time() - start_time
22288
22553
  total_cached = len(self.termbase_cache)
22289
- self.log(f"✅ Termbase batch worker complete: {processed} new + {cached} existing = " +
22290
- f"{total_cached} total cached in {elapsed:.1f}s")
22291
-
22554
+ rate = processed / elapsed if elapsed > 0 else 0
22555
+ self.log(f"✅ Termbase batch worker complete: {processed} segments in {elapsed:.2f}s " +
22556
+ f"({rate:.0f} seg/sec, {with_matches} with matches)")
22557
+
22292
22558
  except Exception as e:
22293
22559
  self.log(f"❌ Termbase batch worker error: {e}")
22294
22560
  import traceback
22295
22561
  self.log(traceback.format_exc())
22296
-
22297
- finally:
22298
- # Close thread-local database connection
22299
- try:
22300
- thread_db_cursor.close()
22301
- thread_db_connection.close()
22302
- self.log("✓ Closed thread-local database connection in batch worker")
22303
- except:
22304
- pass
22305
22562
 
22306
22563
  def _search_termbases_thread_safe(self, source_text: str, cursor, source_lang: str = None, target_lang: str = None, project_id: int = None) -> Dict[str, str]:
22307
22564
  """
@@ -22501,11 +22758,8 @@ class SupervertalerQt(QMainWindow):
22501
22758
  Also triggers PROACTIVE HIGHLIGHTING for upcoming segments with glossary matches.
22502
22759
  """
22503
22760
  import json
22504
-
22505
- print(f"[PROACTIVE DEBUG] _trigger_idle_prefetch called for row {current_row}")
22506
-
22761
+
22507
22762
  if not self.current_project or current_row < 0:
22508
- print(f"[PROACTIVE DEBUG] Early exit: no project or invalid row")
22509
22763
  return
22510
22764
 
22511
22765
  try:
@@ -22514,9 +22768,7 @@ class SupervertalerQt(QMainWindow):
22514
22768
  already_cached_ids = []
22515
22769
  start_idx = current_row + 1
22516
22770
  end_idx = min(start_idx + 5, len(self.current_project.segments))
22517
-
22518
- print(f"[PROACTIVE DEBUG] Checking segments {start_idx} to {end_idx}")
22519
-
22771
+
22520
22772
  for seg in self.current_project.segments[start_idx:end_idx]:
22521
22773
  # Check if already cached
22522
22774
  with self.translation_matches_cache_lock:
@@ -22524,23 +22776,19 @@ class SupervertalerQt(QMainWindow):
22524
22776
  next_segment_ids.append(seg.id)
22525
22777
  else:
22526
22778
  already_cached_ids.append(seg.id)
22527
-
22528
- print(f"[PROACTIVE DEBUG] Already cached IDs: {already_cached_ids}, Need prefetch: {next_segment_ids}")
22529
-
22779
+
22530
22780
  # For already-cached segments, trigger proactive highlighting immediately
22531
22781
  # This handles the case where segments were cached earlier but not highlighted
22532
22782
  for seg_id in already_cached_ids:
22533
22783
  try:
22534
22784
  with self.termbase_cache_lock:
22535
22785
  termbase_raw = self.termbase_cache.get(seg_id, {})
22536
- print(f"[PROACTIVE DEBUG] Segment {seg_id} termbase cache: {len(termbase_raw) if termbase_raw else 0} matches")
22537
22786
  if termbase_raw:
22538
22787
  termbase_json = json.dumps(termbase_raw)
22539
22788
  # Apply highlighting on main thread (we're already on main thread here)
22540
- print(f"[PROACTIVE DEBUG] Calling _apply_proactive_highlighting for seg {seg_id}")
22541
22789
  self._apply_proactive_highlighting(seg_id, termbase_json)
22542
- except Exception as e:
22543
- print(f"[PROACTIVE DEBUG] Error for seg {seg_id}: {e}")
22790
+ except Exception:
22791
+ pass # Silent failure for proactive highlighting
22544
22792
 
22545
22793
  if next_segment_ids:
22546
22794
  # Start prefetch in background (silent, no logging)
@@ -22622,43 +22870,35 @@ class SupervertalerQt(QMainWindow):
22622
22870
 
22623
22871
  # Fetch TM/termbase matches (pass cursor for thread-safe termbase lookups)
22624
22872
  matches = self._fetch_all_matches_for_segment(segment, thread_db_cursor)
22625
-
22626
- # Only cache if we got at least one match (don't cache empty results)
22627
- # This prevents "empty cache hits" when TM database is still empty
22873
+
22874
+ # Count matches for logging and proactive highlighting
22628
22875
  tm_count = len(matches.get("TM", []))
22629
22876
  tb_count = len(matches.get("Termbases", []))
22630
22877
  mt_count = len(matches.get("MT", []))
22631
22878
  llm_count = len(matches.get("LLM", []))
22632
22879
  total_matches = tm_count + tb_count + mt_count + llm_count
22633
22880
 
22634
- print(f"[PREFETCH DEBUG] Segment {segment_id}: TM={tm_count}, TB={tb_count}, MT={mt_count}, LLM={llm_count}")
22635
-
22881
+ # Only cache results if we found something
22882
+ # Don't cache empty results - let main thread do fresh lookup
22636
22883
  if total_matches > 0:
22637
- # Store in cache only if we have results
22638
22884
  with self.translation_matches_cache_lock:
22639
22885
  self.translation_matches_cache[segment_id] = matches
22640
-
22641
- # PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
22642
- # This makes upcoming segments show their glossary matches immediately
22643
- if tb_count > 0:
22644
- try:
22645
- # Extract raw termbase matches from cache for highlighting
22646
- with self.termbase_cache_lock:
22647
- termbase_raw = self.termbase_cache.get(segment_id, {})
22648
-
22649
- print(f"[PREFETCH DEBUG] Segment {segment_id}: termbase_raw has {len(termbase_raw) if termbase_raw else 0} entries")
22650
-
22651
- if termbase_raw:
22652
- # Convert to JSON for thread-safe signal transfer
22653
- termbase_json = json.dumps(termbase_raw)
22654
- # Emit signal - will be handled on main thread
22655
- print(f"[PREFETCH DEBUG] Emitting proactive highlight signal for segment {segment_id}")
22656
- self._proactive_highlight_signal.emit(segment_id, termbase_json)
22657
- else:
22658
- print(f"[PREFETCH DEBUG] WARNING: tb_count={tb_count} but termbase_raw is empty!")
22659
- except Exception as e:
22660
- print(f"[PREFETCH DEBUG] ERROR emitting signal: {e}")
22661
- # else: Don't cache empty results - let it fall through to slow lookup next time
22886
+
22887
+ # PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
22888
+ # This makes upcoming segments show their glossary matches immediately
22889
+ if tb_count > 0:
22890
+ try:
22891
+ # Extract raw termbase matches from cache for highlighting
22892
+ with self.termbase_cache_lock:
22893
+ termbase_raw = self.termbase_cache.get(segment_id, {})
22894
+
22895
+ if termbase_raw:
22896
+ # Convert to JSON for thread-safe signal transfer
22897
+ termbase_json = json.dumps(termbase_raw)
22898
+ # Emit signal - will be handled on main thread
22899
+ self._proactive_highlight_signal.emit(segment_id, termbase_json)
22900
+ except Exception:
22901
+ pass # Silent fail for proactive highlighting
22662
22902
 
22663
22903
  except Exception as e:
22664
22904
  self.log(f"Error in prefetch worker: {e}")
@@ -22708,31 +22948,9 @@ class SupervertalerQt(QMainWindow):
22708
22948
  source_lang_code = self._convert_language_to_code(source_lang)
22709
22949
  target_lang_code = self._convert_language_to_code(target_lang)
22710
22950
 
22711
- # 1. TM matches (if enabled) - thread-safe check
22712
- enable_tm = getattr(self, 'enable_tm_matching', True) # Default to True if not set
22713
- if enable_tm and hasattr(self, 'db_manager') and self.db_manager:
22714
- try:
22715
- tm_results = self.db_manager.search_translation_memory(
22716
- segment.source,
22717
- source_lang,
22718
- target_lang,
22719
- limit=5
22720
- )
22721
-
22722
- if tm_results: # Only add if we got results
22723
- for tm_match in tm_results:
22724
- match_obj = TranslationMatch(
22725
- source=tm_match.get('source', ''),
22726
- target=tm_match.get('target', ''),
22727
- relevance=tm_match.get('similarity', 0),
22728
- metadata={'tm_name': tm_match.get('tm_id', 'project')},
22729
- match_type='TM',
22730
- compare_source=tm_match.get('source', ''),
22731
- provider_code='TM'
22732
- )
22733
- matches_dict["TM"].append(match_obj)
22734
- except Exception as e:
22735
- pass # Silently continue
22951
+ # 1. TM matches - SKIP in prefetch worker (TM search not thread-safe)
22952
+ # TM will be fetched on-demand when user navigates to segment
22953
+ pass
22736
22954
 
22737
22955
  # 2. MT matches (if enabled)
22738
22956
  if self.enable_mt_matching:
@@ -22907,8 +23125,9 @@ class SupervertalerQt(QMainWindow):
22907
23125
  mode_note = " (overwrite)" if overwrite_mode else ""
22908
23126
  msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
22909
23127
  self._queue_tm_save_log(msg)
22910
- # Invalidate cache so prefetched segments get fresh TM matches
22911
- self.invalidate_translation_cache()
23128
+ # NOTE: Removed cache invalidation here - it was destroying batch worker's cache
23129
+ # on every Ctrl+Enter, making navigation extremely slow. The small chance of
23130
+ # seeing stale TM matches is far less important than responsive navigation.
22912
23131
 
22913
23132
  def invalidate_translation_cache(self, smart_invalidation=True):
22914
23133
  """
@@ -23734,9 +23953,9 @@ class SupervertalerQt(QMainWindow):
23734
23953
  # Initialize TM for this project
23735
23954
  self.initialize_tm_database()
23736
23955
 
23737
- # Deactivate all resources for new project (user explicitly activates what they need)
23956
+ # Deactivate all resources for new project, then auto-activate language-matching ones
23738
23957
  self._deactivate_all_resources_for_new_project()
23739
-
23958
+
23740
23959
  # Auto-resize rows for better initial display
23741
23960
  self.auto_resize_rows()
23742
23961
 
@@ -26210,7 +26429,11 @@ class SupervertalerQt(QMainWindow):
26210
26429
 
26211
26430
  # Store memoQ source path in project for persistence across saves
26212
26431
  self.current_project.memoq_source_path = file_path
26213
-
26432
+
26433
+ # Sync global language settings with imported project languages
26434
+ self.source_language = source_lang
26435
+ self.target_language = target_lang
26436
+
26214
26437
  # Create segments with simple sequential IDs
26215
26438
  for idx, source_text in enumerate(source_segments):
26216
26439
  existing_target = target_segments[idx] if idx < len(target_segments) else ""
@@ -26234,15 +26457,15 @@ class SupervertalerQt(QMainWindow):
26234
26457
  self.load_segments_to_grid()
26235
26458
  self.initialize_tm_database()
26236
26459
 
26237
- # Deactivate all resources for new project (user explicitly activates what they need)
26460
+ # Deactivate all resources for new project, then auto-activate language-matching ones
26238
26461
  self._deactivate_all_resources_for_new_project()
26239
-
26462
+
26240
26463
  # Auto-resize rows for better initial display
26241
26464
  self.auto_resize_rows()
26242
-
26465
+
26243
26466
  # Initialize spellcheck for target language
26244
26467
  self._initialize_spellcheck_for_target_language(target_lang)
26245
-
26468
+
26246
26469
  # If smart formatting was used, auto-enable Tags view so user sees the tags
26247
26470
  if self.memoq_smart_formatting:
26248
26471
  self._enable_tag_view_after_import()
@@ -26731,7 +26954,11 @@ class SupervertalerQt(QMainWindow):
26731
26954
 
26732
26955
  # Store memoQ XLIFF source path in project for persistence across saves
26733
26956
  self.current_project.mqxliff_source_path = file_path
26734
-
26957
+
26958
+ # Sync global language settings with imported project languages
26959
+ self.source_language = source_lang
26960
+ self.target_language = target_lang
26961
+
26735
26962
  # Update UI
26736
26963
  self.project_file_path = None
26737
26964
  self.project_modified = True
@@ -26739,15 +26966,15 @@ class SupervertalerQt(QMainWindow):
26739
26966
  self.load_segments_to_grid()
26740
26967
  self.initialize_tm_database()
26741
26968
 
26742
- # Deactivate all resources for new project (user explicitly activates what they need)
26969
+ # Deactivate all resources for new project, then auto-activate language-matching ones
26743
26970
  self._deactivate_all_resources_for_new_project()
26744
-
26971
+
26745
26972
  # Auto-resize rows for better initial display
26746
26973
  self.auto_resize_rows()
26747
-
26974
+
26748
26975
  # Initialize spellcheck for target language
26749
26976
  self._initialize_spellcheck_for_target_language(target_lang)
26750
-
26977
+
26751
26978
  # Log success
26752
26979
  self.log(f"✓ Imported {len(segments)} segments from memoQ XLIFF: {Path(file_path).name}")
26753
26980
  self.log(f" Source: {source_lang}, Target: {target_lang}")
@@ -27006,7 +27233,11 @@ class SupervertalerQt(QMainWindow):
27006
27233
 
27007
27234
  # Store CafeTran source path in project for persistence across saves
27008
27235
  self.current_project.cafetran_source_path = file_path
27009
-
27236
+
27237
+ # Sync global language settings with imported project languages
27238
+ self.source_language = self.current_project.source_lang
27239
+ self.target_language = self.current_project.target_lang
27240
+
27010
27241
  # Update UI
27011
27242
  self.project_file_path = None
27012
27243
  self.project_modified = True
@@ -27014,16 +27245,16 @@ class SupervertalerQt(QMainWindow):
27014
27245
  self.load_segments_to_grid()
27015
27246
  self.initialize_tm_database()
27016
27247
 
27017
- # Deactivate all resources for new project (user explicitly activates what they need)
27248
+ # Deactivate all resources for new project, then auto-activate language-matching ones
27018
27249
  self._deactivate_all_resources_for_new_project()
27019
-
27250
+
27020
27251
  # Auto-resize rows for better initial display
27021
27252
  self.auto_resize_rows()
27022
-
27253
+
27023
27254
  # Initialize spellcheck for target language
27024
27255
  target_lang = self.current_project.target_lang if self.current_project else 'nl'
27025
27256
  self._initialize_spellcheck_for_target_language(target_lang)
27026
-
27257
+
27027
27258
  # Log success
27028
27259
  self.log(f"✓ Imported {len(segments)} segments from CafeTran bilingual DOCX: {Path(file_path).name}")
27029
27260
 
@@ -27230,7 +27461,11 @@ class SupervertalerQt(QMainWindow):
27230
27461
 
27231
27462
  # Store Trados source path in project for persistence across saves
27232
27463
  self.current_project.trados_source_path = file_path
27233
-
27464
+
27465
+ # Sync global language settings with imported project languages
27466
+ self.source_language = source_lang
27467
+ self.target_language = target_lang
27468
+
27234
27469
  # Update UI
27235
27470
  self.project_file_path = None
27236
27471
  self.project_modified = True
@@ -27238,15 +27473,15 @@ class SupervertalerQt(QMainWindow):
27238
27473
  self.load_segments_to_grid()
27239
27474
  self.initialize_tm_database()
27240
27475
 
27241
- # Deactivate all resources for new project (user explicitly activates what they need)
27476
+ # Deactivate all resources for new project, then auto-activate language-matching ones
27242
27477
  self._deactivate_all_resources_for_new_project()
27243
-
27478
+
27244
27479
  # Auto-resize rows for better initial display
27245
27480
  self.auto_resize_rows()
27246
-
27481
+
27247
27482
  # Initialize spellcheck for target language
27248
27483
  self._initialize_spellcheck_for_target_language(target_lang)
27249
-
27484
+
27250
27485
  # Count segments with tags
27251
27486
  tagged_count = sum(1 for s in trados_segments if s.source_tags)
27252
27487
 
@@ -27590,7 +27825,11 @@ class SupervertalerQt(QMainWindow):
27590
27825
  self.sdlppx_handler = handler
27591
27826
  self.sdlppx_source_file = file_path
27592
27827
  self.current_project.sdlppx_source_path = file_path
27593
-
27828
+
27829
+ # Sync global language settings with imported project languages
27830
+ self.source_language = source_lang
27831
+ self.target_language = target_lang
27832
+
27594
27833
  # Update UI
27595
27834
  self.project_file_path = None
27596
27835
  self.project_modified = True
@@ -27923,6 +28162,10 @@ class SupervertalerQt(QMainWindow):
27923
28162
  # Store Phrase source path in project for persistence across saves
27924
28163
  self.current_project.phrase_source_path = file_path
27925
28164
 
28165
+ # Sync global language settings with imported project languages
28166
+ self.source_language = source_lang
28167
+ self.target_language = target_lang
28168
+
27926
28169
  # Update UI
27927
28170
  self.project_file_path = None
27928
28171
  self.project_modified = True
@@ -28203,7 +28446,11 @@ class SupervertalerQt(QMainWindow):
28203
28446
 
28204
28447
  # Store Déjà Vu source path in project for persistence
28205
28448
  self.current_project.dejavu_source_path = file_path
28206
-
28449
+
28450
+ # Sync global language settings with imported project languages
28451
+ self.source_language = source_lang
28452
+ self.target_language = target_lang
28453
+
28207
28454
  # Create segments
28208
28455
  for idx, seg_data in enumerate(segments_data):
28209
28456
  segment = Segment(
@@ -30864,8 +31111,8 @@ class SupervertalerQt(QMainWindow):
30864
31111
  self.show_translation_results_pane = settings.get('show_translation_results_pane', False)
30865
31112
  self.show_compare_panel = settings.get('show_compare_panel', True)
30866
31113
 
30867
- # 🧪 EXPERIMENTAL: Load cache kill switch setting (default: True = caches disabled for stability)
30868
- self.disable_all_caches = settings.get('disable_all_caches', True)
31114
+ # Load cache kill switch setting (default: False = caches ENABLED for performance)
31115
+ self.disable_all_caches = settings.get('disable_all_caches', False)
30869
31116
 
30870
31117
  # Load LLM provider settings for AI Assistant
30871
31118
  llm_settings = self.load_llm_settings()
@@ -31278,7 +31525,7 @@ class SupervertalerQt(QMainWindow):
31278
31525
  """Handle cell selection change"""
31279
31526
  if self.debug_mode_enabled:
31280
31527
  self.log(f"🎯 on_cell_selected called: row {current_row}, col {current_col}")
31281
-
31528
+
31282
31529
  # 🚫 GUARD: Don't re-run lookups if we're staying on the same row
31283
31530
  # This prevents lookups when user edits text (focus changes within same row)
31284
31531
  if hasattr(self, '_last_selected_row') and self._last_selected_row == current_row:
@@ -31286,34 +31533,35 @@ class SupervertalerQt(QMainWindow):
31286
31533
  self.log(f"⏭️ Skipping lookup - already on row {current_row}")
31287
31534
  return
31288
31535
  self._last_selected_row = current_row
31289
-
31290
- # ⚡ FAST PATH: For arrow key OR Ctrl+Enter navigation, defer heavy lookups
31291
- # This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
31292
- is_arrow_nav = getattr(self, '_arrow_key_navigation', False)
31293
- is_ctrl_enter_nav = getattr(self, '_ctrl_enter_navigation', False)
31294
-
31295
- if is_arrow_nav or is_ctrl_enter_nav:
31296
- self._arrow_key_navigation = False # Reset flags
31297
- self._ctrl_enter_navigation = False
31298
-
31299
- # Schedule deferred lookup with short delay (150ms) for rapid navigation
31300
- if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
31301
- self._deferred_lookup_timer.stop()
31302
- from PyQt6.QtCore import QTimer
31303
- self._deferred_lookup_timer = QTimer()
31304
- self._deferred_lookup_timer.setSingleShot(True)
31305
- self._deferred_lookup_timer.timeout.connect(
31306
- lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
31307
- self._on_cell_selected_full(r, c, pr, pc)
31308
- )
31309
- self._deferred_lookup_timer.start(150) # 150ms debounce
31310
-
31311
- # Do minimal UI update immediately (orange highlight, scroll)
31536
+
31537
+ # ⚡ FILTER MODE: Skip ALL heavy lookups when text filters are active
31538
+ # User is quickly navigating through filtered results - don't slow them down
31539
+ is_filtering = getattr(self, 'filtering_active', False)
31540
+ if is_filtering:
31541
+ # Only do minimal UI update (orange highlight) - no TM/termbase lookups
31312
31542
  self._on_cell_selected_minimal(current_row, previous_row)
31313
31543
  return
31314
-
31315
- # Full processing for non-arrow-key navigation (click, etc.)
31316
- self._on_cell_selected_full(current_row, current_col, previous_row, previous_col)
31544
+
31545
+ # FAST PATH: Defer heavy lookups for ALL navigation (arrow keys, Ctrl+Enter, AND mouse clicks)
31546
+ # This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
31547
+ # Reset any navigation flags
31548
+ self._arrow_key_navigation = False
31549
+ self._ctrl_enter_navigation = False
31550
+
31551
+ # Schedule deferred lookup with short delay - debounce prevents hammering during rapid navigation
31552
+ if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
31553
+ self._deferred_lookup_timer.stop()
31554
+ from PyQt6.QtCore import QTimer
31555
+ self._deferred_lookup_timer = QTimer()
31556
+ self._deferred_lookup_timer.setSingleShot(True)
31557
+ self._deferred_lookup_timer.timeout.connect(
31558
+ lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
31559
+ self._on_cell_selected_full(r, c, pr, pc)
31560
+ )
31561
+ self._deferred_lookup_timer.start(10) # 10ms - just enough to batch rapid arrow key holding
31562
+
31563
+ # Do minimal UI update immediately (orange highlight, scroll)
31564
+ self._on_cell_selected_minimal(current_row, previous_row)
31317
31565
 
31318
31566
  def _center_row_in_viewport(self, row: int):
31319
31567
  """Center the given row vertically in the visible table viewport.
@@ -31535,9 +31783,12 @@ class SupervertalerQt(QMainWindow):
31535
31783
  ]
31536
31784
  # Also get NT matches (fresh, not cached - they may have changed)
31537
31785
  nt_matches = self.find_nt_matches_in_source(segment.source)
31538
-
31786
+
31787
+ # Get status hint for termbase activation
31788
+ status_hint = self._get_termbase_status_hint()
31789
+
31539
31790
  # Update both Termview widgets (left and right)
31540
- self._update_both_termviews(segment.source, termbase_matches, nt_matches)
31791
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
31541
31792
  except Exception as e:
31542
31793
  self.log(f"Error updating termview from cache: {e}")
31543
31794
 
@@ -31570,9 +31821,25 @@ class SupervertalerQt(QMainWindow):
31570
31821
  if has_fuzzy_match and not has_100_match:
31571
31822
  self._play_sound_effect('tm_fuzzy_match')
31572
31823
 
31573
- # Skip the slow lookup below, we already have everything
31574
- # Continue to prefetch trigger at the end
31824
+ # Skip the slow TERMBASE lookup below, we already have termbase matches cached
31825
+ # But TM lookup was skipped in prefetch (not thread-safe), so schedule it now
31575
31826
  matches_dict = cached_matches # Set for later use
31827
+
31828
+ # v1.9.182: Schedule TM lookup even on cache hit (prefetch skips TM - not thread-safe)
31829
+ tm_count = len(cached_matches.get("TM", []))
31830
+ if tm_count == 0 and self.enable_tm_matching:
31831
+ find_replace_active = getattr(self, 'find_replace_active', False)
31832
+ if not find_replace_active:
31833
+ # Get termbase matches for the lookup
31834
+ termbase_matches_for_tm = [
31835
+ {
31836
+ 'source_term': match.source,
31837
+ 'target_term': match.target,
31838
+ 'termbase_name': match.metadata.get('termbase_name', '') if match.metadata else '',
31839
+ }
31840
+ for match in cached_matches.get("Termbases", [])
31841
+ ]
31842
+ self._schedule_mt_and_llm_matches(segment, termbase_matches_for_tm)
31576
31843
 
31577
31844
  # Check if TM/Termbase matching is enabled
31578
31845
  if not matches_dict and (not self.enable_tm_matching and not self.enable_termbase_matching):
@@ -31621,9 +31888,12 @@ class SupervertalerQt(QMainWindow):
31621
31888
  ] if stored_matches else []
31622
31889
  # Also get NT matches
31623
31890
  nt_matches = self.find_nt_matches_in_source(segment.source)
31624
-
31891
+
31892
+ # Get status hint for termbase activation
31893
+ status_hint = self._get_termbase_status_hint()
31894
+
31625
31895
  # Update both Termview widgets (left and right)
31626
- self._update_both_termviews(segment.source, termbase_matches, nt_matches)
31896
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
31627
31897
  except Exception as e:
31628
31898
  self.log(f"Error refreshing termview: {e}")
31629
31899
 
@@ -31790,15 +32060,19 @@ class SupervertalerQt(QMainWindow):
31790
32060
 
31791
32061
  # Schedule expensive searches (TM, MT, LLM) with debouncing to prevent UI blocking
31792
32062
  # ONLY schedule if:
31793
- # 1. Cache miss (no prefetched matches)
32063
+ # 1. Cache miss OR cache hit with no TM matches (prefetch doesn't include TM - not thread-safe)
31794
32064
  # 2. TM matching is enabled
31795
32065
  # 3. Find/Replace is not active (to avoid slowdowns during navigation)
32066
+ needs_tm_lookup = True
31796
32067
  with self.translation_matches_cache_lock:
31797
- cache_hit = segment_id in self.translation_matches_cache
31798
-
32068
+ if segment_id in self.translation_matches_cache:
32069
+ cached = self.translation_matches_cache[segment_id]
32070
+ # v1.9.182: Check if TM matches exist - prefetch worker skips TM lookups
32071
+ needs_tm_lookup = len(cached.get("TM", [])) == 0
32072
+
31799
32073
  find_replace_active = getattr(self, 'find_replace_active', False)
31800
-
31801
- if not cache_hit and self.enable_tm_matching and not find_replace_active:
32074
+
32075
+ if needs_tm_lookup and self.enable_tm_matching and not find_replace_active:
31802
32076
  # Get termbase matches if they exist (could be None or empty)
31803
32077
  termbase_matches = matches_dict.get('Termbases', []) if matches_dict else []
31804
32078
  self._schedule_mt_and_llm_matches(segment, termbase_matches)
@@ -31810,9 +32084,7 @@ class SupervertalerQt(QMainWindow):
31810
32084
  next_segment_ids = []
31811
32085
  start_idx = current_row + 1
31812
32086
  end_idx = min(start_idx + 20, len(self.current_project.segments))
31813
-
31814
- print(f"[PROACTIVE NAV DEBUG] Navigation to row {current_row}, checking segments {start_idx} to {end_idx}")
31815
-
32087
+
31816
32088
  for seg in self.current_project.segments[start_idx:end_idx]:
31817
32089
  # Check if already cached
31818
32090
  with self.translation_matches_cache_lock:
@@ -31826,15 +32098,12 @@ class SupervertalerQt(QMainWindow):
31826
32098
  try:
31827
32099
  with self.termbase_cache_lock:
31828
32100
  termbase_raw = self.termbase_cache.get(seg.id, {})
31829
- print(f"[PROACTIVE NAV DEBUG] Seg {seg.id}: cached, termbase_raw has {len(termbase_raw) if termbase_raw else 0} matches")
31830
32101
  if termbase_raw:
31831
32102
  termbase_json = json.dumps(termbase_raw)
31832
- print(f"[PROACTIVE NAV DEBUG] Calling _apply_proactive_highlighting for seg {seg.id}")
31833
32103
  self._apply_proactive_highlighting(seg.id, termbase_json)
31834
- except Exception as e:
31835
- print(f"[PROACTIVE NAV DEBUG] Error for seg {seg.id}: {e}")
31836
-
31837
- print(f"[PROACTIVE NAV DEBUG] Need to prefetch: {len(next_segment_ids)} segments")
32104
+ except Exception:
32105
+ pass # Silent failure for proactive highlighting
32106
+
31838
32107
  if next_segment_ids:
31839
32108
  self._start_prefetch_worker(next_segment_ids)
31840
32109
 
@@ -33765,7 +34034,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
33765
34034
  self.nt_manager.set_list_active(list_name, False)
33766
34035
 
33767
34036
  self.log("📋 New project: All TMs, glossaries, and NT lists deactivated (start clean)")
33768
-
34037
+ self.log("💡 Tip: Go to Resources tab to activate TMs and glossaries for this project")
34038
+
33769
34039
  def search_and_display_tm_matches(self, source_text: str):
33770
34040
  """Search TM and Termbases and display matches with visual diff for fuzzy matches"""
33771
34041
  self.log(f"🚨 search_and_display_tm_matches called with source_text: '{source_text[:50]}...'")
@@ -33788,11 +34058,34 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
33788
34058
  try:
33789
34059
  # Get activated TM IDs for current project
33790
34060
  tm_ids = None
34061
+ no_tms_activated = False
34062
+ tms_wrong_language = False
33791
34063
  if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr and self.current_project:
33792
34064
  project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
33793
34065
  if project_id:
33794
34066
  tm_ids = self.tm_metadata_mgr.get_active_tm_ids(project_id)
33795
-
34067
+ # Check if no TMs are activated for this project
34068
+ if tm_ids is not None and len(tm_ids) == 0:
34069
+ no_tms_activated = True
34070
+ elif tm_ids and len(tm_ids) > 0:
34071
+ # Check if any activated TMs match the project's language pair
34072
+ project_source = (self.current_project.source_lang or '').lower()
34073
+ project_target = (self.current_project.target_lang or '').lower()
34074
+ all_tms = self.tm_metadata_mgr.get_all_tms()
34075
+ has_matching_language = False
34076
+ for tm in all_tms:
34077
+ if tm['id'] in tm_ids:
34078
+ tm_source = (tm.get('source_lang') or '').lower()
34079
+ tm_target = (tm.get('target_lang') or '').lower()
34080
+ # Match if languages align (bidirectional) or TM has no language set
34081
+ if (not tm_source and not tm_target) or \
34082
+ (tm_source == project_source and tm_target == project_target) or \
34083
+ (tm_source == project_target and tm_target == project_source):
34084
+ has_matching_language = True
34085
+ break
34086
+ if not has_matching_language:
34087
+ tms_wrong_language = True
34088
+
33796
34089
  # Search for matches (using activated TMs if available)
33797
34090
  matches = self.tm_database.search_all(source_text, tm_ids=tm_ids, max_matches=5)
33798
34091
 
@@ -33839,10 +34132,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
33839
34132
 
33840
34133
  if not matches:
33841
34134
  if hasattr(self, 'tm_display'):
33842
- self.tm_display.setHtml(
33843
- f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
33844
- f"<p style='color: #999;'><i>No translation memory matches found</i></p>"
33845
- )
34135
+ if no_tms_activated:
34136
+ # Show helpful message when no TMs are activated
34137
+ self.tm_display.setHtml(
34138
+ f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
34139
+ f"<p style='color: #E65100;'><i>No TMs activated for this project.</i></p>"
34140
+ f"<p style='color: #999; font-size: 9pt;'>Go to <b>Resources → TM</b> to activate translation memories.</p>"
34141
+ )
34142
+ elif tms_wrong_language:
34143
+ # Show message when TMs are activated but don't match project language pair
34144
+ project_lang_pair = f"{self.current_project.source_lang} → {self.current_project.target_lang}" if self.current_project else ""
34145
+ self.tm_display.setHtml(
34146
+ f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
34147
+ f"<p style='color: #E65100;'><i>Activated TMs don't match project language ({project_lang_pair}).</i></p>"
34148
+ f"<p style='color: #999; font-size: 9pt;'>Go to <b>Resources → TM</b> to activate TMs for this language pair.</p>"
34149
+ )
34150
+ else:
34151
+ self.tm_display.setHtml(
34152
+ f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
34153
+ f"<p style='color: #999;'><i>No translation memory matches found</i></p>"
34154
+ )
33846
34155
  return
33847
34156
 
33848
34157
  # If using TranslationResultsPanel, populate it with TM and Termbase results
@@ -34197,18 +34506,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34197
34506
  """
34198
34507
  Find all termbase matches in source text
34199
34508
  Returns dict of {term: translation} for all matches found
34509
+
34510
+ v1.9.182: Uses in-memory index for instant lookup when available.
34511
+ Falls back to per-word database queries if index not built.
34200
34512
  """
34201
34513
  if not source_text or not hasattr(self, 'db_manager') or not self.db_manager:
34202
34514
  return {}
34203
34515
 
34204
34516
  try:
34517
+ # v1.9.182: Use in-memory index for instant lookup (1000x faster)
34518
+ # The index is built on project load by _build_termbase_index()
34519
+ with self.termbase_index_lock:
34520
+ has_index = bool(self.termbase_index)
34521
+
34522
+ if has_index:
34523
+ # Fast path: use pre-built in-memory index
34524
+ return self._search_termbase_in_memory(source_text)
34525
+
34526
+ # Fallback: original per-word database query approach
34527
+ # (only used if index not yet built, e.g., during startup)
34205
34528
  source_lang = self.current_project.source_lang if self.current_project else None
34206
34529
  target_lang = self.current_project.target_lang if self.current_project else None
34207
-
34530
+
34208
34531
  # Convert language names to codes for termbase search
34209
34532
  source_lang_code = self._convert_language_to_code(source_lang) if source_lang else None
34210
34533
  target_lang_code = self._convert_language_to_code(target_lang) if target_lang else None
34211
-
34534
+
34212
34535
  # Strip HTML/XML/CAT tool tags from source text before word splitting
34213
34536
  # This handles <b>, </b>, <i>, memoQ {1}, [2}, Trados <1>, Déjà Vu {00001}, etc.
34214
34537
  import re
@@ -34218,7 +34541,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34218
34541
  # memoQ content tags: [uicontrol id="..."} or {uicontrol] or [tagname ...} or {tagname]
34219
34542
  clean_source_text = re.sub(r'\[[^\[\]]*\}', '', clean_source_text) # Opening: [anything}
34220
34543
  clean_source_text = re.sub(r'\{[^\{\}]*\]', '', clean_source_text) # Closing: {anything]
34221
-
34544
+
34222
34545
  # Search termbases for all terms that appear in the source text
34223
34546
  # Split source text into words and search for each one
34224
34547
  words = clean_source_text.split()
@@ -34440,23 +34763,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34440
34763
  termbase_matches_json: JSON-encoded termbase matches dict (thread-safe transfer)
34441
34764
  """
34442
34765
  import json
34443
-
34444
- print(f"[PROACTIVE DEBUG] _apply_proactive_highlighting called for segment {segment_id}")
34445
-
34766
+
34446
34767
  if not self.current_project or not self.table:
34447
- print(f"[PROACTIVE DEBUG] Early exit: no project or table")
34448
34768
  return
34449
-
34769
+
34450
34770
  try:
34451
34771
  # Decode the matches from JSON
34452
34772
  termbase_matches = json.loads(termbase_matches_json) if termbase_matches_json else {}
34453
-
34454
- print(f"[PROACTIVE DEBUG] Decoded {len(termbase_matches)} termbase matches")
34455
-
34773
+
34456
34774
  if not termbase_matches:
34457
- print(f"[PROACTIVE DEBUG] No matches to highlight, returning")
34458
34775
  return # Nothing to highlight
34459
-
34776
+
34460
34777
  # Find the row for this segment ID
34461
34778
  row = -1
34462
34779
  for r in range(self.table.rowCount()):
@@ -34469,44 +34786,25 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34469
34786
  break
34470
34787
  except ValueError:
34471
34788
  continue
34472
-
34473
- print(f"[PROACTIVE DEBUG] Found row {row} for segment {segment_id}")
34474
-
34789
+
34475
34790
  if row < 0:
34476
- print(f"[PROACTIVE DEBUG] Segment not visible in current page")
34477
34791
  return # Segment not visible in current page
34478
-
34792
+
34479
34793
  # Get segment source text
34480
34794
  segment = None
34481
34795
  for seg in self.current_project.segments:
34482
34796
  if seg.id == segment_id:
34483
34797
  segment = seg
34484
34798
  break
34485
-
34799
+
34486
34800
  if not segment:
34487
- print(f"[PROACTIVE DEBUG] Segment object not found")
34488
34801
  return
34489
-
34490
- print(f"[PROACTIVE DEBUG] Applying highlight_source_with_termbase to row {row}")
34491
- print(f"[PROACTIVE DEBUG] Source text: {segment.source[:80]}...")
34492
- print(f"[PROACTIVE DEBUG] Matches keys: {list(termbase_matches.keys())[:5]}")
34493
- if termbase_matches:
34494
- first_key = list(termbase_matches.keys())[0]
34495
- print(f"[PROACTIVE DEBUG] Sample match: {first_key} => {termbase_matches[first_key]}")
34496
-
34497
- # Check if the source widget exists and is the right type
34498
- source_widget = self.table.cellWidget(row, 2)
34499
- print(f"[PROACTIVE DEBUG] Source widget type: {type(source_widget).__name__ if source_widget else 'None'}")
34500
- print(f"[PROACTIVE DEBUG] Has highlight method: {hasattr(source_widget, 'highlight_termbase_matches') if source_widget else 'N/A'}")
34501
-
34802
+
34502
34803
  # Apply highlighting (this updates the source cell widget)
34503
34804
  self.highlight_source_with_termbase(row, segment.source, termbase_matches)
34504
- print(f"[PROACTIVE DEBUG] ✅ Highlighting applied successfully")
34505
-
34506
- except Exception as e:
34507
- print(f"[PROACTIVE DEBUG] ERROR: {e}")
34508
- import traceback
34509
- print(f"[PROACTIVE DEBUG] Traceback: {traceback.format_exc()}")
34805
+
34806
+ except Exception:
34807
+ pass # Silent failure for proactive highlighting
34510
34808
 
34511
34809
  def insert_term_translation(self, row: int, translation: str):
34512
34810
  """
@@ -35494,8 +35792,11 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35494
35792
  'termbase_id': match_info.get('termbase_id'),
35495
35793
  'notes': match_info.get('notes', '')
35496
35794
  })
35795
+ # Get status hint for termbase activation
35796
+ status_hint = self._get_termbase_status_hint()
35797
+
35497
35798
  # Update both Termview widgets
35498
- self._update_both_termviews(segment.source, tb_list, nt_matches)
35799
+ self._update_both_termviews(segment.source, tb_list, nt_matches, status_hint)
35499
35800
  self.log(" ✓ TermView updated")
35500
35801
  except Exception as e:
35501
35802
  self.log(f" ⚠️ TermView update error: {e}")
@@ -38673,95 +38974,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
38673
38974
 
38674
38975
  self.table.clearSelection()
38675
38976
  self.table.setCurrentCell(row, 3) # Column 3 = Target (widget column)
38977
+ self.table.selectRow(row) # v1.9.182: Ensure row is visually selected
38978
+ # Ensure the row is visible by scrolling to it
38979
+ self.table.scrollToItem(self.table.item(row, 0), QTableWidget.ScrollHint.PositionAtCenter)
38676
38980
  self.log(f"⏭️ Moved to next unconfirmed segment {seg.id}")
38677
-
38678
- # Auto-confirm 100% TM matches if setting is enabled
38679
- if self.auto_confirm_100_percent_matches:
38680
- # Get TM matches for this segment
38681
- exact_match = None
38682
- if self.enable_tm_matching and hasattr(self, 'db_manager') and self.db_manager:
38683
- # Get activated TM IDs from project settings
38684
- activated_tm_ids = []
38685
- if hasattr(self.current_project, 'tm_settings') and self.current_project.tm_settings:
38686
- activated_tm_ids = self.current_project.tm_settings.get('activated_tm_ids', [])
38687
-
38688
- if activated_tm_ids:
38689
- # Use get_exact_match for 100% matches instead of fuzzy search
38690
- source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
38691
- target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
38692
- exact_match = self.db_manager.get_exact_match(
38693
- seg.source,
38694
- tm_ids=activated_tm_ids,
38695
- source_lang=source_lang,
38696
- target_lang=target_lang
38697
- )
38698
-
38699
- # Check if there's a 100% match and (target is empty OR overwrite is enabled)
38700
- target_is_empty = not seg.target.strip()
38701
- can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
38702
-
38703
- if exact_match and can_auto_confirm:
38704
- match_target = exact_match.get('target_text', '')
38705
- overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
38706
- self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
38707
-
38708
- # Insert the match into the target cell
38709
- target_widget = self.table.cellWidget(row, 3)
38710
- if target_widget and match_target:
38711
- target_widget.setPlainText(match_target)
38712
- seg.target = match_target
38713
- seg.status = 'confirmed'
38714
- self.update_status_icon(row, 'confirmed')
38715
- self.project_modified = True
38716
-
38717
- # Save to TM
38718
- try:
38719
- self.save_segment_to_activated_tms(seg.source, seg.target)
38720
- self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
38721
- except Exception as e:
38722
- self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
38723
-
38724
- # Continue to the NEXT unconfirmed segment (skip this one)
38725
- for next_row in range(row + 1, self.table.rowCount()):
38726
- if next_row < len(self.current_project.segments):
38727
- next_seg = self.current_project.segments[next_row]
38728
- if next_seg.status not in ['confirmed', 'approved']:
38729
- # Check pagination
38730
- if self.table.isRowHidden(next_row):
38731
- if hasattr(self, 'grid_page_size') and hasattr(self, 'grid_current_page'):
38732
- target_page = next_row // self.grid_page_size
38733
- if target_page != self.grid_current_page:
38734
- self.grid_current_page = target_page
38735
- self._update_pagination_ui()
38736
- self._apply_pagination_to_grid()
38737
-
38738
- # ⚡ INSTANT NAVIGATION
38739
- self._ctrl_enter_navigation = True
38740
-
38741
- self.table.clearSelection()
38742
- self.table.setCurrentCell(next_row, 3)
38743
- self.log(f"⏭️ Auto-skipped to next unconfirmed segment {next_seg.id}")
38744
- next_target_widget = self.table.cellWidget(next_row, 3)
38745
- if next_target_widget:
38746
- next_target_widget.setFocus()
38747
- next_target_widget.moveCursor(QTextCursor.MoveOperation.End)
38748
-
38749
- # Recursively check if this next segment also has a 100% match
38750
- self.confirm_and_next_unconfirmed()
38751
- return
38752
-
38753
- # No more unconfirmed segments after this one
38754
- self.log("✅ No more unconfirmed segments after auto-confirm")
38755
- # Update status bar after auto-confirming
38756
- self.update_progress_stats()
38757
- return
38758
-
38759
- # Get the target cell widget and set focus to it (normal behavior without auto-confirm)
38981
+
38982
+ # v1.9.182: Explicitly update termview (don't rely on deferred signal)
38983
+ self._update_termview_for_segment(seg)
38984
+
38985
+ # v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
38986
+ if self.enable_tm_matching:
38987
+ find_replace_active = getattr(self, 'find_replace_active', False)
38988
+ if not find_replace_active:
38989
+ self._schedule_mt_and_llm_matches(seg, [])
38990
+
38991
+ # Get the target cell widget and set focus to it IMMEDIATELY
38992
+ # (moved BEFORE auto-confirm check for instant responsiveness)
38760
38993
  target_widget = self.table.cellWidget(row, 3)
38761
38994
  if target_widget:
38762
38995
  target_widget.setFocus()
38763
38996
  # Move cursor to end of text
38764
38997
  target_widget.moveCursor(QTextCursor.MoveOperation.End)
38998
+
38999
+ # v1.9.182: Defer auto-confirm check to not block navigation
39000
+ # The TM lookup is slow - do it asynchronously after navigation completes
39001
+ if self.auto_confirm_100_percent_matches:
39002
+ QTimer.singleShot(50, lambda r=row, s=seg: self._check_auto_confirm_100_percent(r, s))
38765
39003
  return
38766
39004
 
38767
39005
  # No more unconfirmed segments, just go to next
@@ -38783,14 +39021,106 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
38783
39021
 
38784
39022
  self.table.clearSelection()
38785
39023
  self.table.setCurrentCell(next_row, 3) # Column 3 = Target (widget column)
39024
+ self.table.selectRow(next_row) # v1.9.182: Ensure row is visually selected
39025
+ # Ensure the row is visible by scrolling to it
39026
+ self.table.scrollToItem(self.table.item(next_row, 0), QTableWidget.ScrollHint.PositionAtCenter)
38786
39027
  self.log(f"⏭️ Moved to next segment (all remaining confirmed)")
39028
+
39029
+ # v1.9.182: Explicitly update termview (don't rely on deferred signal)
39030
+ if next_row < len(self.current_project.segments):
39031
+ next_seg = self.current_project.segments[next_row]
39032
+ self._update_termview_for_segment(next_seg)
39033
+
39034
+ # v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
39035
+ if self.enable_tm_matching:
39036
+ find_replace_active = getattr(self, 'find_replace_active', False)
39037
+ if not find_replace_active:
39038
+ self._schedule_mt_and_llm_matches(next_seg, [])
39039
+
38787
39040
  # Get the target cell widget and set focus to it
38788
39041
  target_widget = self.table.cellWidget(next_row, 3)
38789
39042
  if target_widget:
38790
39043
  target_widget.setFocus()
38791
39044
  # Move cursor to end of text
38792
39045
  target_widget.moveCursor(QTextCursor.MoveOperation.End)
38793
-
39046
+
39047
+ def _check_auto_confirm_100_percent(self, row: int, seg):
39048
+ """
39049
+ v1.9.182: Deferred auto-confirm check for 100% TM matches.
39050
+
39051
+ This is called asynchronously after Ctrl+Enter navigation to avoid blocking
39052
+ the UI thread with slow TM database queries.
39053
+ """
39054
+ try:
39055
+ # Verify we're still on the same segment (user may have navigated away)
39056
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
39057
+ if current_row != row:
39058
+ return # User has moved - don't auto-confirm wrong segment
39059
+
39060
+ if not self.enable_tm_matching or not hasattr(self, 'db_manager') or not self.db_manager:
39061
+ return
39062
+
39063
+ # Get activated TM IDs from project settings
39064
+ activated_tm_ids = []
39065
+ if hasattr(self.current_project, 'tm_settings') and self.current_project.tm_settings:
39066
+ activated_tm_ids = self.current_project.tm_settings.get('activated_tm_ids', [])
39067
+
39068
+ if not activated_tm_ids:
39069
+ return
39070
+
39071
+ # Use get_exact_match for 100% matches
39072
+ source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
39073
+ target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
39074
+ exact_match = self.db_manager.get_exact_match(
39075
+ seg.source,
39076
+ tm_ids=activated_tm_ids,
39077
+ source_lang=source_lang,
39078
+ target_lang=target_lang
39079
+ )
39080
+
39081
+ if not exact_match:
39082
+ return
39083
+
39084
+ # Check if there's a 100% match and (target is empty OR overwrite is enabled)
39085
+ target_is_empty = not seg.target.strip()
39086
+ can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
39087
+
39088
+ if not can_auto_confirm:
39089
+ return
39090
+
39091
+ # Verify AGAIN that we're still on the same segment (TM query may have taken time)
39092
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
39093
+ if current_row != row:
39094
+ return # User has moved during TM lookup
39095
+
39096
+ match_target = exact_match.get('target_text', '')
39097
+ if not match_target:
39098
+ return
39099
+
39100
+ overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
39101
+ self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
39102
+
39103
+ # Insert the match into the target cell
39104
+ target_widget = self.table.cellWidget(row, 3)
39105
+ if target_widget:
39106
+ target_widget.setPlainText(match_target)
39107
+ seg.target = match_target
39108
+ seg.status = 'confirmed'
39109
+ self.update_status_icon(row, 'confirmed')
39110
+ self.project_modified = True
39111
+
39112
+ # Save to TM
39113
+ try:
39114
+ self.save_segment_to_activated_tms(seg.source, seg.target)
39115
+ self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
39116
+ except Exception as e:
39117
+ self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
39118
+
39119
+ # Continue to the NEXT unconfirmed segment (skip this one)
39120
+ self.confirm_and_next_unconfirmed()
39121
+ except Exception as e:
39122
+ self.log(f"⚠️ Error in auto-confirm check: {e}")
39123
+
38794
39124
  def confirm_selected_or_next(self):
38795
39125
  """Smart confirm: if multiple segments selected, confirm all; otherwise confirm and go to next.
38796
39126
 
@@ -43941,12 +44271,12 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43941
44271
  self._pending_mt_llm_segment = segment
43942
44272
  self._pending_termbase_matches = termbase_matches or []
43943
44273
 
43944
- # Start debounced timer - only call APIs after user stops clicking for 0.3 seconds
44274
+ # Start debounced timer - only call APIs after user stops navigating
43945
44275
  from PyQt6.QtCore import QTimer
43946
44276
  self._mt_llm_timer = QTimer()
43947
44277
  self._mt_llm_timer.setSingleShot(True)
43948
44278
  self._mt_llm_timer.timeout.connect(lambda: self._execute_mt_llm_lookup())
43949
- self._mt_llm_timer.start(300) # Wait 0.3 seconds of inactivity
44279
+ self._mt_llm_timer.start(150) # Wait 150ms of inactivity before external API calls
43950
44280
 
43951
44281
  except Exception as e:
43952
44282
  self.log(f"Error scheduling MT/LLM search: {e}")
@@ -43971,7 +44301,21 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43971
44301
  """Search for TM, MT and LLM matches - called only after debounce delay"""
43972
44302
  try:
43973
44303
  from modules.translation_results_panel import TranslationMatch
43974
-
44304
+
44305
+ # v1.9.182: Validate we're still on the same segment before displaying results
44306
+ # This prevents stale results from showing when user navigates quickly
44307
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
44308
+ if current_row >= 0:
44309
+ id_item = self.table.item(current_row, 0)
44310
+ if id_item:
44311
+ try:
44312
+ current_segment_id = int(id_item.text())
44313
+ if current_segment_id != segment.id:
44314
+ # User has moved to a different segment - abort this lookup
44315
+ return
44316
+ except (ValueError, AttributeError):
44317
+ pass
44318
+
43975
44319
  # Get current project languages for all translation services
43976
44320
  source_lang = getattr(self.current_project, 'source_lang', None) if self.current_project else None
43977
44321
  target_lang = getattr(self.current_project, 'target_lang', None) if self.current_project else None
@@ -44044,6 +44388,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44044
44388
 
44045
44389
  # Show TM matches immediately (progressive loading)
44046
44390
  if matches_dict["TM"]:
44391
+ # v1.9.182: Re-validate we're still on same segment before displaying
44392
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
44393
+ if current_row >= 0:
44394
+ id_item = self.table.item(current_row, 0)
44395
+ if id_item:
44396
+ try:
44397
+ current_segment_id = int(id_item.text())
44398
+ if current_segment_id != segment.id:
44399
+ # User moved - still cache results but don't display
44400
+ with self.translation_matches_cache_lock:
44401
+ if segment.id in self.translation_matches_cache:
44402
+ self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
44403
+ return # Don't display stale results
44404
+ except (ValueError, AttributeError):
44405
+ pass
44406
+
44047
44407
  tm_only = {"TM": matches_dict["TM"]}
44048
44408
  if hasattr(self, 'results_panels') and self.results_panels:
44049
44409
  for panel in self.results_panels:
@@ -44097,6 +44457,16 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44097
44457
  has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in matches_dict["TM"])
44098
44458
  if has_fuzzy_match and not has_100_match:
44099
44459
  self._play_sound_effect('tm_fuzzy_match')
44460
+
44461
+ # v1.9.182: Update cache with TM results so subsequent visits are instant
44462
+ if matches_dict["TM"]:
44463
+ with self.translation_matches_cache_lock:
44464
+ if segment.id in self.translation_matches_cache:
44465
+ # Merge TM results into existing cache entry
44466
+ self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
44467
+ else:
44468
+ # Create new cache entry with TM results
44469
+ self.translation_matches_cache[segment.id] = matches_dict
44100
44470
  except Exception as e:
44101
44471
  self.log(f"Error in delayed TM search: {e}")
44102
44472
 
@@ -46675,10 +47045,8 @@ class SuperlookupTab(QWidget):
46675
47045
  for row in db_manager.cursor.fetchall():
46676
47046
  if row[0]:
46677
47047
  all_languages.add(row[0])
46678
- except Exception as e:
46679
- print(f"[DEBUG] Error getting languages from TMs: {e}")
46680
- else:
46681
- print(f"[DEBUG] No db_manager available for language population")
47048
+ except Exception:
47049
+ pass # Silent failure for language population
46682
47050
 
46683
47051
  # Get languages from termbases
46684
47052
  if termbase_mgr:
@@ -46689,8 +47057,8 @@ class SuperlookupTab(QWidget):
46689
47057
  all_languages.add(tb['source_lang'])
46690
47058
  if tb.get('target_lang'):
46691
47059
  all_languages.add(tb['target_lang'])
46692
- except Exception as e:
46693
- print(f"[DEBUG] Error getting languages from termbases: {e}")
47060
+ except Exception:
47061
+ pass # Silent failure for language population
46694
47062
 
46695
47063
  # Group languages by their base language name
46696
47064
  # E.g., "en", "en-US", "en-GB", "English" all map to "English"
@@ -46720,8 +47088,6 @@ class SuperlookupTab(QWidget):
46720
47088
  # Store variants list as the data for this item
46721
47089
  self.lang_from_combo.addItem(base_name, variants)
46722
47090
  self.lang_to_combo.addItem(base_name, variants)
46723
-
46724
- print(f"[DEBUG] Populated language dropdowns with {len(sorted_base_langs)} base languages (from {len(all_languages)} variants)")
46725
47091
 
46726
47092
  def _get_base_language_name(self, lang_code):
46727
47093
  """Extract the base language name from any language code or name.
@@ -46908,37 +47274,20 @@ class SuperlookupTab(QWidget):
46908
47274
  selected_tm_ids = self.get_selected_tm_ids()
46909
47275
  search_direction = self.get_search_direction()
46910
47276
  from_lang, to_lang = self.get_language_filters()
46911
-
46912
- # Write language info to debug file
46913
- with open('superlookup_debug.txt', 'a') as f:
46914
- f.write(f"Language filters: from_lang='{from_lang}', to_lang='{to_lang}'\\n")
46915
- f.write(f"Search direction: {search_direction}\\n")
46916
-
46917
- print(f"[DEBUG] Superlookup: Selected TM IDs: {selected_tm_ids}, direction: {search_direction}", flush=True)
46918
- print(f"[DEBUG] Superlookup: Language filters: from={from_lang}, to={to_lang}", flush=True)
46919
- print(f"[DEBUG] Superlookup: tm_database = {self.tm_database}", flush=True)
47277
+
46920
47278
  if self.engine:
46921
47279
  self.engine.set_enabled_tm_ids(selected_tm_ids if selected_tm_ids else None)
46922
47280
 
46923
47281
  # Perform TM lookup with direction and language filters
46924
47282
  tm_results = []
46925
47283
  if self.tm_database:
46926
- print(f"[DEBUG] Superlookup: Searching TM for '{text[:50]}...'", flush=True)
46927
- tm_results = self.engine.search_tm(text, direction=search_direction,
47284
+ tm_results = self.engine.search_tm(text, direction=search_direction,
46928
47285
  source_lang=from_lang, target_lang=to_lang)
46929
- print(f"[DEBUG] Superlookup: Got {len(tm_results)} TM results", flush=True)
46930
- else:
46931
- print(f"[DEBUG] Superlookup: tm_database is None, skipping TM search!", flush=True)
46932
-
47286
+
46933
47287
  # Perform termbase lookup (search Supervertaler termbases directly)
46934
- print(f"[DEBUG] About to call search_termbases with from_lang='{from_lang}', to_lang='{to_lang}'", flush=True)
46935
47288
  try:
46936
47289
  termbase_results = self.search_termbases(text, source_lang=from_lang, target_lang=to_lang)
46937
- print(f"[DEBUG] search_termbases returned {len(termbase_results)} results", flush=True)
46938
- except Exception as e:
46939
- print(f"[DEBUG] ERROR in search_termbases: {e}", flush=True)
46940
- import traceback
46941
- traceback.print_exc()
47290
+ except Exception:
46942
47291
  termbase_results = []
46943
47292
 
46944
47293
  # Perform Supermemory semantic search