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 +369 -205
- modules/database_manager.py +20 -5
- modules/termview_widget.py +6 -2
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.145.dist-info}/METADATA +19 -3
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.145.dist-info}/RECORD +9 -9
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.145.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.145.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.145.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.145.dist-info}/top_level.txt +0 -0
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.
|
|
7
|
-
Release Date: January
|
|
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.
|
|
37
|
+
__version__ = "1.9.145"
|
|
38
38
|
__phase__ = "0.9"
|
|
39
|
-
__release_date__ = "2026-01-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
5369
|
+
# Save the preference to general_settings.json (where load_general_settings reads from)
|
|
5335
5370
|
if dont_show_checkbox.isChecked():
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
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
|
|
10632
|
-
|
|
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
|
|
10771
|
-
|
|
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
|
|
10906
|
-
|
|
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("🎤
|
|
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:
|
|
18546
|
-
|
|
18547
|
-
|
|
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
|
-
#
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
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
|
-
|
|
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,
|
|
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("🎤
|
|
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
|
-
"""
|
|
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.
|
|
27011
|
-
delete_format.
|
|
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
|
-
|
|
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
|
-
|
|
27152
|
+
# Same in both - show normally
|
|
27153
|
+
result_parts.append(('normal', ' '.join(tm_words[j1:j2])))
|
|
27019
27154
|
elif tag == 'replace':
|
|
27020
|
-
#
|
|
27021
|
-
|
|
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
|
-
|
|
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 -
|
|
27027
|
-
|
|
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
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
|
|
28653
|
-
|
|
28654
|
-
|
|
28655
|
-
|
|
28656
|
-
|
|
28657
|
-
|
|
28658
|
-
|
|
28659
|
-
|
|
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
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
|
|
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.
|
|
36066
|
+
if self.termbase_mgr:
|
|
35861
36067
|
try:
|
|
35862
|
-
|
|
35863
|
-
|
|
35864
|
-
|
|
35865
|
-
|
|
35866
|
-
|
|
35867
|
-
|
|
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
|
-
|
|
35880
|
-
|
|
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
|
-
#
|
|
36003
|
-
|
|
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
|
|
modules/database_manager.py
CHANGED
|
@@ -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
|
-
#
|
|
1337
|
-
#
|
|
1338
|
-
#
|
|
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
|
modules/termview_widget.py
CHANGED
|
@@ -750,7 +750,10 @@ class TermviewWidget(QWidget):
|
|
|
750
750
|
if not source_term or not target_term:
|
|
751
751
|
continue
|
|
752
752
|
|
|
753
|
-
key
|
|
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
|
-
|
|
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.
|
|
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.
|
|
74
|
+
# 🚀 Supervertaler v1.9.145
|
|
75
75
|
|
|
76
76
|
[](https://pypi.org/project/Supervertaler/)
|
|
77
77
|
[](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.
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
81
|
-
supervertaler-1.9.
|
|
82
|
-
supervertaler-1.9.
|
|
83
|
-
supervertaler-1.9.
|
|
84
|
-
supervertaler-1.9.
|
|
85
|
-
supervertaler-1.9.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|