supervertaler 1.9.173__py3-none-any.whl → 1.9.175__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
@@ -34,7 +34,7 @@ License: MIT
34
34
  """
35
35
 
36
36
  # Version Information.
37
- __version__ = "1.9.172"
37
+ __version__ = "1.9.175"
38
38
  __phase__ = "0.9"
39
39
  __release_date__ = "2026-01-28"
40
40
  __edition__ = "Qt"
@@ -753,7 +753,83 @@ def get_wrapping_tag_pair(source_text: str, target_text: str) -> tuple:
753
753
  opening, closing = get_tag_pair(num)
754
754
  if opening not in target_tags or closing not in target_tags:
755
755
  return (opening, closing)
756
-
756
+
757
+ return (None, None)
758
+
759
+
760
+ def get_html_wrapping_tag_pair(source_text: str, target_text: str) -> tuple:
761
+ """
762
+ Get the next available HTML tag pair for wrapping selected text.
763
+
764
+ Finds paired HTML tags (opening + closing) from source that are not yet
765
+ complete in target. Supports common formatting tags: b, i, u, em, strong,
766
+ span, li, p, a, sub, sup, etc.
767
+
768
+ Args:
769
+ source_text: Source segment text with HTML tags
770
+ target_text: Current target text
771
+
772
+ Returns:
773
+ Tuple of (opening_tag, closing_tag) or (None, None) if no pairs available
774
+ """
775
+ import re
776
+
777
+ # Find all HTML tags in source
778
+ # Match: <tag>, <tag attr="...">, </tag>
779
+ tag_pattern = r'<(/?)([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[^>]*)?>'
780
+ source_matches = re.findall(tag_pattern, source_text)
781
+ target_matches = re.findall(tag_pattern, target_text)
782
+
783
+ if not source_matches:
784
+ return (None, None)
785
+
786
+ # Build lists of opening and closing tags in source
787
+ source_opening = [] # [(full_tag, tag_name), ...]
788
+ source_closing = []
789
+
790
+ for match in re.finditer(tag_pattern, source_text):
791
+ is_closing = match.group(1) == '/'
792
+ tag_name = match.group(2).lower()
793
+ full_tag = match.group(0)
794
+
795
+ if is_closing:
796
+ source_closing.append((full_tag, tag_name))
797
+ else:
798
+ source_opening.append((full_tag, tag_name))
799
+
800
+ # Build sets of tag names already in target
801
+ target_opening_names = set()
802
+ target_closing_names = set()
803
+
804
+ for is_closing, tag_name in target_matches:
805
+ if is_closing == '/':
806
+ target_closing_names.add(tag_name.lower())
807
+ else:
808
+ target_opening_names.add(tag_name.lower())
809
+
810
+ # Find first tag pair where both opening and closing exist in source
811
+ # but at least one is missing from target
812
+ seen_tags = set()
813
+ for full_tag, tag_name in source_opening:
814
+ if tag_name in seen_tags:
815
+ continue
816
+ seen_tags.add(tag_name)
817
+
818
+ # Check if there's a matching closing tag in source
819
+ has_closing = any(name == tag_name for _, name in source_closing)
820
+ if not has_closing:
821
+ continue
822
+
823
+ # Check if pair is incomplete in target
824
+ opening_in_target = tag_name in target_opening_names
825
+ closing_in_target = tag_name in target_closing_names
826
+
827
+ if not opening_in_target or not closing_in_target:
828
+ # Find the actual opening and closing tags from source
829
+ opening_tag = full_tag
830
+ closing_tag = f"</{tag_name}>"
831
+ return (opening_tag, closing_tag)
832
+
757
833
  return (None, None)
758
834
 
759
835
 
@@ -1148,48 +1224,48 @@ class GridTextEditor(QTextEdit):
1148
1224
  def _insert_next_tag_or_wrap_selection(self):
1149
1225
  """
1150
1226
  Insert the next memoQ tag, HTML tag, or CafeTran pipe symbol from source, or wrap selection.
1151
-
1227
+
1152
1228
  Behavior:
1153
- - If text is selected: Wrap it with the next available tag pair [N}selection{N] or |selection|
1229
+ - If text is selected: Wrap it with the next available tag pair [N}selection{N] or <tag>selection</tag> or |selection|
1154
1230
  - If no selection: Insert the next unused tag/pipe from source at cursor position
1155
-
1231
+
1156
1232
  Supports:
1157
1233
  - memoQ tags: [1}, {1], [2}, {2], etc.
1158
1234
  - HTML/XML tags: <li>, </li>, <b>, </b>, <i>, </i>, etc.
1159
1235
  - CafeTran pipe symbols: |
1160
-
1236
+
1161
1237
  Shortcut: Ctrl+, (comma)
