supervertaler 1.9.172__py3-none-any.whl → 1.9.174__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of supervertaler might be problematic. Click here for more details.
- Supervertaler.py +434 -162
- modules/database_manager.py +74 -19
- modules/translation_memory.py +2 -2
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.174.dist-info}/METADATA +16 -7
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.174.dist-info}/RECORD +9 -9
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.174.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.174.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.174.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.172.dist-info → supervertaler-1.9.174.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -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
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
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
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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
|
-
|
|
3030
|
-
|
|
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
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
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
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
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():
|
|
@@ -5598,7 +5745,7 @@ class PreTranslationWorker(QThread):
|
|
|
5598
5745
|
match = matches[0]
|
|
5599
5746
|
match_pct = match.get('match_pct', 0)
|
|
5600
5747
|
print(f"🔍 TM PRE-TRANSLATE: Best match pct: {match_pct}")
|
|
5601
|
-
if match_pct >=
|
|
5748
|
+
if match_pct >= 75: # Accept matches 75% and above
|
|
5602
5749
|
return match.get('target', '')
|
|
5603
5750
|
return None
|
|
5604
5751
|
except Exception as e:
|
|
@@ -7499,13 +7646,30 @@ class SupervertalerQt(QMainWindow):
|
|
|
7499
7646
|
|
|
7500
7647
|
# Bulk Operations submenu
|
|
7501
7648
|
bulk_menu = edit_menu.addMenu("Bulk &Operations")
|
|
7502
|
-
|
|
7649
|
+
|
|
7503
7650
|
confirm_selected_action = QAction("✅ &Confirm Selected Segments", self)
|
|
7504
7651
|
confirm_selected_action.setShortcut("Ctrl+Shift+Return")
|
|
7505
7652
|
confirm_selected_action.setToolTip("Confirm all selected segments (Ctrl+Shift+Enter)")
|
|
7506
7653
|
confirm_selected_action.triggered.connect(self.confirm_selected_segments_from_menu)
|
|
7507
7654
|
bulk_menu.addAction(confirm_selected_action)
|
|
7508
|
-
|
|
7655
|
+
|
|
7656
|
+
# Change Status submenu
|
|
7657
|
+
status_submenu = bulk_menu.addMenu("🏷️ Change &Status")
|
|
7658
|
+
user_statuses = [
|
|
7659
|
+
("not_started", "❌ &Not started"),
|
|
7660
|
+
("pretranslated", "🤖 &Pre-translated"),
|
|
7661
|
+
("translated", "✏️ &Translated"),
|
|
7662
|
+
("confirmed", "✔ &Confirmed"),
|
|
7663
|
+
("tr_confirmed", "🌟 T&R confirmed"),
|
|
7664
|
+
("proofread", "🟪 Proo&fread"),
|
|
7665
|
+
("approved", "⭐ &Approved"),
|
|
7666
|
+
("rejected", "🚫 Re&jected"),
|
|
7667
|
+
]
|
|
7668
|
+
for status_key, label in user_statuses:
|
|
7669
|
+
action = QAction(label, self)
|
|
7670
|
+
action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s, from_menu=True))
|
|
7671
|
+
status_submenu.addAction(action)
|
|
7672
|
+
|
|
7509
7673
|
clear_translations_action = QAction("🗑️ &Clear Translations", self)
|
|
7510
7674
|
clear_translations_action.setToolTip("Clear translations for selected segments")
|
|
7511
7675
|
clear_translations_action.triggered.connect(self.clear_selected_translations_from_menu)
|
|
@@ -15408,7 +15572,57 @@ class SupervertalerQt(QMainWindow):
|
|
|
15408
15572
|
|
|
15409
15573
|
model_group.setLayout(model_layout)
|
|
15410
15574
|
layout.addWidget(model_group)
|
|
15411
|
-
|
|
15575
|
+
|
|
15576
|
+
# ========== SECTION 2b: Model Version Checker ==========
|
|
15577
|
+
version_check_group = QGroupBox("🔄 Model Version Checker")
|
|
15578
|
+
version_check_layout = QVBoxLayout()
|
|
15579
|
+
|
|
15580
|
+
version_check_info = QLabel(
|
|
15581
|
+
"Automatically check for new LLM models from OpenAI, Anthropic, and Google.\n"
|
|
15582
|
+
"Get notified when new models are available and easily add them to Supervertaler."
|
|
15583
|
+
)
|
|
15584
|
+
version_check_info.setWordWrap(True)
|
|
15585
|
+
version_check_layout.addWidget(version_check_info)
|
|
15586
|
+
|
|
15587
|
+
# Auto-check setting
|
|
15588
|
+
auto_check_models_cb = CheckmarkCheckBox("Enable automatic model checking (once per day on startup)")
|
|
15589
|
+
auto_check_models_cb.setChecked(general_settings.get('auto_check_models', True))
|
|
15590
|
+
auto_check_models_cb.setToolTip(
|
|
15591
|
+
"When enabled, Supervertaler will check for new models once per day when you start the application.\n"
|
|
15592
|
+
"You'll see a popup if new models are detected."
|
|
15593
|
+
)
|
|
15594
|
+
version_check_layout.addWidget(auto_check_models_cb)
|
|
15595
|
+
|
|
15596
|
+
# Manual check button
|
|
15597
|
+
manual_check_btn = QPushButton("🔍 Check for New Models Now")
|
|
15598
|
+
manual_check_btn.setToolTip("Manually check for new models from all providers")
|
|
15599
|
+
manual_check_btn.clicked.connect(lambda: self._check_for_new_models(force=True))
|
|
15600
|
+
version_check_layout.addWidget(manual_check_btn)
|
|
15601
|
+
|
|
15602
|
+
# Store reference for saving
|
|
15603
|
+
self.auto_check_models_cb = auto_check_models_cb
|
|
15604
|
+
|
|
15605
|
+
version_check_group.setLayout(version_check_layout)
|
|
15606
|
+
layout.addWidget(version_check_group)
|
|
15607
|
+
|
|
15608
|
+
# ========== SECTION 2c: API Keys ==========
|
|
15609
|
+
api_keys_group = QGroupBox("🔑 API Keys")
|
|
15610
|
+
api_keys_layout = QVBoxLayout()
|
|
15611
|
+
|
|
15612
|
+
api_keys_info = QLabel(
|
|
15613
|
+
f"Configure your API keys in:<br>"
|
|
15614
|
+
f"<code>{self.user_data_path / 'api_keys.txt'}</code>"
|
|
15615
|
+
)
|
|
15616
|
+
api_keys_info.setWordWrap(True)
|
|
15617
|
+
api_keys_layout.addWidget(api_keys_info)
|
|
15618
|
+
|
|
15619
|
+
open_keys_btn = QPushButton("📝 Open API Keys File")
|
|
15620
|
+
open_keys_btn.clicked.connect(lambda: self.open_api_keys_file())
|
|
15621
|
+
api_keys_layout.addWidget(open_keys_btn)
|
|
15622
|
+
|
|
15623
|
+
api_keys_group.setLayout(api_keys_layout)
|
|
15624
|
+
layout.addWidget(api_keys_group)
|
|
15625
|
+
|
|
15412
15626
|
# ========== SECTION 3: Enable/Disable LLM Providers ==========
|
|
15413
15627
|
provider_enable_group = QGroupBox("✅ Enable/Disable LLM Providers")
|
|
15414
15628
|
provider_enable_layout = QVBoxLayout()
|
|
@@ -15688,58 +15902,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
15688
15902
|
|
|
15689
15903
|
behavior_group.setLayout(behavior_layout)
|
|
15690
15904
|
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
15905
|
|
|
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
15906
|
# ========== SAVE BUTTON ==========
|
|
15744
15907
|
save_btn = QPushButton("💾 Save AI Settings")
|
|
15745
15908
|
save_btn.setStyleSheet("font-weight: bold; padding: 8px; outline: none;")
|
|
@@ -19844,7 +20007,13 @@ class SupervertalerQt(QMainWindow):
|
|
|
19844
20007
|
self.pagination_label = QLabel("Segments 1-50 of 0")
|
|
19845
20008
|
self.pagination_label.setStyleSheet("color: #555;")
|
|
19846
20009
|
pagination_layout.addWidget(self.pagination_label)
|
|
19847
|
-
|
|
20010
|
+
|
|
20011
|
+
# Tip label for Ctrl+, shortcut (subtle, helpful for new users)
|
|
20012
|
+
tip_label = QLabel("💡 Tip: Ctrl+, inserts the next tag from source")
|
|
20013
|
+
tip_label.setStyleSheet("color: #888; font-size: 9pt; margin-left: 20px;")
|
|
20014
|
+
tip_label.setToolTip("Select text first to wrap it with a tag pair (e.g., <b>selection</b>)")
|
|
20015
|
+
pagination_layout.addWidget(tip_label)
|
|
20016
|
+
|
|
19848
20017
|
pagination_layout.addStretch()
|
|
19849
20018
|
|
|
19850
20019
|
# Pagination controls (right side)
|
|
@@ -19972,6 +20141,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
19972
20141
|
from modules.statuses import get_status, STATUSES
|
|
19973
20142
|
status_label = QLabel("Status:")
|
|
19974
20143
|
tab_status_combo = QComboBox()
|
|
20144
|
+
tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
|
|
19975
20145
|
for status_key in STATUSES.keys():
|
|
19976
20146
|
definition = get_status(status_key)
|
|
19977
20147
|
tab_status_combo.addItem(definition.label, status_key)
|
|
@@ -20870,6 +21040,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
20870
21040
|
from modules.statuses import STATUSES
|
|
20871
21041
|
status_label = QLabel("Status:")
|
|
20872
21042
|
tab_status_combo = QComboBox()
|
|
21043
|
+
tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
|
|
20873
21044
|
for status_key in STATUSES.keys():
|
|
20874
21045
|
definition = get_status(status_key)
|
|
20875
21046
|
tab_status_combo.addItem(definition.label, status_key)
|
|
@@ -22319,27 +22490,31 @@ class SupervertalerQt(QMainWindow):
|
|
|
22319
22490
|
def save_segment_to_activated_tms(self, source: str, target: str):
|
|
22320
22491
|
"""
|
|
22321
22492
|
Save segment to all writable TMs for current project.
|
|
22322
|
-
|
|
22493
|
+
|
|
22323
22494
|
Note: Uses get_writable_tm_ids() which checks the Write checkbox (read_only=0),
|
|
22324
22495
|
NOT get_active_tm_ids() which checks the Read checkbox (is_active=1).
|
|
22325
|
-
|
|
22496
|
+
|
|
22497
|
+
Respects tm_save_mode setting:
|
|
22498
|
+
- 'latest': Overwrites existing entries with same source (keeps only newest translation)
|
|
22499
|
+
- 'all': Keeps all translations with different targets (default SQLite behavior)
|
|
22500
|
+
|
|
22326
22501
|
Args:
|
|
22327
22502
|
source: Source text
|
|
22328
22503
|
target: Target text
|
|
22329
22504
|
"""
|
|
22330
22505
|
if not self.current_project:
|
|
22331
22506
|
return
|
|
22332
|
-
|
|
22507
|
+
|
|
22333
22508
|
if not hasattr(self.current_project, 'source_lang') or not hasattr(self.current_project, 'target_lang'):
|
|
22334
22509
|
return
|
|
22335
|
-
|
|
22510
|
+
|
|
22336
22511
|
# Get WRITABLE TM IDs for this project (Write checkbox enabled)
|
|
22337
22512
|
tm_ids = []
|
|
22338
|
-
|
|
22513
|
+
|
|
22339
22514
|
if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr:
|
|
22340
22515
|
if hasattr(self, 'current_project') and self.current_project:
|
|
22341
22516
|
project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
|
|
22342
|
-
|
|
22517
|
+
|
|
22343
22518
|
if project_id:
|
|
22344
22519
|
# Use get_writable_tm_ids() to find TMs with Write enabled
|
|
22345
22520
|
tm_ids = self.tm_metadata_mgr.get_writable_tm_ids(project_id)
|
|
@@ -22349,13 +22524,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
22349
22524
|
self.log(f"⚠️ Cannot save to TM: No current project loaded!")
|
|
22350
22525
|
else:
|
|
22351
22526
|
self.log(f"⚠️ Cannot save to TM: TM metadata manager not available!")
|
|
22352
|
-
|
|
22527
|
+
|
|
22353
22528
|
# If no TMs have Write enabled, skip saving
|
|
22354
22529
|
if not tm_ids:
|
|
22355
22530
|
self.log("⚠️ No TMs with Write enabled - segment not saved to TM.")
|
|
22356
22531
|
self.log(f" - To fix: Go to Resources > Translation Memories > TM List and enable the Write checkbox")
|
|
22357
22532
|
return
|
|
22358
|
-
|
|
22533
|
+
|
|
22534
|
+
# Check TM save mode: 'latest' = overwrite, 'all' = keep all variants
|
|
22535
|
+
overwrite_mode = getattr(self, 'tm_save_mode', 'latest') == 'latest'
|
|
22536
|
+
|
|
22359
22537
|
# Save to each writable TM
|
|
22360
22538
|
saved_count = 0
|
|
22361
22539
|
for tm_id in tm_ids:
|
|
@@ -22365,14 +22543,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
22365
22543
|
target=target,
|
|
22366
22544
|
source_lang=self.current_project.source_lang,
|
|
22367
22545
|
target_lang=self.current_project.target_lang,
|
|
22368
|
-
tm_id=tm_id
|
|
22546
|
+
tm_id=tm_id,
|
|
22547
|
+
overwrite=overwrite_mode
|
|
22369
22548
|
)
|
|
22370
22549
|
saved_count += 1
|
|
22371
22550
|
except Exception as e:
|
|
22372
22551
|
self.log(f"⚠️ Could not save to TM '{tm_id}': {e}")
|
|
22373
|
-
|
|
22552
|
+
|
|
22374
22553
|
if saved_count > 0:
|
|
22375
|
-
|
|
22554
|
+
mode_note = " (overwrite)" if overwrite_mode else ""
|
|
22555
|
+
msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
|
|
22376
22556
|
self._queue_tm_save_log(msg)
|
|
22377
22557
|
# Invalidate cache so prefetched segments get fresh TM matches
|
|
22378
22558
|
self.invalidate_translation_cache()
|
|
@@ -31476,25 +31656,43 @@ class SupervertalerQt(QMainWindow):
|
|
|
31476
31656
|
def show_grid_context_menu(self, position):
|
|
31477
31657
|
"""Show context menu for grid view with bulk operations"""
|
|
31478
31658
|
selected_segments = self.get_selected_segments_from_grid()
|
|
31479
|
-
|
|
31659
|
+
|
|
31480
31660
|
if not selected_segments:
|
|
31481
31661
|
return
|
|
31482
|
-
|
|
31662
|
+
|
|
31483
31663
|
menu = QMenu(self)
|
|
31484
|
-
|
|
31664
|
+
|
|
31485
31665
|
# Confirm selected segments action
|
|
31486
31666
|
if len(selected_segments) >= 1:
|
|
31487
31667
|
confirm_action = menu.addAction(f"✅ Confirm {len(selected_segments)} Segment(s)")
|
|
31488
31668
|
confirm_action.setToolTip(f"Confirm {len(selected_segments)} selected segment(s)")
|
|
31489
31669
|
confirm_action.triggered.connect(self.confirm_selected_segments)
|
|
31490
|
-
|
|
31670
|
+
|
|
31671
|
+
# Change Status submenu
|
|
31672
|
+
from modules.statuses import get_status
|
|
31673
|
+
status_menu = menu.addMenu(f"🏷️ Change Status ({len(selected_segments)})")
|
|
31674
|
+
# Common user-settable statuses (excluding TM-specific ones like pm, cm, tm_100, etc.)
|
|
31675
|
+
user_statuses = [
|
|
31676
|
+
("not_started", "❌ Not started"),
|
|
31677
|
+
("pretranslated", "🤖 Pre-translated"),
|
|
31678
|
+
("translated", "✏️ Translated"),
|
|
31679
|
+
("confirmed", "✔ Confirmed"),
|
|
31680
|
+
("tr_confirmed", "🌟 TR confirmed"),
|
|
31681
|
+
("proofread", "🟪 Proofread"),
|
|
31682
|
+
("approved", "⭐ Approved"),
|
|
31683
|
+
("rejected", "🚫 Rejected"),
|
|
31684
|
+
]
|
|
31685
|
+
for status_key, label in user_statuses:
|
|
31686
|
+
action = status_menu.addAction(label)
|
|
31687
|
+
action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s))
|
|
31688
|
+
|
|
31491
31689
|
# Clear translations action
|
|
31492
31690
|
clear_action = menu.addAction("🗑️ Clear Translations")
|
|
31493
31691
|
clear_action.setToolTip(f"Clear translations for {len(selected_segments)} selected segment(s)")
|
|
31494
31692
|
clear_action.triggered.connect(lambda: self.clear_selected_translations(selected_segments, 'grid'))
|
|
31495
|
-
|
|
31693
|
+
|
|
31496
31694
|
menu.addSeparator()
|
|
31497
|
-
|
|
31695
|
+
|
|
31498
31696
|
# Clear proofreading notes (if any selected segment has proofreading notes)
|
|
31499
31697
|
has_proofreading = any(seg.notes and "⚠️ PROOFREAD:" in seg.notes for seg in selected_segments)
|
|
31500
31698
|
if has_proofreading:
|
|
@@ -31502,11 +31700,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
31502
31700
|
clear_proofread_action.setToolTip("Remove proofreading issues from selected segment(s)")
|
|
31503
31701
|
clear_proofread_action.triggered.connect(lambda: self._clear_proofreading_from_selected(selected_segments))
|
|
31504
31702
|
menu.addSeparator()
|
|
31505
|
-
|
|
31703
|
+
|
|
31506
31704
|
# Select all action
|
|
31507
31705
|
select_all_action = menu.addAction("📋 Select All (Ctrl+A)")
|
|
31508
31706
|
select_all_action.triggered.connect(lambda: self.table.selectAll())
|
|
31509
|
-
|
|
31707
|
+
|
|
31510
31708
|
menu.exec(self.table.viewport().mapToGlobal(position))
|
|
31511
31709
|
|
|
31512
31710
|
def _clear_proofreading_from_selected(self, segments):
|
|
@@ -38331,17 +38529,75 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38331
38529
|
if not self.current_project:
|
|
38332
38530
|
QMessageBox.information(self, "Not Available", "Please load a project first.")
|
|
38333
38531
|
return
|
|
38334
|
-
|
|
38532
|
+
|
|
38335
38533
|
if not hasattr(self, 'table') or not self.table:
|
|
38336
38534
|
QMessageBox.information(self, "Not Available", "Grid view is not available.")
|
|
38337
38535
|
return
|
|
38338
|
-
|
|
38536
|
+
|
|
38339
38537
|
selected_segments = self.get_selected_segments_from_grid()
|
|
38340
38538
|
if selected_segments:
|
|
38341
38539
|
self.confirm_selected_segments()
|
|
38342
38540
|
else:
|
|
38343
38541
|
QMessageBox.information(self, "No Selection", "Please select one or more segments to confirm.")
|
|
38344
|
-
|
|
38542
|
+
|
|
38543
|
+
def change_status_selected(self, new_status: str, from_menu: bool = False):
|
|
38544
|
+
"""Change status of all selected segments to the specified status.
|
|
38545
|
+
|
|
38546
|
+
Args:
|
|
38547
|
+
new_status: The status key to set (e.g., 'translated', 'pretranslated')
|
|
38548
|
+
from_menu: If True, show message boxes for errors (called from menu)
|
|
38549
|
+
"""
|
|
38550
|
+
if not self.current_project:
|
|
38551
|
+
if from_menu:
|
|
38552
|
+
QMessageBox.information(self, "Not Available", "Please load a project first.")
|
|
38553
|
+
else:
|
|
38554
|
+
self.log("⚠️ No project loaded")
|
|
38555
|
+
return
|
|
38556
|
+
|
|
38557
|
+
if not hasattr(self, 'table') or not self.table:
|
|
38558
|
+
if from_menu:
|
|
38559
|
+
QMessageBox.information(self, "Not Available", "Grid view is not available.")
|
|
38560
|
+
return
|
|
38561
|
+
|
|
38562
|
+
selected_segments = self.get_selected_segments_from_grid()
|
|
38563
|
+
|
|
38564
|
+
if not selected_segments:
|
|
38565
|
+
if from_menu:
|
|
38566
|
+
QMessageBox.information(self, "No Selection", "Please select one or more segments to change.")
|
|
38567
|
+
else:
|
|
38568
|
+
self.log("⚠️ No segments selected")
|
|
38569
|
+
return
|
|
38570
|
+
|
|
38571
|
+
# Sync all target text from grid widgets first
|
|
38572
|
+
self._sync_grid_targets_to_segments(selected_segments)
|
|
38573
|
+
|
|
38574
|
+
# Get status definition for logging
|
|
38575
|
+
from modules.statuses import get_status
|
|
38576
|
+
status_def = get_status(new_status)
|
|
38577
|
+
|
|
38578
|
+
changed_count = 0
|
|
38579
|
+
for segment in selected_segments:
|
|
38580
|
+
# Skip if already has this status
|
|
38581
|
+
if segment.status == new_status:
|
|
38582
|
+
continue
|
|
38583
|
+
|
|
38584
|
+
segment.status = new_status
|
|
38585
|
+
changed_count += 1
|
|
38586
|
+
|
|
38587
|
+
# Update grid status icon
|
|
38588
|
+
row = self._find_row_for_segment(segment.id)
|
|
38589
|
+
if row >= 0:
|
|
38590
|
+
self.update_status_icon(row, new_status)
|
|
38591
|
+
|
|
38592
|
+
if changed_count > 0:
|
|
38593
|
+
self.project_modified = True
|
|
38594
|
+
self.update_window_title()
|
|
38595
|
+
self.log(f"✅ Changed {changed_count} segment(s) to '{status_def.label}'")
|
|
38596
|
+
# Update status bar progress stats
|
|
38597
|
+
self.update_progress_stats()
|
|
38598
|
+
else:
|
|
38599
|
+
self.log(f"ℹ️ All {len(selected_segments)} selected segment(s) already have status '{status_def.label}'")
|
|
38600
|
+
|
|
38345
38601
|
def _sync_grid_targets_to_segments(self, segments):
|
|
38346
38602
|
"""Sync target text from grid widgets to segment objects.
|
|
38347
38603
|
|
|
@@ -41148,25 +41404,41 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41148
41404
|
self.log(f"📖 Pre-translate from TM: Using activated TMs: {tm_ids}")
|
|
41149
41405
|
|
|
41150
41406
|
# Create progress dialog for TM pre-translation
|
|
41407
|
+
import time
|
|
41408
|
+
start_time = time.time()
|
|
41409
|
+
total_segments = len(segments_needing_translation)
|
|
41410
|
+
|
|
41151
41411
|
progress = QProgressDialog(
|
|
41152
|
-
f"Pre-translating {
|
|
41153
|
-
"Cancel", 0,
|
|
41412
|
+
f"Pre-translating {total_segments} segments from TM...",
|
|
41413
|
+
"Cancel", 0, total_segments, self
|
|
41154
41414
|
)
|
|
41155
|
-
progress.setWindowTitle("TM Pre-Translation")
|
|
41415
|
+
progress.setWindowTitle("🔍 TM Pre-Translation")
|
|
41156
41416
|
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
|
41157
41417
|
progress.setMinimumDuration(0) # Show immediately
|
|
41418
|
+
progress.setMinimumWidth(450) # Wider dialog for more info
|
|
41158
41419
|
progress.show()
|
|
41159
41420
|
QApplication.processEvents()
|
|
41160
|
-
|
|
41421
|
+
|
|
41161
41422
|
success_count = 0
|
|
41162
41423
|
no_match_count = 0
|
|
41163
|
-
|
|
41424
|
+
|
|
41164
41425
|
for idx, (row_index, segment) in enumerate(segments_needing_translation):
|
|
41165
41426
|
if progress.wasCanceled():
|
|
41166
41427
|
break
|
|
41167
|
-
|
|
41428
|
+
|
|
41168
41429
|
progress.setValue(idx)
|
|
41169
|
-
|
|
41430
|
+
|
|
41431
|
+
# Build informative progress label
|
|
41432
|
+
elapsed = time.time() - start_time
|
|
41433
|
+
elapsed_str = f"{int(elapsed // 60)}:{int(elapsed % 60):02d}"
|
|
41434
|
+
source_preview = segment.source[:50] + "..." if len(segment.source) > 50 else segment.source
|
|
41435
|
+
label_text = (
|
|
41436
|
+
f"Searching TM for segment {idx + 1} of {total_segments}...\n\n"
|
|
41437
|
+
f"Current: \"{source_preview}\"\n"
|
|
41438
|
+
f"Matches found: {success_count} | Elapsed: {elapsed_str}\n\n"
|
|
41439
|
+
f"ℹ️ This may take a while for large documents."
|
|
41440
|
+
)
|
|
41441
|
+
progress.setLabelText(label_text)
|
|
41170
41442
|
QApplication.processEvents()
|
|
41171
41443
|
|
|
41172
41444
|
try:
|
|
@@ -41184,7 +41456,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41184
41456
|
else:
|
|
41185
41457
|
no_match_count += 1
|
|
41186
41458
|
else:
|
|
41187
|
-
# Fuzzy matching enabled - get best match ≥
|
|
41459
|
+
# Fuzzy matching enabled - get best match ≥75%
|
|
41188
41460
|
matches = self.tm_database.search_all(
|
|
41189
41461
|
segment.source,
|
|
41190
41462
|
tm_ids=tm_ids,
|
|
@@ -41194,7 +41466,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41194
41466
|
if matches and len(matches) > 0:
|
|
41195
41467
|
best_match = matches[0]
|
|
41196
41468
|
match_pct = best_match.get('match_pct', 0)
|
|
41197
|
-
if match_pct >=
|
|
41469
|
+
if match_pct >= 75:
|
|
41198
41470
|
segment.target = best_match.get('target', '')
|
|
41199
41471
|
segment.status = "Translated"
|
|
41200
41472
|
success_count += 1
|
|
@@ -41562,9 +41834,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41562
41834
|
match_pct = match.get('match_pct', 0)
|
|
41563
41835
|
tm_match = match.get('target', '')
|
|
41564
41836
|
|
|
41565
|
-
if match_pct >=
|
|
41837
|
+
if match_pct >= 75: # Accept matches 75% and above
|
|
41566
41838
|
segment.target = tm_match
|
|
41567
|
-
segment.status = "
|
|
41839
|
+
segment.status = "pretranslated"
|
|
41568
41840
|
translated_count += 1
|
|
41569
41841
|
|
|
41570
41842
|
# Update grid immediately
|
|
@@ -41678,7 +41950,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41678
41950
|
if tm_match and len(tm_match) > 0:
|
|
41679
41951
|
translation = tm_match[0]['target']
|
|
41680
41952
|
segment.target = translation
|
|
41681
|
-
segment.status = '
|
|
41953
|
+
segment.status = 'pretranslated'
|
|
41682
41954
|
translated_count += 1
|
|
41683
41955
|
self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}... (TM 100%)")
|
|
41684
41956
|
else:
|
|
@@ -41725,7 +41997,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41725
41997
|
|
|
41726
41998
|
if translation and not translation.startswith('['): # Skip error messages
|
|
41727
41999
|
segment.target = translation
|
|
41728
|
-
segment.status = '
|
|
42000
|
+
segment.status = 'pretranslated'
|
|
41729
42001
|
translated_count += 1
|
|
41730
42002
|
self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}...")
|
|
41731
42003
|
else:
|
modules/database_manager.py
CHANGED
|
@@ -17,12 +17,38 @@ import sqlite3
|
|
|
17
17
|
import os
|
|
18
18
|
import json
|
|
19
19
|
import hashlib
|
|
20
|
+
import unicodedata
|
|
21
|
+
import re
|
|
20
22
|
from datetime import datetime
|
|
21
23
|
from typing import List, Dict, Optional, Tuple
|
|
22
24
|
from pathlib import Path
|
|
23
25
|
from difflib import SequenceMatcher
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def _normalize_for_matching(text: str) -> str:
|
|
29
|
+
"""Normalize text for exact matching.
|
|
30
|
+
|
|
31
|
+
Handles invisible differences that would cause exact match to fail:
|
|
32
|
+
- Unicode normalization (NFC)
|
|
33
|
+
- Multiple whitespace -> single space
|
|
34
|
+
- Leading/trailing whitespace
|
|
35
|
+
- Non-breaking spaces -> regular spaces
|
|
36
|
+
"""
|
|
37
|
+
if not text:
|
|
38
|
+
return ""
|
|
39
|
+
# Unicode normalize (NFC form)
|
|
40
|
+
text = unicodedata.normalize('NFC', text)
|
|
41
|
+
# Convert non-breaking spaces and other whitespace to regular space
|
|
42
|
+
text = text.replace('\u00a0', ' ') # NBSP
|
|
43
|
+
text = text.replace('\u2007', ' ') # Figure space
|
|
44
|
+
text = text.replace('\u202f', ' ') # Narrow NBSP
|
|
45
|
+
# Collapse multiple whitespace to single space
|
|
46
|
+
text = re.sub(r'\s+', ' ', text)
|
|
47
|
+
# Strip leading/trailing whitespace
|
|
48
|
+
text = text.strip()
|
|
49
|
+
return text
|
|
50
|
+
|
|
51
|
+
|
|
26
52
|
class DatabaseManager:
|
|
27
53
|
"""Manages SQLite database for translation resources"""
|
|
28
54
|
|
|
@@ -655,22 +681,46 @@ class DatabaseManager:
|
|
|
655
681
|
# TRANSLATION MEMORY METHODS
|
|
656
682
|
# ============================================
|
|
657
683
|
|
|
658
|
-
def add_translation_unit(self, source: str, target: str, source_lang: str,
|
|
684
|
+
def add_translation_unit(self, source: str, target: str, source_lang: str,
|
|
659
685
|
target_lang: str, tm_id: str = 'project',
|
|
660
686
|
project_id: str = None, context_before: str = None,
|
|
661
|
-
context_after: str = None, notes: str = None
|
|
687
|
+
context_after: str = None, notes: str = None,
|
|
688
|
+
overwrite: bool = False) -> int:
|
|
662
689
|
"""
|
|
663
690
|
Add translation unit to database
|
|
664
|
-
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
source: Source text
|
|
694
|
+
target: Target text
|
|
695
|
+
source_lang: Source language code
|
|
696
|
+
target_lang: Target language code
|
|
697
|
+
tm_id: TM identifier
|
|
698
|
+
project_id: Optional project ID
|
|
699
|
+
context_before: Optional context before
|
|
700
|
+
context_after: Optional context after
|
|
701
|
+
notes: Optional notes
|
|
702
|
+
overwrite: If True, delete existing entries with same source before inserting
|
|
703
|
+
(implements "Save only latest translation" mode)
|
|
704
|
+
|
|
665
705
|
Returns: ID of inserted/updated entry
|
|
666
706
|
"""
|
|
667
|
-
# Generate hash for
|
|
668
|
-
|
|
669
|
-
|
|
707
|
+
# Generate hash from NORMALIZED source for consistent exact matching
|
|
708
|
+
# This handles invisible differences like Unicode normalization, whitespace variations
|
|
709
|
+
normalized_source = _normalize_for_matching(source)
|
|
710
|
+
source_hash = hashlib.md5(normalized_source.encode('utf-8')).hexdigest()
|
|
711
|
+
|
|
670
712
|
try:
|
|
713
|
+
# If overwrite mode, delete ALL existing entries with same source_hash and tm_id
|
|
714
|
+
# This ensures only the latest translation is kept
|
|
715
|
+
if overwrite:
|
|
716
|
+
self.cursor.execute("""
|
|
717
|
+
DELETE FROM translation_units
|
|
718
|
+
WHERE source_hash = ? AND tm_id = ?
|
|
719
|
+
""", (source_hash, tm_id))
|
|
720
|
+
|
|
671
721
|
self.cursor.execute("""
|
|
672
|
-
INSERT INTO translation_units
|
|
673
|
-
(source_text, target_text, source_lang, target_lang, tm_id,
|
|
722
|
+
INSERT INTO translation_units
|
|
723
|
+
(source_text, target_text, source_lang, target_lang, tm_id,
|
|
674
724
|
project_id, context_before, context_after, source_hash, notes)
|
|
675
725
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
676
726
|
ON CONFLICT(source_hash, target_text, tm_id) DO UPDATE SET
|
|
@@ -678,42 +728,47 @@ class DatabaseManager:
|
|
|
678
728
|
modified_date = CURRENT_TIMESTAMP
|
|
679
729
|
""", (source, target, source_lang, target_lang, tm_id,
|
|
680
730
|
project_id, context_before, context_after, source_hash, notes))
|
|
681
|
-
|
|
731
|
+
|
|
682
732
|
self.connection.commit()
|
|
683
733
|
return self.cursor.lastrowid
|
|
684
|
-
|
|
734
|
+
|
|
685
735
|
except Exception as e:
|
|
686
736
|
self.log(f"Error adding translation unit: {e}")
|
|
687
737
|
return None
|
|
688
738
|
|
|
689
739
|
def get_exact_match(self, source: str, tm_ids: List[str] = None,
|
|
690
|
-
source_lang: str = None, target_lang: str = None,
|
|
740
|
+
source_lang: str = None, target_lang: str = None,
|
|
691
741
|
bidirectional: bool = True) -> Optional[Dict]:
|
|
692
742
|
"""
|
|
693
743
|
Get exact match from TM
|
|
694
|
-
|
|
744
|
+
|
|
695
745
|
Args:
|
|
696
746
|
source: Source text to match
|
|
697
747
|
tm_ids: List of TM IDs to search (None = all)
|
|
698
748
|
source_lang: Filter by source language (base code matching: 'en' matches 'en-US', 'en-GB', etc.)
|
|
699
749
|
target_lang: Filter by target language (base code matching)
|
|
700
750
|
bidirectional: If True, search both directions (nl→en AND en→nl)
|
|
701
|
-
|
|
751
|
+
|
|
702
752
|
Returns: Dictionary with match data or None
|
|
703
753
|
"""
|
|
704
754
|
from modules.tmx_generator import get_base_lang_code
|
|
705
|
-
|
|
755
|
+
|
|
756
|
+
# Try both normalized and non-normalized hashes for backward compatibility
|
|
757
|
+
# This handles invisible differences like Unicode normalization, whitespace variations
|
|
706
758
|
source_hash = hashlib.md5(source.encode('utf-8')).hexdigest()
|
|
707
|
-
|
|
759
|
+
normalized_source = _normalize_for_matching(source)
|
|
760
|
+
normalized_hash = hashlib.md5(normalized_source.encode('utf-8')).hexdigest()
|
|
761
|
+
|
|
708
762
|
# Get base language codes for comparison
|
|
709
763
|
src_base = get_base_lang_code(source_lang) if source_lang else None
|
|
710
764
|
tgt_base = get_base_lang_code(target_lang) if target_lang else None
|
|
711
|
-
|
|
765
|
+
|
|
766
|
+
# Search using both original hash and normalized hash
|
|
712
767
|
query = """
|
|
713
|
-
SELECT * FROM translation_units
|
|
714
|
-
WHERE source_hash = ?
|
|
768
|
+
SELECT * FROM translation_units
|
|
769
|
+
WHERE (source_hash = ? OR source_hash = ?)
|
|
715
770
|
"""
|
|
716
|
-
params = [source_hash,
|
|
771
|
+
params = [source_hash, normalized_hash]
|
|
717
772
|
|
|
718
773
|
if tm_ids:
|
|
719
774
|
placeholders = ','.join('?' * len(tm_ids))
|
modules/translation_memory.py
CHANGED
|
@@ -123,8 +123,8 @@ class TMDatabase:
|
|
|
123
123
|
if source_lang and target_lang:
|
|
124
124
|
self.set_tm_languages(source_lang, target_lang)
|
|
125
125
|
|
|
126
|
-
# Global fuzzy threshold (
|
|
127
|
-
self.fuzzy_threshold = 0.
|
|
126
|
+
# Global fuzzy threshold (75% minimum similarity for fuzzy matches)
|
|
127
|
+
self.fuzzy_threshold = 0.75
|
|
128
128
|
|
|
129
129
|
# TM metadata cache (populated from database as needed)
|
|
130
130
|
# Note: Legacy 'project' and 'big_mama' TMs are no longer used.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supervertaler
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.174
|
|
4
4
|
Summary: Professional AI-enhanced translation workbench with multi-LLM support, glossary system, TM, spellcheck, voice commands, and PyQt6 interface. Batteries included (core).
|
|
5
5
|
Home-page: https://supervertaler.com
|
|
6
6
|
Author: Michael Beijer
|
|
@@ -71,7 +71,7 @@ Dynamic: home-page
|
|
|
71
71
|
Dynamic: license-file
|
|
72
72
|
Dynamic: requires-python
|
|
73
73
|
|
|
74
|
-
# 🚀 Supervertaler v1.9.
|
|
74
|
+
# 🚀 Supervertaler v1.9.174
|
|
75
75
|
|
|
76
76
|
[](https://pypi.org/project/Supervertaler/)
|
|
77
77
|
[](https://www.python.org/downloads/)
|
|
@@ -80,15 +80,24 @@ Dynamic: requires-python
|
|
|
80
80
|
AI-enhanced CAT tool with multi-LLM support (GPT-4, Claude, Gemini, Ollama), innovative Superlookup concordance system offering access to multiple terminology sources (TMs, glossaries, web resources, etc.), and seamless CAT tool integration (memoQ, Trados, CafeTran, Phrase).
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
**Current Version:** v1.9.
|
|
83
|
+
**Current Version:** v1.9.174 (January 28, 2026)
|
|
84
84
|
|
|
85
|
-
###
|
|
85
|
+
### NEW in v1.9.174 - 🏷️ Batch Status Change, Ctrl+, Enhancements, TM Fixes
|
|
86
|
+
|
|
87
|
+
- **Batch Status Change**: Change status of multiple selected segments via right-click or Edit → Bulk Operations → Change Status.
|
|
88
|
+
- **Ctrl+, Tag Wrapping**: Now wraps selected text with HTML tag pairs (`<b>...</b>`) in addition to memoQ/CafeTran tags.
|
|
89
|
+
- **TM Overwrite Mode Fixed**: "Save only latest translation" now actually overwrites existing entries.
|
|
90
|
+
- **Shift+Click Multi-Select Fixed**: Grid range selection now works correctly.
|
|
91
|
+
- **Status Dropdown Width**: No more truncated text in status dropdown.
|
|
86
92
|
|
|
87
|
-
|
|
93
|
+
### IMPROVED in v1.9.173 - 🎯 Smarter TM Pre-Translation
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
- **Smarter TM Exact Matching**: Exact matches now use text normalization, so matches are found even with whitespace/Unicode differences.
|
|
96
|
+
- **Improved Pre-Translation Dialog**: Shows current segment, match count, elapsed time, and patience message for large jobs.
|
|
97
|
+
|
|
98
|
+
### FIXED in v1.9.172 - 🐛 Fresh Projects Start Clean
|
|
90
99
|
|
|
91
|
-
- **
|
|
100
|
+
- **Fresh Projects Start Clean**: Fixed bug where TMs and glossaries remained activated from previous sessions.
|
|
92
101
|
|
|
93
102
|
### NEW in v1.9.170 - 📝 Scratchpad Tab, Cache Defaults, TM Target Shortcut Badge
|
|
94
103
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Supervertaler.py,sha256=
|
|
1
|
+
Supervertaler.py,sha256=P2n_d1jzUgaIOpQ0z_pTHqRQbifUYBPwAUkWDh_FkRY,2298190
|
|
2
2
|
modules/__init__.py,sha256=G58XleS-EJ2sX4Kehm-3N2m618_W2Es0Kg8CW_eBG7g,327
|
|
3
3
|
modules/ai_actions.py,sha256=i5MJcM-7Y6CAvKUwxmxrVHeoZAVtAP7aRDdWM5KLkO0,33877
|
|
4
4
|
modules/ai_attachment_manager.py,sha256=juZlrW3UPkIkcnj0SREgOQkQROLf0fcu3ShZcKXMxsI,11361
|
|
@@ -6,7 +6,7 @@ modules/ai_file_viewer_dialog.py,sha256=lKKqUUlOEVgHmmu6aRxqH7P6ds-7dRLk4ltDyjCw
|
|
|
6
6
|
modules/autofingers_engine.py,sha256=eJ7tBi7YJvTToe5hYTfnyGXB-qme_cHrOPZibaoR2Xw,17061
|
|
7
7
|
modules/cafetran_docx_handler.py,sha256=_F7Jh0WPVaDnMhdxEsVSXuD1fN9r-S_V6i0gr86Pdfc,14076
|
|
8
8
|
modules/config_manager.py,sha256=MkPY3xVFgFDkcwewLREg4BfyKueO0OJkT1cTLxehcjM,17894
|
|
9
|
-
modules/database_manager.py,sha256=
|
|
9
|
+
modules/database_manager.py,sha256=FecvNLJa_mw0PZld8-LozUSSgtAq6wDZ8Mhq_u2aiA0,82563
|
|
10
10
|
modules/database_migrations.py,sha256=Y1onFsLDV_6vzJLOpNy3WCZDohBZ2jc4prM-g2_RwLE,14085
|
|
11
11
|
modules/dejavurtf_handler.py,sha256=8NZPPYtHga40SZCypHjPoJPmZTvm9rD-eEUUab7mjtg,28156
|
|
12
12
|
modules/document_analyzer.py,sha256=t1rVvqLaTcpQTEja228C7zZnh8dXshK4wA9t1E9aGVk,19524
|
|
@@ -69,7 +69,7 @@ modules/tmx_editor_qt.py,sha256=PxBIUw_06PHYTBHsd8hZzVJXW8T0A0ljfz1Wjjsa4yU,1170
|
|
|
69
69
|
modules/tmx_generator.py,sha256=pNkxwdMLvSRMMru0lkB1gvViIpg9BQy1EVhRbwoef3k,9426
|
|
70
70
|
modules/tracked_changes.py,sha256=S_BIEC6r7wVAwjG42aSy_RgH4KaMAC8GS5thEvqrYdE,39480
|
|
71
71
|
modules/trados_docx_handler.py,sha256=VPRAQ73cUHs_SEj6x81z1PmSxfjnwPBp9P4fXeK3KpQ,16363
|
|
72
|
-
modules/translation_memory.py,sha256=
|
|
72
|
+
modules/translation_memory.py,sha256=13PDK4_kgYrWTACWBIBypOh2DvoxY9cRT8U6ulilbh4,28739
|
|
73
73
|
modules/translation_results_panel.py,sha256=DmEe0pZRSfcZFg2cWeEREK7H9vrTcPkgeuMW54Pgrys,92505
|
|
74
74
|
modules/translation_services.py,sha256=lyVpWuZK1wtVtYZMDMdLoq1DHBoSaeAnp-Yejb0TlVQ,10530
|
|
75
75
|
modules/unified_prompt_library.py,sha256=lzbevgjUz_qCiYSf141BB0mmuaDhSsevWju_a7welu0,26008
|
|
@@ -77,9 +77,9 @@ modules/unified_prompt_manager_qt.py,sha256=fyF3_r0N8hnImT-CcWo1AuBOQ1Dn_ExeeUCk
|
|
|
77
77
|
modules/voice_commands.py,sha256=iBb-gjWxRMLhFH7-InSRjYJz1EIDBNA2Pog8V7TtJaY,38516
|
|
78
78
|
modules/voice_dictation.py,sha256=QmitXfkG-vRt5hIQATjphHdhXfqmwhzcQcbXB6aRzIg,16386
|
|
79
79
|
modules/voice_dictation_lite.py,sha256=jorY0BmWE-8VczbtGrWwt1zbnOctMoSlWOsQrcufBcc,9423
|
|
80
|
-
supervertaler-1.9.
|
|
81
|
-
supervertaler-1.9.
|
|
82
|
-
supervertaler-1.9.
|
|
83
|
-
supervertaler-1.9.
|
|
84
|
-
supervertaler-1.9.
|
|
85
|
-
supervertaler-1.9.
|
|
80
|
+
supervertaler-1.9.174.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
|
|
81
|
+
supervertaler-1.9.174.dist-info/METADATA,sha256=MjFlkP5U4cAhkgRjfHo7MMcCSOrl3U5fXFEaViQ-iOU,48873
|
|
82
|
+
supervertaler-1.9.174.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
83
|
+
supervertaler-1.9.174.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
|
|
84
|
+
supervertaler-1.9.174.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
|
|
85
|
+
supervertaler-1.9.174.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|