supervertaler 1.9.180__py3-none-any.whl → 1.9.194__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.
Potentially problematic release.
This version of supervertaler might be problematic. Click here for more details.
- Supervertaler.py +1958 -726
- modules/extract_tm.py +518 -0
- modules/keyboard_shortcuts_widget.py +76 -8
- modules/mt_quick_popup.py +670 -0
- modules/project_tm.py +320 -0
- modules/shortcut_manager.py +19 -5
- modules/statuses.py +2 -2
- modules/superbrowser.py +22 -0
- modules/superlookup.py +3 -3
- modules/termbase_manager.py +0 -1
- modules/termview_widget.py +68 -32
- modules/translation_memory.py +3 -12
- modules/translation_results_panel.py +0 -7
- modules/unified_prompt_manager_qt.py +12 -0
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.194.dist-info}/METADATA +1 -1
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.194.dist-info}/RECORD +20 -17
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.194.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.194.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.194.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.194.dist-info}/top_level.txt +0 -0
modules/project_tm.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ProjectTM - In-memory TM for instant grid lookups (Total Recall architecture)
|
|
3
|
+
|
|
4
|
+
This module implements a lightweight in-memory Translation Memory that extracts
|
|
5
|
+
relevant segments from the full TM database on project load. This makes grid
|
|
6
|
+
navigation instant while keeping the full TM for concordance searches.
|
|
7
|
+
|
|
8
|
+
Inspired by CafeTran's "Total Recall" feature.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sqlite3
|
|
12
|
+
import threading
|
|
13
|
+
from difflib import SequenceMatcher
|
|
14
|
+
from typing import Dict, List, Optional, Callable
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProjectTM:
|
|
19
|
+
"""
|
|
20
|
+
Lightweight in-memory TM extracted from the main TM database.
|
|
21
|
+
|
|
22
|
+
On project load, extracts segments that are relevant to the current project
|
|
23
|
+
(fuzzy matches above threshold) into an in-memory SQLite database for
|
|
24
|
+
instant lookups during grid navigation.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
project_tm = ProjectTM()
|
|
28
|
+
project_tm.extract_from_database(
|
|
29
|
+
db_manager,
|
|
30
|
+
project_segments,
|
|
31
|
+
tm_ids=['tm1', 'tm2'],
|
|
32
|
+
threshold=0.75,
|
|
33
|
+
progress_callback=lambda cur, total: print(f"{cur}/{total}")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Fast lookup during grid navigation
|
|
37
|
+
matches = project_tm.search("source text to translate")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
"""Initialize in-memory SQLite database for ProjectTM"""
|
|
42
|
+
self.conn = sqlite3.connect(":memory:", check_same_thread=False)
|
|
43
|
+
self.conn.row_factory = sqlite3.Row
|
|
44
|
+
self.lock = threading.Lock()
|
|
45
|
+
self.is_built = False
|
|
46
|
+
self.segment_count = 0
|
|
47
|
+
|
|
48
|
+
# Create the schema
|
|
49
|
+
self._create_schema()
|
|
50
|
+
|
|
51
|
+
def _create_schema(self):
|
|
52
|
+
"""Create the in-memory database schema"""
|
|
53
|
+
with self.lock:
|
|
54
|
+
cursor = self.conn.cursor()
|
|
55
|
+
cursor.execute("""
|
|
56
|
+
CREATE TABLE IF NOT EXISTS segments (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
source_text TEXT NOT NULL,
|
|
59
|
+
target_text TEXT NOT NULL,
|
|
60
|
+
source_lower TEXT NOT NULL,
|
|
61
|
+
tm_id TEXT,
|
|
62
|
+
tm_name TEXT,
|
|
63
|
+
similarity REAL,
|
|
64
|
+
original_id INTEGER
|
|
65
|
+
)
|
|
66
|
+
""")
|
|
67
|
+
# Index for fast exact match lookups
|
|
68
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_source_lower ON segments(source_lower)")
|
|
69
|
+
# FTS5 for fuzzy text search
|
|
70
|
+
cursor.execute("""
|
|
71
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS segments_fts USING fts5(
|
|
72
|
+
source_text,
|
|
73
|
+
content=segments,
|
|
74
|
+
content_rowid=id
|
|
75
|
+
)
|
|
76
|
+
""")
|
|
77
|
+
self.conn.commit()
|
|
78
|
+
|
|
79
|
+
def clear(self):
|
|
80
|
+
"""Clear all segments from the ProjectTM"""
|
|
81
|
+
with self.lock:
|
|
82
|
+
cursor = self.conn.cursor()
|
|
83
|
+
cursor.execute("DELETE FROM segments")
|
|
84
|
+
cursor.execute("DELETE FROM segments_fts")
|
|
85
|
+
self.conn.commit()
|
|
86
|
+
self.is_built = False
|
|
87
|
+
self.segment_count = 0
|
|
88
|
+
|
|
89
|
+
def extract_from_database(
|
|
90
|
+
self,
|
|
91
|
+
db_manager,
|
|
92
|
+
project_segments: List,
|
|
93
|
+
tm_ids: List[str] = None,
|
|
94
|
+
source_lang: str = None,
|
|
95
|
+
target_lang: str = None,
|
|
96
|
+
threshold: float = 0.75,
|
|
97
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
98
|
+
log_callback: Optional[Callable[[str], None]] = None
|
|
99
|
+
) -> int:
|
|
100
|
+
"""
|
|
101
|
+
Extract relevant segments from the main TM database into ProjectTM.
|
|
102
|
+
|
|
103
|
+
For each unique source text in the project, searches the TM for fuzzy
|
|
104
|
+
matches above the threshold and stores them in memory.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
db_manager: The main database manager with TM data
|
|
108
|
+
project_segments: List of project segments to find matches for
|
|
109
|
+
tm_ids: List of TM IDs to search (None = all active TMs)
|
|
110
|
+
source_lang: Source language filter
|
|
111
|
+
target_lang: Target language filter
|
|
112
|
+
threshold: Minimum similarity threshold (0.0-1.0)
|
|
113
|
+
progress_callback: Optional callback(current, total) for progress
|
|
114
|
+
log_callback: Optional callback(message) for logging
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Number of TM segments extracted
|
|
118
|
+
"""
|
|
119
|
+
def log(msg):
|
|
120
|
+
if log_callback:
|
|
121
|
+
log_callback(msg)
|
|
122
|
+
else:
|
|
123
|
+
print(msg)
|
|
124
|
+
|
|
125
|
+
self.clear()
|
|
126
|
+
|
|
127
|
+
if not project_segments or not db_manager:
|
|
128
|
+
log(f"[ProjectTM] Early exit: segments={bool(project_segments)}, db={bool(db_manager)}")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
# Get unique source texts from project
|
|
132
|
+
unique_sources = {}
|
|
133
|
+
for seg in project_segments:
|
|
134
|
+
# Try both 'source' and 'source_text' attributes (different segment types use different names)
|
|
135
|
+
source = getattr(seg, 'source', None) or getattr(seg, 'source_text', None)
|
|
136
|
+
if source and source.strip():
|
|
137
|
+
# Normalize: strip and lowercase for deduplication
|
|
138
|
+
key = source.strip().lower()
|
|
139
|
+
if key not in unique_sources:
|
|
140
|
+
unique_sources[key] = source.strip()
|
|
141
|
+
|
|
142
|
+
total = len(unique_sources)
|
|
143
|
+
log(f"[ProjectTM] Found {total} unique source texts from {len(project_segments)} segments")
|
|
144
|
+
if total == 0:
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
extracted_count = 0
|
|
148
|
+
seen_sources = set() # Deduplicate TM entries
|
|
149
|
+
|
|
150
|
+
cursor = self.conn.cursor()
|
|
151
|
+
|
|
152
|
+
log(f"[ProjectTM] Searching TMs: {tm_ids}, threshold={threshold}, langs={source_lang}->{target_lang}")
|
|
153
|
+
|
|
154
|
+
for i, (key, source_text) in enumerate(unique_sources.items()):
|
|
155
|
+
if progress_callback and i % 10 == 0:
|
|
156
|
+
progress_callback(i, total)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
# Search main TM database for fuzzy matches
|
|
160
|
+
matches = db_manager.search_fuzzy_matches(
|
|
161
|
+
source_text,
|
|
162
|
+
tm_ids=tm_ids,
|
|
163
|
+
threshold=threshold,
|
|
164
|
+
max_results=10, # Keep top 10 matches per source
|
|
165
|
+
source_lang=source_lang,
|
|
166
|
+
target_lang=target_lang,
|
|
167
|
+
bidirectional=True
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Debug: log first search
|
|
171
|
+
if i == 0:
|
|
172
|
+
log(f"[ProjectTM] First search '{source_text[:50]}...' returned {len(matches)} matches")
|
|
173
|
+
|
|
174
|
+
for match in matches:
|
|
175
|
+
match_source = match.get('source_text', '')
|
|
176
|
+
match_target = match.get('target_text', '')
|
|
177
|
+
|
|
178
|
+
if not match_source or not match_target:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Deduplicate by source text
|
|
182
|
+
source_key = match_source.strip().lower()
|
|
183
|
+
if source_key in seen_sources:
|
|
184
|
+
continue
|
|
185
|
+
seen_sources.add(source_key)
|
|
186
|
+
|
|
187
|
+
# Insert into ProjectTM
|
|
188
|
+
cursor.execute("""
|
|
189
|
+
INSERT INTO segments (source_text, target_text, source_lower,
|
|
190
|
+
tm_id, tm_name, similarity, original_id)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
192
|
+
""", (
|
|
193
|
+
match_source,
|
|
194
|
+
match_target,
|
|
195
|
+
source_key,
|
|
196
|
+
match.get('tm_id'),
|
|
197
|
+
match.get('tm_name', 'Unknown TM'),
|
|
198
|
+
match.get('similarity', 0),
|
|
199
|
+
match.get('id')
|
|
200
|
+
))
|
|
201
|
+
extracted_count += 1
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
# Log but continue - don't fail extraction for one bad segment
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
# Commit all inserts
|
|
208
|
+
self.conn.commit()
|
|
209
|
+
|
|
210
|
+
# Rebuild FTS5 index
|
|
211
|
+
try:
|
|
212
|
+
cursor.execute("INSERT INTO segments_fts(segments_fts) VALUES('rebuild')")
|
|
213
|
+
self.conn.commit()
|
|
214
|
+
except Exception:
|
|
215
|
+
pass # FTS rebuild may fail if no data, that's OK
|
|
216
|
+
|
|
217
|
+
if progress_callback:
|
|
218
|
+
progress_callback(total, total)
|
|
219
|
+
|
|
220
|
+
self.is_built = True
|
|
221
|
+
self.segment_count = extracted_count
|
|
222
|
+
|
|
223
|
+
return extracted_count
|
|
224
|
+
|
|
225
|
+
def search(self, source_text: str, max_results: int = 5) -> List[Dict]:
|
|
226
|
+
"""
|
|
227
|
+
Search ProjectTM for matches (instant lookup).
|
|
228
|
+
|
|
229
|
+
First checks for exact matches, then falls back to fuzzy search.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
source_text: Source text to search for
|
|
233
|
+
max_results: Maximum number of results to return
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of match dictionaries with source_text, target_text, similarity, etc.
|
|
237
|
+
"""
|
|
238
|
+
if not self.is_built or not source_text:
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
source_lower = source_text.strip().lower()
|
|
242
|
+
results = []
|
|
243
|
+
|
|
244
|
+
with self.lock:
|
|
245
|
+
cursor = self.conn.cursor()
|
|
246
|
+
|
|
247
|
+
# 1. Check for exact match first (fastest)
|
|
248
|
+
cursor.execute("""
|
|
249
|
+
SELECT * FROM segments WHERE source_lower = ? LIMIT 1
|
|
250
|
+
""", (source_lower,))
|
|
251
|
+
exact = cursor.fetchone()
|
|
252
|
+
|
|
253
|
+
if exact:
|
|
254
|
+
results.append({
|
|
255
|
+
'source_text': exact['source_text'],
|
|
256
|
+
'target_text': exact['target_text'],
|
|
257
|
+
'tm_id': exact['tm_id'],
|
|
258
|
+
'tm_name': exact['tm_name'],
|
|
259
|
+
'similarity': 1.0, # Exact match
|
|
260
|
+
'match_pct': 100,
|
|
261
|
+
'id': exact['original_id']
|
|
262
|
+
})
|
|
263
|
+
return results # Exact match - no need to search further
|
|
264
|
+
|
|
265
|
+
# 2. FTS5 fuzzy search
|
|
266
|
+
try:
|
|
267
|
+
# Tokenize query for FTS5
|
|
268
|
+
clean_text = re.sub(r'[^\w\s]', ' ', source_text)
|
|
269
|
+
search_terms = [t for t in clean_text.split() if len(t) > 2]
|
|
270
|
+
|
|
271
|
+
if search_terms:
|
|
272
|
+
fts_query = ' OR '.join(f'"{term}"' for term in search_terms[:10])
|
|
273
|
+
|
|
274
|
+
cursor.execute("""
|
|
275
|
+
SELECT s.*, bm25(segments_fts) as rank
|
|
276
|
+
FROM segments s
|
|
277
|
+
JOIN segments_fts ON s.id = segments_fts.rowid
|
|
278
|
+
WHERE segments_fts MATCH ?
|
|
279
|
+
ORDER BY rank
|
|
280
|
+
LIMIT ?
|
|
281
|
+
""", (fts_query, max_results * 3)) # Get more candidates for re-ranking
|
|
282
|
+
|
|
283
|
+
candidates = cursor.fetchall()
|
|
284
|
+
|
|
285
|
+
# Re-rank by actual similarity
|
|
286
|
+
for row in candidates:
|
|
287
|
+
similarity = self._calculate_similarity(source_text, row['source_text'])
|
|
288
|
+
if similarity >= 0.5: # Lower threshold for ProjectTM (pre-filtered)
|
|
289
|
+
results.append({
|
|
290
|
+
'source_text': row['source_text'],
|
|
291
|
+
'target_text': row['target_text'],
|
|
292
|
+
'tm_id': row['tm_id'],
|
|
293
|
+
'tm_name': row['tm_name'],
|
|
294
|
+
'similarity': similarity,
|
|
295
|
+
'match_pct': int(similarity * 100),
|
|
296
|
+
'id': row['original_id']
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
# Sort by similarity and limit
|
|
300
|
+
results.sort(key=lambda x: x['similarity'], reverse=True)
|
|
301
|
+
results = results[:max_results]
|
|
302
|
+
|
|
303
|
+
except Exception:
|
|
304
|
+
pass # FTS search may fail, return what we have
|
|
305
|
+
|
|
306
|
+
return results
|
|
307
|
+
|
|
308
|
+
def _calculate_similarity(self, text1: str, text2: str) -> float:
|
|
309
|
+
"""Calculate similarity ratio between two texts"""
|
|
310
|
+
# Strip HTML/XML tags for comparison
|
|
311
|
+
clean1 = re.sub(r'<[^>]+>', '', text1).lower()
|
|
312
|
+
clean2 = re.sub(r'<[^>]+>', '', text2).lower()
|
|
313
|
+
return SequenceMatcher(None, clean1, clean2).ratio()
|
|
314
|
+
|
|
315
|
+
def get_stats(self) -> Dict:
|
|
316
|
+
"""Get statistics about the ProjectTM"""
|
|
317
|
+
return {
|
|
318
|
+
'is_built': self.is_built,
|
|
319
|
+
'segment_count': self.segment_count
|
|
320
|
+
}
|
modules/shortcut_manager.py
CHANGED
|
@@ -584,6 +584,15 @@ class ShortcutManager:
|
|
|
584
584
|
"default": "Ctrl+Shift+F",
|
|
585
585
|
"action": "filter_on_selected_text"
|
|
586
586
|
},
|
|
587
|
+
|
|
588
|
+
# QuickTrans (GT4T-style instant translation popup)
|
|
589
|
+
"mt_quick_lookup": {
|
|
590
|
+
"category": "Translation",
|
|
591
|
+
"description": "QuickTrans (instant translation popup)",
|
|
592
|
+
"default": "Ctrl+M",
|
|
593
|
+
"action": "show_mt_quick_popup",
|
|
594
|
+
"context": "editor"
|
|
595
|
+
},
|
|
587
596
|
}
|
|
588
597
|
|
|
589
598
|
def __init__(self, settings_file: Optional[Path] = None):
|
|
@@ -744,25 +753,30 @@ class ShortcutManager:
|
|
|
744
753
|
def find_conflicts(self, shortcut_id: str, key_sequence: str) -> List[str]:
|
|
745
754
|
"""
|
|
746
755
|
Find conflicts with a proposed shortcut
|
|
747
|
-
|
|
756
|
+
|
|
748
757
|
Args:
|
|
749
758
|
shortcut_id: The shortcut being changed
|
|
750
759
|
key_sequence: The proposed new key sequence
|
|
751
|
-
|
|
760
|
+
|
|
752
761
|
Returns:
|
|
753
|
-
List of conflicting shortcut IDs
|
|
762
|
+
List of conflicting shortcut IDs (only enabled shortcuts)
|
|
754
763
|
"""
|
|
755
764
|
conflicts = []
|
|
756
765
|
for other_id, data in self.get_all_shortcuts().items():
|
|
757
766
|
if other_id != shortcut_id and data["current"] == key_sequence:
|
|
767
|
+
# Skip disabled shortcuts - they don't count as conflicts
|
|
768
|
+
# (their key combination is freed up for other uses)
|
|
769
|
+
if not self.is_enabled(other_id):
|
|
770
|
+
continue
|
|
771
|
+
|
|
758
772
|
# Check if they're in different contexts (context-specific shortcuts don't conflict)
|
|
759
773
|
this_context = self.DEFAULT_SHORTCUTS.get(shortcut_id, {}).get("context")
|
|
760
774
|
other_context = self.DEFAULT_SHORTCUTS.get(other_id, {}).get("context")
|
|
761
|
-
|
|
775
|
+
|
|
762
776
|
# Only conflict if same context or no context specified
|
|
763
777
|
if this_context == other_context or not this_context or not other_context:
|
|
764
778
|
conflicts.append(other_id)
|
|
765
|
-
|
|
779
|
+
|
|
766
780
|
return conflicts
|
|
767
781
|
|
|
768
782
|
def export_shortcuts(self, file_path: Path):
|
modules/statuses.py
CHANGED
|
@@ -20,7 +20,7 @@ STATUSES: Dict[str, StatusDefinition] = {
|
|
|
20
20
|
"not_started": StatusDefinition(
|
|
21
21
|
key="not_started",
|
|
22
22
|
label="Not started",
|
|
23
|
-
icon="❌", # Red X
|
|
23
|
+
icon="❌", # Red X emoji (naturally colored, slightly larger)
|
|
24
24
|
color="#ffe6e6",
|
|
25
25
|
memoq_label="Not started",
|
|
26
26
|
memoQ_equivalents=("not started", "not translated"),
|
|
@@ -46,7 +46,7 @@ STATUSES: Dict[str, StatusDefinition] = {
|
|
|
46
46
|
"confirmed": StatusDefinition(
|
|
47
47
|
key="confirmed",
|
|
48
48
|
label="Confirmed",
|
|
49
|
-
icon="✔", # Checkmark - will be
|
|
49
|
+
icon="✔", # Checkmark (text character - will be black)
|
|
50
50
|
color="#d1ffd6",
|
|
51
51
|
memoq_label="Confirmed",
|
|
52
52
|
memoQ_equivalents=("confirmed",),
|
modules/superbrowser.py
CHANGED
|
@@ -160,6 +160,20 @@ class ChatColumn(QWidget):
|
|
|
160
160
|
"""Update URL bar when page changes"""
|
|
161
161
|
self.url_input.setText(url.toString())
|
|
162
162
|
|
|
163
|
+
def cleanup(self):
|
|
164
|
+
"""Clean up web engine resources before deletion"""
|
|
165
|
+
try:
|
|
166
|
+
from PyQt6.QtCore import QUrl
|
|
167
|
+
if hasattr(self, 'web_view'):
|
|
168
|
+
self.web_view.stop()
|
|
169
|
+
self.web_view.setPage(None)
|
|
170
|
+
self.web_view.setUrl(QUrl('about:blank'))
|
|
171
|
+
self.web_view.deleteLater()
|
|
172
|
+
if hasattr(self, 'profile'):
|
|
173
|
+
self.profile.deleteLater()
|
|
174
|
+
except:
|
|
175
|
+
pass
|
|
176
|
+
|
|
163
177
|
|
|
164
178
|
class SuperbrowserWidget(QWidget):
|
|
165
179
|
"""
|
|
@@ -304,6 +318,14 @@ class SuperbrowserWidget(QWidget):
|
|
|
304
318
|
self.claude_column.go_home()
|
|
305
319
|
self.gemini_column.go_home()
|
|
306
320
|
|
|
321
|
+
def cleanup(self):
|
|
322
|
+
"""Clean up all web engine resources before widget deletion"""
|
|
323
|
+
try:
|
|
324
|
+
for column in self.chat_columns:
|
|
325
|
+
column.cleanup()
|
|
326
|
+
except:
|
|
327
|
+
pass
|
|
328
|
+
|
|
307
329
|
|
|
308
330
|
# ============================================================================
|
|
309
331
|
# STANDALONE USAGE
|
modules/superlookup.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
SuperLookup Engine
|
|
3
3
|
==================
|
|
4
4
|
System-wide translation lookup that works anywhere on your computer.
|
|
5
5
|
Captures text from any application and provides:
|
|
@@ -37,7 +37,7 @@ class LookupResult:
|
|
|
37
37
|
|
|
38
38
|
class SuperlookupEngine:
|
|
39
39
|
"""
|
|
40
|
-
|
|
40
|
+
SuperLookup text lookup engine.
|
|
41
41
|
Captures text from any application and provides translation results.
|
|
42
42
|
"""
|
|
43
43
|
|
|
@@ -164,7 +164,7 @@ class SuperlookupEngine:
|
|
|
164
164
|
# Use 'source' and 'target' keys (matches database column names)
|
|
165
165
|
source_text = match.get('source', '')
|
|
166
166
|
target_text = match.get('target', '')
|
|
167
|
-
print(f"[
|
|
167
|
+
print(f"[SuperLookup] Extracted: source='{source_text[:50]}...', target='{target_text[:50]}...'")
|
|
168
168
|
results.append(LookupResult(
|
|
169
169
|
source=source_text,
|
|
170
170
|
target=target_text,
|
modules/termbase_manager.py
CHANGED
|
@@ -609,7 +609,6 @@ class TermbaseManager:
|
|
|
609
609
|
""", (project_id,))
|
|
610
610
|
|
|
611
611
|
active_ids = [row[0] for row in cursor.fetchall()]
|
|
612
|
-
self.log(f"📋 Found {len(active_ids)} active termbases for project {project_id}: {active_ids}")
|
|
613
612
|
return active_ids
|
|
614
613
|
except Exception as e:
|
|
615
614
|
self.log(f"✗ Error getting active termbase IDs: {e}")
|
modules/termview_widget.py
CHANGED
|
@@ -172,7 +172,7 @@ class TermBlock(QWidget):
|
|
|
172
172
|
# Get theme colors
|
|
173
173
|
is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
|
|
174
174
|
separator_color = "#555555" if is_dark else "#CCCCCC"
|
|
175
|
-
source_text_color = "#
|
|
175
|
+
source_text_color = "#FFFFFF" if is_dark else "#333"
|
|
176
176
|
no_match_color = "#666666" if is_dark else "#ddd"
|
|
177
177
|
no_match_bg = "#2A2A2A" if is_dark else "#F5F5F5"
|
|
178
178
|
|
|
@@ -224,10 +224,17 @@ class TermBlock(QWidget):
|
|
|
224
224
|
if self.translations:
|
|
225
225
|
target_text = primary_translation.get('target_term', primary_translation.get('target', ''))
|
|
226
226
|
termbase_name = primary_translation.get('termbase_name', '')
|
|
227
|
-
|
|
228
|
-
# Background color based on termbase type
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
|
|
228
|
+
# Background color based on termbase type (theme-aware)
|
|
229
|
+
is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
|
|
230
|
+
if is_dark:
|
|
231
|
+
# Dark mode: darker backgrounds
|
|
232
|
+
bg_color = "#4A2D3A" if self.is_effective_project else "#2D3E4A" # Dark pink/blue
|
|
233
|
+
hover_color = "#5A3D4A" if self.is_effective_project else "#3D4E5A" # Lighter on hover
|
|
234
|
+
else:
|
|
235
|
+
# Light mode: original colors
|
|
236
|
+
bg_color = "#FFE5F0" if self.is_effective_project else "#D6EBFF" # Pink for project, light blue for regular
|
|
237
|
+
hover_color = "#FFD0E8" if self.is_effective_project else "#BBDEFB" # Slightly darker on hover
|
|
231
238
|
|
|
232
239
|
# Create horizontal layout for target + shortcut badge
|
|
233
240
|
# Apply background to container so it covers both text and badge
|
|
@@ -251,9 +258,11 @@ class TermBlock(QWidget):
|
|
|
251
258
|
target_font.setBold(self.font_bold)
|
|
252
259
|
target_label.setFont(target_font)
|
|
253
260
|
target_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
261
|
+
# Theme-aware text color
|
|
262
|
+
target_text_color = "#B0C4DE" if is_dark else "#0052A3" # Light blue in dark mode
|
|
254
263
|
target_label.setStyleSheet(f"""
|
|
255
264
|
QLabel {{
|
|
256
|
-
color:
|
|
265
|
+
color: {target_text_color};
|
|
257
266
|
padding: 0px;
|
|
258
267
|
background-color: transparent;
|
|
259
268
|
border: none;
|
|
@@ -312,11 +321,12 @@ class TermBlock(QWidget):
|
|
|
312
321
|
if len(self.translations) > 1:
|
|
313
322
|
count_label = QLabel(f"+{len(self.translations) - 1}")
|
|
314
323
|
count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
|
|
325
|
+
count_label.setStyleSheet(f"""
|
|
326
|
+
QLabel {{
|
|
327
|
+
color: {count_color};
|
|
318
328
|
font-size: 7px;
|
|
319
|
-
}
|
|
329
|
+
}}
|
|
320
330
|
""")
|
|
321
331
|
layout.addWidget(count_label)
|
|
322
332
|
return
|
|
@@ -336,10 +346,13 @@ class TermBlock(QWidget):
|
|
|
336
346
|
badge_label = QLabel(badge_text)
|
|
337
347
|
badge_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
338
348
|
badge_label.setFixedSize(badge_width, 14)
|
|
349
|
+
# Theme-aware badge colors
|
|
350
|
+
badge_bg = "#4A90E2" if is_dark else "#1976D2" # Lighter blue in dark mode
|
|
351
|
+
badge_text_color = "#FFFFFF" if is_dark else "white"
|
|
339
352
|
badge_label.setStyleSheet(f"""
|
|
340
353
|
QLabel {{
|
|
341
|
-
background-color:
|
|
342
|
-
color:
|
|
354
|
+
background-color: {badge_bg};
|
|
355
|
+
color: {badge_text_color};
|
|
343
356
|
font-size: 9px;
|
|
344
357
|
font-weight: bold;
|
|
345
358
|
border-radius: 7px;
|
|
@@ -352,16 +365,17 @@ class TermBlock(QWidget):
|
|
|
352
365
|
target_layout.addWidget(badge_label)
|
|
353
366
|
|
|
354
367
|
layout.addWidget(target_container)
|
|
355
|
-
|
|
368
|
+
|
|
356
369
|
# Show count if multiple translations - very compact
|
|
357
370
|
if len(self.translations) > 1:
|
|
358
371
|
count_label = QLabel(f"+{len(self.translations) - 1}")
|
|
359
372
|
count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
|
|
374
|
+
count_label.setStyleSheet(f"""
|
|
375
|
+
QLabel {{
|
|
376
|
+
color: {count_color};
|
|
363
377
|
font-size: 7px;
|
|
364
|
-
}
|
|
378
|
+
}}
|
|
365
379
|
""")
|
|
366
380
|
layout.addWidget(count_label)
|
|
367
381
|
else:
|
|
@@ -439,7 +453,7 @@ class NTBlock(QWidget):
|
|
|
439
453
|
|
|
440
454
|
# Get theme colors
|
|
441
455
|
is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
|
|
442
|
-
source_text_color = "#
|
|
456
|
+
source_text_color = "#FFFFFF" if is_dark else "#5D4E37"
|
|
443
457
|
|
|
444
458
|
# Pastel yellow border for non-translatables
|
|
445
459
|
border_color = "#E6C200" # Darker yellow for border
|
|
@@ -637,6 +651,17 @@ class TermviewWidget(QWidget):
|
|
|
637
651
|
is_dark = theme.name == "Dark"
|
|
638
652
|
info_label_color = "#909090" if is_dark else info_text
|
|
639
653
|
self.info_label.setStyleSheet(f"color: {info_label_color}; font-size: 10px; padding: 5px;")
|
|
654
|
+
|
|
655
|
+
# Refresh term blocks to pick up new theme colors
|
|
656
|
+
if hasattr(self, '_last_termbase_matches') and hasattr(self, '_last_nt_matches') and hasattr(self, 'current_source'):
|
|
657
|
+
# Re-render with stored matches to apply new theme colors
|
|
658
|
+
if self.current_source:
|
|
659
|
+
self.update_with_matches(
|
|
660
|
+
self.current_source,
|
|
661
|
+
self._last_termbase_matches or [],
|
|
662
|
+
self._last_nt_matches,
|
|
663
|
+
self._status_hint if hasattr(self, '_status_hint') else None
|
|
664
|
+
)
|
|
640
665
|
|
|
641
666
|
def set_font_settings(self, font_family: str = "Segoe UI", font_size: int = 10, bold: bool = False):
|
|
642
667
|
"""Update font settings for Termview
|
|
@@ -694,27 +719,31 @@ class TermviewWidget(QWidget):
|
|
|
694
719
|
font.setBold(self.current_font_bold)
|
|
695
720
|
block.source_label.setFont(font)
|
|
696
721
|
|
|
697
|
-
def update_with_matches(self, source_text: str, termbase_matches: List[Dict], nt_matches: List[Dict] = None):
|
|
722
|
+
def update_with_matches(self, source_text: str, termbase_matches: List[Dict], nt_matches: List[Dict] = None, status_hint: str = None):
|
|
698
723
|
"""
|
|
699
724
|
Update the termview display with pre-computed termbase and NT matches
|
|
700
|
-
|
|
725
|
+
|
|
701
726
|
RYS-STYLE DISPLAY: Show source text as tokens with translations underneath
|
|
702
|
-
|
|
727
|
+
|
|
703
728
|
Args:
|
|
704
729
|
source_text: Source segment text
|
|
705
730
|
termbase_matches: List of termbase match dicts from Translation Results
|
|
706
731
|
nt_matches: Optional list of NT match dicts with 'text', 'start', 'end', 'list_name' keys
|
|
732
|
+
status_hint: Optional hint about why there might be no matches (e.g., 'no_termbases_activated', 'wrong_language')
|
|
707
733
|
"""
|
|
708
734
|
self.current_source = source_text
|
|
709
|
-
|
|
735
|
+
# Store matches for theme refresh
|
|
736
|
+
self._last_termbase_matches = termbase_matches
|
|
737
|
+
self._last_nt_matches = nt_matches
|
|
738
|
+
|
|
710
739
|
# Clear existing blocks and shortcut mappings
|
|
711
740
|
self.clear_terms()
|
|
712
741
|
self.shortcut_terms = {} # Reset shortcut mappings
|
|
713
|
-
|
|
742
|
+
|
|
714
743
|
if not source_text or not source_text.strip():
|
|
715
744
|
self.info_label.setText("No segment selected")
|
|
716
745
|
return
|
|
717
|
-
|
|
746
|
+
|
|
718
747
|
# Strip HTML/XML tags from source text for display in TermView
|
|
719
748
|
# This handles CAT tool tags like <b>, </b>, <i>, </i>, <u>, </u>, <bi>, <sub>, <sup>, <li-o>, <li-b>
|
|
720
749
|
# as well as memoQ tags {1}, [2}, {3], Trados tags <1>, </1>, and Déjà Vu tags {00001}
|
|
@@ -725,17 +754,17 @@ class TermviewWidget(QWidget):
|
|
|
725
754
|
display_text = re.sub(r'\[[^\[\]]*\}', '', display_text) # Opening: [anything}
|
|
726
755
|
display_text = re.sub(r'\{[^\{\}]*\]', '', display_text) # Closing: {anything]
|
|
727
756
|
display_text = display_text.strip()
|
|
728
|
-
|
|
757
|
+
|
|
729
758
|
# If stripping tags leaves nothing, fall back to original
|
|
730
759
|
if not display_text:
|
|
731
760
|
display_text = source_text
|
|
732
|
-
|
|
761
|
+
|
|
733
762
|
has_termbase = termbase_matches and len(termbase_matches) > 0
|
|
734
763
|
has_nt = nt_matches and len(nt_matches) > 0
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
764
|
+
|
|
765
|
+
# Store status hint for info label (will be set at the end)
|
|
766
|
+
self._status_hint = status_hint
|
|
767
|
+
self._has_any_matches = has_termbase or has_nt
|
|
739
768
|
|
|
740
769
|
# Convert termbase matches to dict for easy lookup: {source_term.lower(): [translations]}
|
|
741
770
|
matches_dict = {}
|
|
@@ -867,11 +896,18 @@ class TermviewWidget(QWidget):
|
|
|
867
896
|
info_parts.append(f"{blocks_with_translations} terms")
|
|
868
897
|
if blocks_with_nt > 0:
|
|
869
898
|
info_parts.append(f"{blocks_with_nt} NTs")
|
|
870
|
-
|
|
899
|
+
|
|
871
900
|
if info_parts:
|
|
872
901
|
self.info_label.setText(f"✓ Found {', '.join(info_parts)} in {len(tokens)} words")
|
|
873
902
|
else:
|
|
874
|
-
|
|
903
|
+
# Show appropriate message based on status hint when no matches
|
|
904
|
+
status_hint = getattr(self, '_status_hint', None)
|
|
905
|
+
if status_hint == 'no_termbases_activated':
|
|
906
|
+
self.info_label.setText(f"No glossaries activated ({len(tokens)} words)")
|
|
907
|
+
elif status_hint == 'wrong_language':
|
|
908
|
+
self.info_label.setText(f"Glossaries don't match language pair ({len(tokens)} words)")
|
|
909
|
+
else:
|
|
910
|
+
self.info_label.setText(f"No matches in {len(tokens)} words")
|
|
875
911
|
|
|
876
912
|
def get_all_termbase_matches(self, text: str) -> Dict[str, List[Dict]]:
|
|
877
913
|
"""
|