1162
1238
  """
1163
1239
  # Get the main window and current segment
1164
1240
  if not self.table_widget or self.current_row is None:
1165
1241
  return
1166
-
1242
+
1167
1243
  # Navigate up to find main window
1168
1244
  main_window = self.table_widget.parent()
1169
1245
  while main_window and not hasattr(main_window, 'current_project'):
1170
1246
  main_window = main_window.parent()
1171
-
1247
+
1172
1248
  if not main_window or not hasattr(main_window, 'current_project'):
1173
1249
  return
1174
-
1250
+
1175
1251
  if not main_window.current_project or self.current_row >= len(main_window.current_project.segments):
1176
1252
  return
1177
-
1253
+
1178
1254
  segment = main_window.current_project.segments[self.current_row]
1179
1255
  source_text = segment.source
1180
1256
  current_target = self.toPlainText()
1181
-
1257
+
1182
1258
  # Check what type of tags are in the source
1183
1259
  has_memoq_tags = bool(extract_memoq_tags(source_text))
1184
1260
  has_html_tags = bool(extract_html_tags(source_text))
1185
1261
  has_any_tags = has_memoq_tags or has_html_tags
1186
1262
  has_pipe_symbols = '|' in source_text
1187
-
1263
+
1188
1264
  # Check if there's a selection
1189
1265
  cursor = self.textCursor()
1190
1266
  if cursor.hasSelection():
1191
1267
  selected_text = cursor.selectedText()
1192
-
1268
+
1193
1269
  # Try memoQ tag pair first
1194
1270
  if has_memoq_tags:
1195
1271
  opening_tag, closing_tag = get_wrapping_tag_pair(source_text, current_target)
@@ -1199,7 +1275,17 @@ class GridTextEditor(QTextEdit):
1199
1275
  if hasattr(main_window, 'log'):
1200
1276
  main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
1201
1277
  return
1202
-
1278
+
1279
+ # Try HTML tag pairs (e.g., <b>...</b>, <i>...</i>)
1280
+ if has_html_tags:
1281
+ opening_tag, closing_tag = get_html_wrapping_tag_pair(source_text, current_target)
1282
+ if opening_tag and closing_tag:
1283
+ wrapped_text = f"{opening_tag}{selected_text}{closing_tag}"
1284
+ cursor.insertText(wrapped_text)
1285
+ if hasattr(main_window, 'log'):
1286
+ main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
1287
+ return
1288
+
1203
1289
  # Try CafeTran pipe symbols
1204
1290
  if has_pipe_symbols:
1205
1291
  pipes_needed = get_next_pipe_count_needed(source_text, current_target)
@@ -1210,12 +1296,12 @@ class GridTextEditor(QTextEdit):
1210
1296
  if hasattr(main_window, 'log'):
1211
1297
  main_window.log(f"🏷️ Wrapped selection with |...|")
1212
1298
  return
1213
-
1299
+
1214
1300
  if hasattr(main_window, 'log'):
1215
1301
  main_window.log("⚠️ No tag pairs available from source")
1216
1302
  else:
1217
1303
  # No selection - insert next unused tag or pipe at cursor
1218
-
1304
+
1219
1305
  # Try memoQ tags and HTML tags (find_next_unused_tag handles both)
1220
1306
  if has_any_tags:
1221
1307
  next_tag = find_next_unused_tag(source_text, current_target)
@@ -1224,7 +1310,7 @@ class GridTextEditor(QTextEdit):
1224
1310
  if hasattr(main_window, 'log'):
1225
1311
  main_window.log(f"🏷️ Inserted tag: {next_tag}")
1226
1312
  return
1227
-
1313
+
1228
1314
  # Try CafeTran pipe symbols
1229
1315
  if has_pipe_symbols:
1230
1316
  pipes_needed = get_next_pipe_count_needed(source_text, current_target)
@@ -1233,7 +1319,7 @@ class GridTextEditor(QTextEdit):
1233
1319
  if hasattr(main_window, 'log'):
1234
1320
  main_window.log(f"🏷️ Inserted pipe symbol (|)")
1235
1321
  return
1236
-
1322
+
1237
1323
  if hasattr(main_window, 'log'):
1238
1324
  main_window.log("✓ All tags from source already in target")
1239
1325
 
@@ -2080,16 +2166,27 @@ class ReadOnlyGridTextEditor(QTextEdit):
2080
2166
  # Use stored table reference and row number
2081
2167
  if self.table_ref and self.row >= 0:
2082
2168
  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)
2169
+ # Check for Shift or Ctrl modifier - let Qt handle native multi-selection
2170
+ modifiers = event.modifiers()
2171
+ is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
2172
+ is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
2173
+
2174
+ if is_shift or is_ctrl:
2175
+ # For Shift+click (range) or Ctrl+click (toggle), just set current cell
2176
+ # but don't call selectRow() which would clear the selection
2177
+ self.table_ref.setCurrentCell(self.row, 2)
2178
+ else:
2179
+ # Normal click - select just this row
2180
+ self.table_ref.selectRow(self.row)
2181
+ self.table_ref.setCurrentCell(self.row, 2)
2182
+
2183
+ # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
2184
+ # Find the main window and call the method directly
2185
+ main_window = self.table_ref.parent()
2186
+ while main_window and not hasattr(main_window, 'on_cell_selected'):
2187
+ main_window = main_window.parent()
2188
+ if main_window and hasattr(main_window, 'on_cell_selected'):
2189
+ main_window.on_cell_selected(self.row, 2, -1, -1)
2093
2190
  except Exception as e:
2094
2191
  print(f"Error triggering manual cell selection: {e}")
2095
2192
 
@@ -2166,20 +2263,32 @@ class ReadOnlyGridTextEditor(QTextEdit):
2166
2263
  """Select text when focused for easy copying and trigger row selection"""
2167
2264
  super().focusInEvent(event)
2168
2265
  # Don't auto-select - let user select manually
2169
-
2266
+
2170
2267
  # Use stored table reference and row number
2171
2268
  if self.table_ref and self.row >= 0:
2172
2269
  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)
2270
+ # Check for Shift or Ctrl modifier - let Qt handle native multi-selection
2271
+ from PyQt6.QtWidgets import QApplication
2272
+ modifiers = QApplication.keyboardModifiers()
2273
+ is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
2274
+ is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
2275
+
2276
+ if is_shift or is_ctrl:
2277
+ # For Shift+click (range) or Ctrl+click (toggle), just set current cell
2278
+ # but don't call selectRow() which would clear the selection
2279
+ self.table_ref.setCurrentCell(self.row, 2)
2280
+ else:
2281
+ # Normal focus - select just this row
2282
+ self.table_ref.selectRow(self.row)
2283
+ self.table_ref.setCurrentCell(self.row, 2)
2284
+
2285
+ # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
2286
+ # Find the main window and call the method directly
2287
+ main_window = self.table_ref.parent()
2288
+ while main_window and not hasattr(main_window, 'on_cell_selected'):
2289
+ main_window = main_window.parent()
2290
+ if main_window and hasattr(main_window, 'on_cell_selected'):
2291
+ main_window.on_cell_selected(self.row, 2, -1, -1)
2183
2292
  except Exception as e:
2184
2293
  print(f"Error triggering manual cell selection: {e}")
2185
2294
 
@@ -3026,19 +3135,30 @@ class EditableGridTextEditor(QTextEdit):
3026
3135
  super().mousePressEvent(event)
3027
3136
  # Auto-select the row when clicking in the target cell
3028
3137
  if self.table and self.row >= 0:
3029
- self.table.selectRow(self.row)
3030
- self.table.setCurrentCell(self.row, 3) # Column 3 is Target
3138
+ # Check for Shift or Ctrl modifier - let Qt handle native multi-selection
3139
+ modifiers = event.modifiers()
3140
+ is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
3141
+ is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
3142
+
3143
+ if is_shift or is_ctrl:
3144
+ # For Shift+click (range) or Ctrl+click (toggle), just set current cell
3145
+ # but don't call selectRow() which would clear the selection
3146
+ self.table.setCurrentCell(self.row, 3) # Column 3 is Target
3147
+ else:
3148
+ # Normal click - select just this row
3149
+ self.table.selectRow(self.row)
3150
+ self.table.setCurrentCell(self.row, 3) # Column 3 is Target
3031
3151
 
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}")
3152
+ # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
3153
+ # Find the main window and call the method directly
3154
+ try:
3155
+ main_window = self.table.parent()
3156
+ while main_window and not hasattr(main_window, 'on_cell_selected'):
3157
+ main_window = main_window.parent()
3158
+ if main_window and hasattr(main_window, 'on_cell_selected'):
3159
+ main_window.on_cell_selected(self.row, 3, -1, -1)
3160
+ except Exception as e:
3161
+ print(f"Error triggering manual cell selection: {e}")
3042
3162
 
3043
3163
  def mouseReleaseEvent(self, event):
3044
3164
  """Smart word selection - expand partial selections to full words
@@ -3108,19 +3228,31 @@ class EditableGridTextEditor(QTextEdit):
3108
3228
  self.show()
3109
3229
  # Auto-select the row when focusing the target cell
3110
3230
  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}")
