supervertaler 1.9.132__py3-none-any.whl → 1.9.146__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 +395 -214
- modules/database_manager.py +20 -5
- modules/termview_widget.py +6 -2
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.146.dist-info}/METADATA +23 -3
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.146.dist-info}/RECORD +9 -9
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.146.dist-info}/WHEEL +1 -1
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.146.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.146.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.132.dist-info → supervertaler-1.9.146.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.146"
|
|
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)
|
|
27012
27142
|
|
|
27013
|
-
# Use SequenceMatcher
|
|
27014
|
-
|
|
27143
|
+
# Use SequenceMatcher at word level for better readability
|
|
27144
|
+
current_words = current.split()
|
|
27145
|
+
tm_words = tm_source.split()
|
|
27015
27146
|
|
|
27147
|
+
matcher = difflib.SequenceMatcher(None, current_words, tm_words)
|
|
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'):
|
|
@@ -29625,12 +29814,13 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
29625
29814
|
from modules.llm_clients import LLMClient
|
|
29626
29815
|
|
|
29627
29816
|
# Get appropriate API key for provider
|
|
29817
|
+
# Note: api_keys is normalized so both 'gemini' and 'google' exist if either is set
|
|
29628
29818
|
if provider == 'openai':
|
|
29629
29819
|
api_key = api_keys.get('openai', '')
|
|
29630
|
-
elif provider == '
|
|
29820
|
+
elif provider == 'claude':
|
|
29631
29821
|
api_key = api_keys.get('claude', '')
|
|
29632
|
-
elif provider == '
|
|
29633
|
-
api_key = api_keys.get('
|
|
29822
|
+
elif provider == 'gemini':
|
|
29823
|
+
api_key = api_keys.get('gemini', '')
|
|
29634
29824
|
elif provider == 'ollama':
|
|
29635
29825
|
api_key = '' # Ollama doesn't need an API key
|
|
29636
29826
|
else:
|
|
@@ -31168,8 +31358,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31168
31358
|
|
|
31169
31359
|
# Comprehensive set of quote and punctuation characters to strip
|
|
31170
31360
|
# Includes: straight quotes, curly quotes (left/right), German quotes, guillemets, single quotes
|
|
31361
|
+
# ALSO includes parentheses, brackets, and braces for terms like "(typisch)" or "[example]"
|
|
31171
31362
|
# Using explicit characters to avoid encoding issues
|
|
31172
|
-
PUNCT_CHARS = '.,!?;:\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A'
|
|
31363
|
+
PUNCT_CHARS = '.,!?;:\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A()[]{}'
|
|
31173
31364
|
|
|
31174
31365
|
for word in words:
|
|
31175
31366
|
# Remove punctuation including quotes (preserve internal punctuation like "gew.%")
|
|
@@ -31184,13 +31375,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31184
31375
|
# Get project ID for termbase priority lookup
|
|
31185
31376
|
project_id = self.current_project.id if self.current_project and hasattr(self.current_project, 'id') else None
|
|
31186
31377
|
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
|
|
31193
|
-
)
|
|
31378
|
+
# CRITICAL FIX: Search for BOTH the cleaned word AND the original word with punctuation
|
|
31379
|
+
# This allows glossary entries like "ca." to match source text "ca." where
|
|
31380
|
+
# the tokenized/cleaned version is "ca" but the glossary entry has the period
|
|
31381
|
+
words_to_search = [clean_word]
|
|
31382
|
+
# Also try searching with trailing punctuation (for entries like "ca.", "gew.%")
|
|
31383
|
+
original_word_stripped_leading = word.lstrip(PUNCT_CHARS)
|
|
31384
|
+
if original_word_stripped_leading != clean_word and len(original_word_stripped_leading) >= 2:
|
|
31385
|
+
words_to_search.append(original_word_stripped_leading)
|
|
31386
|
+
|
|
31387
|
+
termbase_results = []
|
|
31388
|
+
for search_word in words_to_search:
|
|
31389
|
+
results = self.db_manager.search_termbases(
|
|
31390
|
+
search_word,
|
|
31391
|
+
source_lang=source_lang_code,
|
|
31392
|
+
target_lang=target_lang_code,
|
|
31393
|
+
project_id=project_id,
|
|
31394
|
+
min_length=2
|
|
31395
|
+
)
|
|
31396
|
+
if results:
|
|
31397
|
+
termbase_results.extend(results)
|
|
31194
31398
|
|
|
31195
31399
|
if termbase_results:
|
|
31196
31400
|
for result in termbase_results:
|
|
@@ -35650,6 +35854,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35650
35854
|
|
|
35651
35855
|
self.update_progress_stats()
|
|
35652
35856
|
|
|
35857
|
+
# Play sound effect for segment confirmation
|
|
35858
|
+
self._play_sound_effect('segment_confirmed')
|
|
35859
|
+
|
|
35653
35860
|
if segment.target and segment.target.strip():
|
|
35654
35861
|
try:
|
|
35655
35862
|
self.save_segment_to_activated_tms(segment.source, segment.target)
|
|
@@ -35857,17 +36064,15 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35857
36064
|
)
|
|
35858
36065
|
|
|
35859
36066
|
if reply == QMessageBox.StandardButton.Yes:
|
|
35860
|
-
if self.
|
|
36067
|
+
if self.termbase_mgr:
|
|
35861
36068
|
try:
|
|
35862
|
-
|
|
35863
|
-
|
|
35864
|
-
|
|
35865
|
-
|
|
35866
|
-
|
|
35867
|
-
|
|
35868
|
-
|
|
35869
|
-
# Refresh termview and translation results
|
|
35870
|
-
self._refresh_current_segment_matches()
|
|
36069
|
+
if self.termbase_mgr.delete_term(term_id):
|
|
36070
|
+
self.log(f"✓ Deleted glossary entry: {source_term} → {target_term}")
|
|
36071
|
+
|
|
36072
|
+
# Refresh termview and translation results
|
|
36073
|
+
self._refresh_current_segment_matches()
|
|
36074
|
+
else:
|
|
36075
|
+
self.log(f"✗ Failed to delete glossary entry")
|
|
35871
36076
|
except Exception as e:
|
|
35872
36077
|
self.log(f"✗ Error deleting glossary entry from database: {e}")
|
|
35873
36078
|
except Exception as e:
|
|
@@ -35876,23 +36081,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35876
36081
|
def _refresh_current_segment_matches(self):
|
|
35877
36082
|
"""Refresh termbase matches for the current segment (after edit/delete)"""
|
|
35878
36083
|
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)
|
|
36084
|
+
# Use the targeted refresh method that doesn't trigger TM search
|
|
36085
|
+
self._refresh_termbase_display_for_current_segment()
|
|
35896
36086
|
except Exception as e:
|
|
35897
36087
|
self.log(f"✗ Error refreshing segment matches: {e}")
|
|
35898
36088
|
|
|
@@ -35999,41 +36189,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35999
36189
|
except Exception as e:
|
|
36000
36190
|
self.log(f"Error updating tabbed panel: {e}")
|
|
36001
36191
|
|
|
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()}")
|
|
36192
|
+
# NOTE: Termview is updated AFTER termbase search completes in _on_cell_selected_full
|
|
36193
|
+
# Do NOT update Termview here - the cache may not be populated yet
|
|
36037
36194
|
|
|
36038
36195
|
# ========================================================================
|
|
36039
36196
|
# UTILITY
|
|
@@ -37440,7 +37597,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37440
37597
|
return
|
|
37441
37598
|
|
|
37442
37599
|
# Check if API key exists for selected provider
|
|
37443
|
-
|
|
37600
|
+
# Note: 'gemini' and 'google' are aliases for the same API key
|
|
37601
|
+
has_api_key = provider in api_keys or (provider == 'gemini' and 'google' in api_keys)
|
|
37602
|
+
if not has_api_key:
|
|
37444
37603
|
reply = QMessageBox.question(
|
|
37445
37604
|
self, f"{provider.title()} API Key Missing",
|
|
37446
37605
|
f"{provider.title()} API key not found in api_keys.txt\n\n"
|
|
@@ -37519,8 +37678,10 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37519
37678
|
# Use modular LLM client with user's settings
|
|
37520
37679
|
from modules.llm_clients import LLMClient
|
|
37521
37680
|
|
|
37681
|
+
# Get API key (handle gemini/google alias)
|
|
37682
|
+
api_key = api_keys.get(provider) or (api_keys.get('google') if provider == 'gemini' else None)
|
|
37522
37683
|
client = LLMClient(
|
|
37523
|
-
api_key=
|
|
37684
|
+
api_key=api_key,
|
|
37524
37685
|
provider=provider,
|
|
37525
37686
|
model=model
|
|
37526
37687
|
)
|
|
@@ -38512,7 +38673,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38512
38673
|
"Please configure your API keys in Settings first."
|
|
38513
38674
|
)
|
|
38514
38675
|
return
|
|
38515
|
-
|
|
38676
|
+
# Note: 'gemini' and 'google' are aliases for the same API key
|
|
38677
|
+
elif llm_provider not in api_keys and not (llm_provider == 'gemini' and 'google' in api_keys):
|
|
38516
38678
|
QMessageBox.critical(
|
|
38517
38679
|
self, f"{llm_provider.title()} API Key Missing",
|
|
38518
38680
|
f"Please configure your {llm_provider.title()} API key in Settings."
|
|
@@ -38761,8 +38923,10 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38761
38923
|
|
|
38762
38924
|
if translation_provider_type == 'LLM':
|
|
38763
38925
|
from modules.llm_clients import LLMClient
|
|
38926
|
+
# Handle gemini/google alias
|
|
38927
|
+
api_key = api_keys.get(translation_provider_name) or (api_keys.get('google') if translation_provider_name == 'gemini' else None)
|
|
38764
38928
|
client = LLMClient(
|
|
38765
|
-
api_key=
|
|
38929
|
+
api_key=api_key,
|
|
38766
38930
|
provider=translation_provider_name,
|
|
38767
38931
|
model=model
|
|
38768
38932
|
)
|
|
@@ -39288,7 +39452,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
39288
39452
|
"gemini": "gemini"
|
|
39289
39453
|
}
|
|
39290
39454
|
api_key_name = provider_key_map.get(provider)
|
|
39291
|
-
|
|
39455
|
+
# Handle gemini/google alias
|
|
39456
|
+
api_key_value = api_keys.get(api_key_name) or (api_keys.get('google') if provider == 'gemini' else None)
|
|
39457
|
+
if not api_key_value:
|
|
39292
39458
|
return # No API key for selected provider
|
|
39293
39459
|
|
|
39294
39460
|
# Get model based on provider
|
|
@@ -39310,7 +39476,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
39310
39476
|
from modules.translation_results_panel import TranslationMatch
|
|
39311
39477
|
|
|
39312
39478
|
client = LLMClient(
|
|
39313
|
-
api_key=
|
|
39479
|
+
api_key=api_key_value,
|
|
39314
39480
|
provider=provider,
|
|
39315
39481
|
model=model
|
|
39316
39482
|
)
|
|
@@ -39800,6 +39966,13 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
39800
39966
|
except Exception as e:
|
|
39801
39967
|
self.log(f"⚠ Error loading API keys: {str(e)}")
|
|
39802
39968
|
|
|
39969
|
+
# Normalize gemini/google aliases - users can use either name
|
|
39970
|
+
# This allows downstream code to just use api_keys.get('gemini') directly
|
|
39971
|
+
if api_keys.get('google') and not api_keys.get('gemini'):
|
|
39972
|
+
api_keys['gemini'] = api_keys['google']
|
|
39973
|
+
elif api_keys.get('gemini') and not api_keys.get('google'):
|
|
39974
|
+
api_keys['google'] = api_keys['gemini']
|
|
39975
|
+
|
|
39803
39976
|
return api_keys
|
|
39804
39977
|
|
|
39805
39978
|
def ensure_example_api_keys(self):
|
|
@@ -40447,6 +40620,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
40447
40620
|
|
|
40448
40621
|
if best_match:
|
|
40449
40622
|
self._auto_insert_tm_match(segment, best_match.target, None) # Let function find row
|
|
40623
|
+
# Play 100% TM match alert sound
|
|
40624
|
+
self._play_sound_effect('tm_100_percent_match')
|
|
40625
|
+
|
|
40626
|
+
# 🔊 Play fuzzy match sound if fuzzy matches found (but not 100%)
|
|
40627
|
+
has_100_match = any(float(tm.relevance) >= 99.5 for tm in matches_dict["TM"])
|
|
40628
|
+
has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in matches_dict["TM"])
|
|
40629
|
+
if has_fuzzy_match and not has_100_match:
|
|
40630
|
+
self._play_sound_effect('tm_fuzzy_match')
|
|
40450
40631
|
except Exception as e:
|
|
40451
40632
|
self.log(f"Error in delayed TM search: {e}")
|
|
40452
40633
|
|
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.146
|
|
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.146
|
|
75
75
|
|
|
76
76
|
[](https://pypi.org/project/Supervertaler/)
|
|
77
77
|
[](https://www.python.org/downloads/)
|
|
@@ -79,7 +79,27 @@ 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.146 (January 21, 2026)
|
|
83
|
+
|
|
84
|
+
### FIXED in v1.9.146 - 🔑 Gemini/Google API Key Alias
|
|
85
|
+
|
|
86
|
+
**Bug Fix:** Fixed "Gemini API Key Missing" error when users had `google=...` instead of `gemini=...` in their api_keys.txt. Both names now work identically thanks to automatic normalization at load time.
|
|
87
|
+
|
|
88
|
+
### FIXED in v1.9.140 - 🐛 Glossary Add No Longer Triggers TM Search
|
|
89
|
+
|
|
90
|
+
**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.
|
|
91
|
+
|
|
92
|
+
**Also:** Renamed "Voice OFF" → "Voice Commands OFF" and "Dictate" → "Dictation" for clarity.
|
|
93
|
+
|
|
94
|
+
### FIXED in v1.9.138 - 🏷️ Termview Punctuated Terms & Auto-Sizing Columns
|
|
95
|
+
|
|
96
|
+
**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.
|
|
97
|
+
|
|
98
|
+
**Grid UX:** Segment number column now auto-sizes based on font size and segment count - no more truncated numbers!
|
|
99
|
+
|
|
100
|
+
### FIXED in v1.9.137 - 🔧 Termview Race Condition
|
|
101
|
+
|
|
102
|
+
**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
103
|
|
|
84
104
|
### ENHANCED in v1.9.128 - 📝 Placeholders Tab Layout Optimization
|
|
85
105
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Supervertaler.py,sha256=
|
|
1
|
+
Supervertaler.py,sha256=vNxA4ZPfXTerYIDaHRsbz842S9TbC634rraFSPV6VZI,2153239
|
|
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.146.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
|
|
81
|
+
supervertaler-1.9.146.dist-info/METADATA,sha256=cPeDrCE6z6Kbfb2_vdQFxORfwE-WJQim-v6YFBxmgPs,43796
|
|
82
|
+
supervertaler-1.9.146.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
83
|
+
supervertaler-1.9.146.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
|
|
84
|
+
supervertaler-1.9.146.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
|
|
85
|
+
supervertaler-1.9.146.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|