supervertaler 1.9.131__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 +373 -208
- modules/database_manager.py +20 -5
- modules/termview_widget.py +6 -2
- {supervertaler-1.9.131.dist-info → supervertaler-1.9.145.dist-info}/METADATA +19 -3
- {supervertaler-1.9.131.dist-info → supervertaler-1.9.145.dist-info}/RECORD +9 -9
- {supervertaler-1.9.131.dist-info → supervertaler-1.9.145.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.131.dist-info → supervertaler-1.9.145.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.131.dist-info → supervertaler-1.9.145.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.131.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)")
|
|
@@ -6532,9 +6557,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6532
6557
|
supercleaner_action.triggered.connect(lambda: self._navigate_to_tool("Supercleaner"))
|
|
6533
6558
|
tools_menu.addAction(supercleaner_action)
|
|
6534
6559
|
|
|
6535
|
-
superlookup_action = QAction("🔍 Super&lookup...", self)
|
|
6536
|
-
|
|
6537
|
-
|
|
6560
|
+
superlookup_action = QAction("🔍 Super&lookup (Ctrl+K)...", self)
|
|
6561
|
+
# Note: Actual Ctrl+K shortcut handled by QShortcut in setup_global_shortcuts()
|
|
6562
|
+
# which calls show_concordance_search() for proper selection capture
|
|
6563
|
+
superlookup_action.triggered.connect(self.show_concordance_search)
|
|
6538
6564
|
tools_menu.addAction(superlookup_action)
|
|
6539
6565
|
|
|
6540
6566
|
supervoice_action = QAction("🎤 Super&voice...", self)
|
|
@@ -10487,6 +10513,111 @@ class SupervertalerQt(QMainWindow):
|
|
|
10487
10513
|
f"Error testing segmentation:\n\n{e}"
|
|
10488
10514
|
)
|
|
10489
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
|
+
|
|
10490
10621
|
def add_term_pair_to_termbase(self, source_text: str, target_text: str):
|
|
10491
10622
|
"""Add a term pair to active termbase(s) with metadata dialog"""
|
|
10492
10623
|
# Check if we have a current project
|
|
@@ -10627,26 +10758,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
10627
10758
|
|
|
10628
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)'}")
|
|
10629
10760
|
|
|
10630
|
-
# Refresh
|
|
10631
|
-
|
|
10632
|
-
if current_row >= 0 and current_row < len(self.current_project.segments):
|
|
10633
|
-
segment = self.current_project.segments[current_row]
|
|
10634
|
-
|
|
10635
|
-
# Clear BOTH caches for this segment to force refresh
|
|
10636
|
-
with self.translation_matches_cache_lock:
|
|
10637
|
-
if segment.id in self.translation_matches_cache:
|
|
10638
|
-
del self.translation_matches_cache[segment.id]
|
|
10639
|
-
self.log(f"🗑️ Cleared translation matches cache for segment {segment.id}")
|
|
10640
|
-
|
|
10641
|
-
with self.termbase_cache_lock:
|
|
10642
|
-
if segment.id in self.termbase_cache:
|
|
10643
|
-
del self.termbase_cache[segment.id]
|
|
10644
|
-
self.log(f"🗑️ Cleared termbase cache for segment {segment.id}")
|
|
10645
|
-
|
|
10646
|
-
# Trigger lookup refresh by simulating segment change
|
|
10647
|
-
self._last_selected_row = -1 # Reset to force refresh
|
|
10648
|
-
self.on_cell_selected(current_row, self.table.currentColumn(), -1, -1)
|
|
10649
|
-
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()
|
|
10650
10763
|
|
|
10651
10764
|
# IMPORTANT: Refresh the termbase list UI if it's currently open to update term counts
|
|
10652
10765
|
# Find the termbase tab and call its refresh function
|
|
@@ -10766,23 +10879,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
10766
10879
|
else:
|
|
10767
10880
|
self.statusBar().showMessage(f"✓ Added: {source_text} → {target_text} (to {success_count} termbases)", 3000)
|
|
10768
10881
|
|
|
10769
|
-
# Refresh
|
|
10770
|
-
|
|
10771
|
-
if current_row >= 0 and current_row < len(self.current_project.segments):
|
|
10772
|
-
segment = self.current_project.segments[current_row]
|
|
10773
|
-
|
|
10774
|
-
# Clear BOTH caches for this segment to force refresh
|
|
10775
|
-
with self.translation_matches_cache_lock:
|
|
10776
|
-
if segment.id in self.translation_matches_cache:
|
|
10777
|
-
del self.translation_matches_cache[segment.id]
|
|
10778
|
-
|
|
10779
|
-
with self.termbase_cache_lock:
|
|
10780
|
-
if segment.id in self.termbase_cache:
|
|
10781
|
-
del self.termbase_cache[segment.id]
|
|
10782
|
-
|
|
10783
|
-
# Trigger lookup refresh by simulating segment change
|
|
10784
|
-
self._last_selected_row = -1 # Reset to force refresh
|
|
10785
|
-
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()
|
|
10786
10884
|
|
|
10787
10885
|
# Refresh termbase list UI if open
|
|
10788
10886
|
if hasattr(self, 'termbase_tab_refresh_callback') and self.termbase_tab_refresh_callback:
|
|
@@ -10901,17 +10999,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
10901
10999
|
self.log(f"✓ Added to '{target_termbase['name']}': {source_text} → {target_text}")
|
|
10902
11000
|
self.statusBar().showMessage(f"✓ Added to '{target_termbase['name']}': {source_text} → {target_text}", 3000)
|
|
10903
11001
|
|
|
10904
|
-
# Refresh
|
|
10905
|
-
|
|
10906
|
-
segment = self.current_project.segments[current_row]
|
|
10907
|
-
with self.translation_matches_cache_lock:
|
|
10908
|
-
if segment.id in self.translation_matches_cache:
|
|
10909
|
-
del self.translation_matches_cache[segment.id]
|
|
10910
|
-
with self.termbase_cache_lock:
|
|
10911
|
-
if segment.id in self.termbase_cache:
|
|
10912
|
-
del self.termbase_cache[segment.id]
|
|
10913
|
-
self._last_selected_row = -1
|
|
10914
|
-
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()
|
|
10915
11004
|
|
|
10916
11005
|
if hasattr(self, 'termbase_tab_refresh_callback') and self.termbase_tab_refresh_callback:
|
|
10917
11006
|
self.termbase_tab_refresh_callback()
|
|
@@ -14483,6 +14572,21 @@ class SupervertalerQt(QMainWindow):
|
|
|
14483
14572
|
sound_event_combos['glossary_term_error'] = make_sound_combo(default_term_error)
|
|
14484
14573
|
event_rows.addRow("Glossary add error:", sound_event_combos['glossary_term_error'])
|
|
14485
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
|
+
|
|
14486
14590
|
sound_layout.addLayout(event_rows)
|
|
14487
14591
|
|
|
14488
14592
|
sound_note = QLabel("💡 Uses Windows system beeps (no audio files).")
|
|
@@ -16138,7 +16242,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
16138
16242
|
# Update status bar indicator (always visible when active)
|
|
16139
16243
|
if hasattr(self, 'alwayson_indicator_label'):
|
|
16140
16244
|
if status == "listening" or status == "waiting":
|
|
16141
|
-
self.alwayson_indicator_label.setText("🎤 VOICE ON")
|
|
16245
|
+
self.alwayson_indicator_label.setText("🎤 VOICE COMMANDS ON")
|
|
16142
16246
|
self.alwayson_indicator_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #2E7D32; background-color: #C8E6C9; padding: 2px 6px; border-radius: 3px;")
|
|
16143
16247
|
self.alwayson_indicator_label.setToolTip("Always-on voice listening ACTIVE\nClick to stop")
|
|
16144
16248
|
self.alwayson_indicator_label.show()
|
|
@@ -16158,7 +16262,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
16158
16262
|
# Update grid toolbar button (if exists)
|
|
16159
16263
|
if hasattr(self, 'grid_alwayson_btn'):
|
|
16160
16264
|
if status == "listening" or status == "waiting":
|
|
16161
|
-
self.grid_alwayson_btn.setText("🎧 Voice ON")
|
|
16265
|
+
self.grid_alwayson_btn.setText("🎧 Voice Commands ON")
|
|
16162
16266
|
self.grid_alwayson_btn.setChecked(True)
|
|
16163
16267
|
self.grid_alwayson_btn.setStyleSheet("""
|
|
16164
16268
|
QPushButton {
|
|
@@ -16201,7 +16305,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
16201
16305
|
}
|
|
16202
16306
|
""")
|
|
16203
16307
|
else: # stopped or other
|
|
16204
|
-
self.grid_alwayson_btn.setText("🎧 Voice OFF")
|
|
16308
|
+
self.grid_alwayson_btn.setText("🎧 Voice Commands OFF")
|
|
16205
16309
|
self.grid_alwayson_btn.setChecked(False)
|
|
16206
16310
|
self.grid_alwayson_btn.setStyleSheet("""
|
|
16207
16311
|
QPushButton {
|
|
@@ -18263,14 +18367,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
18263
18367
|
preview_prompt_btn.clicked.connect(self._preview_combined_prompt_from_grid)
|
|
18264
18368
|
toolbar_layout.addWidget(preview_prompt_btn)
|
|
18265
18369
|
|
|
18266
|
-
dictate_btn = QPushButton("🎤
|
|
18370
|
+
dictate_btn = QPushButton("🎤 Dictation")
|
|
18267
18371
|
dictate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
|
|
18268
18372
|
dictate_btn.clicked.connect(self.start_voice_dictation)
|
|
18269
18373
|
dictate_btn.setToolTip("Start/stop voice dictation (F9)")
|
|
18270
18374
|
toolbar_layout.addWidget(dictate_btn)
|
|
18271
18375
|
|
|
18272
18376
|
# Always-On Voice toggle button
|
|
18273
|
-
alwayson_btn = QPushButton("🎧 Voice OFF")
|
|
18377
|
+
alwayson_btn = QPushButton("🎧 Voice Commands OFF")
|
|
18274
18378
|
alwayson_btn.setCheckable(True)
|
|
18275
18379
|
alwayson_btn.setChecked(False)
|
|
18276
18380
|
alwayson_btn.setStyleSheet("""
|
|
@@ -18541,9 +18645,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
18541
18645
|
def _apply_pagination_to_grid(self):
|
|
18542
18646
|
"""Show/hide rows based on current page, optionally constrained by active filters.
|
|
18543
18647
|
|
|
18544
|
-
IMPORTANT:
|
|
18545
|
-
|
|
18546
|
-
|
|
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.
|
|
18547
18653
|
"""
|
|
18548
18654
|
if not self.current_project or not self.current_project.segments:
|
|
18549
18655
|
return
|
|
@@ -18555,28 +18661,38 @@ class SupervertalerQt(QMainWindow):
|
|
|
18555
18661
|
if not hasattr(self, 'grid_page_size'):
|
|
18556
18662
|
self.grid_page_size = 50
|
|
18557
18663
|
|
|
18558
|
-
# Calculate which rows should be visible
|
|
18559
|
-
if self.grid_page_size >= 999999:
|
|
18560
|
-
# "All" mode - show everything
|
|
18561
|
-
start_row = 0
|
|
18562
|
-
end_row = total_segments
|
|
18563
|
-
else:
|
|
18564
|
-
start_row = self.grid_current_page * self.grid_page_size
|
|
18565
|
-
end_row = min(start_row + self.grid_page_size, total_segments)
|
|
18566
|
-
|
|
18567
18664
|
# If a text filter (Filter Source/Target) is active, it will populate an allowlist
|
|
18568
18665
|
# of row indices that are allowed to be visible.
|
|
18569
18666
|
filter_allowlist = getattr(self, '_active_text_filter_rows', None)
|
|
18570
|
-
|
|
18571
|
-
#
|
|
18572
|
-
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
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)
|
|
18580
18696
|
|
|
18581
18697
|
# Update pagination UI
|
|
18582
18698
|
self._update_pagination_ui()
|
|
@@ -18859,11 +18975,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
18859
18975
|
header.setStretchLastSection(False) # Don't auto-stretch last section (we use Stretch mode for Source/Target)
|
|
18860
18976
|
|
|
18861
18977
|
# Set initial column widths - give Source and Target equal space
|
|
18862
|
-
|
|
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
|
|
18863
18980
|
self.table.setColumnWidth(1, 40) # Type - narrower
|
|
18864
18981
|
self.table.setColumnWidth(2, 400) # Source
|
|
18865
18982
|
self.table.setColumnWidth(3, 400) # Target
|
|
18866
|
-
self.table.setColumnWidth(4,
|
|
18983
|
+
self.table.setColumnWidth(4, 60) # Status - compact
|
|
18867
18984
|
|
|
18868
18985
|
# Enable word wrap in cells (both display and edit mode)
|
|
18869
18986
|
self.table.setWordWrap(True)
|
|
@@ -19161,7 +19278,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
19161
19278
|
clear_btn.clicked.connect(self.clear_tab_target)
|
|
19162
19279
|
|
|
19163
19280
|
# Voice dictation button
|
|
19164
|
-
dictate_btn = QPushButton("🎤
|
|
19281
|
+
dictate_btn = QPushButton("🎤 Dictation (F9)")
|
|
19165
19282
|
dictate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
|
|
19166
19283
|
dictate_btn.clicked.connect(self.start_voice_dictation)
|
|
19167
19284
|
dictate_btn.setToolTip("Click or press F9 to start/stop voice dictation")
|
|
@@ -26997,33 +27114,65 @@ class SupervertalerQt(QMainWindow):
|
|
|
26997
27114
|
self.set_compare_panel_matches(segment_id, current_source, tm_matches, mt_matches)
|
|
26998
27115
|
|
|
26999
27116
|
def _set_compare_panel_text_with_diff(self, text_edit: QTextEdit, current: str, tm_source: str):
|
|
27000
|
-
"""
|
|
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
|
+
"""
|
|
27001
27125
|
import difflib
|
|
27002
27126
|
|
|
27003
27127
|
text_edit.clear()
|
|
27004
27128
|
cursor = text_edit.textCursor()
|
|
27005
27129
|
|
|
27006
|
-
# Create formatters
|
|
27130
|
+
# Create formatters - memoQ track changes style
|
|
27007
27131
|
normal_format = QTextCharFormat()
|
|
27132
|
+
|
|
27133
|
+
# Red strikethrough for deletions (text in TM but not in current)
|
|
27008
27134
|
delete_format = QTextCharFormat()
|
|
27009
|
-
delete_format.
|
|
27010
|
-
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()
|
|
27011
27146
|
|
|
27012
|
-
|
|
27013
|
-
matcher = difflib.SequenceMatcher(None, current, tm_source)
|
|
27147
|
+
matcher = difflib.SequenceMatcher(None, current_words, tm_words)
|
|
27014
27148
|
|
|
27149
|
+
result_parts = []
|
|
27015
27150
|
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
27016
27151
|
if tag == 'equal':
|
|
27017
|
-
|
|
27152
|
+
# Same in both - show normally
|
|
27153
|
+
result_parts.append(('normal', ' '.join(tm_words[j1:j2])))
|
|
27018
27154
|
elif tag == 'replace':
|
|
27019
|
-
#
|
|
27020
|
-
|
|
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])))
|
|
27021
27158
|
elif tag == 'insert':
|
|
27022
|
-
# Text in TM but not in current
|
|
27023
|
-
|
|
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])))
|
|
27024
27161
|
elif tag == 'delete':
|
|
27025
|
-
# Text in current but not in TM -
|
|
27026
|
-
|
|
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)
|
|
27027
27176
|
|
|
27028
27177
|
text_edit.setTextCursor(cursor)
|
|
27029
27178
|
|
|
@@ -27641,6 +27790,37 @@ class SupervertalerQt(QMainWindow):
|
|
|
27641
27790
|
target_widget = self.table.cellWidget(row, 3)
|
|
27642
27791
|
if target_widget and isinstance(target_widget, EditableGridTextEditor):
|
|
27643
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)
|
|
27644
27824
|
|
|
27645
27825
|
def set_font_family(self, family_name: str):
|
|
27646
27826
|
"""Set font family from menu"""
|
|
@@ -28598,12 +28778,22 @@ class SupervertalerQt(QMainWindow):
|
|
|
28598
28778
|
if best_match:
|
|
28599
28779
|
self.log(f"✨ CACHE: Auto-inserting 100% TM match into segment {segment.id} at row {current_row}")
|
|
28600
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')
|
|
28601
28783
|
else:
|
|
28602
28784
|
relevances = [(tm.relevance, type(tm.relevance).__name__) for tm in cached_matches.get("TM", [])]
|
|
28603
28785
|
self.log(f"⚠️ CACHE: No 100% match found. All relevances: {relevances}")
|
|
28604
28786
|
else:
|
|
28605
28787
|
self.log(f"⚠️ CACHE: Target not empty ('{segment.target}') - skipping auto-insert")
|
|
28606
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
|
+
|
|
28607
28797
|
# Skip the slow lookup below, we already have everything
|
|
28608
28798
|
# Continue to prefetch trigger at the end
|
|
28609
28799
|
matches_dict = cached_matches # Set for later use
|
|
@@ -28633,29 +28823,29 @@ class SupervertalerQt(QMainWindow):
|
|
|
28633
28823
|
# Store in cache for future access (thread-safe) - EVEN IF EMPTY
|
|
28634
28824
|
with self.termbase_cache_lock:
|
|
28635
28825
|
self.termbase_cache[segment_id] = stored_matches
|
|
28636
|
-
|
|
28637
|
-
|
|
28638
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
|
|
28653
|
-
|
|
28654
|
-
|
|
28655
|
-
|
|
28656
|
-
|
|
28657
|
-
|
|
28658
|
-
|
|
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}")
|
|
28659
28849
|
|
|
28660
28850
|
# Store in widget for backwards compatibility
|
|
28661
28851
|
if source_widget and hasattr(source_widget, 'termbase_matches'):
|
|
@@ -31167,8 +31357,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31167
31357
|
|
|
31168
31358
|
# Comprehensive set of quote and punctuation characters to strip
|
|
31169
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]"
|
|
31170
31361
|
# Using explicit characters to avoid encoding issues
|
|
31171
|
-
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()[]{}'
|
|
31172
31363
|
|
|
31173
31364
|
for word in words:
|
|
31174
31365
|
# Remove punctuation including quotes (preserve internal punctuation like "gew.%")
|
|
@@ -31183,13 +31374,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31183
31374
|
# Get project ID for termbase priority lookup
|
|
31184
31375
|
project_id = self.current_project.id if self.current_project and hasattr(self.current_project, 'id') else None
|
|
31185
31376
|
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
)
|
|
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)
|
|
31193
31397
|
|
|
31194
31398
|
if termbase_results:
|
|
31195
31399
|
for result in termbase_results:
|
|
@@ -35649,6 +35853,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35649
35853
|
|
|
35650
35854
|
self.update_progress_stats()
|
|
35651
35855
|
|
|
35856
|
+
# Play sound effect for segment confirmation
|
|
35857
|
+
self._play_sound_effect('segment_confirmed')
|
|
35858
|
+
|
|
35652
35859
|
if segment.target and segment.target.strip():
|
|
35653
35860
|
try:
|
|
35654
35861
|
self.save_segment_to_activated_tms(segment.source, segment.target)
|
|
@@ -35856,17 +36063,15 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35856
36063
|
)
|
|
35857
36064
|
|
|
35858
36065
|
if reply == QMessageBox.StandardButton.Yes:
|
|
35859
|
-
if self.
|
|
36066
|
+
if self.termbase_mgr:
|
|
35860
36067
|
try:
|
|
35861
|
-
|
|
35862
|
-
|
|
35863
|
-
|
|
35864
|
-
|
|
35865
|
-
|
|
35866
|
-
|
|
35867
|
-
|
|
35868
|
-
# Refresh termview and translation results
|
|
35869
|
-
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")
|
|
35870
36075
|
except Exception as e:
|
|
35871
36076
|
self.log(f"✗ Error deleting glossary entry from database: {e}")
|
|
35872
36077
|
except Exception as e:
|
|
@@ -35875,23 +36080,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35875
36080
|
def _refresh_current_segment_matches(self):
|
|
35876
36081
|
"""Refresh termbase matches for the current segment (after edit/delete)"""
|
|
35877
36082
|
try:
|
|
35878
|
-
|
|
35879
|
-
|
|
35880
|
-
# Get the actual segment index
|
|
35881
|
-
segment_idx = current_row
|
|
35882
|
-
if hasattr(self, 'current_page') and hasattr(self, 'items_per_page'):
|
|
35883
|
-
segment_idx = (self.current_page - 1) * self.items_per_page + current_row
|
|
35884
|
-
|
|
35885
|
-
if 0 <= segment_idx < len(self.current_project.segments):
|
|
35886
|
-
segment = self.current_project.segments[segment_idx]
|
|
35887
|
-
|
|
35888
|
-
# Clear termbase cache for this segment
|
|
35889
|
-
segment_id = id(segment)
|
|
35890
|
-
if hasattr(self, 'termbase_cache') and segment_id in self.termbase_cache:
|
|
35891
|
-
del self.termbase_cache[segment_id]
|
|
35892
|
-
|
|
35893
|
-
# Trigger refresh by re-selecting the cell
|
|
35894
|
-
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()
|
|
35895
36085
|
except Exception as e:
|
|
35896
36086
|
self.log(f"✗ Error refreshing segment matches: {e}")
|
|
35897
36087
|
|
|
@@ -35998,41 +36188,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35998
36188
|
except Exception as e:
|
|
35999
36189
|
self.log(f"Error updating tabbed panel: {e}")
|
|
36000
36190
|
|
|
36001
|
-
#
|
|
36002
|
-
|
|
36003
|
-
try:
|
|
36004
|
-
# Get termbase matches from the segment's cached data if available
|
|
36005
|
-
termbase_matches = []
|
|
36006
|
-
if hasattr(self, 'termbase_cache') and segment_id in self.termbase_cache:
|
|
36007
|
-
cached_matches = self.termbase_cache[segment_id]
|
|
36008
|
-
|
|
36009
|
-
# Convert dict format to list format expected by Termview
|
|
36010
|
-
if isinstance(cached_matches, dict):
|
|
36011
|
-
termbase_matches = [
|
|
36012
|
-
{
|
|
36013
|
-
'source_term': source_key, # Use dict key as source term
|
|
36014
|
-
'target_term': match_data.get('translation', ''),
|
|
36015
|
-
'termbase_name': match_data.get('termbase_name', ''),
|
|
36016
|
-
'ranking': match_data.get('ranking', 99),
|
|
36017
|
-
'is_project_termbase': match_data.get('is_project_termbase', False),
|
|
36018
|
-
'target_synonyms': match_data.get('target_synonyms', []),
|
|
36019
|
-
'term_id': match_data.get('term_id'),
|
|
36020
|
-
'termbase_id': match_data.get('termbase_id')
|
|
36021
|
-
}
|
|
36022
|
-
for source_key, match_data in cached_matches.items()
|
|
36023
|
-
]
|
|
36024
|
-
else:
|
|
36025
|
-
# Already in list format (shouldn't happen, but handle it)
|
|
36026
|
-
termbase_matches = cached_matches
|
|
36027
|
-
|
|
36028
|
-
# Also get NT matches
|
|
36029
|
-
nt_matches = self.find_nt_matches_in_source(source_text)
|
|
36030
|
-
|
|
36031
|
-
self.termview_widget.update_with_matches(source_text, termbase_matches, nt_matches)
|
|
36032
|
-
except Exception as e:
|
|
36033
|
-
self.log(f"Error updating termview: {e}")
|
|
36034
|
-
import traceback
|
|
36035
|
-
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
|
|
36036
36193
|
|
|
36037
36194
|
# ========================================================================
|
|
36038
36195
|
# UTILITY
|
|
@@ -40446,6 +40603,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
40446
40603
|
|
|
40447
40604
|
if best_match:
|
|
40448
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')
|
|
40449
40614
|
except Exception as e:
|
|
40450
40615
|
self.log(f"Error in delayed TM search: {e}")
|
|
40451
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
|