3231
+ # Check for Shift or Ctrl modifier - let Qt handle native multi-selection
3232
+ from PyQt6.QtWidgets import QApplication
3233
+ modifiers = QApplication.keyboardModifiers()
3234
+ is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
3235
+ is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
3236
+
3237
+ if is_shift or is_ctrl:
3238
+ # For Shift+click (range) or Ctrl+click (toggle), just set current cell
3239
+ # but don't call selectRow() which would clear the selection
3240
+ self.table.setCurrentCell(self.row, 3) # Column 3 is Target
3241
+ else:
3242
+ # Normal focus - select just this row
3243
+ self.table.selectRow(self.row)
3244
+ self.table.setCurrentCell(self.row, 3) # Column 3 is Target
3245
+
3246
+ # CRITICAL: Manually trigger on_cell_selected since signals aren't firing
3247
+ # Find the main window and call the method directly
3248
+ try:
3249
+ main_window = self.table.parent()
3250
+ while main_window and not hasattr(main_window, 'on_cell_selected'):
3251
+ main_window = main_window.parent()
3252
+ if main_window and hasattr(main_window, 'on_cell_selected'):
3253
+ main_window.on_cell_selected(self.row, 3, -1, -1)
3254
+ except Exception as e:
3255
+ print(f"Error triggering manual cell selection: {e}")
3124
3256
 
3125
3257
  def keyPressEvent(self, event):
3126
3258
  """Handle Tab and Ctrl+E keys to cycle between source and target cells"""
@@ -3423,7 +3555,7 @@ class EditableGridTextEditor(QTextEdit):
3423
3555
  cursor = self.textCursor()
3424
3556
  if cursor.hasSelection():
3425
3557
  selected_text = cursor.selectedText()
3426
-
3558
+
3427
3559
  # Try memoQ tag pair first
3428
3560
  if has_memoq_tags:
3429
3561
  opening_tag, closing_tag = get_wrapping_tag_pair(source_text, current_target)
@@ -3433,7 +3565,17 @@ class EditableGridTextEditor(QTextEdit):
3433
3565
  if hasattr(main_window, 'log'):
3434
3566
  main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
3435
3567
  return
3436
-
3568
+
3569
+ # Try HTML tag pairs (e.g., <b>...</b>, <i>...</i>)
3570
+ if has_html_tags:
3571
+ opening_tag, closing_tag = get_html_wrapping_tag_pair(source_text, current_target)
3572
+ if opening_tag and closing_tag:
3573
+ wrapped_text = f"{opening_tag}{selected_text}{closing_tag}"
3574
+ cursor.insertText(wrapped_text)
3575
+ if hasattr(main_window, 'log'):
3576
+ main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
3577
+ return
3578
+
3437
3579
  # Try CafeTran pipe symbols
3438
3580
  if has_pipe_symbols:
3439
3581
  pipes_needed = get_next_pipe_count_needed(source_text, current_target)
@@ -3444,12 +3586,12 @@ class EditableGridTextEditor(QTextEdit):
3444
3586
  if hasattr(main_window, 'log'):
3445
3587
  main_window.log(f"🏷️ Wrapped selection with |...|")
3446
3588
  return
3447
-
3589
+
3448
3590
  if hasattr(main_window, 'log'):
3449
3591
  main_window.log("⚠️ No tag pairs available from source")
3450
3592
  else:
3451
3593
  # No selection - insert next unused tag or pipe at cursor
3452
-
3594
+
3453
3595
  # Try memoQ tags and HTML tags (find_next_unused_tag handles both)
3454
3596
  if has_any_tags:
3455
3597
  next_tag = find_next_unused_tag(source_text, current_target)
@@ -3458,7 +3600,7 @@ class EditableGridTextEditor(QTextEdit):
3458
3600
  if hasattr(main_window, 'log'):
3459
3601
  main_window.log(f"🏷️ Inserted tag: {next_tag}")
3460
3602
  return
3461
-
3603
+
3462
3604
  # Try CafeTran pipe symbols
3463
3605
  if has_pipe_symbols:
3464
3606
  pipes_needed = get_next_pipe_count_needed(source_text, current_target)
@@ -3467,10 +3609,10 @@ class EditableGridTextEditor(QTextEdit):
3467
3609
  if hasattr(main_window, 'log'):
3468
3610
  main_window.log(f"🏷️ Inserted pipe symbol (|)")
3469
3611
  return
3470
-
3612
+
3471
3613
  if hasattr(main_window, 'log'):
3472
3614
  main_window.log("✓ All tags from source already in target")
3473
-
3615
+
3474
3616
  def _copy_source_to_target(self):
