supervertaler 1.9.172__py3-none-any.whl → 1.9.180__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 +1133 -310
- modules/database_manager.py +243 -83
- modules/database_migrations.py +54 -7
- modules/mqxliff_handler.py +71 -2
- modules/termbase_manager.py +105 -1
- modules/theme_manager.py +41 -4
- modules/tm_metadata_manager.py +23 -18
- modules/translation_memory.py +2 -2
- modules/unified_prompt_library.py +2 -2
- modules/unified_prompt_manager_qt.py +35 -18
- supervertaler-1.9.180.dist-info/METADATA +151 -0
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.180.dist-info}/RECORD +16 -16
- supervertaler-1.9.172.dist-info/METADATA +0 -930
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.180.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.180.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.180.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.180.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -3,8 +3,6 @@ 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.153 (Tab Layout Reorganization)
|
|
7
|
-
Release Date: January 23, 2026
|
|
8
6
|
Framework: PyQt6
|
|
9
7
|
|
|
10
8
|
This is the modern edition of Supervertaler using PyQt6 framework.
|
|
@@ -34,7 +32,7 @@ License: MIT
|
|
|
34
32
|
"""
|
|
35
33
|
|
|
36
34
|
# Version Information.
|
|
37
|
-
__version__ = "1.9.
|
|
35
|
+
__version__ = "1.9.180"
|
|
38
36
|
__phase__ = "0.9"
|
|
39
37
|
__release_date__ = "2026-01-28"
|
|
40
38
|
__edition__ = "Qt"
|
|
@@ -753,7 +751,83 @@ def get_wrapping_tag_pair(source_text: str, target_text: str) -> tuple:
|
|
|
753
751
|
opening, closing = get_tag_pair(num)
|
|
754
752
|
if opening not in target_tags or closing not in target_tags:
|
|
755
753
|
return (opening, closing)
|
|
756
|
-
|
|
754
|
+
|
|
755
|
+
return (None, None)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def get_html_wrapping_tag_pair(source_text: str, target_text: str) -> tuple:
|
|
759
|
+
"""
|
|
760
|
+
Get the next available HTML tag pair for wrapping selected text.
|
|
761
|
+
|
|
762
|
+
Finds paired HTML tags (opening + closing) from source that are not yet
|
|
763
|
+
complete in target. Supports common formatting tags: b, i, u, em, strong,
|
|
764
|
+
span, li, p, a, sub, sup, etc.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
source_text: Source segment text with HTML tags
|
|
768
|
+
target_text: Current target text
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
Tuple of (opening_tag, closing_tag) or (None, None) if no pairs available
|
|
772
|
+
"""
|
|
773
|
+
import re
|
|
774
|
+
|
|
775
|
+
# Find all HTML tags in source
|
|
776
|
+
# Match: <tag>, <tag attr="...">, </tag>
|
|
777
|
+
tag_pattern = r'<(/?)([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[^>]*)?>'
|
|
778
|
+
source_matches = re.findall(tag_pattern, source_text)
|
|
779
|
+
target_matches = re.findall(tag_pattern, target_text)
|
|
780
|
+
|
|
781
|
+
if not source_matches:
|
|
782
|
+
return (None, None)
|
|
783
|
+
|
|
784
|
+
# Build lists of opening and closing tags in source
|
|
785
|
+
source_opening = [] # [(full_tag, tag_name), ...]
|
|
786
|
+
source_closing = []
|
|
787
|
+
|
|
788
|
+
for match in re.finditer(tag_pattern, source_text):
|
|
789
|
+
is_closing = match.group(1) == '/'
|
|
790
|
+
tag_name = match.group(2).lower()
|
|
791
|
+
full_tag = match.group(0)
|
|
792
|
+
|
|
793
|
+
if is_closing:
|
|
794
|
+
source_closing.append((full_tag, tag_name))
|
|
795
|
+
else:
|
|
796
|
+
source_opening.append((full_tag, tag_name))
|
|
797
|
+
|
|
798
|
+
# Build sets of tag names already in target
|
|
799
|
+
target_opening_names = set()
|
|
800
|
+
target_closing_names = set()
|
|
801
|
+
|
|
802
|
+
for is_closing, tag_name in target_matches:
|
|
803
|
+
if is_closing == '/':
|
|
804
|
+
target_closing_names.add(tag_name.lower())
|
|
805
|
+
else:
|
|
806
|
+
target_opening_names.add(tag_name.lower())
|
|
807
|
+
|
|
808
|
+
# Find first tag pair where both opening and closing exist in source
|
|
809
|
+
# but at least one is missing from target
|
|
810
|
+
seen_tags = set()
|
|
811
|
+
for full_tag, tag_name in source_opening:
|
|
812
|
+
if tag_name in seen_tags:
|
|
813
|
+
continue
|
|
814
|
+
seen_tags.add(tag_name)
|
|
815
|
+
|
|
816
|
+
# Check if there's a matching closing tag in source
|
|
817
|
+
has_closing = any(name == tag_name for _, name in source_closing)
|
|
818
|
+
if not has_closing:
|
|
819
|
+
continue
|
|
820
|
+
|
|
821
|
+
# Check if pair is incomplete in target
|
|
822
|
+
opening_in_target = tag_name in target_opening_names
|
|
823
|
+
closing_in_target = tag_name in target_closing_names
|
|
824
|
+
|
|
825
|
+
if not opening_in_target or not closing_in_target:
|
|
826
|
+
# Find the actual opening and closing tags from source
|
|
827
|
+
opening_tag = full_tag
|
|
828
|
+
closing_tag = f"</{tag_name}>"
|
|
829
|
+
return (opening_tag, closing_tag)
|
|
830
|
+
|
|
757
831
|
return (None, None)
|
|
758
832
|
|
|
759
833
|
|
|
@@ -1148,48 +1222,48 @@ class GridTextEditor(QTextEdit):
|
|
|
1148
1222
|
def _insert_next_tag_or_wrap_selection(self):
|
|
1149
1223
|
"""
|
|
1150
1224
|
Insert the next memoQ tag, HTML tag, or CafeTran pipe symbol from source, or wrap selection.
|
|
1151
|
-
|
|
1225
|
+
|
|
1152
1226
|
Behavior:
|
|
1153
|
-
- If text is selected: Wrap it with the next available tag pair [N}selection{N] or |selection|
|
|
1227
|
+
- If text is selected: Wrap it with the next available tag pair [N}selection{N] or <tag>selection</tag> or |selection|
|
|
1154
1228
|
- If no selection: Insert the next unused tag/pipe from source at cursor position
|
|
1155
|
-
|
|
1229
|
+
|
|
1156
1230
|
Supports:
|
|
1157
1231
|
- memoQ tags: [1}, {1], [2}, {2], etc.
|
|
1158
1232
|
- HTML/XML tags: <li>, </li>, <b>, </b>, <i>, </i>, etc.
|
|
1159
1233
|
- CafeTran pipe symbols: |
|
|
1160
|
-
|
|
1234
|
+
|
|
1161
1235
|
Shortcut: Ctrl+, (comma)
|
|
1162
1236
|
"""
|
|
1163
1237
|
# Get the main window and current segment
|
|
1164
1238
|
if not self.table_widget or self.current_row is None:
|
|
1165
1239
|
return
|
|
1166
|
-
|
|
1240
|
+
|
|
1167
1241
|
# Navigate up to find main window
|
|
1168
1242
|
main_window = self.table_widget.parent()
|
|
1169
1243
|
while main_window and not hasattr(main_window, 'current_project'):
|
|
1170
1244
|
main_window = main_window.parent()
|
|
1171
|
-
|
|
1245
|
+
|
|
1172
1246
|
if not main_window or not hasattr(main_window, 'current_project'):
|
|
1173
1247
|
return
|
|
1174
|
-
|
|
1248
|
+
|
|
1175
1249
|
if not main_window.current_project or self.current_row >= len(main_window.current_project.segments):
|
|
1176
1250
|
return
|
|
1177
|
-
|
|
1251
|
+
|
|
1178
1252
|
segment = main_window.current_project.segments[self.current_row]
|
|
1179
1253
|
source_text = segment.source
|
|
1180
1254
|
current_target = self.toPlainText()
|
|
1181
|
-
|
|
1255
|
+
|
|
1182
1256
|
# Check what type of tags are in the source
|
|
1183
1257
|
has_memoq_tags = bool(extract_memoq_tags(source_text))
|
|
1184
1258
|
has_html_tags = bool(extract_html_tags(source_text))
|
|
1185
1259
|
has_any_tags = has_memoq_tags or has_html_tags
|
|
1186
1260
|
has_pipe_symbols = '|' in source_text
|
|
1187
|
-
|
|
1261
|
+
|
|
1188
1262
|
# Check if there's a selection
|
|
1189
1263
|
cursor = self.textCursor()
|
|
1190
1264
|
if cursor.hasSelection():
|
|
1191
1265
|
selected_text = cursor.selectedText()
|
|
1192
|
-
|
|
1266
|
+
|
|
1193
1267
|
# Try memoQ tag pair first
|
|
1194
1268
|
if has_memoq_tags:
|
|
1195
1269
|
opening_tag, closing_tag = get_wrapping_tag_pair(source_text, current_target)
|
|
@@ -1199,7 +1273,17 @@ class GridTextEditor(QTextEdit):
|
|
|
1199
1273
|
if hasattr(main_window, 'log'):
|
|
1200
1274
|
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
1201
1275
|
return
|
|
1202
|
-
|
|
1276
|
+
|
|
1277
|
+
# Try HTML tag pairs (e.g., <b>...</b>, <i>...</i>)
|
|
1278
|
+
if has_html_tags:
|
|
1279
|
+
opening_tag, closing_tag = get_html_wrapping_tag_pair(source_text, current_target)
|
|
1280
|
+
if opening_tag and closing_tag:
|
|
1281
|
+
wrapped_text = f"{opening_tag}{selected_text}{closing_tag}"
|
|
1282
|
+
cursor.insertText(wrapped_text)
|
|
1283
|
+
if hasattr(main_window, 'log'):
|
|
1284
|
+
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
1285
|
+
return
|
|
1286
|
+
|
|
1203
1287
|
# Try CafeTran pipe symbols
|
|
1204
1288
|
if has_pipe_symbols:
|
|
1205
1289
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -1210,12 +1294,12 @@ class GridTextEditor(QTextEdit):
|
|
|
1210
1294
|
if hasattr(main_window, 'log'):
|
|
1211
1295
|
main_window.log(f"🏷️ Wrapped selection with |...|")
|
|
1212
1296
|
return
|
|
1213
|
-
|
|
1297
|
+
|
|
1214
1298
|
if hasattr(main_window, 'log'):
|
|
1215
1299
|
main_window.log("⚠️ No tag pairs available from source")
|
|
1216
1300
|
else:
|
|
1217
1301
|
# No selection - insert next unused tag or pipe at cursor
|
|
1218
|
-
|
|
1302
|
+
|
|
1219
1303
|
# Try memoQ tags and HTML tags (find_next_unused_tag handles both)
|
|
1220
1304
|
if has_any_tags:
|
|
1221
1305
|
next_tag = find_next_unused_tag(source_text, current_target)
|
|
@@ -1224,7 +1308,7 @@ class GridTextEditor(QTextEdit):
|
|
|
1224
1308
|
if hasattr(main_window, 'log'):
|
|
1225
1309
|
main_window.log(f"🏷️ Inserted tag: {next_tag}")
|
|
1226
1310
|
return
|
|
1227
|
-
|
|
1311
|
+
|
|
1228
1312
|
# Try CafeTran pipe symbols
|
|
1229
1313
|
if has_pipe_symbols:
|
|
1230
1314
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -1233,7 +1317,7 @@ class GridTextEditor(QTextEdit):
|
|
|
1233
1317
|
if hasattr(main_window, 'log'):
|
|
1234
1318
|
main_window.log(f"🏷️ Inserted pipe symbol (|)")
|
|
1235
1319
|
return
|
|
1236
|
-
|
|
1320
|
+
|
|
1237
1321
|
if hasattr(main_window, 'log'):
|
|
1238
1322
|
main_window.log("✓ All tags from source already in target")
|
|
1239
1323
|
|
|
@@ -2080,16 +2164,27 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
2080
2164
|
# Use stored table reference and row number
|
|
2081
2165
|
if self.table_ref and self.row >= 0:
|
|
2082
2166
|
try:
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2167
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
2168
|
+
modifiers = event.modifiers()
|
|
2169
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
2170
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
2171
|
+
|
|
2172
|
+
if is_shift or is_ctrl:
|
|
2173
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
2174
|
+
# but don't call selectRow() which would clear the selection
|
|
2175
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2176
|
+
else:
|
|
2177
|
+
# Normal click - select just this row
|
|
2178
|
+
self.table_ref.selectRow(self.row)
|
|
2179
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2180
|
+
|
|
2181
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
2182
|
+
# Find the main window and call the method directly
|
|
2183
|
+
main_window = self.table_ref.parent()
|
|
2184
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
2185
|
+
main_window = main_window.parent()
|
|
2186
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
2187
|
+
main_window.on_cell_selected(self.row, 2, -1, -1)
|
|
2093
2188
|
except Exception as e:
|
|
2094
2189
|
print(f"Error triggering manual cell selection: {e}")
|
|
2095
2190
|
|
|
@@ -2166,20 +2261,32 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
2166
2261
|
"""Select text when focused for easy copying and trigger row selection"""
|
|
2167
2262
|
super().focusInEvent(event)
|
|
2168
2263
|
# Don't auto-select - let user select manually
|
|
2169
|
-
|
|
2264
|
+
|
|
2170
2265
|
# Use stored table reference and row number
|
|
2171
2266
|
if self.table_ref and self.row >= 0:
|
|
2172
2267
|
try:
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2268
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
2269
|
+
from PyQt6.QtWidgets import QApplication
|
|
2270
|
+
modifiers = QApplication.keyboardModifiers()
|
|
2271
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
2272
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
2273
|
+
|
|
2274
|
+
if is_shift or is_ctrl:
|
|
2275
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
2276
|
+
# but don't call selectRow() which would clear the selection
|
|
2277
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2278
|
+
else:
|
|
2279
|
+
# Normal focus - select just this row
|
|
2280
|
+
self.table_ref.selectRow(self.row)
|
|
2281
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2282
|
+
|
|
2283
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
2284
|
+
# Find the main window and call the method directly
|
|
2285
|
+
main_window = self.table_ref.parent()
|
|
2286
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
2287
|
+
main_window = main_window.parent()
|
|
2288
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
2289
|
+
main_window.on_cell_selected(self.row, 2, -1, -1)
|
|
2183
2290
|
except Exception as e:
|
|
2184
2291
|
print(f"Error triggering manual cell selection: {e}")
|
|
2185
2292
|
|
|
@@ -3026,19 +3133,30 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3026
3133
|
super().mousePressEvent(event)
|
|
3027
3134
|
# Auto-select the row when clicking in the target cell
|
|
3028
3135
|
if self.table and self.row >= 0:
|
|
3029
|
-
|
|
3030
|
-
|
|
3136
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
3137
|
+
modifiers = event.modifiers()
|
|
3138
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
3139
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
3140
|
+
|
|
3141
|
+
if is_shift or is_ctrl:
|
|
3142
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
3143
|
+
# but don't call selectRow() which would clear the selection
|
|
3144
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3145
|
+
else:
|
|
3146
|
+
# Normal click - select just this row
|
|
3147
|
+
self.table.selectRow(self.row)
|
|
3148
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3031
3149
|
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3150
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
3151
|
+
# Find the main window and call the method directly
|
|
3152
|
+
try:
|
|
3153
|
+
main_window = self.table.parent()
|
|
3154
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
3155
|
+
main_window = main_window.parent()
|
|
3156
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
3157
|
+
main_window.on_cell_selected(self.row, 3, -1, -1)
|
|
3158
|
+
except Exception as e:
|
|
3159
|
+
print(f"Error triggering manual cell selection: {e}")
|
|
3042
3160
|
|
|
3043
3161
|
def mouseReleaseEvent(self, event):
|
|
3044
3162
|
"""Smart word selection - expand partial selections to full words
|
|
@@ -3108,19 +3226,31 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3108
3226
|
self.show()
|
|
3109
3227
|
# Auto-select the row when focusing the target cell
|
|
3110
3228
|
if self.table and self.row >= 0:
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3229
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
3230
|
+
from PyQt6.QtWidgets import QApplication
|
|
3231
|
+
modifiers = QApplication.keyboardModifiers()
|
|
3232
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
3233
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
3234
|
+
|
|
3235
|
+
if is_shift or is_ctrl:
|
|
3236
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
3237
|
+
# but don't call selectRow() which would clear the selection
|
|
3238
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3239
|
+
else:
|
|
3240
|
+
# Normal focus - select just this row
|
|
3241
|
+
self.table.selectRow(self.row)
|
|
3242
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3243
|
+
|
|
3244
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
3245
|
+
# Find the main window and call the method directly
|
|
3246
|
+
try:
|
|
3247
|
+
main_window = self.table.parent()
|
|
3248
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
3249
|
+
main_window = main_window.parent()
|
|
3250
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
3251
|
+
main_window.on_cell_selected(self.row, 3, -1, -1)
|
|
3252
|
+
except Exception as e:
|
|
3253
|
+
print(f"Error triggering manual cell selection: {e}")
|
|
3124
3254
|
|
|
3125
3255
|
def keyPressEvent(self, event):
|
|
3126
3256
|
"""Handle Tab and Ctrl+E keys to cycle between source and target cells"""
|
|
@@ -3423,7 +3553,7 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3423
3553
|
cursor = self.textCursor()
|
|
3424
3554
|
if cursor.hasSelection():
|
|
3425
3555
|
selected_text = cursor.selectedText()
|
|
3426
|
-
|
|
3556
|
+
|
|
3427
3557
|
# Try memoQ tag pair first
|
|
3428
3558
|
if has_memoq_tags:
|
|
3429
3559
|
opening_tag, closing_tag = get_wrapping_tag_pair(source_text, current_target)
|
|
@@ -3433,7 +3563,17 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3433
3563
|
if hasattr(main_window, 'log'):
|
|
3434
3564
|
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
3435
3565
|
return
|
|
3436
|
-
|
|
3566
|
+
|
|
3567
|
+
# Try HTML tag pairs (e.g., <b>...</b>, <i>...</i>)
|
|
3568
|
+
if has_html_tags:
|
|
3569
|
+
opening_tag, closing_tag = get_html_wrapping_tag_pair(source_text, current_target)
|
|
3570
|
+
if opening_tag and closing_tag:
|
|
3571
|
+
wrapped_text = f"{opening_tag}{selected_text}{closing_tag}"
|
|
3572
|
+
cursor.insertText(wrapped_text)
|
|
3573
|
+
if hasattr(main_window, 'log'):
|
|
3574
|
+
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
3575
|
+
return
|
|
3576
|
+
|
|
3437
3577
|
# Try CafeTran pipe symbols
|
|
3438
3578
|
if has_pipe_symbols:
|
|
3439
3579
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -3444,12 +3584,12 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3444
3584
|
if hasattr(main_window, 'log'):
|
|
3445
3585
|
main_window.log(f"🏷️ Wrapped selection with |...|")
|
|
3446
3586
|
return
|
|
3447
|
-
|
|
3587
|
+
|
|
3448
3588
|
if hasattr(main_window, 'log'):
|
|
3449
3589
|
main_window.log("⚠️ No tag pairs available from source")
|
|
3450
3590
|
else:
|
|
3451
3591
|
# No selection - insert next unused tag or pipe at cursor
|
|
3452
|
-
|
|
3592
|
+
|
|
3453
3593
|
# Try memoQ tags and HTML tags (find_next_unused_tag handles both)
|
|
3454
3594
|
if has_any_tags:
|
|
3455
3595
|
next_tag = find_next_unused_tag(source_text, current_target)
|
|
@@ -3458,7 +3598,7 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3458
3598
|
if hasattr(main_window, 'log'):
|
|
3459
3599
|
main_window.log(f"🏷️ Inserted tag: {next_tag}")
|
|
3460
3600
|
return
|
|
3461
|
-
|
|
3601
|
+
|
|
3462
3602
|
# Try CafeTran pipe symbols
|
|
3463
3603
|
if has_pipe_symbols:
|
|
3464
3604
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -3467,10 +3607,10 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3467
3607
|
if hasattr(main_window, 'log'):
|
|
3468
3608
|
main_window.log(f"🏷️ Inserted pipe symbol (|)")
|
|
3469
3609
|
return
|
|
3470
|
-
|
|
3610
|
+
|
|
3471
3611
|
if hasattr(main_window, 'log'):
|
|
3472
3612
|
main_window.log("✓ All tags from source already in target")
|
|
3473
|
-
|
|
3613
|
+
|
|
3474
3614
|
def _copy_source_to_target(self):
|
|
3475
3615
|
"""
|
|
3476
3616
|
Copy source text to target cell.
|
|
@@ -5179,12 +5319,14 @@ class AdvancedFiltersDialog(QDialog):
|
|
|
5179
5319
|
|
|
5180
5320
|
self.status_not_started = CheckmarkCheckBox("Not started")
|
|
5181
5321
|
self.status_edited = CheckmarkCheckBox("Edited")
|
|
5322
|
+
self.status_pretranslated = CheckmarkCheckBox("Pre-translated")
|
|
5182
5323
|
self.status_translated = CheckmarkCheckBox("Translated")
|
|
5183
5324
|
self.status_confirmed = CheckmarkCheckBox("Confirmed")
|
|
5184
5325
|
self.status_draft = CheckmarkCheckBox("Draft")
|
|
5185
|
-
|
|
5326
|
+
|
|
5186
5327
|
status_layout.addWidget(self.status_not_started)
|
|
5187
5328
|
status_layout.addWidget(self.status_edited)
|
|
5329
|
+
status_layout.addWidget(self.status_pretranslated)
|
|
5188
5330
|
status_layout.addWidget(self.status_translated)
|
|
5189
5331
|
status_layout.addWidget(self.status_confirmed)
|
|
5190
5332
|
status_layout.addWidget(self.status_draft)
|
|
@@ -5257,6 +5399,7 @@ class AdvancedFiltersDialog(QDialog):
|
|
|
5257
5399
|
|
|
5258
5400
|
self.status_not_started.setChecked(False)
|
|
5259
5401
|
self.status_edited.setChecked(False)
|
|
5402
|
+
self.status_pretranslated.setChecked(False)
|
|
5260
5403
|
self.status_translated.setChecked(False)
|
|
5261
5404
|
self.status_confirmed.setChecked(False)
|
|
5262
5405
|
self.status_draft.setChecked(False)
|
|
@@ -5283,6 +5426,8 @@ class AdvancedFiltersDialog(QDialog):
|
|
|
5283
5426
|
row_status.append('not_started')
|
|
5284
5427
|
if self.status_edited.isChecked():
|
|
5285
5428
|
row_status.append('edited')
|
|
5429
|
+
if self.status_pretranslated.isChecked():
|
|
5430
|
+
row_status.append('pretranslated')
|
|
5286
5431
|
if self.status_translated.isChecked():
|
|
5287
5432
|
row_status.append('translated')
|
|
5288
5433
|
if self.status_confirmed.isChecked():
|
|
@@ -5598,7 +5743,7 @@ class PreTranslationWorker(QThread):
|
|
|
5598
5743
|
match = matches[0]
|
|
5599
5744
|
match_pct = match.get('match_pct', 0)
|
|
5600
5745
|
print(f"🔍 TM PRE-TRANSLATE: Best match pct: {match_pct}")
|
|
5601
|
-
if match_pct >=
|
|
5746
|
+
if match_pct >= 75: # Accept matches 75% and above
|
|
5602
5747
|
return match.get('target', '')
|
|
5603
5748
|
return None
|
|
5604
5749
|
except Exception as e:
|
|
@@ -5647,11 +5792,14 @@ class PreTranslationWorker(QThread):
|
|
|
5647
5792
|
custom_prompt = None
|
|
5648
5793
|
if self.prompt_manager:
|
|
5649
5794
|
try:
|
|
5795
|
+
# Get glossary terms for AI injection
|
|
5796
|
+
glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
|
|
5650
5797
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5651
5798
|
source_text=segment.source,
|
|
5652
5799
|
source_lang=source_lang,
|
|
5653
5800
|
target_lang=target_lang,
|
|
5654
|
-
mode="single"
|
|
5801
|
+
mode="single",
|
|
5802
|
+
glossary_terms=glossary_terms
|
|
5655
5803
|
)
|
|
5656
5804
|
# Extract just the instruction part (without the source text section)
|
|
5657
5805
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -5710,11 +5858,14 @@ class PreTranslationWorker(QThread):
|
|
|
5710
5858
|
if self.prompt_manager and batch_segments:
|
|
5711
5859
|
try:
|
|
5712
5860
|
first_segment = batch_segments[0][1]
|
|
5861
|
+
# Get glossary terms for AI injection
|
|
5862
|
+
glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
|
|
5713
5863
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5714
5864
|
source_text=first_segment.source,
|
|
5715
5865
|
source_lang=source_lang,
|
|
5716
5866
|
target_lang=target_lang,
|
|
5717
|
-
mode="single"
|
|
5867
|
+
mode="single",
|
|
5868
|
+
glossary_terms=glossary_terms
|
|
5718
5869
|
)
|
|
5719
5870
|
# Extract just the instruction part
|
|
5720
5871
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -6161,6 +6312,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6161
6312
|
# TM Metadata Manager - needed for TM list in Superlookup
|
|
6162
6313
|
from modules.tm_metadata_manager import TMMetadataManager
|
|
6163
6314
|
self.tm_metadata_mgr = TMMetadataManager(self.db_manager, self.log)
|
|
6315
|
+
|
|
6316
|
+
# Termbase Manager - needed for glossary AI injection
|
|
6317
|
+
from modules.termbase_manager import TermbaseManager
|
|
6318
|
+
self.termbase_mgr = TermbaseManager(self.db_manager, self.log)
|
|
6164
6319
|
|
|
6165
6320
|
# Spellcheck Manager for target language spell checking
|
|
6166
6321
|
self.spellcheck_manager = get_spellcheck_manager(str(self.user_data_path))
|
|
@@ -6202,6 +6357,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
6202
6357
|
|
|
6203
6358
|
# Initialize theme manager and apply theme
|
|
6204
6359
|
self.theme_manager = ThemeManager(self.user_data_path)
|
|
6360
|
+
# Apply saved global UI font scale
|
|
6361
|
+
saved_font_scale = self._get_global_ui_font_scale()
|
|
6362
|
+
self.theme_manager.font_scale = saved_font_scale
|
|
6205
6363
|
self.theme_manager.apply_theme(QApplication.instance())
|
|
6206
6364
|
|
|
6207
6365
|
# Update widgets that were created before theme_manager existed
|
|
@@ -6278,13 +6436,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6278
6436
|
from PyQt6.QtCore import QTimer
|
|
6279
6437
|
QTimer.singleShot(2000, lambda: self._check_for_new_models(force=False)) # 2 second delay
|
|
6280
6438
|
|
|
6281
|
-
# First-run check - show
|
|
6282
|
-
if self._needs_data_location_dialog:
|
|
6439
|
+
# First-run check - show unified setup wizard
|
|
6440
|
+
if self._needs_data_location_dialog or not general_settings.get('first_run_completed', False):
|
|
6283
6441
|
from PyQt6.QtCore import QTimer
|
|
6284
|
-
QTimer.singleShot(300, self.
|
|
6285
|
-
elif not general_settings.get('first_run_completed', False):
|
|
6286
|
-
from PyQt6.QtCore import QTimer
|
|
6287
|
-
QTimer.singleShot(500, self._show_first_run_welcome)
|
|
6442
|
+
QTimer.singleShot(300, lambda: self._show_setup_wizard(is_first_run=True))
|
|
6288
6443
|
|
|
6289
6444
|
def _show_data_location_dialog(self):
|
|
6290
6445
|
"""Show dialog to let user choose their data folder location on first run."""
|
|
@@ -6444,6 +6599,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
6444
6599
|
# Update theme manager
|
|
6445
6600
|
from modules.theme_manager import ThemeManager
|
|
6446
6601
|
self.theme_manager = ThemeManager(self.user_data_path)
|
|
6602
|
+
# Apply saved global UI font scale
|
|
6603
|
+
saved_font_scale = self._get_global_ui_font_scale()
|
|
6604
|
+
self.theme_manager.font_scale = saved_font_scale
|
|
6447
6605
|
self.theme_manager.apply_theme(QApplication.instance())
|
|
6448
6606
|
|
|
6449
6607
|
# Update recent projects file path
|
|
@@ -6515,7 +6673,273 @@ class SupervertalerQt(QMainWindow):
|
|
|
6515
6673
|
self.log("✅ First-run welcome shown (will show again next time)")
|
|
6516
6674
|
except Exception as e:
|
|
6517
6675
|
self.log(f"⚠️ First-run welcome error: {e}")
|
|
6518
|
-
|
|
6676
|
+
|
|
6677
|
+
def _show_setup_wizard(self, is_first_run: bool = False):
|
|
6678
|
+
"""
|
|
6679
|
+
Show unified setup wizard that combines data folder selection and features intro.
|
|
6680
|
+
|
|
6681
|
+
Args:
|
|
6682
|
+
is_first_run: If True, this is an automatic first-run trigger. If False, user
|
|
6683
|
+
manually invoked from menu (skip data folder if already configured).
|
|
6684
|
+
"""
|
|
6685
|
+
try:
|
|
6686
|
+
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
|
6687
|
+
QPushButton, QLineEdit, QFileDialog, QStackedWidget,
|
|
6688
|
+
QWidget, QFrame, QCheckBox)
|
|
6689
|
+
from PyQt6.QtCore import Qt
|
|
6690
|
+
|
|
6691
|
+
dialog = QDialog(self)
|
|
6692
|
+
dialog.setWindowTitle("Supervertaler Setup Wizard")
|
|
6693
|
+
dialog.setMinimumWidth(600)
|
|
6694
|
+
dialog.setMinimumHeight(450)
|
|
6695
|
+
dialog.setModal(True)
|
|
6696
|
+
|
|
6697
|
+
main_layout = QVBoxLayout(dialog)
|
|
6698
|
+
main_layout.setSpacing(15)
|
|
6699
|
+
main_layout.setContentsMargins(20, 20, 20, 20)
|
|
6700
|
+
|
|
6701
|
+
# Stacked widget for wizard pages
|
|
6702
|
+
stacked = QStackedWidget()
|
|
6703
|
+
|
|
6704
|
+
# Determine if we need to show data folder page
|
|
6705
|
+
show_data_folder_page = is_first_run and self._needs_data_location_dialog
|
|
6706
|
+
|
|
6707
|
+
# ==================== PAGE 1: Data Folder Selection ====================
|
|
6708
|
+
page1 = QWidget()
|
|
6709
|
+
page1_layout = QVBoxLayout(page1)
|
|
6710
|
+
page1_layout.setSpacing(15)
|
|
6711
|
+
|
|
6712
|
+
# Step indicator
|
|
6713
|
+
step1_indicator = QLabel("<span style='color: #888;'>Step 1 of 2</span>")
|
|
6714
|
+
page1_layout.addWidget(step1_indicator)
|
|
6715
|
+
|
|
6716
|
+
# Title
|
|
6717
|
+
page1_title = QLabel("<h2>📁 Choose Your Data Folder</h2>")
|
|
6718
|
+
page1_layout.addWidget(page1_title)
|
|
6719
|
+
|
|
6720
|
+
# Explanation
|
|
6721
|
+
page1_msg = QLabel(
|
|
6722
|
+
"Supervertaler stores your data in a folder of your choice:<br><br>"
|
|
6723
|
+
"• <b>API keys</b> – Your LLM provider credentials<br>"
|
|
6724
|
+
"• <b>Translation memories</b> – Reusable translation pairs<br>"
|
|
6725
|
+
"• <b>Glossaries</b> – Terminology databases<br>"
|
|
6726
|
+
"• <b>Prompts</b> – Custom AI prompts<br>"
|
|
6727
|
+
"• <b>Settings</b> – Application configuration<br><br>"
|
|
6728
|
+
"Choose a location that's easy to find and backup."
|
|
6729
|
+
)
|
|
6730
|
+
page1_msg.setWordWrap(True)
|
|
6731
|
+
page1_layout.addWidget(page1_msg)
|
|
6732
|
+
|
|
6733
|
+
# Path input with browse button
|
|
6734
|
+
path_layout = QHBoxLayout()
|
|
6735
|
+
path_edit = QLineEdit()
|
|
6736
|
+
default_path = get_default_user_data_path()
|
|
6737
|
+
path_edit.setText(str(default_path))
|
|
6738
|
+
path_edit.setMinimumWidth(350)
|
|
6739
|
+
path_layout.addWidget(path_edit)
|
|
6740
|
+
|
|
6741
|
+
browse_btn = QPushButton("Browse...")
|
|
6742
|
+
def browse_folder():
|
|
6743
|
+
folder = QFileDialog.getExistingDirectory(
|
|
6744
|
+
dialog,
|
|
6745
|
+
"Choose Data Folder",
|
|
6746
|
+
str(Path.home())
|
|
6747
|
+
)
|
|
6748
|
+
if folder:
|
|
6749
|
+
folder_path = Path(folder)
|
|
6750
|
+
if folder_path.name != "Supervertaler":
|
|
6751
|
+
folder_path = folder_path / "Supervertaler"
|
|
6752
|
+
path_edit.setText(str(folder_path))
|
|
6753
|
+
|
|
6754
|
+
browse_btn.clicked.connect(browse_folder)
|
|
6755
|
+
path_layout.addWidget(browse_btn)
|
|
6756
|
+
page1_layout.addLayout(path_layout)
|
|
6757
|
+
|
|
6758
|
+
# Tip
|
|
6759
|
+
page1_tip = QLabel(
|
|
6760
|
+
"💡 <b>Tip:</b> The default location is in your home folder, "
|
|
6761
|
+
"making it easy to find and backup."
|
|
6762
|
+
)
|
|
6763
|
+
page1_tip.setWordWrap(True)
|
|
6764
|
+
page1_tip.setStyleSheet("color: #666;")
|
|
6765
|
+
page1_layout.addWidget(page1_tip)
|
|
6766
|
+
|
|
6767
|
+
page1_layout.addStretch()
|
|
6768
|
+
stacked.addWidget(page1)
|
|
6769
|
+
|
|
6770
|
+
# ==================== PAGE 2: Features Introduction ====================
|
|
6771
|
+
page2 = QWidget()
|
|
6772
|
+
page2_layout = QVBoxLayout(page2)
|
|
6773
|
+
page2_layout.setSpacing(15)
|
|
6774
|
+
|
|
6775
|
+
# Step indicator
|
|
6776
|
+
step2_label = "Step 2 of 2" if show_data_folder_page else "Setup"
|
|
6777
|
+
step2_indicator = QLabel(f"<span style='color: #888;'>{step2_label}</span>")
|
|
6778
|
+
page2_layout.addWidget(step2_indicator)
|
|
6779
|
+
|
|
6780
|
+
# Data folder info (shown when skipping page 1)
|
|
6781
|
+
if not show_data_folder_page:
|
|
6782
|
+
from PyQt6.QtGui import QDesktopServices
|
|
6783
|
+
from PyQt6.QtCore import QUrl
|
|
6784
|
+
|
|
6785
|
+
data_folder_path = str(self.user_data_path)
|
|
6786
|
+
data_folder_info = QLabel(
|
|
6787
|
+
f"<b>📁 Data Folder:</b> <a href='file:///{data_folder_path}' "
|
|
6788
|
+
f"style='color: #3b82f6;'>{data_folder_path}</a><br>"
|
|
6789
|
+
"<span style='color: #666; font-size: 0.9em;'>"
|
|
6790
|
+
"Your settings, TMs, glossaries and prompts are stored here. "
|
|
6791
|
+
"Change in Settings → General.</span>"
|
|
6792
|
+
)
|
|
6793
|
+
data_folder_info.setWordWrap(True)
|
|
6794
|
+
data_folder_info.setTextFormat(Qt.TextFormat.RichText)
|
|
6795
|
+
data_folder_info.setOpenExternalLinks(False) # Handle clicks ourselves
|
|
6796
|
+
data_folder_info.linkActivated.connect(
|
|
6797
|
+
lambda url: QDesktopServices.openUrl(QUrl.fromLocalFile(data_folder_path))
|
|
6798
|
+
)
|
|
6799
|
+
data_folder_info.setStyleSheet(
|
|
6800
|
+
"background: #f0f4ff; padding: 12px; border-radius: 6px; "
|
|
6801
|
+
"border-left: 4px solid #3b82f6; margin-bottom: 10px;"
|
|
6802
|
+
)
|
|
6803
|
+
page2_layout.addWidget(data_folder_info)
|
|
6804
|
+
|
|
6805
|
+
# Title
|
|
6806
|
+
page2_title = QLabel("<h2>✨ Modular Features</h2>")
|
|
6807
|
+
page2_layout.addWidget(page2_title)
|
|
6808
|
+
|
|
6809
|
+
# Message
|
|
6810
|
+
page2_msg = QLabel(
|
|
6811
|
+
"Supervertaler uses a <b>modular architecture</b> – you can install "
|
|
6812
|
+
"only the features you need.<br><br>"
|
|
6813
|
+
"<b>Core features</b> (always available):<br>"
|
|
6814
|
+
"• AI translation with OpenAI, Claude, Gemini, Ollama<br>"
|
|
6815
|
+
"• Translation Memory and Glossaries<br>"
|
|
6816
|
+
"• XLIFF, SDLXLIFF, memoQ support<br>"
|
|
6817
|
+
"• Basic spellchecking<br><br>"
|
|
6818
|
+
"<b>Optional features</b> (install via pip):<br>"
|
|
6819
|
+
"• <code>openai-whisper</code> – Local voice dictation (no API needed)<br><br>"
|
|
6820
|
+
"You can view and manage features in <b>Settings → Features</b>."
|
|
6821
|
+
)
|
|
6822
|
+
page2_msg.setWordWrap(True)
|
|
6823
|
+
page2_msg.setTextFormat(Qt.TextFormat.RichText)
|
|
6824
|
+
page2_layout.addWidget(page2_msg)
|
|
6825
|
+
|
|
6826
|
+
# Checkbox
|
|
6827
|
+
dont_show_checkbox = CheckmarkCheckBox("Don't show this wizard on startup")
|
|
6828
|
+
dont_show_checkbox.setChecked(True)
|
|
6829
|
+
page2_layout.addWidget(dont_show_checkbox)
|
|
6830
|
+
|
|
6831
|
+
# Open Features tab checkbox
|
|
6832
|
+
open_features_checkbox = CheckmarkCheckBox("Open Features tab after closing")
|
|
6833
|
+
open_features_checkbox.setChecked(True)
|
|
6834
|
+
page2_layout.addWidget(open_features_checkbox)
|
|
6835
|
+
|
|
6836
|
+
page2_layout.addStretch()
|
|
6837
|
+
stacked.addWidget(page2)
|
|
6838
|
+
|
|
6839
|
+
main_layout.addWidget(stacked)
|
|
6840
|
+
|
|
6841
|
+
# ==================== Navigation Buttons ====================
|
|
6842
|
+
nav_layout = QHBoxLayout()
|
|
6843
|
+
|
|
6844
|
+
back_btn = QPushButton("← Back")
|
|
6845
|
+
back_btn.setVisible(False) # Hidden on first page
|
|
6846
|
+
|
|
6847
|
+
next_btn = QPushButton("Next →")
|
|
6848
|
+
finish_btn = QPushButton("Finish")
|
|
6849
|
+
finish_btn.setVisible(False)
|
|
6850
|
+
finish_btn.setDefault(True)
|
|
6851
|
+
|
|
6852
|
+
# Use Default button (only on page 1)
|
|
6853
|
+
default_btn = QPushButton("Use Default")
|
|
6854
|
+
default_btn.clicked.connect(lambda: path_edit.setText(str(default_path)))
|
|
6855
|
+
|
|
6856
|
+
nav_layout.addWidget(default_btn)
|
|
6857
|
+
nav_layout.addStretch()
|
|
6858
|
+
nav_layout.addWidget(back_btn)
|
|
6859
|
+
nav_layout.addWidget(next_btn)
|
|
6860
|
+
nav_layout.addWidget(finish_btn)
|
|
6861
|
+
|
|
6862
|
+
main_layout.addLayout(nav_layout)
|
|
6863
|
+
|
|
6864
|
+
# Track chosen path for later
|
|
6865
|
+
chosen_path_holder = [None]
|
|
6866
|
+
|
|
6867
|
+
def go_to_page(page_index):
|
|
6868
|
+
stacked.setCurrentIndex(page_index)
|
|
6869
|
+
if page_index == 0:
|
|
6870
|
+
back_btn.setVisible(False)
|
|
6871
|
+
next_btn.setVisible(True)
|
|
6872
|
+
finish_btn.setVisible(False)
|
|
6873
|
+
default_btn.setVisible(True)
|
|
6874
|
+
else:
|
|
6875
|
+
back_btn.setVisible(show_data_folder_page)
|
|
6876
|
+
next_btn.setVisible(False)
|
|
6877
|
+
finish_btn.setVisible(True)
|
|
6878
|
+
default_btn.setVisible(False)
|
|
6879
|
+
|
|
6880
|
+
def on_next():
|
|
6881
|
+
# Save the data folder choice
|
|
6882
|
+
chosen_path = Path(path_edit.text())
|
|
6883
|
+
chosen_path_holder[0] = chosen_path
|
|
6884
|
+
|
|
6885
|
+
# Create the folder and save config
|
|
6886
|
+
chosen_path.mkdir(parents=True, exist_ok=True)
|
|
6887
|
+
save_user_data_path(chosen_path)
|
|
6888
|
+
|
|
6889
|
+
# Update our path if different
|
|
6890
|
+
if chosen_path != self.user_data_path:
|
|
6891
|
+
self.user_data_path = chosen_path
|
|
6892
|
+
self._reinitialize_with_new_data_path()
|
|
6893
|
+
else:
|
|
6894
|
+
if hasattr(self, 'db_manager') and self.db_manager and not self.db_manager.connection:
|
|
6895
|
+
self.db_manager.connect()
|
|
6896
|
+
|
|
6897
|
+
self.log(f"📁 Data folder set to: {chosen_path}")
|
|
6898
|
+
go_to_page(1)
|
|
6899
|
+
|
|
6900
|
+
def on_back():
|
|
6901
|
+
go_to_page(0)
|
|
6902
|
+
|
|
6903
|
+
def on_finish():
|
|
6904
|
+
# Save first_run preference
|
|
6905
|
+
if dont_show_checkbox.isChecked():
|
|
6906
|
+
settings = self.load_general_settings()
|
|
6907
|
+
settings['first_run_completed'] = True
|
|
6908
|
+
self.save_general_settings(settings)
|
|
6909
|
+
self.log("✅ Setup wizard completed (won't show again on startup)")
|
|
6910
|
+
else:
|
|
6911
|
+
self.log("✅ Setup wizard shown (will show again next time)")
|
|
6912
|
+
|
|
6913
|
+
dialog.accept()
|
|
6914
|
+
|
|
6915
|
+
# Navigate to Features tab if checkbox is checked
|
|
6916
|
+
if open_features_checkbox.isChecked():
|
|
6917
|
+
self.main_tabs.setCurrentIndex(4) # Settings tab
|
|
6918
|
+
if hasattr(self, 'settings_tabs'):
|
|
6919
|
+
for i in range(self.settings_tabs.count()):
|
|
6920
|
+
if "Features" in self.settings_tabs.tabText(i):
|
|
6921
|
+
self.settings_tabs.setCurrentIndex(i)
|
|
6922
|
+
break
|
|
6923
|
+
|
|
6924
|
+
back_btn.clicked.connect(on_back)
|
|
6925
|
+
next_btn.clicked.connect(on_next)
|
|
6926
|
+
finish_btn.clicked.connect(on_finish)
|
|
6927
|
+
|
|
6928
|
+
# Start on appropriate page
|
|
6929
|
+
if show_data_folder_page:
|
|
6930
|
+
go_to_page(0)
|
|
6931
|
+
else:
|
|
6932
|
+
# Skip to features page if data folder already configured
|
|
6933
|
+
go_to_page(1)
|
|
6934
|
+
step2_indicator.setText("<span style='color: #888;'>Supervertaler Setup</span>")
|
|
6935
|
+
|
|
6936
|
+
dialog.exec()
|
|
6937
|
+
|
|
6938
|
+
except Exception as e:
|
|
6939
|
+
self.log(f"⚠️ Setup wizard error: {e}")
|
|
6940
|
+
import traceback
|
|
6941
|
+
traceback.print_exc()
|
|
6942
|
+
|
|
6519
6943
|
def _check_for_new_models(self, force: bool = False):
|
|
6520
6944
|
"""
|
|
6521
6945
|
Check for new LLM models from providers
|
|
@@ -7499,13 +7923,30 @@ class SupervertalerQt(QMainWindow):
|
|
|
7499
7923
|
|
|
7500
7924
|
# Bulk Operations submenu
|
|
7501
7925
|
bulk_menu = edit_menu.addMenu("Bulk &Operations")
|
|
7502
|
-
|
|
7926
|
+
|
|
7503
7927
|
confirm_selected_action = QAction("✅ &Confirm Selected Segments", self)
|
|
7504
7928
|
confirm_selected_action.setShortcut("Ctrl+Shift+Return")
|
|
7505
7929
|
confirm_selected_action.setToolTip("Confirm all selected segments (Ctrl+Shift+Enter)")
|
|
7506
7930
|
confirm_selected_action.triggered.connect(self.confirm_selected_segments_from_menu)
|
|
7507
7931
|
bulk_menu.addAction(confirm_selected_action)
|
|
7508
|
-
|
|
7932
|
+
|
|
7933
|
+
# Change Status submenu
|
|
7934
|
+
status_submenu = bulk_menu.addMenu("🏷️ Change &Status")
|
|
7935
|
+
user_statuses = [
|
|
7936
|
+
("not_started", "❌ &Not started"),
|
|
7937
|
+
("pretranslated", "🤖 &Pre-translated"),
|
|
7938
|
+
("translated", "✏️ &Translated"),
|
|
7939
|
+
("confirmed", "✔ &Confirmed"),
|
|
7940
|
+
("tr_confirmed", "🌟 T&R confirmed"),
|
|
7941
|
+
("proofread", "🟪 Proo&fread"),
|
|
7942
|
+
("approved", "⭐ &Approved"),
|
|
7943
|
+
("rejected", "🚫 Re&jected"),
|
|
7944
|
+
]
|
|
7945
|
+
for status_key, label in user_statuses:
|
|
7946
|
+
action = QAction(label, self)
|
|
7947
|
+
action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s, from_menu=True))
|
|
7948
|
+
status_submenu.addAction(action)
|
|
7949
|
+
|
|
7509
7950
|
clear_translations_action = QAction("🗑️ &Clear Translations", self)
|
|
7510
7951
|
clear_translations_action.setToolTip("Clear translations for selected segments")
|
|
7511
7952
|
clear_translations_action.triggered.connect(self.clear_selected_translations_from_menu)
|
|
@@ -7762,6 +8203,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
7762
8203
|
superdocs_action.triggered.connect(lambda: self._open_url("https://supervertaler.gitbook.io/superdocs/"))
|
|
7763
8204
|
help_menu.addAction(superdocs_action)
|
|
7764
8205
|
|
|
8206
|
+
setup_wizard_action = QAction("🚀 Setup Wizard...", self)
|
|
8207
|
+
setup_wizard_action.setToolTip("Run the initial setup wizard (data folder location, features overview)")
|
|
8208
|
+
setup_wizard_action.triggered.connect(lambda: self._show_setup_wizard(is_first_run=False))
|
|
8209
|
+
help_menu.addAction(setup_wizard_action)
|
|
8210
|
+
|
|
7765
8211
|
help_menu.addSeparator()
|
|
7766
8212
|
|
|
7767
8213
|
shortcuts_action = QAction("⌨️ Keyboard Shortcuts", self)
|
|
@@ -10180,7 +10626,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
10180
10626
|
button_layout = QHBoxLayout()
|
|
10181
10627
|
|
|
10182
10628
|
create_btn = QPushButton("+ Create New TM")
|
|
10183
|
-
|
|
10629
|
+
# Get project_id dynamically - use 0 (global) when no project is loaded
|
|
10630
|
+
create_btn.clicked.connect(lambda: self._show_create_tm_dialog(
|
|
10631
|
+
tm_metadata_mgr, refresh_tm_list,
|
|
10632
|
+
self.current_project.id if (hasattr(self, 'current_project') and self.current_project and hasattr(self.current_project, 'id')) else 0
|
|
10633
|
+
))
|
|
10184
10634
|
button_layout.addWidget(create_btn)
|
|
10185
10635
|
|
|
10186
10636
|
import_btn = QPushButton("📥 Import TMX")
|
|
@@ -12586,7 +13036,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12586
13036
|
"💡 <b>Glossaries</b><br>"
|
|
12587
13037
|
"• <b>Read</b> (green ✓): Glossary is used for terminology matching<br>"
|
|
12588
13038
|
"• <b>Write</b> (blue ✓): Glossary is updated with new terms<br>"
|
|
12589
|
-
"• <b>Priority</b>: Manually set 1-N (lower = higher priority).
|
|
13039
|
+
"• <b>Priority</b>: Manually set 1-N (lower = higher priority). Priority #1 = Project Glossary.<br>"
|
|
13040
|
+
"• <b>AI</b> (orange ✓): Send glossary terms to LLM with every translation (increases prompt size)"
|
|
12590
13041
|
)
|
|
12591
13042
|
help_msg.setWordWrap(True)
|
|
12592
13043
|
help_msg.setStyleSheet("background-color: #e3f2fd; padding: 8px; border-radius: 4px; color: #1976d2;")
|
|
@@ -12608,8 +13059,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12608
13059
|
# Termbase list with table
|
|
12609
13060
|
termbase_table = QTableWidget()
|
|
12610
13061
|
self.termbase_table = termbase_table # Store for external access (Superlookup navigation)
|
|
12611
|
-
termbase_table.setColumnCount(
|
|
12612
|
-
termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority"])
|
|
13062
|
+
termbase_table.setColumnCount(8)
|
|
13063
|
+
termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority", "AI"])
|
|
12613
13064
|
termbase_table.horizontalHeader().setStretchLastSection(False)
|
|
12614
13065
|
termbase_table.setColumnWidth(0, 80) # Type (Project/Background)
|
|
12615
13066
|
termbase_table.setColumnWidth(1, 180) # Name
|
|
@@ -12618,6 +13069,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
12618
13069
|
termbase_table.setColumnWidth(4, 50) # Read checkbox
|
|
12619
13070
|
termbase_table.setColumnWidth(5, 50) # Write checkbox
|
|
12620
13071
|
termbase_table.setColumnWidth(6, 60) # Priority
|
|
13072
|
+
termbase_table.setColumnWidth(7, 40) # AI checkbox
|
|
12621
13073
|
termbase_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
12622
13074
|
termbase_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
|
12623
13075
|
termbase_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Disable inline editing
|
|
@@ -13311,7 +13763,43 @@ class SupervertalerQt(QMainWindow):
|
|
|
13311
13763
|
priority_item.setToolTip("No priority - glossary not readable")
|
|
13312
13764
|
priority_item.setFlags(priority_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
13313
13765
|
termbase_table.setItem(row, 6, priority_item)
|
|
13314
|
-
|
|
13766
|
+
|
|
13767
|
+
# AI checkbox (purple/orange) - whether to inject terms into LLM prompts
|
|
13768
|
+
ai_enabled = termbase_mgr.get_termbase_ai_inject(tb['id'])
|
|
13769
|
+
ai_checkbox = OrangeCheckmarkCheckBox()
|
|
13770
|
+
ai_checkbox.setChecked(ai_enabled)
|
|
13771
|
+
ai_checkbox.setToolTip("AI: Send glossary terms to LLM with translation prompts")
|
|
13772
|
+
|
|
13773
|
+
def on_ai_toggle(checked, tb_id=tb['id'], tb_name=tb['name']):
|
|
13774
|
+
if checked:
|
|
13775
|
+
# Show warning when enabling
|
|
13776
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
13777
|
+
msg = QMessageBox()
|
|
13778
|
+
msg.setWindowTitle("Enable AI Injection")
|
|
13779
|
+
msg.setText(f"Enable AI injection for '{tb_name}'?")
|
|
13780
|
+
msg.setInformativeText(
|
|
13781
|
+
"When enabled, ALL terms from this glossary will be sent to the LLM "
|
|
13782
|
+
"with every translation request.\n\n"
|
|
13783
|
+
"This helps the AI consistently use your preferred terminology "
|
|
13784
|
+
"throughout the translation.\n\n"
|
|
13785
|
+
"Recommended for small, curated glossaries (< 500 terms)."
|
|
13786
|
+
)
|
|
13787
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
13788
|
+
msg.setDefaultButton(QMessageBox.StandardButton.Yes)
|
|
13789
|
+
if msg.exec() != QMessageBox.StandardButton.Yes:
|
|
13790
|
+
# User cancelled - revert checkbox
|
|
13791
|
+
sender = termbase_table.cellWidget(termbase_table.currentRow(), 7)
|
|
13792
|
+
if sender:
|
|
13793
|
+
sender.blockSignals(True)
|
|
13794
|
+
sender.setChecked(False)
|
|
13795
|
+
sender.blockSignals(False)
|
|
13796
|
+
return
|
|
13797
|
+
termbase_mgr.set_termbase_ai_inject(tb_id, checked)
|
|
13798
|
+
self.log(f"{'✅ Enabled' if checked else '❌ Disabled'} AI injection for glossary: {tb_name}")
|
|
13799
|
+
|
|
13800
|
+
ai_checkbox.toggled.connect(on_ai_toggle)
|
|
13801
|
+
termbase_table.setCellWidget(row, 7, ai_checkbox)
|
|
13802
|
+
|
|
13315
13803
|
# Update header checkbox states based on current selection
|
|
13316
13804
|
tb_read_header_checkbox.blockSignals(True)
|
|
13317
13805
|
tb_write_header_checkbox.blockSignals(True)
|
|
@@ -14494,8 +14982,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
14494
14982
|
)
|
|
14495
14983
|
|
|
14496
14984
|
if result:
|
|
14497
|
-
# Auto-activate for current project
|
|
14498
|
-
if project_id:
|
|
14985
|
+
# Auto-activate for current project (or global=0 if no project loaded)
|
|
14986
|
+
if project_id is not None:
|
|
14499
14987
|
tm_metadata_mgr.activate_tm(result, project_id)
|
|
14500
14988
|
|
|
14501
14989
|
QMessageBox.information(self, "Success", f"Translation Memory '{name}' created successfully!")
|
|
@@ -15069,11 +15557,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
15069
15557
|
layout.addWidget(settings_tabs)
|
|
15070
15558
|
|
|
15071
15559
|
# Apply saved UI font scale on startup
|
|
15072
|
-
saved_scale = self.
|
|
15560
|
+
saved_scale = self._get_global_ui_font_scale()
|
|
15073
15561
|
if saved_scale != 100:
|
|
15074
15562
|
# Defer application to ensure widgets are fully created
|
|
15075
15563
|
from PyQt6.QtCore import QTimer
|
|
15076
|
-
QTimer.singleShot(100, lambda: self.
|
|
15564
|
+
QTimer.singleShot(100, lambda: self._apply_global_ui_font_scale(saved_scale))
|
|
15077
15565
|
|
|
15078
15566
|
return tab
|
|
15079
15567
|
|
|
@@ -15408,7 +15896,57 @@ class SupervertalerQt(QMainWindow):
|
|
|
15408
15896
|
|
|
15409
15897
|
model_group.setLayout(model_layout)
|
|
15410
15898
|
layout.addWidget(model_group)
|
|
15411
|
-
|
|
15899
|
+
|
|
15900
|
+
# ========== SECTION 2b: Model Version Checker ==========
|
|
15901
|
+
version_check_group = QGroupBox("🔄 Model Version Checker")
|
|
15902
|
+
version_check_layout = QVBoxLayout()
|
|
15903
|
+
|
|
15904
|
+
version_check_info = QLabel(
|
|
15905
|
+
"Automatically check for new LLM models from OpenAI, Anthropic, and Google.\n"
|
|
15906
|
+
"Get notified when new models are available and easily add them to Supervertaler."
|
|
15907
|
+
)
|
|
15908
|
+
version_check_info.setWordWrap(True)
|
|
15909
|
+
version_check_layout.addWidget(version_check_info)
|
|
15910
|
+
|
|
15911
|
+
# Auto-check setting
|
|
15912
|
+
auto_check_models_cb = CheckmarkCheckBox("Enable automatic model checking (once per day on startup)")
|
|
15913
|
+
auto_check_models_cb.setChecked(general_settings.get('auto_check_models', True))
|
|
15914
|
+
auto_check_models_cb.setToolTip(
|
|
15915
|
+
"When enabled, Supervertaler will check for new models once per day when you start the application.\n"
|
|
15916
|
+
"You'll see a popup if new models are detected."
|
|
15917
|
+
)
|
|
15918
|
+
version_check_layout.addWidget(auto_check_models_cb)
|
|
15919
|
+
|
|
15920
|
+
# Manual check button
|
|
15921
|
+
manual_check_btn = QPushButton("🔍 Check for New Models Now")
|
|
15922
|
+
manual_check_btn.setToolTip("Manually check for new models from all providers")
|
|
15923
|
+
manual_check_btn.clicked.connect(lambda: self._check_for_new_models(force=True))
|
|
15924
|
+
version_check_layout.addWidget(manual_check_btn)
|
|
15925
|
+
|
|
15926
|
+
# Store reference for saving
|
|
15927
|
+
self.auto_check_models_cb = auto_check_models_cb
|
|
15928
|
+
|
|
15929
|
+
version_check_group.setLayout(version_check_layout)
|
|
15930
|
+
layout.addWidget(version_check_group)
|
|
15931
|
+
|
|
15932
|
+
# ========== SECTION 2c: API Keys ==========
|
|
15933
|
+
api_keys_group = QGroupBox("🔑 API Keys")
|
|
15934
|
+
api_keys_layout = QVBoxLayout()
|
|
15935
|
+
|
|
15936
|
+
api_keys_info = QLabel(
|
|
15937
|
+
f"Configure your API keys in:<br>"
|
|
15938
|
+
f"<code>{self.user_data_path / 'api_keys.txt'}</code>"
|
|
15939
|
+
)
|
|
15940
|
+
api_keys_info.setWordWrap(True)
|
|
15941
|
+
api_keys_layout.addWidget(api_keys_info)
|
|
15942
|
+
|
|
15943
|
+
open_keys_btn = QPushButton("📝 Open API Keys File")
|
|
15944
|
+
open_keys_btn.clicked.connect(lambda: self.open_api_keys_file())
|
|
15945
|
+
api_keys_layout.addWidget(open_keys_btn)
|
|
15946
|
+
|
|
15947
|
+
api_keys_group.setLayout(api_keys_layout)
|
|
15948
|
+
layout.addWidget(api_keys_group)
|
|
15949
|
+
|
|
15412
15950
|
# ========== SECTION 3: Enable/Disable LLM Providers ==========
|
|
15413
15951
|
provider_enable_group = QGroupBox("✅ Enable/Disable LLM Providers")
|
|
15414
15952
|
provider_enable_layout = QVBoxLayout()
|
|
@@ -15688,58 +16226,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
15688
16226
|
|
|
15689
16227
|
behavior_group.setLayout(behavior_layout)
|
|
15690
16228
|
layout.addWidget(behavior_group)
|
|
15691
|
-
|
|
15692
|
-
# ========== SECTION 7: API Keys ==========
|
|
15693
|
-
# ========== MODEL VERSION CHECKER ==========
|
|
15694
|
-
version_check_group = QGroupBox("🔄 Model Version Checker")
|
|
15695
|
-
version_check_layout = QVBoxLayout()
|
|
15696
|
-
|
|
15697
|
-
version_check_info = QLabel(
|
|
15698
|
-
"Automatically check for new LLM models from OpenAI, Anthropic, and Google.\n"
|
|
15699
|
-
"Get notified when new models are available and easily add them to Supervertaler."
|
|
15700
|
-
)
|
|
15701
|
-
version_check_info.setWordWrap(True)
|
|
15702
|
-
version_check_layout.addWidget(version_check_info)
|
|
15703
|
-
|
|
15704
|
-
# Auto-check setting
|
|
15705
|
-
auto_check_models_cb = CheckmarkCheckBox("Enable automatic model checking (once per day on startup)")
|
|
15706
|
-
auto_check_models_cb.setChecked(general_settings.get('auto_check_models', True))
|
|
15707
|
-
auto_check_models_cb.setToolTip(
|
|
15708
|
-
"When enabled, Supervertaler will check for new models once per day when you start the application.\n"
|
|
15709
|
-
"You'll see a popup if new models are detected."
|
|
15710
|
-
)
|
|
15711
|
-
version_check_layout.addWidget(auto_check_models_cb)
|
|
15712
|
-
|
|
15713
|
-
# Manual check button
|
|
15714
|
-
manual_check_btn = QPushButton("🔍 Check for New Models Now")
|
|
15715
|
-
manual_check_btn.setToolTip("Manually check for new models from all providers")
|
|
15716
|
-
manual_check_btn.clicked.connect(lambda: self._check_for_new_models(force=True))
|
|
15717
|
-
version_check_layout.addWidget(manual_check_btn)
|
|
15718
|
-
|
|
15719
|
-
# Store reference for saving
|
|
15720
|
-
self.auto_check_models_cb = auto_check_models_cb
|
|
15721
16229
|
|
|
15722
|
-
version_check_group.setLayout(version_check_layout)
|
|
15723
|
-
layout.addWidget(version_check_group)
|
|
15724
|
-
|
|
15725
|
-
# ========== API KEYS ==========
|
|
15726
|
-
api_keys_group = QGroupBox("🔑 API Keys")
|
|
15727
|
-
api_keys_layout = QVBoxLayout()
|
|
15728
|
-
|
|
15729
|
-
api_keys_info = QLabel(
|
|
15730
|
-
f"Configure your API keys in:<br>"
|
|
15731
|
-
f"<code>{self.user_data_path / 'api_keys.txt'}</code>"
|
|
15732
|
-
)
|
|
15733
|
-
api_keys_info.setWordWrap(True)
|
|
15734
|
-
api_keys_layout.addWidget(api_keys_info)
|
|
15735
|
-
|
|
15736
|
-
open_keys_btn = QPushButton("📝 Open API Keys File")
|
|
15737
|
-
open_keys_btn.clicked.connect(lambda: self.open_api_keys_file())
|
|
15738
|
-
api_keys_layout.addWidget(open_keys_btn)
|
|
15739
|
-
|
|
15740
|
-
api_keys_group.setLayout(api_keys_layout)
|
|
15741
|
-
layout.addWidget(api_keys_group)
|
|
15742
|
-
|
|
15743
16230
|
# ========== SAVE BUTTON ==========
|
|
15744
16231
|
save_btn = QPushButton("💾 Save AI Settings")
|
|
15745
16232
|
save_btn.setStyleSheet("font-weight: bold; padding: 8px; outline: none;")
|
|
@@ -17236,26 +17723,27 @@ class SupervertalerQt(QMainWindow):
|
|
|
17236
17723
|
termview_group.setLayout(termview_layout)
|
|
17237
17724
|
layout.addWidget(termview_group)
|
|
17238
17725
|
|
|
17239
|
-
# ===== UI Font Scale
|
|
17240
|
-
ui_scale_group = QGroupBox("🖥️
|
|
17726
|
+
# ===== Global UI Font Scale =====
|
|
17727
|
+
ui_scale_group = QGroupBox("🖥️ Global UI Font Scale")
|
|
17241
17728
|
ui_scale_layout = QVBoxLayout()
|
|
17242
|
-
|
|
17729
|
+
|
|
17243
17730
|
ui_scale_info = QLabel(
|
|
17244
|
-
"Adjust the font size for
|
|
17731
|
+
"Adjust the font size for the entire application UI. Useful for Linux/macOS users where\n"
|
|
17732
|
+
"Qt applications may render with smaller fonts, or for high-DPI displays.\n"
|
|
17245
17733
|
"Changes apply immediately. Default is 100%."
|
|
17246
17734
|
)
|
|
17247
17735
|
ui_scale_info.setWordWrap(True)
|
|
17248
17736
|
ui_scale_layout.addWidget(ui_scale_info)
|
|
17249
|
-
|
|
17737
|
+
|
|
17250
17738
|
ui_scale_row = QHBoxLayout()
|
|
17251
17739
|
ui_scale_row.addWidget(QLabel("UI Font Scale:"))
|
|
17252
17740
|
ui_scale_spin = QSpinBox()
|
|
17253
|
-
ui_scale_spin.setMinimum(
|
|
17741
|
+
ui_scale_spin.setMinimum(50)
|
|
17254
17742
|
ui_scale_spin.setMaximum(200)
|
|
17255
|
-
ui_scale_spin.setValue(font_settings.get('settings_ui_font_scale', 100))
|
|
17743
|
+
ui_scale_spin.setValue(font_settings.get('global_ui_font_scale', font_settings.get('settings_ui_font_scale', 100)))
|
|
17256
17744
|
ui_scale_spin.setSuffix("%")
|
|
17257
17745
|
ui_scale_spin.setSingleStep(10)
|
|
17258
|
-
ui_scale_spin.setToolTip("Scale
|
|
17746
|
+
ui_scale_spin.setToolTip("Scale entire application UI text (50%-200%)")
|
|
17259
17747
|
ui_scale_spin.setMinimumHeight(28)
|
|
17260
17748
|
ui_scale_spin.setMinimumWidth(90)
|
|
17261
17749
|
ui_scale_spin.setStyleSheet("""
|
|
@@ -17272,11 +17760,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
17272
17760
|
}
|
|
17273
17761
|
""")
|
|
17274
17762
|
ui_scale_row.addWidget(ui_scale_spin)
|
|
17275
|
-
|
|
17763
|
+
|
|
17276
17764
|
# Apply button for immediate feedback
|
|
17277
17765
|
apply_scale_btn = QPushButton("Apply")
|
|
17278
17766
|
apply_scale_btn.setToolTip("Apply font scale immediately")
|
|
17279
|
-
apply_scale_btn.clicked.connect(lambda: self.
|
|
17767
|
+
apply_scale_btn.clicked.connect(lambda: self._apply_global_ui_font_scale(ui_scale_spin.value()))
|
|
17280
17768
|
ui_scale_row.addWidget(apply_scale_btn)
|
|
17281
17769
|
|
|
17282
17770
|
ui_scale_row.addStretch()
|
|
@@ -17316,7 +17804,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
17316
17804
|
def save_view_settings_with_scale():
|
|
17317
17805
|
# Save the UI scale setting first
|
|
17318
17806
|
if hasattr(self, '_ui_scale_spin'):
|
|
17319
|
-
self.
|
|
17807
|
+
self._apply_global_ui_font_scale(self._ui_scale_spin.value())
|
|
17320
17808
|
# Then save other view settings
|
|
17321
17809
|
self._save_view_settings_from_ui(
|
|
17322
17810
|
grid_font_spin, match_font_spin, compare_font_spin, show_tags_check, tag_color_btn,
|
|
@@ -19444,72 +19932,51 @@ class SupervertalerQt(QMainWindow):
|
|
|
19444
19932
|
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
19445
19933
|
msg.exec()
|
|
19446
19934
|
|
|
19447
|
-
def
|
|
19448
|
-
"""Apply font scale to
|
|
19449
|
-
# Save the setting
|
|
19935
|
+
def _apply_global_ui_font_scale(self, scale_percent: int):
|
|
19936
|
+
"""Apply font scale to the entire application UI"""
|
|
19450
19937
|
general_settings = self.load_general_settings()
|
|
19451
|
-
general_settings['
|
|
19938
|
+
general_settings['global_ui_font_scale'] = scale_percent
|
|
19939
|
+
# Remove old key if present (migration)
|
|
19940
|
+
if 'settings_ui_font_scale' in general_settings:
|
|
19941
|
+
del general_settings['settings_ui_font_scale']
|
|
19452
19942
|
self.save_general_settings(general_settings)
|
|
19453
|
-
|
|
19454
|
-
#
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19469
|
-
|
|
19470
|
-
|
|
19471
|
-
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
19475
|
-
|
|
19476
|
-
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
|
|
19481
|
-
|
|
19482
|
-
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
font-size: {scaled_size}pt;
|
|
19487
|
-
}}
|
|
19488
|
-
QGroupBox QPlainTextEdit {{
|
|
19489
|
-
font-size: {scaled_size}pt;
|
|
19490
|
-
}}
|
|
19491
|
-
"""
|
|
19492
|
-
|
|
19493
|
-
# Apply to settings_tabs if it exists
|
|
19494
|
-
if hasattr(self, 'settings_tabs') and self.settings_tabs is not None:
|
|
19495
|
-
self.settings_tabs.setStyleSheet(
|
|
19496
|
-
"QTabBar::tab { outline: 0; font-size: " + str(scaled_size) + "pt; } "
|
|
19497
|
-
"QTabBar::tab:focus { outline: none; } "
|
|
19498
|
-
"QTabBar::tab:selected { border-bottom: 1px solid #2196F3; background-color: rgba(33, 150, 243, 0.08); }"
|
|
19499
|
-
)
|
|
19500
|
-
|
|
19501
|
-
# Apply to each tab's content
|
|
19502
|
-
for i in range(self.settings_tabs.count()):
|
|
19503
|
-
widget = self.settings_tabs.widget(i)
|
|
19504
|
-
if widget:
|
|
19505
|
-
widget.setStyleSheet(settings_stylesheet)
|
|
19506
|
-
|
|
19507
|
-
self.log(f"✓ Settings UI font scale set to {scale_percent}% (base: {scaled_size}pt)")
|
|
19508
|
-
|
|
19509
|
-
def _get_settings_ui_font_scale(self) -> int:
|
|
19510
|
-
"""Get the current Settings UI font scale percentage"""
|
|
19943
|
+
|
|
19944
|
+
# Update ThemeManager and reapply theme
|
|
19945
|
+
if hasattr(self, 'theme_manager') and self.theme_manager is not None:
|
|
19946
|
+
self.theme_manager.font_scale = scale_percent
|
|
19947
|
+
self.theme_manager.apply_theme(QApplication.instance())
|
|
19948
|
+
|
|
19949
|
+
# Update status bar and main tabs fonts
|
|
19950
|
+
self._update_status_bar_fonts(scale_percent)
|
|
19951
|
+
self._update_main_tabs_fonts(scale_percent)
|
|
19952
|
+
self.log(f"✓ Global UI font scale set to {scale_percent}%")
|
|
19953
|
+
|
|
19954
|
+
def _update_status_bar_fonts(self, scale_percent: int):
|
|
19955
|
+
"""Update status bar label fonts based on scale percentage"""
|
|
19956
|
+
base_size = int(9 * scale_percent / 100)
|
|
19957
|
+
small_size = max(7, base_size)
|
|
19958
|
+
style = f"font-size: {small_size}pt;"
|
|
19959
|
+
|
|
19960
|
+
# Update all status bar labels if they exist
|
|
19961
|
+
for attr_name in ['segment_count_label', 'file_label', 'tm_status_label',
|
|
19962
|
+
'termbase_status_label', 'source_lang_label', 'target_lang_label']:
|
|
19963
|
+
if hasattr(self, attr_name):
|
|
19964
|
+
label = getattr(self, attr_name)
|
|
19965
|
+
if label is not None:
|
|
19966
|
+
label.setStyleSheet(style)
|
|
19967
|
+
|
|
19968
|
+
def _update_main_tabs_fonts(self, scale_percent: int):
|
|
19969
|
+
"""Update main tab bar fonts based on scale percentage"""
|
|
19970
|
+
base_size = int(10 * scale_percent / 100)
|
|
19971
|
+
if hasattr(self, 'main_tabs') and self.main_tabs is not None:
|
|
19972
|
+
self.main_tabs.tabBar().setStyleSheet(f"font-size: {base_size}pt;")
|
|
19973
|
+
|
|
19974
|
+
def _get_global_ui_font_scale(self) -> int:
|
|
19975
|
+
"""Get the current global UI font scale percentage"""
|
|
19511
19976
|
general_settings = self.load_general_settings()
|
|
19512
|
-
|
|
19977
|
+
# Check new key first, fall back to old key for migration
|
|
19978
|
+
return general_settings.get('global_ui_font_scale',
|
|
19979
|
+
general_settings.get('settings_ui_font_scale', 100))
|
|
19513
19980
|
|
|
19514
19981
|
def create_grid_view_widget(self):
|
|
19515
19982
|
"""Create the Grid View widget (existing grid functionality)"""
|
|
@@ -19844,7 +20311,13 @@ class SupervertalerQt(QMainWindow):
|
|
|
19844
20311
|
self.pagination_label = QLabel("Segments 1-50 of 0")
|
|
19845
20312
|
self.pagination_label.setStyleSheet("color: #555;")
|
|
19846
20313
|
pagination_layout.addWidget(self.pagination_label)
|
|
19847
|
-
|
|
20314
|
+
|
|
20315
|
+
# Tip label for Ctrl+, shortcut (subtle, helpful for new users)
|
|
20316
|
+
tip_label = QLabel("💡 Tip: Ctrl+, inserts the next tag from source")
|
|
20317
|
+
tip_label.setStyleSheet("color: #888; font-size: 9pt; margin-left: 20px;")
|
|
20318
|
+
tip_label.setToolTip("Select text first to wrap it with a tag pair (e.g., <b>selection</b>)")
|
|
20319
|
+
pagination_layout.addWidget(tip_label)
|
|
20320
|
+
|
|
19848
20321
|
pagination_layout.addStretch()
|
|
19849
20322
|
|
|
19850
20323
|
# Pagination controls (right side)
|
|
@@ -19972,6 +20445,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
19972
20445
|
from modules.statuses import get_status, STATUSES
|
|
19973
20446
|
status_label = QLabel("Status:")
|
|
19974
20447
|
tab_status_combo = QComboBox()
|
|
20448
|
+
tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
|
|
19975
20449
|
for status_key in STATUSES.keys():
|
|
19976
20450
|
definition = get_status(status_key)
|
|
19977
20451
|
tab_status_combo.addItem(definition.label, status_key)
|
|
@@ -20870,6 +21344,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
20870
21344
|
from modules.statuses import STATUSES
|
|
20871
21345
|
status_label = QLabel("Status:")
|
|
20872
21346
|
tab_status_combo = QComboBox()
|
|
21347
|
+
tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
|
|
20873
21348
|
for status_key in STATUSES.keys():
|
|
20874
21349
|
definition = get_status(status_key)
|
|
20875
21350
|
tab_status_combo.addItem(definition.label, status_key)
|
|
@@ -21832,20 +22307,24 @@ class SupervertalerQt(QMainWindow):
|
|
|
21832
22307
|
"""
|
|
21833
22308
|
Search termbases using a provided cursor (thread-safe for background threads).
|
|
21834
22309
|
This method allows background workers to query the database without SQLite threading errors.
|
|
21835
|
-
|
|
22310
|
+
|
|
22311
|
+
Implements BIDIRECTIONAL matching: searches both source_term and target_term columns.
|
|
22312
|
+
When a match is found on target_term, source and target are swapped in the result.
|
|
22313
|
+
This matches memoQ/Trados behavior where a NL→EN termbase also works for EN→NL projects.
|
|
22314
|
+
|
|
21836
22315
|
Args:
|
|
21837
22316
|
source_text: The source text to search for
|
|
21838
22317
|
cursor: A database cursor from a thread-local connection
|
|
21839
22318
|
source_lang: Source language code
|
|
21840
22319
|
target_lang: Target language code
|
|
21841
22320
|
project_id: Current project ID (required to filter by activated termbases)
|
|
21842
|
-
|
|
22321
|
+
|
|
21843
22322
|
Returns:
|
|
21844
22323
|
Dictionary of {term: translation} matches
|
|
21845
22324
|
"""
|
|
21846
22325
|
if not source_text or not cursor:
|
|
21847
22326
|
return {}
|
|
21848
|
-
|
|
22327
|
+
|
|
21849
22328
|
try:
|
|
21850
22329
|
# Convert language names to codes (match interactive search logic)
|
|
21851
22330
|
source_lang_code = self._convert_language_to_code(source_lang) if source_lang else None
|
|
@@ -21865,20 +22344,26 @@ class SupervertalerQt(QMainWindow):
|
|
|
21865
22344
|
try:
|
|
21866
22345
|
# JOIN termbases AND termbase_activation to filter by activated termbases
|
|
21867
22346
|
# This matches the logic in database_manager.py search_termbases()
|
|
22347
|
+
# BIDIRECTIONAL: Search both source_term (forward) and target_term (reverse)
|
|
22348
|
+
# Using UNION to combine both directions
|
|
21868
22349
|
query = """
|
|
21869
|
-
SELECT
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
|
|
21875
|
-
|
|
21876
|
-
|
|
21877
|
-
|
|
21878
|
-
|
|
22350
|
+
SELECT * FROM (
|
|
22351
|
+
-- Forward match: search source_term
|
|
22352
|
+
SELECT
|
|
22353
|
+
t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
|
|
22354
|
+
t.domain, t.notes, t.project, t.client, t.forbidden,
|
|
22355
|
+
tb.is_project_termbase, tb.name as termbase_name,
|
|
22356
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
22357
|
+
'source' as match_direction
|
|
22358
|
+
FROM termbase_terms t
|
|
22359
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
22360
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
22361
|
+
WHERE LOWER(t.source_term) LIKE ?
|
|
22362
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
21879
22363
|
"""
|
|
21880
22364
|
params = [project_id if project_id else 0, f"%{clean_word.lower()}%"]
|
|
21881
22365
|
|
|
22366
|
+
# Language filters for forward query
|
|
21882
22367
|
if source_lang_code:
|
|
21883
22368
|
query += " AND (t.source_lang = ? OR (t.source_lang IS NULL AND tb.source_lang = ?) OR (t.source_lang IS NULL AND tb.source_lang IS NULL))"
|
|
21884
22369
|
params.extend([source_lang_code, source_lang_code])
|
|
@@ -21886,15 +22371,45 @@ class SupervertalerQt(QMainWindow):
|
|
|
21886
22371
|
query += " AND (t.target_lang = ? OR (t.target_lang IS NULL AND tb.target_lang = ?) OR (t.target_lang IS NULL AND tb.target_lang IS NULL))"
|
|
21887
22372
|
params.extend([target_lang_code, target_lang_code])
|
|
21888
22373
|
|
|
21889
|
-
#
|
|
21890
|
-
query += "
|
|
22374
|
+
# Reverse match: search target_term, swap source/target in output
|
|
22375
|
+
query += """
|
|
22376
|
+
UNION ALL
|
|
22377
|
+
-- Reverse match: search target_term, swap columns
|
|
22378
|
+
SELECT
|
|
22379
|
+
t.id, t.target_term as source_term, t.source_term as target_term,
|
|
22380
|
+
t.termbase_id, t.priority,
|
|
22381
|
+
t.domain, t.notes, t.project, t.client, t.forbidden,
|
|
22382
|
+
tb.is_project_termbase, tb.name as termbase_name,
|
|
22383
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
22384
|
+
'target' as match_direction
|
|
22385
|
+
FROM termbase_terms t
|
|
22386
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
22387
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
22388
|
+
WHERE LOWER(t.target_term) LIKE ?
|
|
22389
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
22390
|
+
"""
|
|
22391
|
+
params.extend([project_id if project_id else 0, f"%{clean_word.lower()}%"])
|
|
22392
|
+
|
|
22393
|
+
# Language filters for reverse query (swapped)
|
|
22394
|
+
if source_lang_code:
|
|
22395
|
+
# For reverse: source_lang filters target_lang column
|
|
22396
|
+
query += " AND (t.target_lang = ? OR (t.target_lang IS NULL AND tb.target_lang = ?) OR (t.target_lang IS NULL AND tb.target_lang IS NULL))"
|
|
22397
|
+
params.extend([source_lang_code, source_lang_code])
|
|
22398
|
+
if target_lang_code:
|
|
22399
|
+
# For reverse: target_lang filters source_lang column
|
|
22400
|
+
query += " AND (t.source_lang = ? OR (t.source_lang IS NULL AND tb.source_lang = ?) OR (t.source_lang IS NULL AND tb.source_lang IS NULL))"
|
|
22401
|
+
params.extend([target_lang_code, target_lang_code])
|
|
22402
|
+
|
|
22403
|
+
# Close UNION and limit
|
|
22404
|
+
query += ") combined LIMIT 30"
|
|
21891
22405
|
cursor.execute(query, params)
|
|
21892
22406
|
results = cursor.fetchall()
|
|
21893
22407
|
|
|
21894
22408
|
for row in results:
|
|
21895
|
-
# Uniform access
|
|
22409
|
+
# Uniform access (columns are already swapped for reverse matches)
|
|
21896
22410
|
source_term = row[1] if isinstance(row, tuple) else row['source_term']
|
|
21897
22411
|
target_term = row[2] if isinstance(row, tuple) else row['target_term']
|
|
22412
|
+
match_direction = row[13] if isinstance(row, tuple) else row.get('match_direction', 'source')
|
|
21898
22413
|
if not source_term or not target_term:
|
|
21899
22414
|
continue
|
|
21900
22415
|
|
|
@@ -21918,8 +22433,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
21918
22433
|
existing = matches.get(source_term.strip())
|
|
21919
22434
|
# Deduplicate: keep numerically lowest ranking (highest priority)
|
|
21920
22435
|
# For project termbases, ranking is None so they always win
|
|
22436
|
+
# Also prefer 'source' matches over 'target' matches when equal
|
|
21921
22437
|
if existing:
|
|
21922
22438
|
existing_ranking = existing.get('ranking', None)
|
|
22439
|
+
existing_direction = existing.get('match_direction', 'source')
|
|
21923
22440
|
if is_project_tb:
|
|
21924
22441
|
# Project termbase always wins
|
|
21925
22442
|
pass
|
|
@@ -21928,8 +22445,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
21928
22445
|
continue
|
|
21929
22446
|
elif existing_ranking is not None and ranking is not None:
|
|
21930
22447
|
# Both have rankings, keep lower (higher priority)
|
|
21931
|
-
if existing_ranking
|
|
22448
|
+
if existing_ranking < ranking:
|
|
21932
22449
|
continue
|
|
22450
|
+
elif existing_ranking == ranking:
|
|
22451
|
+
# Same ranking: prefer source match over target match
|
|
22452
|
+
if existing_direction == 'source' and match_direction == 'target':
|
|
22453
|
+
continue
|
|
21933
22454
|
|
|
21934
22455
|
matches[source_term.strip()] = {
|
|
21935
22456
|
'translation': target_term.strip(),
|
|
@@ -21944,15 +22465,18 @@ class SupervertalerQt(QMainWindow):
|
|
|
21944
22465
|
'forbidden': forbidden or False,
|
|
21945
22466
|
'is_project_termbase': bool(is_project_tb),
|
|
21946
22467
|
'termbase_name': termbase_name or '',
|
|
21947
|
-
'target_synonyms': [] # Will be populated below
|
|
22468
|
+
'target_synonyms': [], # Will be populated below
|
|
22469
|
+
'match_direction': match_direction # Track if this was a reverse match
|
|
21948
22470
|
}
|
|
21949
|
-
|
|
22471
|
+
|
|
21950
22472
|
# Fetch synonyms for this term
|
|
22473
|
+
# For reverse matches, fetch 'source' synonyms since they become targets
|
|
21951
22474
|
try:
|
|
22475
|
+
synonym_lang = 'source' if match_direction == 'target' else 'target'
|
|
21952
22476
|
cursor.execute("""
|
|
21953
|
-
SELECT synonym_text FROM termbase_synonyms
|
|
21954
|
-
WHERE term_id = ? AND language =
|
|
21955
|
-
""", (term_id,))
|
|
22477
|
+
SELECT synonym_text FROM termbase_synonyms
|
|
22478
|
+
WHERE term_id = ? AND language = ? AND forbidden = 0
|
|
22479
|
+
""", (term_id, synonym_lang))
|
|
21956
22480
|
synonym_rows = cursor.fetchall()
|
|
21957
22481
|
for syn_row in synonym_rows:
|
|
21958
22482
|
synonym = syn_row[0] if isinstance(syn_row, tuple) else syn_row['synonym_text']
|
|
@@ -22319,27 +22843,31 @@ class SupervertalerQt(QMainWindow):
|
|
|
22319
22843
|
def save_segment_to_activated_tms(self, source: str, target: str):
|
|
22320
22844
|
"""
|
|
22321
22845
|
Save segment to all writable TMs for current project.
|
|
22322
|
-
|
|
22846
|
+
|
|
22323
22847
|
Note: Uses get_writable_tm_ids() which checks the Write checkbox (read_only=0),
|
|
22324
22848
|
NOT get_active_tm_ids() which checks the Read checkbox (is_active=1).
|
|
22325
|
-
|
|
22849
|
+
|
|
22850
|
+
Respects tm_save_mode setting:
|
|
22851
|
+
- 'latest': Overwrites existing entries with same source (keeps only newest translation)
|
|
22852
|
+
- 'all': Keeps all translations with different targets (default SQLite behavior)
|
|
22853
|
+
|
|
22326
22854
|
Args:
|
|
22327
22855
|
source: Source text
|
|
22328
22856
|
target: Target text
|
|
22329
22857
|
"""
|
|
22330
22858
|
if not self.current_project:
|
|
22331
22859
|
return
|
|
22332
|
-
|
|
22860
|
+
|
|
22333
22861
|
if not hasattr(self.current_project, 'source_lang') or not hasattr(self.current_project, 'target_lang'):
|
|
22334
22862
|
return
|
|
22335
|
-
|
|
22863
|
+
|
|
22336
22864
|
# Get WRITABLE TM IDs for this project (Write checkbox enabled)
|
|
22337
22865
|
tm_ids = []
|
|
22338
|
-
|
|
22866
|
+
|
|
22339
22867
|
if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr:
|
|
22340
22868
|
if hasattr(self, 'current_project') and self.current_project:
|
|
22341
22869
|
project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
|
|
22342
|
-
|
|
22870
|
+
|
|
22343
22871
|
if project_id:
|
|
22344
22872
|
# Use get_writable_tm_ids() to find TMs with Write enabled
|
|
22345
22873
|
tm_ids = self.tm_metadata_mgr.get_writable_tm_ids(project_id)
|
|
@@ -22349,13 +22877,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
22349
22877
|
self.log(f"⚠️ Cannot save to TM: No current project loaded!")
|
|
22350
22878
|
else:
|
|
22351
22879
|
self.log(f"⚠️ Cannot save to TM: TM metadata manager not available!")
|
|
22352
|
-
|
|
22880
|
+
|
|
22353
22881
|
# If no TMs have Write enabled, skip saving
|
|
22354
22882
|
if not tm_ids:
|
|
22355
22883
|
self.log("⚠️ No TMs with Write enabled - segment not saved to TM.")
|
|
22356
22884
|
self.log(f" - To fix: Go to Resources > Translation Memories > TM List and enable the Write checkbox")
|
|
22357
22885
|
return
|
|
22358
|
-
|
|
22886
|
+
|
|
22887
|
+
# Check TM save mode: 'latest' = overwrite, 'all' = keep all variants
|
|
22888
|
+
overwrite_mode = getattr(self, 'tm_save_mode', 'latest') == 'latest'
|
|
22889
|
+
|
|
22359
22890
|
# Save to each writable TM
|
|
22360
22891
|
saved_count = 0
|
|
22361
22892
|
for tm_id in tm_ids:
|
|
@@ -22365,14 +22896,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
22365
22896
|
target=target,
|
|
22366
22897
|
source_lang=self.current_project.source_lang,
|
|
22367
22898
|
target_lang=self.current_project.target_lang,
|
|
22368
|
-
tm_id=tm_id
|
|
22899
|
+
tm_id=tm_id,
|
|
22900
|
+
overwrite=overwrite_mode
|
|
22369
22901
|
)
|
|
22370
22902
|
saved_count += 1
|
|
22371
22903
|
except Exception as e:
|
|
22372
22904
|
self.log(f"⚠️ Could not save to TM '{tm_id}': {e}")
|
|
22373
|
-
|
|
22905
|
+
|
|
22374
22906
|
if saved_count > 0:
|
|
22375
|
-
|
|
22907
|
+
mode_note = " (overwrite)" if overwrite_mode else ""
|
|
22908
|
+
msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
|
|
22376
22909
|
self._queue_tm_save_log(msg)
|
|
22377
22910
|
# Invalidate cache so prefetched segments get fresh TM matches
|
|
22378
22911
|
self.invalidate_translation_cache()
|
|
@@ -26149,24 +26682,32 @@ class SupervertalerQt(QMainWindow):
|
|
|
26149
26682
|
)
|
|
26150
26683
|
return
|
|
26151
26684
|
|
|
26152
|
-
# Extract segments
|
|
26153
|
-
mqxliff_segments = handler.
|
|
26154
|
-
|
|
26685
|
+
# Extract segments (including targets for pretranslated files)
|
|
26686
|
+
mqxliff_segments = handler.extract_bilingual_segments()
|
|
26687
|
+
|
|
26155
26688
|
if not mqxliff_segments:
|
|
26156
26689
|
QMessageBox.warning(
|
|
26157
26690
|
self, "No Segments",
|
|
26158
26691
|
"No segments found in the memoQ XLIFF file."
|
|
26159
26692
|
)
|
|
26160
26693
|
return
|
|
26161
|
-
|
|
26694
|
+
|
|
26695
|
+
# Count pretranslated segments
|
|
26696
|
+
pretranslated_count = sum(1 for s in mqxliff_segments if s.get('target', '').strip())
|
|
26697
|
+
|
|
26162
26698
|
# Convert to internal Segment format
|
|
26163
26699
|
segments = []
|
|
26164
26700
|
for i, mq_seg in enumerate(mqxliff_segments):
|
|
26701
|
+
# Map status from mqxliff
|
|
26702
|
+
status = mq_seg.get('status', 'not_started')
|
|
26703
|
+
if status not in ['not_started', 'pre_translated', 'translated', 'confirmed', 'locked']:
|
|
26704
|
+
status = 'not_started'
|
|
26705
|
+
|
|
26165
26706
|
segment = Segment(
|
|
26166
26707
|
id=i + 1,
|
|
26167
|
-
source=mq_seg.
|
|
26168
|
-
target=
|
|
26169
|
-
status=
|
|
26708
|
+
source=mq_seg.get('source', ''),
|
|
26709
|
+
target=mq_seg.get('target', ''),
|
|
26710
|
+
status=status,
|
|
26170
26711
|
notes="",
|
|
26171
26712
|
)
|
|
26172
26713
|
segments.append(segment)
|
|
@@ -26210,11 +26751,17 @@ class SupervertalerQt(QMainWindow):
|
|
|
26210
26751
|
# Log success
|
|
26211
26752
|
self.log(f"✓ Imported {len(segments)} segments from memoQ XLIFF: {Path(file_path).name}")
|
|
26212
26753
|
self.log(f" Source: {source_lang}, Target: {target_lang}")
|
|
26213
|
-
|
|
26754
|
+
if pretranslated_count > 0:
|
|
26755
|
+
self.log(f" Pretranslated: {pretranslated_count} segments with target text")
|
|
26756
|
+
|
|
26757
|
+
# Build message with pretranslation info
|
|
26758
|
+
msg = f"Successfully imported {len(segments)} segment(s) from memoQ XLIFF.\n\nLanguages: {source_lang} → {target_lang}"
|
|
26759
|
+
if pretranslated_count > 0:
|
|
26760
|
+
msg += f"\n\nPretranslated: {pretranslated_count} segment(s) with target text loaded."
|
|
26761
|
+
|
|
26214
26762
|
QMessageBox.information(
|
|
26215
26763
|
self, "Import Successful",
|
|
26216
|
-
|
|
26217
|
-
f"Languages: {source_lang} → {target_lang}"
|
|
26764
|
+
msg
|
|
26218
26765
|
)
|
|
26219
26766
|
except Exception as e:
|
|
26220
26767
|
self.log(f"❌ Error importing memoQ XLIFF: {e}")
|
|
@@ -26623,7 +27170,19 @@ class SupervertalerQt(QMainWindow):
|
|
|
26623
27170
|
source_row = QHBoxLayout()
|
|
26624
27171
|
source_row.addWidget(QLabel("Source Language:"))
|
|
26625
27172
|
source_combo = QComboBox()
|
|
26626
|
-
|
|
27173
|
+
# Full language list (same as New Project dialog)
|
|
27174
|
+
available_languages = [
|
|
27175
|
+
"Afrikaans", "Albanian", "Arabic", "Armenian", "Basque", "Bengali",
|
|
27176
|
+
"Bulgarian", "Catalan", "Chinese (Simplified)", "Chinese (Traditional)",
|
|
27177
|
+
"Croatian", "Czech", "Danish", "Dutch", "English", "Estonian",
|
|
27178
|
+
"Finnish", "French", "Galician", "Georgian", "German", "Greek",
|
|
27179
|
+
"Hebrew", "Hindi", "Hungarian", "Icelandic", "Indonesian", "Irish",
|
|
27180
|
+
"Italian", "Japanese", "Korean", "Latvian", "Lithuanian", "Macedonian",
|
|
27181
|
+
"Malay", "Norwegian", "Persian", "Polish", "Portuguese", "Romanian",
|
|
27182
|
+
"Russian", "Serbian", "Slovak", "Slovenian", "Spanish", "Swahili",
|
|
27183
|
+
"Swedish", "Thai", "Turkish", "Ukrainian", "Urdu", "Vietnamese", "Welsh"
|
|
27184
|
+
]
|
|
27185
|
+
source_combo.addItems(available_languages)
|
|
26627
27186
|
# Try to match current UI selection
|
|
26628
27187
|
current_source = self.source_lang_combo.currentText() if hasattr(self, 'source_lang_combo') else "English"
|
|
26629
27188
|
source_idx = source_combo.findText(current_source)
|
|
@@ -26631,12 +27190,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
26631
27190
|
source_combo.setCurrentIndex(source_idx)
|
|
26632
27191
|
source_row.addWidget(source_combo)
|
|
26633
27192
|
lang_group_layout.addLayout(source_row)
|
|
26634
|
-
|
|
27193
|
+
|
|
26635
27194
|
# Target language
|
|
26636
27195
|
target_row = QHBoxLayout()
|
|
26637
27196
|
target_row.addWidget(QLabel("Target Language:"))
|
|
26638
27197
|
target_combo = QComboBox()
|
|
26639
|
-
target_combo.addItems(
|
|
27198
|
+
target_combo.addItems(available_languages)
|
|
26640
27199
|
# Try to match current UI selection
|
|
26641
27200
|
current_target = self.target_lang_combo.currentText() if hasattr(self, 'target_lang_combo') else "Dutch"
|
|
26642
27201
|
target_idx = target_combo.findText(current_target)
|
|
@@ -26644,15 +27203,15 @@ class SupervertalerQt(QMainWindow):
|
|
|
26644
27203
|
target_combo.setCurrentIndex(target_idx)
|
|
26645
27204
|
target_row.addWidget(target_combo)
|
|
26646
27205
|
lang_group_layout.addLayout(target_row)
|
|
26647
|
-
|
|
27206
|
+
|
|
26648
27207
|
lang_layout.addWidget(lang_group)
|
|
26649
|
-
|
|
27208
|
+
|
|
26650
27209
|
# Buttons
|
|
26651
27210
|
lang_buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
26652
27211
|
lang_buttons.accepted.connect(lang_dialog.accept)
|
|
26653
27212
|
lang_buttons.rejected.connect(lang_dialog.reject)
|
|
26654
27213
|
lang_layout.addWidget(lang_buttons)
|
|
26655
|
-
|
|
27214
|
+
|
|
26656
27215
|
if lang_dialog.exec() != QDialog.DialogCode.Accepted:
|
|
26657
27216
|
self.log("✗ User cancelled Trados import during language selection")
|
|
26658
27217
|
return
|
|
@@ -27303,7 +27862,19 @@ class SupervertalerQt(QMainWindow):
|
|
|
27303
27862
|
source_row = QHBoxLayout()
|
|
27304
27863
|
source_row.addWidget(QLabel("Source Language:"))
|
|
27305
27864
|
source_combo = QComboBox()
|
|
27306
|
-
|
|
27865
|
+
# Full language list (same as New Project dialog)
|
|
27866
|
+
available_languages = [
|
|
27867
|
+
"Afrikaans", "Albanian", "Arabic", "Armenian", "Basque", "Bengali",
|
|
27868
|
+
"Bulgarian", "Catalan", "Chinese (Simplified)", "Chinese (Traditional)",
|
|
27869
|
+
"Croatian", "Czech", "Danish", "Dutch", "English", "Estonian",
|
|
27870
|
+
"Finnish", "French", "Galician", "Georgian", "German", "Greek",
|
|
27871
|
+
"Hebrew", "Hindi", "Hungarian", "Icelandic", "Indonesian", "Irish",
|
|
27872
|
+
"Italian", "Japanese", "Korean", "Latvian", "Lithuanian", "Macedonian",
|
|
27873
|
+
"Malay", "Norwegian", "Persian", "Polish", "Portuguese", "Romanian",
|
|
27874
|
+
"Russian", "Serbian", "Slovak", "Slovenian", "Spanish", "Swahili",
|
|
27875
|
+
"Swedish", "Thai", "Turkish", "Ukrainian", "Urdu", "Vietnamese", "Welsh"
|
|
27876
|
+
]
|
|
27877
|
+
source_combo.addItems(available_languages)
|
|
27307
27878
|
# Try to match current UI selection
|
|
27308
27879
|
current_source = self.source_lang_combo.currentText() if hasattr(self, 'source_lang_combo') else "English"
|
|
27309
27880
|
source_idx = source_combo.findText(current_source)
|
|
@@ -27316,7 +27887,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
27316
27887
|
target_row = QHBoxLayout()
|
|
27317
27888
|
target_row.addWidget(QLabel("Target Language:"))
|
|
27318
27889
|
target_combo = QComboBox()
|
|
27319
|
-
target_combo.addItems(
|
|
27890
|
+
target_combo.addItems(available_languages)
|
|
27320
27891
|
# Try to match current UI selection
|
|
27321
27892
|
current_target = self.target_lang_combo.currentText() if hasattr(self, 'target_lang_combo') else "Dutch"
|
|
27322
27893
|
target_idx = target_combo.findText(current_target)
|
|
@@ -31385,9 +31956,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31385
31956
|
|
|
31386
31957
|
# Get source text
|
|
31387
31958
|
source_text = current_segment.source
|
|
31388
|
-
|
|
31959
|
+
|
|
31960
|
+
# Get glossary terms for AI injection
|
|
31961
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
31962
|
+
|
|
31389
31963
|
# Build combined prompt
|
|
31390
|
-
combined = self.prompt_manager_qt.build_final_prompt(
|
|
31964
|
+
combined = self.prompt_manager_qt.build_final_prompt(
|
|
31965
|
+
source_text, source_lang, target_lang, glossary_terms=glossary_terms
|
|
31966
|
+
)
|
|
31391
31967
|
|
|
31392
31968
|
# Check for figure/image context
|
|
31393
31969
|
figure_info = ""
|
|
@@ -31414,11 +31990,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31414
31990
|
composition_parts.append(f"📏 Total prompt: {len(combined):,} characters")
|
|
31415
31991
|
|
|
31416
31992
|
if self.prompt_manager_qt.library.active_primary_prompt:
|
|
31417
|
-
composition_parts.append(f"✓
|
|
31993
|
+
composition_parts.append(f"✓ Custom prompt attached")
|
|
31418
31994
|
|
|
31419
31995
|
if self.prompt_manager_qt.library.attached_prompts:
|
|
31420
31996
|
composition_parts.append(f"✓ {len(self.prompt_manager_qt.library.attached_prompts)} additional prompt(s) attached")
|
|
31421
|
-
|
|
31997
|
+
|
|
31998
|
+
if glossary_terms:
|
|
31999
|
+
composition_parts.append(f"📚 {len(glossary_terms)} glossary term(s) injected")
|
|
32000
|
+
|
|
31422
32001
|
if figure_info:
|
|
31423
32002
|
composition_parts.append(figure_info)
|
|
31424
32003
|
|
|
@@ -31453,12 +32032,59 @@ class SupervertalerQt(QMainWindow):
|
|
|
31453
32032
|
image_notice.setStyleSheet("padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 4px solid #ff9800;")
|
|
31454
32033
|
layout.addWidget(image_notice)
|
|
31455
32034
|
|
|
31456
|
-
# Text editor for preview
|
|
32035
|
+
# Text editor for preview with syntax highlighting
|
|
31457
32036
|
text_edit = QTextEdit()
|
|
31458
|
-
text_edit.setPlainText(combined)
|
|
31459
32037
|
text_edit.setReadOnly(True)
|
|
31460
32038
|
text_edit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
|
|
31461
32039
|
text_edit.setStyleSheet("font-family: 'Consolas', 'Courier New', monospace; font-size: 9pt;")
|
|
32040
|
+
|
|
32041
|
+
# Format the prompt with color highlighting
|
|
32042
|
+
import html
|
|
32043
|
+
import re
|
|
32044
|
+
|
|
32045
|
+
# Escape HTML entities first
|
|
32046
|
+
formatted_html = html.escape(combined)
|
|
32047
|
+
|
|
32048
|
+
# Replace newlines with <br> for HTML
|
|
32049
|
+
formatted_html = formatted_html.replace('\n', '<br>')
|
|
32050
|
+
|
|
32051
|
+
# Make "# SYSTEM PROMPT" bold and red
|
|
32052
|
+
formatted_html = re.sub(
|
|
32053
|
+
r'(#\s*SYSTEM\s*PROMPT)',
|
|
32054
|
+
r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
32055
|
+
formatted_html,
|
|
32056
|
+
flags=re.IGNORECASE
|
|
32057
|
+
)
|
|
32058
|
+
|
|
32059
|
+
# Make "# CUSTOM PROMPT" bold and red
|
|
32060
|
+
formatted_html = re.sub(
|
|
32061
|
+
r'(#\s*CUSTOM\s*PROMPT)',
|
|
32062
|
+
r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
32063
|
+
formatted_html,
|
|
32064
|
+
flags=re.IGNORECASE
|
|
32065
|
+
)
|
|
32066
|
+
|
|
32067
|
+
# Make "# GLOSSARY" bold and orange
|
|
32068
|
+
formatted_html = re.sub(
|
|
32069
|
+
r'(#\s*GLOSSARY)',
|
|
32070
|
+
r'<span style="color: #FF9800; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
32071
|
+
formatted_html,
|
|
32072
|
+
flags=re.IGNORECASE
|
|
32073
|
+
)
|
|
32074
|
+
|
|
32075
|
+
# Make source text section blue (pattern: "XX text:<br>..." until double line break or # header)
|
|
32076
|
+
# Match language code + " text:" followed by content until "# " or end
|
|
32077
|
+
formatted_html = re.sub(
|
|
32078
|
+
r'(\w{2,5}\s+text:)(<br>)(.*?)(<br><br>(?:#|\*\*YOUR TRANSLATION)|$)',
|
|
32079
|
+
r'<span style="color: #1565c0; font-weight: bold;">\1</span>\2<span style="color: #1565c0;">\3</span>\4',
|
|
32080
|
+
formatted_html,
|
|
32081
|
+
flags=re.DOTALL
|
|
32082
|
+
)
|
|
32083
|
+
|
|
32084
|
+
# Wrap in pre-like styling div
|
|
32085
|
+
formatted_html = f'<div style="font-family: Consolas, Courier New, monospace; white-space: pre-wrap;">{formatted_html}</div>'
|
|
32086
|
+
|
|
32087
|
+
text_edit.setHtml(formatted_html)
|
|
31462
32088
|
layout.addWidget(text_edit, 1)
|
|
31463
32089
|
|
|
31464
32090
|
# Close button
|
|
@@ -31476,25 +32102,43 @@ class SupervertalerQt(QMainWindow):
|
|
|
31476
32102
|
def show_grid_context_menu(self, position):
|
|
31477
32103
|
"""Show context menu for grid view with bulk operations"""
|
|
31478
32104
|
selected_segments = self.get_selected_segments_from_grid()
|
|
31479
|
-
|
|
32105
|
+
|
|
31480
32106
|
if not selected_segments:
|
|
31481
32107
|
return
|
|
31482
|
-
|
|
32108
|
+
|
|
31483
32109
|
menu = QMenu(self)
|
|
31484
|
-
|
|
32110
|
+
|
|
31485
32111
|
# Confirm selected segments action
|
|
31486
32112
|
if len(selected_segments) >= 1:
|
|
31487
32113
|
confirm_action = menu.addAction(f"✅ Confirm {len(selected_segments)} Segment(s)")
|
|
31488
32114
|
confirm_action.setToolTip(f"Confirm {len(selected_segments)} selected segment(s)")
|
|
31489
32115
|
confirm_action.triggered.connect(self.confirm_selected_segments)
|
|
31490
|
-
|
|
32116
|
+
|
|
32117
|
+
# Change Status submenu
|
|
32118
|
+
from modules.statuses import get_status
|
|
32119
|
+
status_menu = menu.addMenu(f"🏷️ Change Status ({len(selected_segments)})")
|
|
32120
|
+
# Common user-settable statuses (excluding TM-specific ones like pm, cm, tm_100, etc.)
|
|
32121
|
+
user_statuses = [
|
|
32122
|
+
("not_started", "❌ Not started"),
|
|
32123
|
+
("pretranslated", "🤖 Pre-translated"),
|
|
32124
|
+
("translated", "✏️ Translated"),
|
|
32125
|
+
("confirmed", "✔ Confirmed"),
|
|
32126
|
+
("tr_confirmed", "🌟 TR confirmed"),
|
|
32127
|
+
("proofread", "🟪 Proofread"),
|
|
32128
|
+
("approved", "⭐ Approved"),
|
|
32129
|
+
("rejected", "🚫 Rejected"),
|
|
32130
|
+
]
|
|
32131
|
+
for status_key, label in user_statuses:
|
|
32132
|
+
action = status_menu.addAction(label)
|
|
32133
|
+
action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s))
|
|
32134
|
+
|
|
31491
32135
|
# Clear translations action
|
|
31492
32136
|
clear_action = menu.addAction("🗑️ Clear Translations")
|
|
31493
32137
|
clear_action.setToolTip(f"Clear translations for {len(selected_segments)} selected segment(s)")
|
|
31494
32138
|
clear_action.triggered.connect(lambda: self.clear_selected_translations(selected_segments, 'grid'))
|
|
31495
|
-
|
|
32139
|
+
|
|
31496
32140
|
menu.addSeparator()
|
|
31497
|
-
|
|
32141
|
+
|
|
31498
32142
|
# Clear proofreading notes (if any selected segment has proofreading notes)
|
|
31499
32143
|
has_proofreading = any(seg.notes and "⚠️ PROOFREAD:" in seg.notes for seg in selected_segments)
|
|
31500
32144
|
if has_proofreading:
|
|
@@ -31502,11 +32146,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
31502
32146
|
clear_proofread_action.setToolTip("Remove proofreading issues from selected segment(s)")
|
|
31503
32147
|
clear_proofread_action.triggered.connect(lambda: self._clear_proofreading_from_selected(selected_segments))
|
|
31504
32148
|
menu.addSeparator()
|
|
31505
|
-
|
|
32149
|
+
|
|
31506
32150
|
# Select all action
|
|
31507
32151
|
select_all_action = menu.addAction("📋 Select All (Ctrl+A)")
|
|
31508
32152
|
select_all_action.triggered.connect(lambda: self.table.selectAll())
|
|
31509
|
-
|
|
32153
|
+
|
|
31510
32154
|
menu.exec(self.table.viewport().mapToGlobal(position))
|
|
31511
32155
|
|
|
31512
32156
|
def _clear_proofreading_from_selected(self, segments):
|
|
@@ -35182,7 +35826,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35182
35826
|
primary_prompt_text = f"✅ {prompt_path}"
|
|
35183
35827
|
attached_count = len(library.attached_prompt_paths) if library.attached_prompt_paths else 0
|
|
35184
35828
|
|
|
35185
|
-
ai_layout.addWidget(QLabel(f"<b>
|
|
35829
|
+
ai_layout.addWidget(QLabel(f"<b>Custom Prompt:</b> {primary_prompt_text}"))
|
|
35186
35830
|
if attached_count > 0:
|
|
35187
35831
|
ai_layout.addWidget(QLabel(f"<b>Attached Prompts:</b> {attached_count}"))
|
|
35188
35832
|
|
|
@@ -38331,17 +38975,75 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38331
38975
|
if not self.current_project:
|
|
38332
38976
|
QMessageBox.information(self, "Not Available", "Please load a project first.")
|
|
38333
38977
|
return
|
|
38334
|
-
|
|
38978
|
+
|
|
38335
38979
|
if not hasattr(self, 'table') or not self.table:
|
|
38336
38980
|
QMessageBox.information(self, "Not Available", "Grid view is not available.")
|
|
38337
38981
|
return
|
|
38338
|
-
|
|
38982
|
+
|
|
38339
38983
|
selected_segments = self.get_selected_segments_from_grid()
|
|
38340
38984
|
if selected_segments:
|
|
38341
38985
|
self.confirm_selected_segments()
|
|
38342
38986
|
else:
|
|
38343
38987
|
QMessageBox.information(self, "No Selection", "Please select one or more segments to confirm.")
|
|
38344
|
-
|
|
38988
|
+
|
|
38989
|
+
def change_status_selected(self, new_status: str, from_menu: bool = False):
|
|
38990
|
+
"""Change status of all selected segments to the specified status.
|
|
38991
|
+
|
|
38992
|
+
Args:
|
|
38993
|
+
new_status: The status key to set (e.g., 'translated', 'pretranslated')
|
|
38994
|
+
from_menu: If True, show message boxes for errors (called from menu)
|
|
38995
|
+
"""
|
|
38996
|
+
if not self.current_project:
|
|
38997
|
+
if from_menu:
|
|
38998
|
+
QMessageBox.information(self, "Not Available", "Please load a project first.")
|
|
38999
|
+
else:
|
|
39000
|
+
self.log("⚠️ No project loaded")
|
|
39001
|
+
return
|
|
39002
|
+
|
|
39003
|
+
if not hasattr(self, 'table') or not self.table:
|
|
39004
|
+
if from_menu:
|
|
39005
|
+
QMessageBox.information(self, "Not Available", "Grid view is not available.")
|
|
39006
|
+
return
|
|
39007
|
+
|
|
39008
|
+
selected_segments = self.get_selected_segments_from_grid()
|
|
39009
|
+
|
|
39010
|
+
if not selected_segments:
|
|
39011
|
+
if from_menu:
|
|
39012
|
+
QMessageBox.information(self, "No Selection", "Please select one or more segments to change.")
|
|
39013
|
+
else:
|
|
39014
|
+
self.log("⚠️ No segments selected")
|
|
39015
|
+
return
|
|
39016
|
+
|
|
39017
|
+
# Sync all target text from grid widgets first
|
|
39018
|
+
self._sync_grid_targets_to_segments(selected_segments)
|
|
39019
|
+
|
|
39020
|
+
# Get status definition for logging
|
|
39021
|
+
from modules.statuses import get_status
|
|
39022
|
+
status_def = get_status(new_status)
|
|
39023
|
+
|
|
39024
|
+
changed_count = 0
|
|
39025
|
+
for segment in selected_segments:
|
|
39026
|
+
# Skip if already has this status
|
|
39027
|
+
if segment.status == new_status:
|
|
39028
|
+
continue
|
|
39029
|
+
|
|
39030
|
+
segment.status = new_status
|
|
39031
|
+
changed_count += 1
|
|
39032
|
+
|
|
39033
|
+
# Update grid status icon
|
|
39034
|
+
row = self._find_row_for_segment(segment.id)
|
|
39035
|
+
if row >= 0:
|
|
39036
|
+
self.update_status_icon(row, new_status)
|
|
39037
|
+
|
|
39038
|
+
if changed_count > 0:
|
|
39039
|
+
self.project_modified = True
|
|
39040
|
+
self.update_window_title()
|
|
39041
|
+
self.log(f"✅ Changed {changed_count} segment(s) to '{status_def.label}'")
|
|
39042
|
+
# Update status bar progress stats
|
|
39043
|
+
self.update_progress_stats()
|
|
39044
|
+
else:
|
|
39045
|
+
self.log(f"ℹ️ All {len(selected_segments)} selected segment(s) already have status '{status_def.label}'")
|
|
39046
|
+
|
|
38345
39047
|
def _sync_grid_targets_to_segments(self, segments):
|
|
38346
39048
|
"""Sync target text from grid widgets to segment objects.
|
|
38347
39049
|
|
|
@@ -40210,13 +40912,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
40210
40912
|
except Exception as e:
|
|
40211
40913
|
self.log(f"⚠ Could not add surrounding segments: {e}")
|
|
40212
40914
|
|
|
40915
|
+
# Get glossary terms for AI injection
|
|
40916
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
40917
|
+
|
|
40213
40918
|
custom_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
40214
40919
|
source_text=segment.source,
|
|
40215
40920
|
source_lang=self.current_project.source_lang,
|
|
40216
40921
|
target_lang=self.current_project.target_lang,
|
|
40217
|
-
mode="single"
|
|
40922
|
+
mode="single",
|
|
40923
|
+
glossary_terms=glossary_terms
|
|
40218
40924
|
)
|
|
40219
|
-
|
|
40925
|
+
|
|
40220
40926
|
# Add surrounding context before the translation delimiter
|
|
40221
40927
|
if surrounding_context:
|
|
40222
40928
|
# Insert before the "YOUR TRANSLATION" delimiter
|
|
@@ -41148,25 +41854,41 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41148
41854
|
self.log(f"📖 Pre-translate from TM: Using activated TMs: {tm_ids}")
|
|
41149
41855
|
|
|
41150
41856
|
# Create progress dialog for TM pre-translation
|
|
41857
|
+
import time
|
|
41858
|
+
start_time = time.time()
|
|
41859
|
+
total_segments = len(segments_needing_translation)
|
|
41860
|
+
|
|
41151
41861
|
progress = QProgressDialog(
|
|
41152
|
-
f"Pre-translating {
|
|
41153
|
-
"Cancel", 0,
|
|
41862
|
+
f"Pre-translating {total_segments} segments from TM...",
|
|
41863
|
+
"Cancel", 0, total_segments, self
|
|
41154
41864
|
)
|
|
41155
|
-
progress.setWindowTitle("TM Pre-Translation")
|
|
41865
|
+
progress.setWindowTitle("🔍 TM Pre-Translation")
|
|
41156
41866
|
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
|
41157
41867
|
progress.setMinimumDuration(0) # Show immediately
|
|
41868
|
+
progress.setMinimumWidth(450) # Wider dialog for more info
|
|
41158
41869
|
progress.show()
|
|
41159
41870
|
QApplication.processEvents()
|
|
41160
|
-
|
|
41871
|
+
|
|
41161
41872
|
success_count = 0
|
|
41162
41873
|
no_match_count = 0
|
|
41163
|
-
|
|
41874
|
+
|
|
41164
41875
|
for idx, (row_index, segment) in enumerate(segments_needing_translation):
|
|
41165
41876
|
if progress.wasCanceled():
|
|
41166
41877
|
break
|
|
41167
|
-
|
|
41878
|
+
|
|
41168
41879
|
progress.setValue(idx)
|
|
41169
|
-
|
|
41880
|
+
|
|
41881
|
+
# Build informative progress label
|
|
41882
|
+
elapsed = time.time() - start_time
|
|
41883
|
+
elapsed_str = f"{int(elapsed // 60)}:{int(elapsed % 60):02d}"
|
|
41884
|
+
source_preview = segment.source[:50] + "..." if len(segment.source) > 50 else segment.source
|
|
41885
|
+
label_text = (
|
|
41886
|
+
f"Searching TM for segment {idx + 1} of {total_segments}...\n\n"
|
|
41887
|
+
f"Current: \"{source_preview}\"\n"
|
|
41888
|
+
f"Matches found: {success_count} | Elapsed: {elapsed_str}\n\n"
|
|
41889
|
+
f"ℹ️ This may take a while for large documents."
|
|
41890
|
+
)
|
|
41891
|
+
progress.setLabelText(label_text)
|
|
41170
41892
|
QApplication.processEvents()
|
|
41171
41893
|
|
|
41172
41894
|
try:
|
|
@@ -41184,7 +41906,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41184
41906
|
else:
|
|
41185
41907
|
no_match_count += 1
|
|
41186
41908
|
else:
|
|
41187
|
-
# Fuzzy matching enabled - get best match ≥
|
|
41909
|
+
# Fuzzy matching enabled - get best match ≥75%
|
|
41188
41910
|
matches = self.tm_database.search_all(
|
|
41189
41911
|
segment.source,
|
|
41190
41912
|
tm_ids=tm_ids,
|
|
@@ -41194,7 +41916,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41194
41916
|
if matches and len(matches) > 0:
|
|
41195
41917
|
best_match = matches[0]
|
|
41196
41918
|
match_pct = best_match.get('match_pct', 0)
|
|
41197
|
-
if match_pct >=
|
|
41919
|
+
if match_pct >= 75:
|
|
41198
41920
|
segment.target = best_match.get('target', '')
|
|
41199
41921
|
segment.status = "Translated"
|
|
41200
41922
|
success_count += 1
|
|
@@ -41562,9 +42284,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41562
42284
|
match_pct = match.get('match_pct', 0)
|
|
41563
42285
|
tm_match = match.get('target', '')
|
|
41564
42286
|
|
|
41565
|
-
if match_pct >=
|
|
42287
|
+
if match_pct >= 75: # Accept matches 75% and above
|
|
41566
42288
|
segment.target = tm_match
|
|
41567
|
-
segment.status = "
|
|
42289
|
+
segment.status = "pretranslated"
|
|
41568
42290
|
translated_count += 1
|
|
41569
42291
|
|
|
41570
42292
|
# Update grid immediately
|
|
@@ -41678,7 +42400,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41678
42400
|
if tm_match and len(tm_match) > 0:
|
|
41679
42401
|
translation = tm_match[0]['target']
|
|
41680
42402
|
segment.target = translation
|
|
41681
|
-
segment.status = '
|
|
42403
|
+
segment.status = 'pretranslated'
|
|
41682
42404
|
translated_count += 1
|
|
41683
42405
|
self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}... (TM 100%)")
|
|
41684
42406
|
else:
|
|
@@ -41725,7 +42447,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41725
42447
|
|
|
41726
42448
|
if translation and not translation.startswith('['): # Skip error messages
|
|
41727
42449
|
segment.target = translation
|
|
41728
|
-
segment.status = '
|
|
42450
|
+
segment.status = 'pretranslated'
|
|
41729
42451
|
translated_count += 1
|
|
41730
42452
|
self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}...")
|
|
41731
42453
|
else:
|
|
@@ -41751,11 +42473,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41751
42473
|
if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt and batch_segments:
|
|
41752
42474
|
try:
|
|
41753
42475
|
first_segment = batch_segments[0][1]
|
|
42476
|
+
# Get glossary terms for AI injection
|
|
42477
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
41754
42478
|
base_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
41755
42479
|
source_text=first_segment.source,
|
|
41756
42480
|
source_lang=source_lang,
|
|
41757
42481
|
target_lang=target_lang,
|
|
41758
|
-
mode="single"
|
|
42482
|
+
mode="single",
|
|
42483
|
+
glossary_terms=glossary_terms
|
|
41759
42484
|
)
|
|
41760
42485
|
if "**SOURCE TEXT:**" in base_prompt:
|
|
41761
42486
|
base_prompt = base_prompt.split("**SOURCE TEXT:**")[0].strip()
|
|
@@ -42183,11 +42908,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42183
42908
|
# Access parent through closure
|
|
42184
42909
|
parent = self
|
|
42185
42910
|
if hasattr(parent, 'prompt_manager_qt') and parent.prompt_manager_qt:
|
|
42911
|
+
# Get glossary terms for AI injection
|
|
42912
|
+
glossary_terms = parent.get_ai_inject_glossary_terms() if hasattr(parent, 'get_ai_inject_glossary_terms') else []
|
|
42186
42913
|
custom_prompt = parent.prompt_manager_qt.build_final_prompt(
|
|
42187
42914
|
source_text=source_text,
|
|
42188
42915
|
source_lang=source_lang,
|
|
42189
42916
|
target_lang=target_lang,
|
|
42190
|
-
mode="single"
|
|
42917
|
+
mode="single",
|
|
42918
|
+
glossary_terms=glossary_terms
|
|
42191
42919
|
)
|
|
42192
42920
|
except Exception as e:
|
|
42193
42921
|
self.log(f"⚠ Could not build LLM prompt from manager: {e}")
|
|
@@ -42670,7 +43398,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42670
43398
|
api_keys['google'] = api_keys['gemini']
|
|
42671
43399
|
|
|
42672
43400
|
return api_keys
|
|
42673
|
-
|
|
43401
|
+
|
|
43402
|
+
def get_ai_inject_glossary_terms(self) -> list:
|
|
43403
|
+
"""Get glossary terms from AI-inject-enabled termbases for the current project.
|
|
43404
|
+
|
|
43405
|
+
Returns:
|
|
43406
|
+
List of term dictionaries with source_term, target_term, forbidden keys
|
|
43407
|
+
"""
|
|
43408
|
+
if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
|
|
43409
|
+
return []
|
|
43410
|
+
|
|
43411
|
+
project_id = None
|
|
43412
|
+
if hasattr(self, 'current_project') and self.current_project:
|
|
43413
|
+
project_id = getattr(self.current_project, 'id', None)
|
|
43414
|
+
|
|
43415
|
+
return self.termbase_mgr.get_ai_inject_terms(project_id)
|
|
43416
|
+
|
|
42674
43417
|
def ensure_example_api_keys(self):
|
|
42675
43418
|
"""Create example API keys file on first launch for new users"""
|
|
42676
43419
|
example_file = self.user_data_path / "api_keys.example.txt"
|
|
@@ -47985,7 +48728,87 @@ class BlueCheckmarkCheckBox(QCheckBox):
|
|
|
47985
48728
|
|
|
47986
48729
|
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
47987
48730
|
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
47988
|
-
|
|
48731
|
+
|
|
48732
|
+
painter.end()
|
|
48733
|
+
|
|
48734
|
+
|
|
48735
|
+
class OrangeCheckmarkCheckBox(QCheckBox):
|
|
48736
|
+
"""Custom checkbox with orange background and white checkmark when checked (for AI injection)"""
|
|
48737
|
+
|
|
48738
|
+
def __init__(self, text="", parent=None):
|
|
48739
|
+
super().__init__(text, parent)
|
|
48740
|
+
self.setCheckable(True)
|
|
48741
|
+
self.setEnabled(True)
|
|
48742
|
+
self.setStyleSheet("""
|
|
48743
|
+
QCheckBox {
|
|
48744
|
+
font-size: 9pt;
|
|
48745
|
+
spacing: 6px;
|
|
48746
|
+
}
|
|
48747
|
+
QCheckBox::indicator {
|
|
48748
|
+
width: 16px;
|
|
48749
|
+
height: 16px;
|
|
48750
|
+
border: 2px solid #999;
|
|
48751
|
+
border-radius: 3px;
|
|
48752
|
+
background-color: white;
|
|
48753
|
+
}
|
|
48754
|
+
QCheckBox::indicator:checked {
|
|
48755
|
+
background-color: #FF9800;
|
|
48756
|
+
border-color: #FF9800;
|
|
48757
|
+
}
|
|
48758
|
+
QCheckBox::indicator:hover {
|
|
48759
|
+
border-color: #666;
|
|
48760
|
+
}
|
|
48761
|
+
QCheckBox::indicator:checked:hover {
|
|
48762
|
+
background-color: #F57C00;
|
|
48763
|
+
border-color: #F57C00;
|
|
48764
|
+
}
|
|
48765
|
+
""")
|
|
48766
|
+
|
|
48767
|
+
def paintEvent(self, event):
|
|
48768
|
+
"""Override paint event to draw white checkmark when checked"""
|
|
48769
|
+
super().paintEvent(event)
|
|
48770
|
+
|
|
48771
|
+
if self.isChecked():
|
|
48772
|
+
from PyQt6.QtWidgets import QStyleOptionButton
|
|
48773
|
+
from PyQt6.QtGui import QPainter, QPen, QColor
|
|
48774
|
+
from PyQt6.QtCore import QPointF
|
|
48775
|
+
|
|
48776
|
+
opt = QStyleOptionButton()
|
|
48777
|
+
self.initStyleOption(opt)
|
|
48778
|
+
indicator_rect = self.style().subElementRect(
|
|
48779
|
+
self.style().SubElement.SE_CheckBoxIndicator,
|
|
48780
|
+
opt,
|
|
48781
|
+
self
|
|
48782
|
+
)
|
|
48783
|
+
|
|
48784
|
+
if indicator_rect.isValid():
|
|
48785
|
+
painter = QPainter(self)
|
|
48786
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
48787
|
+
pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
|
|
48788
|
+
painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
|
|
48789
|
+
painter.setBrush(QColor(255, 255, 255))
|
|
48790
|
+
|
|
48791
|
+
x = indicator_rect.x()
|
|
48792
|
+
y = indicator_rect.y()
|
|
48793
|
+
w = indicator_rect.width()
|
|
48794
|
+
h = indicator_rect.height()
|
|
48795
|
+
|
|
48796
|
+
padding = min(w, h) * 0.15
|
|
48797
|
+
x += padding
|
|
48798
|
+
y += padding
|
|
48799
|
+
w -= padding * 2
|
|
48800
|
+
h -= padding * 2
|
|
48801
|
+
|
|
48802
|
+
check_x1 = x + w * 0.10
|
|
48803
|
+
check_y1 = y + h * 0.50
|
|
48804
|
+
check_x2 = x + w * 0.35
|
|
48805
|
+
check_y2 = y + h * 0.70
|
|
48806
|
+
check_x3 = x + w * 0.90
|
|
48807
|
+
check_y3 = y + h * 0.25
|
|
48808
|
+
|
|
48809
|
+
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
48810
|
+
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
48811
|
+
|
|
47989
48812
|
painter.end()
|
|
47990
48813
|
|
|
47991
48814
|
|