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 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.172"
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
- self.table_ref.selectRow(self.row)
2084
- self.table_ref.setCurrentCell(self.row, 2)
2085
-
2086
- # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
2087
- # Find the main window and call the method directly
2088
- main_window = self.table_ref.parent()
2089
- while main_window and not hasattr(main_window, 'on_cell_selected'):
2090
- main_window = main_window.parent()
2091
- if main_window and hasattr(main_window, 'on_cell_selected'):
2092
- main_window.on_cell_selected(self.row, 2, -1, -1)
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
- self.table_ref.selectRow(self.row)
2174
- self.table_ref.setCurrentCell(self.row, 2)
2175
-
2176
- # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
2177
- # Find the main window and call the method directly
2178
- main_window = self.table_ref.parent()
2179
- while main_window and not hasattr(main_window, 'on_cell_selected'):
2180
- main_window = main_window.parent()
2181
- if main_window and hasattr(main_window, 'on_cell_selected'):
2182
- main_window.on_cell_selected(self.row, 2, -1, -1)
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
- self.table.selectRow(self.row)
3030
- self.table.setCurrentCell(self.row, 3) # Column 3 is Target
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
- # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
3033
- # Find the main window and call the method directly
3034
- try:
3035
- main_window = self.table.parent()
3036
- while main_window and not hasattr(main_window, 'on_cell_selected'):
3037
- main_window = main_window.parent()
3038
- if main_window and hasattr(main_window, 'on_cell_selected'):
3039
- main_window.on_cell_selected(self.row, 3, -1, -1)
3040
- except Exception as e:
3041
- print(f"Error triggering manual cell selection: {e}")
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
- self.table.selectRow(self.row)
3112
- self.table.setCurrentCell(self.row, 3) # Column 3 is Target
3113
-
3114
- # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
3115
- # Find the main window and call the method directly
3116
- try:
3117
- main_window = self.table.parent()
3118
- while main_window and not hasattr(main_window, 'on_cell_selected'):
3119
- main_window = main_window.parent()
3120
- if main_window and hasattr(main_window, 'on_cell_selected'):
3121
- main_window.on_cell_selected(self.row, 3, -1, -1)
3122
- except Exception as e:
3123
- print(f"Error triggering manual cell selection: {e}")
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 >= 70: # Accept matches 70% and above
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 data location dialog, then Features tab
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._show_data_location_dialog)
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
- create_btn.clicked.connect(lambda: self._show_create_tm_dialog(tm_metadata_mgr, refresh_tm_list, project_id))
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). Multiple glossaries can share same priority. Priority #1 = Project Glossary."
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(7)
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._get_settings_ui_font_scale()
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._apply_settings_ui_font_scale(saved_scale))
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 (for Settings panels) =====
17240
- ui_scale_group = QGroupBox("🖥️ Settings Panel Font Size")
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 all Settings panel text. Useful for high-DPI/4K displays.\n"
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(80)
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 Settings panel text (80%-200%)")
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._apply_settings_ui_font_scale(ui_scale_spin.value()))
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._apply_settings_ui_font_scale(self._ui_scale_spin.value())
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 _apply_settings_ui_font_scale(self, scale_percent: int):
19448
- """Apply font scale to all Settings panels for better readability on high-DPI displays"""
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['settings_ui_font_scale'] = scale_percent
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
- # Calculate base font size (default system font is typically 9-10pt)
19455
- base_size = 10 # Base font size in points
19456
- scaled_size = int(base_size * scale_percent / 100)
19457
-
19458
- # Create stylesheet for Settings panels
19459
- settings_stylesheet = f"""
19460
- QGroupBox {{
19461
- font-size: {scaled_size + 1}pt;
19462
- font-weight: bold;
19463
- }}
19464
- QGroupBox QLabel {{
19465
- font-size: {scaled_size}pt;
19466
- }}
19467
- QGroupBox QCheckBox {{
19468
- font-size: {scaled_size}pt;
19469
- }}
19470
- QGroupBox QRadioButton {{
19471
- font-size: {scaled_size}pt;
19472
- }}
19473
- QGroupBox QComboBox {{
19474
- font-size: {scaled_size}pt;
19475
- }}
19476
- QGroupBox QSpinBox {{
19477
- font-size: {scaled_size}pt;
19478
- }}
19479
- QGroupBox QLineEdit {{
19480
- font-size: {scaled_size}pt;
19481
- }}
19482
- QGroupBox QPushButton {{
19483
- font-size: {scaled_size}pt;
19484
- }}
19485
- QGroupBox QTextEdit {{
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
- return general_settings.get('settings_ui_font_scale', 100)
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
- t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
21871
- t.domain, t.notes, t.project, t.client, t.forbidden,
21872
- tb.is_project_termbase, tb.name as termbase_name,
21873
- COALESCE(ta.priority, tb.ranking) as ranking
21874
- FROM termbase_terms t
21875
- LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
21876
- LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
21877
- WHERE LOWER(t.source_term) LIKE ?
21878
- AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
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
- # Limit raw hits per word to keep batch worker light
21890
- query += " LIMIT 15"
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 <= 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 = 'target' AND forbidden = 0
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
- msg = f"💾 Saved segment to {saved_count} TM(s)"
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.extract_source_segments()
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.plain_text,
26168
- target="",
26169
- status=DEFAULT_STATUS.key,
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
- f"Successfully imported {len(segments)} segment(s) from memoQ XLIFF.\n\n"
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
- source_combo.addItems(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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
- source_combo.addItems(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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(source_text, source_lang, target_lang)
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"✓ Primary prompt attached")
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>Primary Prompt:</b> {primary_prompt_text}"))
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 {len(segments_needing_translation)} segments from TM...",
41153
- "Cancel", 0, len(segments_needing_translation), self
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
- progress.setLabelText(f"Searching TM for segment {idx + 1}/{len(segments_needing_translation)}...")
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 ≥70%
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 >= 70:
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 >= 70: # Accept matches 70% and above
42287
+ if match_pct >= 75: # Accept matches 75% and above
41566
42288
  segment.target = tm_match
41567
- segment.status = "translated" if match_pct == 100 else "pre-translated"
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 = 'pre-translated'
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 = 'pre-translated'
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