3475
3617
  """
3476
3618
  Copy source text to target cell.
@@ -5179,12 +5321,14 @@ class AdvancedFiltersDialog(QDialog):
5179
5321
 
5180
5322
  self.status_not_started = CheckmarkCheckBox("Not started")
5181
5323
  self.status_edited = CheckmarkCheckBox("Edited")
5324
+ self.status_pretranslated = CheckmarkCheckBox("Pre-translated")
5182
5325
  self.status_translated = CheckmarkCheckBox("Translated")
5183
5326
  self.status_confirmed = CheckmarkCheckBox("Confirmed")
5184
5327
  self.status_draft = CheckmarkCheckBox("Draft")
5185
-
5328
+
5186
5329
  status_layout.addWidget(self.status_not_started)
5187
5330
  status_layout.addWidget(self.status_edited)
5331
+ status_layout.addWidget(self.status_pretranslated)
5188
5332
  status_layout.addWidget(self.status_translated)
5189
5333
  status_layout.addWidget(self.status_confirmed)
5190
5334
  status_layout.addWidget(self.status_draft)
@@ -5257,6 +5401,7 @@ class AdvancedFiltersDialog(QDialog):
5257
5401
 
5258
5402
  self.status_not_started.setChecked(False)
5259
5403
  self.status_edited.setChecked(False)
5404
+ self.status_pretranslated.setChecked(False)
5260
5405
  self.status_translated.setChecked(False)
5261
5406
  self.status_confirmed.setChecked(False)
5262
5407
  self.status_draft.setChecked(False)
@@ -5283,6 +5428,8 @@ class AdvancedFiltersDialog(QDialog):
5283
5428
  row_status.append('not_started')
5284
5429
  if self.status_edited.isChecked():
5285
5430
  row_status.append('edited')
5431
+ if self.status_pretranslated.isChecked():
5432
+ row_status.append('pretranslated')
5286
5433
  if self.status_translated.isChecked():
5287
5434
  row_status.append('translated')
5288
5435
  if self.status_confirmed.isChecked():
@@ -5647,11 +5794,14 @@ class PreTranslationWorker(QThread):
5647
5794
  custom_prompt = None
5648
5795
  if self.prompt_manager:
5649
5796
  try:
5797
+ # Get glossary terms for AI injection
5798
+ glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
5650
5799
  full_prompt = self.prompt_manager.build_final_prompt(
5651
5800
  source_text=segment.source,
5652
5801
  source_lang=source_lang,
5653
5802
  target_lang=target_lang,
5654
- mode="single"
5803
+ mode="single",
5804
+ glossary_terms=glossary_terms
5655
5805
  )
5656
5806
  # Extract just the instruction part (without the source text section)
5657
5807
  if "**SOURCE TEXT:**" in full_prompt:
@@ -5710,11 +5860,14 @@ class PreTranslationWorker(QThread):
5710
5860
  if self.prompt_manager and batch_segments:
5711
5861
  try:
5712
5862
  first_segment = batch_segments[0][1]
5863
+ # Get glossary terms for AI injection
5864
+ glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
5713
5865
  full_prompt = self.prompt_manager.build_final_prompt(
5714
5866
  source_text=first_segment.source,
5715
5867
  source_lang=source_lang,
5716
5868
  target_lang=target_lang,
5717
- mode="single"
5869
+ mode="single",
5870
+ glossary_terms=glossary_terms
5718
5871
  )
5719
5872
  # Extract just the instruction part
5720
5873
  if "**SOURCE TEXT:**" in full_prompt:
@@ -6161,6 +6314,10 @@ class SupervertalerQt(QMainWindow):
6161
6314
  # TM Metadata Manager - needed for TM list in Superlookup
6162
6315
  from modules.tm_metadata_manager import TMMetadataManager
6163
6316
  self.tm_metadata_mgr = TMMetadataManager(self.db_manager, self.log)
6317
+
6318
+ # Termbase Manager - needed for glossary AI injection
6319
+ from modules.termbase_manager import TermbaseManager
6320
+ self.termbase_mgr = TermbaseManager(self.db_manager, self.log)
6164
6321
 
6165
6322
  # Spellcheck Manager for target language spell checking
6166
6323
  self.spellcheck_manager = get_spellcheck_manager(str(self.user_data_path))
@@ -7499,13 +7656,30 @@ class SupervertalerQt(QMainWindow):
7499
7656
 
7500
7657
  # Bulk Operations submenu
7501
7658
  bulk_menu = edit_menu.addMenu("Bulk &Operations")
7502
-
7659
+
7503
7660
  confirm_selected_action = QAction("✅ &Confirm Selected Segments", self)
7504
7661
  confirm_selected_action.setShortcut("Ctrl+Shift+Return")
7505
7662
  confirm_selected_action.setToolTip("Confirm all selected segments (Ctrl+Shift+Enter)")
7506
7663
  confirm_selected_action.triggered.connect(self.confirm_selected_segments_from_menu)
7507
7664
  bulk_menu.addAction(confirm_selected_action)
7508
-
7665
+
7666
+ # Change Status submenu
7667
+ status_submenu = bulk_menu.addMenu("🏷️ Change &Status")
7668
+ user_statuses = [
7669
+ ("not_started", "❌ &Not started"),
7670
+ ("pretranslated", "🤖 &Pre-translated"),
7671
+ ("translated", "✏️ &Translated"),
7672
+ ("confirmed", "✔ &Confirmed"),
7673
+ ("tr_confirmed", "🌟 T&R confirmed"),
7674
+ ("proofread", "🟪 Proo&fread"),
7675
+ ("approved", "⭐ &Approved"),
7676
+ ("rejected", "🚫 Re&jected"),
7677
+ ]
7678
+ for status_key, label in user_statuses:
7679
+ action = QAction(label, self)
7680
+ action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s, from_menu=True))
7681
+ status_submenu.addAction(action)
7682
+
7509
7683
  clear_translations_action = QAction("🗑️ &Clear Translations", self)
7510
7684
  clear_translations_action.setToolTip("Clear translations for selected segments")
7511
7685
  clear_translations_action.triggered.connect(self.clear_selected_translations_from_menu)
@@ -12586,7 +12760,8 @@ class SupervertalerQt(QMainWindow):
12586
12760
  "💡 <b>Glossaries</b><br>"
12587
12761
  "• <b>Read</b> (green ✓): Glossary is used for terminology matching<br>"
12588
12762
  "• <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."
12763
+ "• <b>Priority</b>: Manually set 1-N (lower = higher priority). Priority #1 = Project Glossary.<br>"
12764
+ "• <b>AI</b> (orange ✓): Send glossary terms to LLM with every translation (increases prompt size)"
12590
12765
  )
12591
12766
  help_msg.setWordWrap(True)
12592
12767
  help_msg.setStyleSheet("background-color: #e3f2fd; padding: 8px; border-radius: 4px; color: #1976d2;")
@@ -12608,8 +12783,8 @@ class SupervertalerQt(QMainWindow):
12608
12783
  # Termbase list with table
12609
12784
  termbase_table = QTableWidget()
12610
12785
  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"])
12786
+ termbase_table.setColumnCount(8)
12787
+ termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority", "AI"])
12613
12788
  termbase_table.horizontalHeader().setStretchLastSection(False)
12614
12789
  termbase_table.setColumnWidth(0, 80) # Type (Project/Background)
12615
12790
  termbase_table.setColumnWidth(1, 180) # Name
@@ -12618,6 +12793,7 @@ class SupervertalerQt(QMainWindow):
12618
12793
  termbase_table.setColumnWidth(4, 50) # Read checkbox
12619
12794
  termbase_table.setColumnWidth(5, 50) # Write checkbox
12620
12795
  termbase_table.setColumnWidth(6, 60) # Priority
12796
+ termbase_table.setColumnWidth(7, 40) # AI checkbox
12621
12797
  termbase_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
12622
12798
  termbase_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
12623
12799
  termbase_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Disable inline editing
@@ -13311,7 +13487,43 @@ class SupervertalerQt(QMainWindow):
13311
13487
  priority_item.setToolTip("No priority - glossary not readable")
13312
13488
  priority_item.setFlags(priority_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
13313
13489
  termbase_table.setItem(row, 6, priority_item)
13314
-
13490
+
13491
+ # AI checkbox (purple/orange) - whether to inject terms into LLM prompts
13492
+ ai_enabled = termbase_mgr.get_termbase_ai_inject(tb['id'])
13493
+ ai_checkbox = OrangeCheckmarkCheckBox()
13494
+ ai_checkbox.setChecked(ai_enabled)
13495
+ ai_checkbox.setToolTip("AI: Send glossary terms to LLM with translation prompts")
13496
+
13497
+ def on_ai_toggle(checked, tb_id=tb['id'], tb_name=tb['name']):
13498
+ if checked:
13499
+ # Show warning when enabling
13500
+ from PyQt6.QtWidgets import QMessageBox
13501
+ msg = QMessageBox()
13502
+ msg.setWindowTitle("Enable AI Injection")
13503
+ msg.setText(f"Enable AI injection for '{tb_name}'?")
13504
+ msg.setInformativeText(
13505
+ "When enabled, ALL terms from this glossary will be sent to the LLM "
13506
+ "with every translation request.\n\n"
13507
+ "This helps the AI consistently use your preferred terminology "
13508
+ "throughout the translation.\n\n"
13509
+ "Recommended for small, curated glossaries (< 500 terms)."
13510
+ )
13511
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
13512
+ msg.setDefaultButton(QMessageBox.StandardButton.Yes)
13513
+ if msg.exec() != QMessageBox.StandardButton.Yes:
13514
+ # User cancelled - revert checkbox
13515
+ sender = termbase_table.cellWidget(termbase_table.currentRow(), 7)
13516
+ if sender:
13517
+ sender.blockSignals(True)
13518
+ sender.setChecked(False)
13519
+ sender.blockSignals(False)
13520
+ return
13521
+ termbase_mgr.set_termbase_ai_inject(tb_id, checked)
13522
+ self.log(f"{'✅ Enabled' if checked else '❌ Disabled'} AI injection for glossary: {tb_name}")
13523
+
13524
+ ai_checkbox.toggled.connect(on_ai_toggle)
13525
+ termbase_table.setCellWidget(row, 7, ai_checkbox)
13526
+
13315
13527
  # Update header checkbox states based on current selection
13316
13528
  tb_read_header_checkbox.blockSignals(True)
13317
13529
  tb_write_header_checkbox.blockSignals(True)
@@ -15408,7 +15620,57 @@ class SupervertalerQt(QMainWindow):
15408
15620
 
15409
15621
  model_group.setLayout(model_layout)
15410
15622
  layout.addWidget(model_group)
15411
-
15623
+
15624
+ # ========== SECTION 2b: Model Version Checker ==========
15625
+ version_check_group = QGroupBox("🔄 Model Version Checker")
15626
+ version_check_layout = QVBoxLayout()
15627
+
15628
+ version_check_info = QLabel(
15629
+ "Automatically check for new LLM models from OpenAI, Anthropic, and Google.\n"
15630
+ "Get notified when new models are available and easily add them to Supervertaler."
15631
+ )
15632
+ version_check_info.setWordWrap(True)
15633
+ version_check_layout.addWidget(version_check_info)
15634
+
15635
+ # Auto-check setting
15636
+ auto_check_models_cb = CheckmarkCheckBox("Enable automatic model checking (once per day on startup)")
15637
+ auto_check_models_cb.setChecked(general_settings.get('auto_check_models', True))
15638
+ auto_check_models_cb.setToolTip(
15639
+ "When enabled, Supervertaler will check for new models once per day when you start the application.\n"
15640
+ "You'll see a popup if new models are detected."
15641
+ )
15642
+ version_check_layout.addWidget(auto_check_models_cb)
15643
+
15644
+ # Manual check button
15645
+ manual_check_btn = QPushButton("🔍 Check for New Models Now")
15646
+ manual_check_btn.setToolTip("Manually check for new models from all providers")
15647
+ manual_check_btn.clicked.connect(lambda: self._check_for_new_models(force=True))
15648
+ version_check_layout.addWidget(manual_check_btn)
15649
+
15650
+ # Store reference for saving
15651
+ self.auto_check_models_cb = auto_check_models_cb
15652
+
15653
+ version_check_group.setLayout(version_check_layout)
15654
+ layout.addWidget(version_check_group)
15655
+
15656
+ # ========== SECTION 2c: API Keys ==========
15657
+ api_keys_group = QGroupBox("🔑 API Keys")
15658
+ api_keys_layout = QVBoxLayout()
15659
+
15660
+ api_keys_info = QLabel(
15661
+ f"Configure your API keys in:<br>"
15662
+ f"<code>{self.user_data_path / 'api_keys.txt'}</code>"
15663
+ )
15664
+ api_keys_info.setWordWrap(True)
15665
+ api_keys_layout.addWidget(api_keys_info)
15666
+
15667
+ open_keys_btn = QPushButton("📝 Open API Keys File")
15668
+ open_keys_btn.clicked.connect(lambda: self.open_api_keys_file())
15669
+ api_keys_layout.addWidget(open_keys_btn)
15670
+
15671
+ api_keys_group.setLayout(api_keys_layout)
15672
+ layout.addWidget(api_keys_group)
15673
+
15412
15674
  # ========== SECTION 3: Enable/Disable LLM Providers ==========
15413
15675
  provider_enable_group = QGroupBox("✅ Enable/Disable LLM Providers")
15414
15676
  provider_enable_layout = QVBoxLayout()
@@ -15688,58 +15950,7 @@ class SupervertalerQt(QMainWindow):
15688
15950
 
15689
15951
  behavior_group.setLayout(behavior_layout)
15690
15952
  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
-
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
15953
 
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
15954
  # ========== SAVE BUTTON ==========
15744
15955
  save_btn = QPushButton("💾 Save AI Settings")
15745
15956
  save_btn.setStyleSheet("font-weight: bold; padding: 8px; outline: none;")
@@ -19844,7 +20055,13 @@ class SupervertalerQt(QMainWindow):
19844
20055
  self.pagination_label = QLabel("Segments 1-50 of 0")
19845
20056
  self.pagination_label.setStyleSheet("color: #555;")
19846
20057
  pagination_layout.addWidget(self.pagination_label)
19847
-
20058
+
20059
+ # Tip label for Ctrl+, shortcut (subtle, helpful for new users)
20060
+ tip_label = QLabel("💡 Tip: Ctrl+, inserts the next tag from source")
20061
+ tip_label.setStyleSheet("color: #888; font-size: 9pt; margin-left: 20px;")
20062
+ tip_label.setToolTip("Select text first to wrap it with a tag pair (e.g., <b>selection</b>)")
20063
+ pagination_layout.addWidget(tip_label)
20064
+
19848
20065
  pagination_layout.addStretch()
19849
20066
 
19850
20067
  # Pagination controls (right side)
@@ -19972,6 +20189,7 @@ class SupervertalerQt(QMainWindow):
19972
20189
  from modules.statuses import get_status, STATUSES
19973
20190
  status_label = QLabel("Status:")
19974
20191
  tab_status_combo = QComboBox()
20192
+ tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
19975
20193
  for status_key in STATUSES.keys():
19976
20194
  definition = get_status(status_key)
19977
20195
  tab_status_combo.addItem(definition.label, status_key)
@@ -20870,6 +21088,7 @@ class SupervertalerQt(QMainWindow):
20870
21088
  from modules.statuses import STATUSES
20871
21089
  status_label = QLabel("Status:")
20872
21090
  tab_status_combo = QComboBox()
21091
+ tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
20873
21092
  for status_key in STATUSES.keys():
20874
21093
  definition = get_status(status_key)
20875
21094
  tab_status_combo.addItem(definition.label, status_key)
@@ -22319,27 +22538,31 @@ class SupervertalerQt(QMainWindow):
22319
22538
  def save_segment_to_activated_tms(self, source: str, target: str):
22320
22539
  """
22321
22540
  Save segment to all writable TMs for current project.
22322
-
22541
+
22323
22542
  Note: Uses get_writable_tm_ids() which checks the Write checkbox (read_only=0),
22324
22543
  NOT get_active_tm_ids() which checks the Read checkbox (is_active=1).
22325
-
22544
+
22545
+ Respects tm_save_mode setting:
22546
+ - 'latest': Overwrites existing entries with same source (keeps only newest translation)
22547
+ - 'all': Keeps all translations with different targets (default SQLite behavior)
22548
+
22326
22549
  Args:
22327
22550
  source: Source text
22328
22551
  target: Target text
22329
22552
  """
22330
22553
  if not self.current_project:
22331
22554
  return
22332
-
22555
+
22333
22556
  if not hasattr(self.current_project, 'source_lang') or not hasattr(self.current_project, 'target_lang'):
22334
22557
  return
22335
-
22558
+
22336
22559
  # Get WRITABLE TM IDs for this project (Write checkbox enabled)
22337
22560
  tm_ids = []
22338
-
22561
+
22339
22562
  if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr:
22340
22563
  if hasattr(self, 'current_project') and self.current_project:
22341
22564
  project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
22342
-
22565
+
22343
22566
  if project_id:
22344
22567
  # Use get_writable_tm_ids() to find TMs with Write enabled
22345
22568
  tm_ids = self.tm_metadata_mgr.get_writable_tm_ids(project_id)
@@ -22349,13 +22572,16 @@ class SupervertalerQt(QMainWindow):
22349
22572
  self.log(f"⚠️ Cannot save to TM: No current project loaded!")
22350
22573
  else:
22351
22574
  self.log(f"⚠️ Cannot save to TM: TM metadata manager not available!")
22352
-
22575
+
22353
22576
  # If no TMs have Write enabled, skip saving
22354
22577
  if not tm_ids:
22355
22578
  self.log("⚠️ No TMs with Write enabled - segment not saved to TM.")
22356
22579
  self.log(f" - To fix: Go to Resources > Translation Memories > TM List and enable the Write checkbox")
22357
22580
  return
22358
-
22581
+
22582
+ # Check TM save mode: 'latest' = overwrite, 'all' = keep all variants
22583
+ overwrite_mode = getattr(self, 'tm_save_mode', 'latest') == 'latest'
22584
+
22359
22585
  # Save to each writable TM
22360
22586
  saved_count = 0
22361
22587
  for tm_id in tm_ids:
@@ -22365,14 +22591,16 @@ class SupervertalerQt(QMainWindow):
22365
22591
  target=target,
22366
22592
  source_lang=self.current_project.source_lang,
22367
22593
  target_lang=self.current_project.target_lang,
22368
- tm_id=tm_id
22594
+ tm_id=tm_id,
22595
+ overwrite=overwrite_mode
22369
22596
  )
22370
22597
  saved_count += 1
22371
22598
  except Exception as e:
22372
22599
  self.log(f"⚠️ Could not save to TM '{tm_id}': {e}")
22373
-
22600
+
22374
22601
  if saved_count > 0:
22375
- msg = f"💾 Saved segment to {saved_count} TM(s)"
22602
+ mode_note = " (overwrite)" if overwrite_mode else ""
22603
+ msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
22376
22604
  self._queue_tm_save_log(msg)
22377
22605
  # Invalidate cache so prefetched segments get fresh TM matches
22378
22606
  self.invalidate_translation_cache()
@@ -31385,9 +31613,14 @@ class SupervertalerQt(QMainWindow):
31385
31613
 
31386
31614
  # Get source text
31387
31615
  source_text = current_segment.source
31388
-
31616
+
31617
+ # Get glossary terms for AI injection
31618
+ glossary_terms = self.get_ai_inject_glossary_terms()
31619
+
31389
31620
  # Build combined prompt
31390
- combined = self.prompt_manager_qt.build_final_prompt(source_text, source_lang, target_lang)
31621
+ combined = self.prompt_manager_qt.build_final_prompt(
31622
+ source_text, source_lang, target_lang, glossary_terms=glossary_terms
31623
+ )
31391
31624
 
31392
31625
  # Check for figure/image context
31393
31626
  figure_info = ""
@@ -31414,11 +31647,14 @@ class SupervertalerQt(QMainWindow):
31414
31647
  composition_parts.append(f"📏 Total prompt: {len(combined):,} characters")
31415
31648
 
31416
31649
  if self.prompt_manager_qt.library.active_primary_prompt:
31417
- composition_parts.append(f"✓ Primary prompt attached")
31650
+ composition_parts.append(f"✓ Custom prompt attached")
31418
31651
 
31419
31652
  if self.prompt_manager_qt.library.attached_prompts:
31420
31653
  composition_parts.append(f"✓ {len(self.prompt_manager_qt.library.attached_prompts)} additional prompt(s) attached")
31421
-
31654
+
31655
+ if glossary_terms:
31656
+ composition_parts.append(f"📚 {len(glossary_terms)} glossary term(s) injected")
31657
+
31422
31658
  if figure_info:
31423
31659
  composition_parts.append(figure_info)
31424
31660
 
@@ -31453,12 +31689,59 @@ class SupervertalerQt(QMainWindow):
31453
31689
  image_notice.setStyleSheet("padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 4px solid #ff9800;")
31454
31690
  layout.addWidget(image_notice)
31455
31691
 
31456
- # Text editor for preview
31692
+ # Text editor for preview with syntax highlighting
31457
31693
  text_edit = QTextEdit()
31458
- text_edit.setPlainText(combined)
31459
31694
  text_edit.setReadOnly(True)
31460
31695
  text_edit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
31461
31696
  text_edit.setStyleSheet("font-family: 'Consolas', 'Courier New', monospace; font-size: 9pt;")
31697
+
31698
+ # Format the prompt with color highlighting
31699
+ import html
31700
+ import re
31701
+
31702
+ # Escape HTML entities first
31703
+ formatted_html = html.escape(combined)
31704
+
31705
+ # Replace newlines with <br> for HTML
31706
+ formatted_html = formatted_html.replace('\n', '<br>')
31707
+
31708
+ # Make "# SYSTEM PROMPT" bold and red
31709
+ formatted_html = re.sub(
31710
+ r'(#\s*SYSTEM\s*PROMPT)',
31711
+ r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
31712
+ formatted_html,
31713
+ flags=re.IGNORECASE
31714
+ )
31715
+
31716
+ # Make "# CUSTOM PROMPT" bold and red
31717
+ formatted_html = re.sub(
31718
+ r'(#\s*CUSTOM\s*PROMPT)',
31719
+ r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
31720
+ formatted_html,
31721
+ flags=re.IGNORECASE
31722
+ )
31723
+
31724
+ # Make "# GLOSSARY" bold and orange
31725
+ formatted_html = re.sub(
31726
+ r'(#\s*GLOSSARY)',
31727
+ r'<span style="color: #FF9800; font-weight: bold; font-size: 11pt;">\1</span>',
31728
+ formatted_html,
31729
+ flags=re.IGNORECASE
31730
+ )
31731
+
31732
+ # Make source text section blue (pattern: "XX text:<br>..." until double line break or # header)
31733
+ # Match language code + " text:" followed by content until "# " or end
31734
+ formatted_html = re.sub(
31735
+ r'(\w{2,5}\s+text:)(<br>)(.*?)(<br><br>(?:#|\*\*YOUR TRANSLATION)|$)',
31736
+ r'<span style="color: #1565c0; font-weight: bold;">\1</span>\2<span style="color: #1565c0;">\3</span>\4',
31737
+ formatted_html,
31738
+ flags=re.DOTALL
31739
+ )
31740
+
31741
+ # Wrap in pre-like styling div
31742
+ formatted_html = f'<div style="font-family: Consolas, Courier New, monospace; white-space: pre-wrap;">{formatted_html}</div>'
31743
+
31744
+ text_edit.setHtml(formatted_html)
31462
31745
  layout.addWidget(text_edit, 1)
31463
31746
 
31464
31747
  # Close button
@@ -31476,25 +31759,43 @@ class SupervertalerQt(QMainWindow):
31476
31759
  def show_grid_context_menu(self, position):
31477
31760
  """Show context menu for grid view with bulk operations"""
31478
31761
  selected_segments = self.get_selected_segments_from_grid()
31479
-
31762
+
31480
31763
  if not selected_segments:
31481
31764
  return
31482
-
31765
+
31483
31766
  menu = QMenu(self)
31484
-
31767
+
31485
31768
  # Confirm selected segments action
31486
31769
  if len(selected_segments) >= 1:
31487
31770
  confirm_action = menu.addAction(f"✅ Confirm {len(selected_segments)} Segment(s)")
31488
31771
  confirm_action.setToolTip(f"Confirm {len(selected_segments)} selected segment(s)")
31489
31772
  confirm_action.triggered.connect(self.confirm_selected_segments)
31490
-
31773
+
31774
+ # Change Status submenu
31775
+ from modules.statuses import get_status
31776
+ status_menu = menu.addMenu(f"🏷️ Change Status ({len(selected_segments)})")
31777
+ # Common user-settable statuses (excluding TM-specific ones like pm, cm, tm_100, etc.)
31778
+ user_statuses = [
31779
+ ("not_started", "❌ Not started"),
31780
+ ("pretranslated", "🤖 Pre-translated"),
31781
+ ("translated", "✏️ Translated"),
31782
+ ("confirmed", "✔ Confirmed"),
31783
+ ("tr_confirmed", "🌟 TR confirmed"),
31784
+ ("proofread", "🟪 Proofread"),
31785
+ ("approved", "⭐ Approved"),
31786
+ ("rejected", "🚫 Rejected"),
31787
+ ]
31788
+ for status_key, label in user_statuses:
31789
+ action = status_menu.addAction(label)
31790
+ action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s))
31791
+
31491
31792
  # Clear translations action
31492
31793
  clear_action = menu.addAction("🗑️ Clear Translations")
31493
31794
  clear_action.setToolTip(f"Clear translations for {len(selected_segments)} selected segment(s)")
31494
31795
  clear_action.triggered.connect(lambda: self.clear_selected_translations(selected_segments, 'grid'))
31495
-
31796
+
31496
31797
  menu.addSeparator()
31497
-
31798
+
31498
31799
  # Clear proofreading notes (if any selected segment has proofreading notes)
31499
31800
  has_proofreading = any(seg.notes and "⚠️ PROOFREAD:" in seg.notes for seg in selected_segments)
31500
31801
  if has_proofreading:
@@ -31502,11 +31803,11 @@ class SupervertalerQt(QMainWindow):
31502
31803
  clear_proofread_action.setToolTip("Remove proofreading issues from selected segment(s)")
31503
31804
  clear_proofread_action.triggered.connect(lambda: self._clear_proofreading_from_selected(selected_segments))
31504
31805
  menu.addSeparator()
31505
-
31806
+
31506
31807
  # Select all action
31507
31808
  select_all_action = menu.addAction("📋 Select All (Ctrl+A)")
31508
31809
  select_all_action.triggered.connect(lambda: self.table.selectAll())
31509
-
31810
+
31510
31811
  menu.exec(self.table.viewport().mapToGlobal(position))
31511
31812
 
31512
31813
  def _clear_proofreading_from_selected(self, segments):
@@ -35182,7 +35483,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35182
35483
  primary_prompt_text = f"✅ {prompt_path}"
35183
35484
  attached_count = len(library.attached_prompt_paths) if library.attached_prompt_paths else 0
35184
35485
 
35185
- ai_layout.addWidget(QLabel(f"<b>Primary Prompt:</b> {primary_prompt_text}"))
35486
+ ai_layout.addWidget(QLabel(f"<b>Custom Prompt:</b> {primary_prompt_text}"))
35186
35487
  if attached_count > 0:
35187
35488
  ai_layout.addWidget(QLabel(f"<b>Attached Prompts:</b> {attached_count}"))
35188
35489
 
@@ -38331,17 +38632,75 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
38331
38632
  if not self.current_project:
38332
38633
  QMessageBox.information(self, "Not Available", "Please load a project first.")
38333
38634
  return
38334
-
38635
+
38335
38636
  if not hasattr(self, 'table') or not self.table:
38336
38637
  QMessageBox.information(self, "Not Available", "Grid view is not available.")
38337
38638
  return
38338
-
38639
+
38339
38640
  selected_segments = self.get_selected_segments_from_grid()
38340
38641
  if selected_segments:
38341
38642
  self.confirm_selected_segments()
38342
38643
  else:
38343
38644
  QMessageBox.information(self, "No Selection", "Please select one or more segments to confirm.")
38344
-
38645
+
38646
+ def change_status_selected(self, new_status: str, from_menu: bool = False):
38647
+ """Change status of all selected segments to the specified status.
38648
+
38649
+ Args:
38650
+ new_status: The status key to set (e.g., 'translated', 'pretranslated')
38651
+ from_menu: If True, show message boxes for errors (called from menu)
38652
+ """
38653
+ if not self.current_project:
38654
+ if from_menu:
38655
+ QMessageBox.information(self, "Not Available", "Please load a project first.")
38656
+ else:
38657
+ self.log("⚠️ No project loaded")
38658
+ return
38659
+
38660
+ if not hasattr(self, 'table') or not self.table:
38661
+ if from_menu:
38662
+ QMessageBox.information(self, "Not Available", "Grid view is not available.")
38663
+ return
38664
+
38665
+ selected_segments = self.get_selected_segments_from_grid()
38666
+
38667
+ if not selected_segments:
38668
+ if from_menu:
38669
+ QMessageBox.information(self, "No Selection", "Please select one or more segments to change.")
38670
+ else:
38671
+ self.log("⚠️ No segments selected")
38672
+ return
38673
+
38674
+ # Sync all target text from grid widgets first
38675
+ self._sync_grid_targets_to_segments(selected_segments)
38676
+
38677
+ # Get status definition for logging
38678
+ from modules.statuses import get_status
38679
+ status_def = get_status(new_status)
38680
+
38681
+ changed_count = 0
38682
+ for segment in selected_segments:
38683
+ # Skip if already has this status
38684
+ if segment.status == new_status:
38685
+ continue
38686
+
38687
+ segment.status = new_status
38688
+ changed_count += 1
38689
+
38690
+ # Update grid status icon
38691
+ row = self._find_row_for_segment(segment.id)
38692
+ if row >= 0:
38693
+ self.update_status_icon(row, new_status)
38694
+
38695
+ if changed_count > 0:
38696
+ self.project_modified = True
38697
+ self.update_window_title()
38698
+ self.log(f"✅ Changed {changed_count} segment(s) to '{status_def.label}'")
38699
+ # Update status bar progress stats
38700
+ self.update_progress_stats()
38701
+ else:
38702
+ self.log(f"ℹ️ All {len(selected_segments)} selected segment(s) already have status '{status_def.label}'")
38703
+
38345
38704
  def _sync_grid_targets_to_segments(self, segments):
38346
38705
  """Sync target text from grid widgets to segment objects.
38347
38706
 
@@ -40210,13 +40569,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
40210
40569
  except Exception as e:
40211
40570
  self.log(f"⚠ Could not add surrounding segments: {e}")
40212
40571
 
40572
+ # Get glossary terms for AI injection
40573
+ glossary_terms = self.get_ai_inject_glossary_terms()
40574
+
40213
40575
  custom_prompt = self.prompt_manager_qt.build_final_prompt(
40214
40576
  source_text=segment.source,
40215
40577
  source_lang=self.current_project.source_lang,
40216
40578
  target_lang=self.current_project.target_lang,
40217
- mode="single"
40579
+ mode="single",
40580
+ glossary_terms=glossary_terms
40218
40581
  )
40219
-
40582
+
40220
40583
  # Add surrounding context before the translation delimiter
40221
40584
  if surrounding_context:
40222
40585
  # Insert before the "YOUR TRANSLATION" delimiter
@@ -41580,7 +41943,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
41580
41943
 
41581
41944
  if match_pct >= 75: # Accept matches 75% and above
41582
41945
  segment.target = tm_match
41583
- segment.status = "translated" if match_pct == 100 else "pre-translated"
41946
+ segment.status = "pretranslated"
41584
41947
  translated_count += 1
41585
41948
 
41586
41949
  # Update grid immediately
@@ -41694,7 +42057,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
41694
42057
  if tm_match and len(tm_match) > 0:
41695
42058
  translation = tm_match[0]['target']
41696
42059
  segment.target = translation
41697
- segment.status = 'pre-translated'
42060
+ segment.status = 'pretranslated'
41698
42061
  translated_count += 1
41699
42062
  self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}... (TM 100%)")
41700
42063
  else:
@@ -41741,7 +42104,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
41741
42104
 
41742
42105
  if translation and not translation.startswith('['): # Skip error messages
41743
42106
  segment.target = translation
41744
- segment.status = 'pre-translated'
42107
+ segment.status = 'pretranslated'
41745
42108
  translated_count += 1
41746
42109
  self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}...")
41747
42110
  else:
@@ -41767,11 +42130,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
41767
42130
  if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt and batch_segments:
41768
42131
  try:
41769
42132
  first_segment = batch_segments[0][1]
42133
+ # Get glossary terms for AI injection
42134
+ glossary_terms = self.get_ai_inject_glossary_terms()
41770
42135
  base_prompt = self.prompt_manager_qt.build_final_prompt(
41771
42136
  source_text=first_segment.source,
41772
42137
  source_lang=source_lang,
41773
42138
  target_lang=target_lang,
41774
- mode="single"
42139
+ mode="single",
42140
+ glossary_terms=glossary_terms
41775
42141
  )
41776
42142
  if "**SOURCE TEXT:**" in base_prompt:
41777
42143
  base_prompt = base_prompt.split("**SOURCE TEXT:**")[0].strip()
@@ -42199,11 +42565,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42199
42565
  # Access parent through closure
42200
42566
  parent = self
42201
42567
  if hasattr(parent, 'prompt_manager_qt') and parent.prompt_manager_qt:
42568
+ # Get glossary terms for AI injection
42569
+ glossary_terms = parent.get_ai_inject_glossary_terms() if hasattr(parent, 'get_ai_inject_glossary_terms') else []
42202
42570
  custom_prompt = parent.prompt_manager_qt.build_final_prompt(
42203
42571
  source_text=source_text,
42204
42572
  source_lang=source_lang,
42205
42573
  target_lang=target_lang,
42206
- mode="single"
42574
+ mode="single",
42575
+ glossary_terms=glossary_terms
42207
42576
  )
42208
42577
  except Exception as e:
42209
42578
  self.log(f"⚠ Could not build LLM prompt from manager: {e}")
@@ -42686,7 +43055,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42686
43055
  api_keys['google'] = api_keys['gemini']
42687
43056
 
42688
43057
  return api_keys
42689
-
43058
+
43059
+ def get_ai_inject_glossary_terms(self) -> list:
43060
+ """Get glossary terms from AI-inject-enabled termbases for the current project.
43061
+
43062
+ Returns:
43063
+ List of term dictionaries with source_term, target_term, forbidden keys
43064
+ """
43065
+ if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
43066
+ return []
43067
+
43068
+ project_id = None
43069
+ if hasattr(self, 'current_project') and self.current_project:
43070
+ project_id = getattr(self.current_project, 'id', None)
43071
+
43072
+ return self.termbase_mgr.get_ai_inject_terms(project_id)
43073
+
42690
43074
  def ensure_example_api_keys(self):
42691
43075
  """Create example API keys file on first launch for new users"""
42692
43076
  example_file = self.user_data_path / "api_keys.example.txt"
@@ -48001,7 +48385,87 @@ class BlueCheckmarkCheckBox(QCheckBox):
48001
48385
 
48002
48386
  painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
48003
48387
  painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
48004
-
48388
+
48389
+ painter.end()
48390
+
48391
+
48392
+ class OrangeCheckmarkCheckBox(QCheckBox):
48393
+ """Custom checkbox with orange background and white checkmark when checked (for AI injection)"""
48394
+
48395
+ def __init__(self, text="", parent=None):
48396
+ super().__init__(text, parent)
48397
+ self.setCheckable(True)
48398
+ self.setEnabled(True)
48399
+ self.setStyleSheet("""
48400
+ QCheckBox {
48401
+ font-size: 9pt;
48402
+ spacing: 6px;
48403
+ }
48404
+ QCheckBox::indicator {
48405
+ width: 16px;
48406
+ height: 16px;
48407
+ border: 2px solid #999;
48408
+ border-radius: 3px;
48409
+ background-color: white;
48410
+ }
48411
+ QCheckBox::indicator:checked {
48412
+ background-color: #FF9800;
48413
+ border-color: #FF9800;
48414
+ }
48415
+ QCheckBox::indicator:hover {
48416
+ border-color: #666;
48417
+ }
48418
+ QCheckBox::indicator:checked:hover {
48419
+ background-color: #F57C00;
48420
+ border-color: #F57C00;
48421
+ }
48422
+ """)
48423
+
48424
+ def paintEvent(self, event):
48425
+ """Override paint event to draw white checkmark when checked"""
48426
+ super().paintEvent(event)
48427
+
48428
+ if self.isChecked():
48429
+ from PyQt6.QtWidgets import QStyleOptionButton
48430
+ from PyQt6.QtGui import QPainter, QPen, QColor
48431
+ from PyQt6.QtCore import QPointF
48432
+
48433
+ opt = QStyleOptionButton()
48434
+ self.initStyleOption(opt)
48435
+ indicator_rect = self.style().subElementRect(
48436
+ self.style().SubElement.SE_CheckBoxIndicator,
48437
+ opt,
48438
+ self
48439
+ )
48440
+
48441
+ if indicator_rect.isValid():
48442
+ painter = QPainter(self)
48443
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
48444
+ pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
48445
+ painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
48446
+ painter.setBrush(QColor(255, 255, 255))
48447
+
48448
+ x = indicator_rect.x()
48449
+ y = indicator_rect.y()
48450
+ w = indicator_rect.width()
48451
+ h = indicator_rect.height()
48452
+
48453
+ padding = min(w, h) * 0.15
48454
+ x += padding
48455
+ y += padding
48456
+ w -= padding * 2
48457
+ h -= padding * 2
48458
+
48459
+ check_x1 = x + w * 0.10
48460
+ check_y1 = y + h * 0.50
48461
+ check_x2 = x + w * 0.35
48462
+ check_y2 = y + h * 0.70
48463
+ check_x3 = x + w * 0.90
48464
+ check_y3 = y + h * 0.25
48465
+
48466
+ painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
48467
+ painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
48468
+
48005
48469
  painter.end()
48006
48470
 
48007
48471