supervertaler 1.9.176b0__py3-none-any.whl → 1.9.178__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 +135 -46
- modules/database_manager.py +169 -64
- modules/mqxliff_handler.py +71 -2
- modules/tm_metadata_manager.py +23 -18
- {supervertaler-1.9.176b0.dist-info → supervertaler-1.9.178.dist-info}/METADATA +1 -1
- {supervertaler-1.9.176b0.dist-info → supervertaler-1.9.178.dist-info}/RECORD +10 -10
- {supervertaler-1.9.176b0.dist-info → supervertaler-1.9.178.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.176b0.dist-info → supervertaler-1.9.178.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.176b0.dist-info → supervertaler-1.9.178.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.176b0.dist-info → supervertaler-1.9.178.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -3,8 +3,6 @@ Supervertaler
|
|
|
3
3
|
=============
|
|
4
4
|
The Ultimate Translation Workbench.
|
|
5
5
|
Modern PyQt6 interface with specialised modules to handle any problem.
|
|
6
|
-
Version: 1.9.153 (Tab Layout Reorganization)
|
|
7
|
-
Release Date: January 23, 2026
|
|
8
6
|
Framework: PyQt6
|
|
9
7
|
|
|
10
8
|
This is the modern edition of Supervertaler using PyQt6 framework.
|
|
@@ -34,7 +32,7 @@ License: MIT
|
|
|
34
32
|
"""
|
|
35
33
|
|
|
36
34
|
# Version Information.
|
|
37
|
-
__version__ = "1.9.
|
|
35
|
+
__version__ = "1.9.178"
|
|
38
36
|
__phase__ = "0.9"
|
|
39
37
|
__release_date__ = "2026-01-28"
|
|
40
38
|
__edition__ = "Qt"
|
|
@@ -10622,7 +10620,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
10622
10620
|
button_layout = QHBoxLayout()
|
|
10623
10621
|
|
|
10624
10622
|
create_btn = QPushButton("+ Create New TM")
|
|
10625
|
-
|
|
10623
|
+
# Get project_id dynamically - use 0 (global) when no project is loaded
|
|
10624
|
+
create_btn.clicked.connect(lambda: self._show_create_tm_dialog(
|
|
10625
|
+
tm_metadata_mgr, refresh_tm_list,
|
|
10626
|
+
self.current_project.id if (hasattr(self, 'current_project') and self.current_project and hasattr(self.current_project, 'id')) else 0
|
|
10627
|
+
))
|
|
10626
10628
|
button_layout.addWidget(create_btn)
|
|
10627
10629
|
|
|
10628
10630
|
import_btn = QPushButton("📥 Import TMX")
|
|
@@ -14974,8 +14976,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
14974
14976
|
)
|
|
14975
14977
|
|
|
14976
14978
|
if result:
|
|
14977
|
-
# Auto-activate for current project
|
|
14978
|
-
if project_id:
|
|
14979
|
+
# Auto-activate for current project (or global=0 if no project loaded)
|
|
14980
|
+
if project_id is not None:
|
|
14979
14981
|
tm_metadata_mgr.activate_tm(result, project_id)
|
|
14980
14982
|
|
|
14981
14983
|
QMessageBox.information(self, "Success", f"Translation Memory '{name}' created successfully!")
|
|
@@ -22319,20 +22321,24 @@ class SupervertalerQt(QMainWindow):
|
|
|
22319
22321
|
"""
|
|
22320
22322
|
Search termbases using a provided cursor (thread-safe for background threads).
|
|
22321
22323
|
This method allows background workers to query the database without SQLite threading errors.
|
|
22322
|
-
|
|
22324
|
+
|
|
22325
|
+
Implements BIDIRECTIONAL matching: searches both source_term and target_term columns.
|
|
22326
|
+
When a match is found on target_term, source and target are swapped in the result.
|
|
22327
|
+
This matches memoQ/Trados behavior where a NL→EN termbase also works for EN→NL projects.
|
|
22328
|
+
|
|
22323
22329
|
Args:
|
|
22324
22330
|
source_text: The source text to search for
|
|
22325
22331
|
cursor: A database cursor from a thread-local connection
|
|
22326
22332
|
source_lang: Source language code
|
|
22327
22333
|
target_lang: Target language code
|
|
22328
22334
|
project_id: Current project ID (required to filter by activated termbases)
|
|
22329
|
-
|
|
22335
|
+
|
|
22330
22336
|
Returns:
|
|
22331
22337
|
Dictionary of {term: translation} matches
|
|
22332
22338
|
"""
|
|
22333
22339
|
if not source_text or not cursor:
|
|
22334
22340
|
return {}
|
|
22335
|
-
|
|
22341
|
+
|
|
22336
22342
|
try:
|
|
22337
22343
|
# Convert language names to codes (match interactive search logic)
|
|
22338
22344
|
source_lang_code = self._convert_language_to_code(source_lang) if source_lang else None
|
|
@@ -22352,20 +22358,26 @@ class SupervertalerQt(QMainWindow):
|
|
|
22352
22358
|
try:
|
|
22353
22359
|
# JOIN termbases AND termbase_activation to filter by activated termbases
|
|
22354
22360
|
# This matches the logic in database_manager.py search_termbases()
|
|
22361
|
+
# BIDIRECTIONAL: Search both source_term (forward) and target_term (reverse)
|
|
22362
|
+
# Using UNION to combine both directions
|
|
22355
22363
|
query = """
|
|
22356
|
-
SELECT
|
|
22357
|
-
|
|
22358
|
-
|
|
22359
|
-
|
|
22360
|
-
|
|
22361
|
-
|
|
22362
|
-
|
|
22363
|
-
|
|
22364
|
-
|
|
22365
|
-
|
|
22364
|
+
SELECT * FROM (
|
|
22365
|
+
-- Forward match: search source_term
|
|
22366
|
+
SELECT
|
|
22367
|
+
t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
|
|
22368
|
+
t.domain, t.notes, t.project, t.client, t.forbidden,
|
|
22369
|
+
tb.is_project_termbase, tb.name as termbase_name,
|
|
22370
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
22371
|
+
'source' as match_direction
|
|
22372
|
+
FROM termbase_terms t
|
|
22373
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
22374
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
22375
|
+
WHERE LOWER(t.source_term) LIKE ?
|
|
22376
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
22366
22377
|
"""
|
|
22367
22378
|
params = [project_id if project_id else 0, f"%{clean_word.lower()}%"]
|
|
22368
22379
|
|
|
22380
|
+
# Language filters for forward query
|
|
22369
22381
|
if source_lang_code:
|
|
22370
22382
|
query += " AND (t.source_lang = ? OR (t.source_lang IS NULL AND tb.source_lang = ?) OR (t.source_lang IS NULL AND tb.source_lang IS NULL))"
|
|
22371
22383
|
params.extend([source_lang_code, source_lang_code])
|
|
@@ -22373,15 +22385,45 @@ class SupervertalerQt(QMainWindow):
|
|
|
22373
22385
|
query += " AND (t.target_lang = ? OR (t.target_lang IS NULL AND tb.target_lang = ?) OR (t.target_lang IS NULL AND tb.target_lang IS NULL))"
|
|
22374
22386
|
params.extend([target_lang_code, target_lang_code])
|
|
22375
22387
|
|
|
22376
|
-
#
|
|
22377
|
-
query += "
|
|
22388
|
+
# Reverse match: search target_term, swap source/target in output
|
|
22389
|
+
query += """
|
|
22390
|
+
UNION ALL
|
|
22391
|
+
-- Reverse match: search target_term, swap columns
|
|
22392
|
+
SELECT
|
|
22393
|
+
t.id, t.target_term as source_term, t.source_term as target_term,
|
|
22394
|
+
t.termbase_id, t.priority,
|
|
22395
|
+
t.domain, t.notes, t.project, t.client, t.forbidden,
|
|
22396
|
+
tb.is_project_termbase, tb.name as termbase_name,
|
|
22397
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
22398
|
+
'target' as match_direction
|
|
22399
|
+
FROM termbase_terms t
|
|
22400
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
22401
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
22402
|
+
WHERE LOWER(t.target_term) LIKE ?
|
|
22403
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
22404
|
+
"""
|
|
22405
|
+
params.extend([project_id if project_id else 0, f"%{clean_word.lower()}%"])
|
|
22406
|
+
|
|
22407
|
+
# Language filters for reverse query (swapped)
|
|
22408
|
+
if source_lang_code:
|
|
22409
|
+
# For reverse: source_lang filters target_lang column
|
|
22410
|
+
query += " AND (t.target_lang = ? OR (t.target_lang IS NULL AND tb.target_lang = ?) OR (t.target_lang IS NULL AND tb.target_lang IS NULL))"
|
|
22411
|
+
params.extend([source_lang_code, source_lang_code])
|
|
22412
|
+
if target_lang_code:
|
|
22413
|
+
# For reverse: target_lang filters source_lang column
|
|
22414
|
+
query += " AND (t.source_lang = ? OR (t.source_lang IS NULL AND tb.source_lang = ?) OR (t.source_lang IS NULL AND tb.source_lang IS NULL))"
|
|
22415
|
+
params.extend([target_lang_code, target_lang_code])
|
|
22416
|
+
|
|
22417
|
+
# Close UNION and limit
|
|
22418
|
+
query += ") combined LIMIT 30"
|
|
22378
22419
|
cursor.execute(query, params)
|
|
22379
22420
|
results = cursor.fetchall()
|
|
22380
22421
|
|
|
22381
22422
|
for row in results:
|
|
22382
|
-
# Uniform access
|
|
22423
|
+
# Uniform access (columns are already swapped for reverse matches)
|
|
22383
22424
|
source_term = row[1] if isinstance(row, tuple) else row['source_term']
|
|
22384
22425
|
target_term = row[2] if isinstance(row, tuple) else row['target_term']
|
|
22426
|
+
match_direction = row[13] if isinstance(row, tuple) else row.get('match_direction', 'source')
|
|
22385
22427
|
if not source_term or not target_term:
|
|
22386
22428
|
continue
|
|
22387
22429
|
|
|
@@ -22405,8 +22447,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
22405
22447
|
existing = matches.get(source_term.strip())
|
|
22406
22448
|
# Deduplicate: keep numerically lowest ranking (highest priority)
|
|
22407
22449
|
# For project termbases, ranking is None so they always win
|
|
22450
|
+
# Also prefer 'source' matches over 'target' matches when equal
|
|
22408
22451
|
if existing:
|
|
22409
22452
|
existing_ranking = existing.get('ranking', None)
|
|
22453
|
+
existing_direction = existing.get('match_direction', 'source')
|
|
22410
22454
|
if is_project_tb:
|
|
22411
22455
|
# Project termbase always wins
|
|
22412
22456
|
pass
|
|
@@ -22415,8 +22459,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
22415
22459
|
continue
|
|
22416
22460
|
elif existing_ranking is not None and ranking is not None:
|
|
22417
22461
|
# Both have rankings, keep lower (higher priority)
|
|
22418
|
-
if existing_ranking
|
|
22462
|
+
if existing_ranking < ranking:
|
|
22419
22463
|
continue
|
|
22464
|
+
elif existing_ranking == ranking:
|
|
22465
|
+
# Same ranking: prefer source match over target match
|
|
22466
|
+
if existing_direction == 'source' and match_direction == 'target':
|
|
22467
|
+
continue
|
|
22420
22468
|
|
|
22421
22469
|
matches[source_term.strip()] = {
|
|
22422
22470
|
'translation': target_term.strip(),
|
|
@@ -22431,15 +22479,18 @@ class SupervertalerQt(QMainWindow):
|
|
|
22431
22479
|
'forbidden': forbidden or False,
|
|
22432
22480
|
'is_project_termbase': bool(is_project_tb),
|
|
22433
22481
|
'termbase_name': termbase_name or '',
|
|
22434
|
-
'target_synonyms': [] # Will be populated below
|
|
22482
|
+
'target_synonyms': [], # Will be populated below
|
|
22483
|
+
'match_direction': match_direction # Track if this was a reverse match
|
|
22435
22484
|
}
|
|
22436
|
-
|
|
22485
|
+
|
|
22437
22486
|
# Fetch synonyms for this term
|
|
22487
|
+
# For reverse matches, fetch 'source' synonyms since they become targets
|
|
22438
22488
|
try:
|
|
22489
|
+
synonym_lang = 'source' if match_direction == 'target' else 'target'
|
|
22439
22490
|
cursor.execute("""
|
|
22440
|
-
SELECT synonym_text FROM termbase_synonyms
|
|
22441
|
-
WHERE term_id = ? AND language =
|
|
22442
|
-
""", (term_id,))
|
|
22491
|
+
SELECT synonym_text FROM termbase_synonyms
|
|
22492
|
+
WHERE term_id = ? AND language = ? AND forbidden = 0
|
|
22493
|
+
""", (term_id, synonym_lang))
|
|
22443
22494
|
synonym_rows = cursor.fetchall()
|
|
22444
22495
|
for syn_row in synonym_rows:
|
|
22445
22496
|
synonym = syn_row[0] if isinstance(syn_row, tuple) else syn_row['synonym_text']
|
|
@@ -26645,24 +26696,32 @@ class SupervertalerQt(QMainWindow):
|
|
|
26645
26696
|
)
|
|
26646
26697
|
return
|
|
26647
26698
|
|
|
26648
|
-
# Extract segments
|
|
26649
|
-
mqxliff_segments = handler.
|
|
26650
|
-
|
|
26699
|
+
# Extract segments (including targets for pretranslated files)
|
|
26700
|
+
mqxliff_segments = handler.extract_bilingual_segments()
|
|
26701
|
+
|
|
26651
26702
|
if not mqxliff_segments:
|
|
26652
26703
|
QMessageBox.warning(
|
|
26653
26704
|
self, "No Segments",
|
|
26654
26705
|
"No segments found in the memoQ XLIFF file."
|
|
26655
26706
|
)
|
|
26656
26707
|
return
|
|
26657
|
-
|
|
26708
|
+
|
|
26709
|
+
# Count pretranslated segments
|
|
26710
|
+
pretranslated_count = sum(1 for s in mqxliff_segments if s.get('target', '').strip())
|
|
26711
|
+
|
|
26658
26712
|
# Convert to internal Segment format
|
|
26659
26713
|
segments = []
|
|
26660
26714
|
for i, mq_seg in enumerate(mqxliff_segments):
|
|
26715
|
+
# Map status from mqxliff
|
|
26716
|
+
status = mq_seg.get('status', 'not_started')
|
|
26717
|
+
if status not in ['not_started', 'pre_translated', 'translated', 'confirmed', 'locked']:
|
|
26718
|
+
status = 'not_started'
|
|
26719
|
+
|
|
26661
26720
|
segment = Segment(
|
|
26662
26721
|
id=i + 1,
|
|
26663
|
-
source=mq_seg.
|
|
26664
|
-
target=
|
|
26665
|
-
status=
|
|
26722
|
+
source=mq_seg.get('source', ''),
|
|
26723
|
+
target=mq_seg.get('target', ''),
|
|
26724
|
+
status=status,
|
|
26666
26725
|
notes="",
|
|
26667
26726
|
)
|
|
26668
26727
|
segments.append(segment)
|
|
@@ -26706,11 +26765,17 @@ class SupervertalerQt(QMainWindow):
|
|
|
26706
26765
|
# Log success
|
|
26707
26766
|
self.log(f"✓ Imported {len(segments)} segments from memoQ XLIFF: {Path(file_path).name}")
|
|
26708
26767
|
self.log(f" Source: {source_lang}, Target: {target_lang}")
|
|
26709
|
-
|
|
26768
|
+
if pretranslated_count > 0:
|
|
26769
|
+
self.log(f" Pretranslated: {pretranslated_count} segments with target text")
|
|
26770
|
+
|
|
26771
|
+
# Build message with pretranslation info
|
|
26772
|
+
msg = f"Successfully imported {len(segments)} segment(s) from memoQ XLIFF.\n\nLanguages: {source_lang} → {target_lang}"
|
|
26773
|
+
if pretranslated_count > 0:
|
|
26774
|
+
msg += f"\n\nPretranslated: {pretranslated_count} segment(s) with target text loaded."
|
|
26775
|
+
|
|
26710
26776
|
QMessageBox.information(
|
|
26711
26777
|
self, "Import Successful",
|
|
26712
|
-
|
|
26713
|
-
f"Languages: {source_lang} → {target_lang}"
|
|
26778
|
+
msg
|
|
26714
26779
|
)
|
|
26715
26780
|
except Exception as e:
|
|
26716
26781
|
self.log(f"❌ Error importing memoQ XLIFF: {e}")
|
|
@@ -27119,7 +27184,19 @@ class SupervertalerQt(QMainWindow):
|
|
|
27119
27184
|
source_row = QHBoxLayout()
|
|
27120
27185
|
source_row.addWidget(QLabel("Source Language:"))
|
|
27121
27186
|
source_combo = QComboBox()
|
|
27122
|
-
|
|
27187
|
+
# Full language list (same as New Project dialog)
|
|
27188
|
+
available_languages = [
|
|
27189
|
+
"Afrikaans", "Albanian", "Arabic", "Armenian", "Basque", "Bengali",
|
|
27190
|
+
"Bulgarian", "Catalan", "Chinese (Simplified)", "Chinese (Traditional)",
|
|
27191
|
+
"Croatian", "Czech", "Danish", "Dutch", "English", "Estonian",
|
|
27192
|
+
"Finnish", "French", "Galician", "Georgian", "German", "Greek",
|
|
27193
|
+
"Hebrew", "Hindi", "Hungarian", "Icelandic", "Indonesian", "Irish",
|
|
27194
|
+
"Italian", "Japanese", "Korean", "Latvian", "Lithuanian", "Macedonian",
|
|
27195
|
+
"Malay", "Norwegian", "Persian", "Polish", "Portuguese", "Romanian",
|
|
27196
|
+
"Russian", "Serbian", "Slovak", "Slovenian", "Spanish", "Swahili",
|
|
27197
|
+
"Swedish", "Thai", "Turkish", "Ukrainian", "Urdu", "Vietnamese", "Welsh"
|
|
27198
|
+
]
|
|
27199
|
+
source_combo.addItems(available_languages)
|
|
27123
27200
|
# Try to match current UI selection
|
|
27124
27201
|
current_source = self.source_lang_combo.currentText() if hasattr(self, 'source_lang_combo') else "English"
|
|
27125
27202
|
source_idx = source_combo.findText(current_source)
|
|
@@ -27127,12 +27204,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
27127
27204
|
source_combo.setCurrentIndex(source_idx)
|
|
27128
27205
|
source_row.addWidget(source_combo)
|
|
27129
27206
|
lang_group_layout.addLayout(source_row)
|
|
27130
|
-
|
|
27207
|
+
|
|
27131
27208
|
# Target language
|
|
27132
27209
|
target_row = QHBoxLayout()
|
|
27133
27210
|
target_row.addWidget(QLabel("Target Language:"))
|
|
27134
27211
|
target_combo = QComboBox()
|
|
27135
|
-
target_combo.addItems(
|
|
27212
|
+
target_combo.addItems(available_languages)
|
|
27136
27213
|
# Try to match current UI selection
|
|
27137
27214
|
current_target = self.target_lang_combo.currentText() if hasattr(self, 'target_lang_combo') else "Dutch"
|
|
27138
27215
|
target_idx = target_combo.findText(current_target)
|
|
@@ -27140,15 +27217,15 @@ class SupervertalerQt(QMainWindow):
|
|
|
27140
27217
|
target_combo.setCurrentIndex(target_idx)
|
|
27141
27218
|
target_row.addWidget(target_combo)
|
|
27142
27219
|
lang_group_layout.addLayout(target_row)
|
|
27143
|
-
|
|
27220
|
+
|
|
27144
27221
|
lang_layout.addWidget(lang_group)
|
|
27145
|
-
|
|
27222
|
+
|
|
27146
27223
|
# Buttons
|
|
27147
27224
|
lang_buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
27148
27225
|
lang_buttons.accepted.connect(lang_dialog.accept)
|
|
27149
27226
|
lang_buttons.rejected.connect(lang_dialog.reject)
|
|
27150
27227
|
lang_layout.addWidget(lang_buttons)
|
|
27151
|
-
|
|
27228
|
+
|
|
27152
27229
|
if lang_dialog.exec() != QDialog.DialogCode.Accepted:
|
|
27153
27230
|
self.log("✗ User cancelled Trados import during language selection")
|
|
27154
27231
|
return
|
|
@@ -27799,7 +27876,19 @@ class SupervertalerQt(QMainWindow):
|
|
|
27799
27876
|
source_row = QHBoxLayout()
|
|
27800
27877
|
source_row.addWidget(QLabel("Source Language:"))
|
|
27801
27878
|
source_combo = QComboBox()
|
|
27802
|
-
|
|
27879
|
+
# Full language list (same as New Project dialog)
|
|
27880
|
+
available_languages = [
|
|
27881
|
+
"Afrikaans", "Albanian", "Arabic", "Armenian", "Basque", "Bengali",
|
|
27882
|
+
"Bulgarian", "Catalan", "Chinese (Simplified)", "Chinese (Traditional)",
|
|
27883
|
+
"Croatian", "Czech", "Danish", "Dutch", "English", "Estonian",
|
|
27884
|
+
"Finnish", "French", "Galician", "Georgian", "German", "Greek",
|
|
27885
|
+
"Hebrew", "Hindi", "Hungarian", "Icelandic", "Indonesian", "Irish",
|
|
27886
|
+
"Italian", "Japanese", "Korean", "Latvian", "Lithuanian", "Macedonian",
|
|
27887
|
+
"Malay", "Norwegian", "Persian", "Polish", "Portuguese", "Romanian",
|
|
27888
|
+
"Russian", "Serbian", "Slovak", "Slovenian", "Spanish", "Swahili",
|
|
27889
|
+
"Swedish", "Thai", "Turkish", "Ukrainian", "Urdu", "Vietnamese", "Welsh"
|
|
27890
|
+
]
|
|
27891
|
+
source_combo.addItems(available_languages)
|
|
27803
27892
|
# Try to match current UI selection
|
|
27804
27893
|
current_source = self.source_lang_combo.currentText() if hasattr(self, 'source_lang_combo') else "English"
|
|
27805
27894
|
source_idx = source_combo.findText(current_source)
|
|
@@ -27812,7 +27901,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
27812
27901
|
target_row = QHBoxLayout()
|
|
27813
27902
|
target_row.addWidget(QLabel("Target Language:"))
|
|
27814
27903
|
target_combo = QComboBox()
|
|
27815
|
-
target_combo.addItems(
|
|
27904
|
+
target_combo.addItems(available_languages)
|
|
27816
27905
|
# Try to match current UI selection
|
|
27817
27906
|
current_target = self.target_lang_combo.currentText() if hasattr(self, 'target_lang_combo') else "Dutch"
|
|
27818
27907
|
target_idx = target_combo.findText(current_target)
|
modules/database_manager.py
CHANGED
|
@@ -1477,120 +1477,225 @@ class DatabaseManager:
|
|
|
1477
1477
|
# TODO: Implement in Phase 3
|
|
1478
1478
|
pass
|
|
1479
1479
|
|
|
1480
|
-
def search_termbases(self, search_term: str, source_lang: str = None,
|
|
1480
|
+
def search_termbases(self, search_term: str, source_lang: str = None,
|
|
1481
1481
|
target_lang: str = None, project_id: str = None,
|
|
1482
|
-
min_length: int = 0) -> List[Dict]:
|
|
1482
|
+
min_length: int = 0, bidirectional: bool = True) -> List[Dict]:
|
|
1483
1483
|
"""
|
|
1484
|
-
Search termbases for matching
|
|
1485
|
-
|
|
1484
|
+
Search termbases for matching terms (bidirectional by default)
|
|
1485
|
+
|
|
1486
1486
|
Args:
|
|
1487
|
-
search_term:
|
|
1487
|
+
search_term: Term to search for
|
|
1488
1488
|
source_lang: Filter by source language (optional)
|
|
1489
1489
|
target_lang: Filter by target language (optional)
|
|
1490
1490
|
project_id: Filter by project (optional)
|
|
1491
1491
|
min_length: Minimum term length to return
|
|
1492
|
-
|
|
1492
|
+
bidirectional: If True, also search target_term and swap results (default True)
|
|
1493
|
+
|
|
1493
1494
|
Returns:
|
|
1494
1495
|
List of termbase hits, sorted by priority (lower = higher priority)
|
|
1496
|
+
Each result includes 'match_direction' ('source' or 'target') indicating
|
|
1497
|
+
which column matched. For 'target' matches, source_term and target_term
|
|
1498
|
+
are swapped so results are always oriented correctly for the current project.
|
|
1495
1499
|
"""
|
|
1496
1500
|
# Build query with filters - include termbase name and ranking via JOIN
|
|
1497
1501
|
# Note: termbase_id is stored as TEXT in termbase_terms but INTEGER in termbases
|
|
1498
1502
|
# Use CAST to ensure proper comparison
|
|
1499
1503
|
# IMPORTANT: Join with termbase_activation to get the ACTUAL priority for this project
|
|
1500
1504
|
# CRITICAL FIX: Also match when search_term starts with the glossary term
|
|
1501
|
-
# This handles cases like searching for "ca." when glossary has "ca."
|
|
1505
|
+
# This handles cases like searching for "ca." when glossary has "ca."
|
|
1502
1506
|
# AND searching for "ca" when glossary has "ca."
|
|
1503
1507
|
# We also strip trailing punctuation from glossary terms for comparison
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1508
|
+
|
|
1509
|
+
# Build matching conditions for a given column
|
|
1510
|
+
def build_match_conditions(column: str) -> str:
|
|
1511
|
+
return f"""(
|
|
1512
|
+
LOWER(t.{column}) = LOWER(?) OR
|
|
1513
|
+
LOWER(t.{column}) LIKE LOWER(?) OR
|
|
1514
|
+
LOWER(t.{column}) LIKE LOWER(?) OR
|
|
1515
|
+
LOWER(t.{column}) LIKE LOWER(?) OR
|
|
1516
|
+
LOWER(RTRIM(t.{column}, '.!?,;:')) = LOWER(?) OR
|
|
1517
|
+
LOWER(?) LIKE LOWER(t.{column}) || '%' OR
|
|
1518
|
+
LOWER(?) = LOWER(RTRIM(t.{column}, '.!?,;:'))
|
|
1519
|
+
)"""
|
|
1520
|
+
|
|
1521
|
+
# Build match params for one direction
|
|
1522
|
+
def build_match_params() -> list:
|
|
1523
|
+
return [
|
|
1524
|
+
search_term,
|
|
1525
|
+
f"{search_term} %",
|
|
1526
|
+
f"% {search_term}",
|
|
1527
|
+
f"% {search_term} %",
|
|
1528
|
+
search_term, # For RTRIM comparison
|
|
1529
|
+
search_term, # For reverse LIKE
|
|
1530
|
+
search_term # For reverse RTRIM comparison
|
|
1531
|
+
]
|
|
1532
|
+
|
|
1533
|
+
# Matching patterns:
|
|
1534
|
+
# 1. Exact match: column = search_term
|
|
1535
|
+
# 2. Glossary term starts with search: column LIKE "search_term %"
|
|
1536
|
+
# 3. Glossary term ends with search: column LIKE "% search_term"
|
|
1537
|
+
# 4. Glossary term contains search: column LIKE "% search_term %"
|
|
1538
|
+
# 5. Glossary term (stripped) = search_term: RTRIM(column) = search_term (handles "ca." = "ca")
|
|
1539
|
+
# 6. Search starts with glossary term: search_term LIKE column || '%'
|
|
1540
|
+
# 7. Search = glossary term stripped: search_term = RTRIM(column)
|
|
1541
|
+
|
|
1542
|
+
# Base SELECT for forward matches (source_term matches)
|
|
1543
|
+
base_select_forward = """
|
|
1544
|
+
SELECT
|
|
1545
|
+
t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
|
|
1507
1546
|
t.forbidden, t.source_lang, t.target_lang, t.definition, t.domain,
|
|
1508
1547
|
t.notes, t.project, t.client,
|
|
1509
1548
|
tb.name as termbase_name,
|
|
1510
1549
|
tb.source_lang as termbase_source_lang,
|
|
1511
1550
|
tb.target_lang as termbase_target_lang,
|
|
1512
1551
|
tb.is_project_termbase,
|
|
1513
|
-
COALESCE(ta.priority, tb.ranking) as ranking
|
|
1552
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
1553
|
+
'source' as match_direction
|
|
1514
1554
|
FROM termbase_terms t
|
|
1515
1555
|
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
1516
1556
|
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
1517
|
-
WHERE
|
|
1518
|
-
LOWER(t.source_term) = LOWER(?) OR
|
|
1519
|
-
LOWER(t.source_term) LIKE LOWER(?) OR
|
|
1520
|
-
LOWER(t.source_term) LIKE LOWER(?) OR
|
|
1521
|
-
LOWER(t.source_term) LIKE LOWER(?) OR
|
|
1522
|
-
LOWER(RTRIM(t.source_term, '.!?,;:')) = LOWER(?) OR
|
|
1523
|
-
LOWER(?) LIKE LOWER(t.source_term) || '%' OR
|
|
1524
|
-
LOWER(?) = LOWER(RTRIM(t.source_term, '.!?,;:'))
|
|
1525
|
-
)
|
|
1557
|
+
WHERE {match_conditions}
|
|
1526
1558
|
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
1527
|
-
"""
|
|
1528
|
-
|
|
1529
|
-
#
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1559
|
+
""".format(match_conditions=build_match_conditions('source_term'))
|
|
1560
|
+
|
|
1561
|
+
# Base SELECT for reverse matches (target_term matches) - swap source/target in output
|
|
1562
|
+
base_select_reverse = """
|
|
1563
|
+
SELECT
|
|
1564
|
+
t.id, t.target_term as source_term, t.source_term as target_term,
|
|
1565
|
+
t.termbase_id, t.priority,
|
|
1566
|
+
t.forbidden, t.target_lang as source_lang, t.source_lang as target_lang,
|
|
1567
|
+
t.definition, t.domain,
|
|
1568
|
+
t.notes, t.project, t.client,
|
|
1569
|
+
tb.name as termbase_name,
|
|
1570
|
+
tb.target_lang as termbase_source_lang,
|
|
1571
|
+
tb.source_lang as termbase_target_lang,
|
|
1572
|
+
tb.is_project_termbase,
|
|
1573
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
1574
|
+
'target' as match_direction
|
|
1575
|
+
FROM termbase_terms t
|
|
1576
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
1577
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
1578
|
+
WHERE {match_conditions}
|
|
1579
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
1580
|
+
""".format(match_conditions=build_match_conditions('target_term'))
|
|
1581
|
+
|
|
1582
|
+
# Build params
|
|
1583
|
+
project_param = project_id if project_id else 0
|
|
1584
|
+
forward_params = [project_param] + build_match_params()
|
|
1585
|
+
reverse_params = [project_param] + build_match_params()
|
|
1586
|
+
|
|
1587
|
+
# Build language filter conditions
|
|
1588
|
+
lang_conditions_forward = ""
|
|
1589
|
+
lang_conditions_reverse = ""
|
|
1590
|
+
lang_params_forward = []
|
|
1591
|
+
lang_params_reverse = []
|
|
1592
|
+
|
|
1548
1593
|
if source_lang:
|
|
1549
|
-
|
|
1550
|
-
|
|
1594
|
+
# For forward: filter on source_lang
|
|
1595
|
+
lang_conditions_forward += """ AND (
|
|
1596
|
+
t.source_lang = ? OR
|
|
1551
1597
|
(t.source_lang IS NULL AND tb.source_lang = ?) OR
|
|
1552
1598
|
(t.source_lang IS NULL AND tb.source_lang IS NULL)
|
|
1553
1599
|
)"""
|
|
1554
|
-
|
|
1555
|
-
|
|
1600
|
+
lang_params_forward.extend([source_lang, source_lang])
|
|
1601
|
+
# For reverse: source_lang becomes target_lang (swapped)
|
|
1602
|
+
lang_conditions_reverse += """ AND (
|
|
1603
|
+
t.target_lang = ? OR
|
|
1604
|
+
(t.target_lang IS NULL AND tb.target_lang = ?) OR
|
|
1605
|
+
(t.target_lang IS NULL AND tb.target_lang IS NULL)
|
|
1606
|
+
)"""
|
|
1607
|
+
lang_params_reverse.extend([source_lang, source_lang])
|
|
1608
|
+
|
|
1556
1609
|
if target_lang:
|
|
1557
|
-
|
|
1558
|
-
|
|
1610
|
+
# For forward: filter on target_lang
|
|
1611
|
+
lang_conditions_forward += """ AND (
|
|
1612
|
+
t.target_lang = ? OR
|
|
1559
1613
|
(t.target_lang IS NULL AND tb.target_lang = ?) OR
|
|
1560
1614
|
(t.target_lang IS NULL AND tb.target_lang IS NULL)
|
|
1561
1615
|
)"""
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1616
|
+
lang_params_forward.extend([target_lang, target_lang])
|
|
1617
|
+
# For reverse: target_lang becomes source_lang (swapped)
|
|
1618
|
+
lang_conditions_reverse += """ AND (
|
|
1619
|
+
t.source_lang = ? OR
|
|
1620
|
+
(t.source_lang IS NULL AND tb.source_lang = ?) OR
|
|
1621
|
+
(t.source_lang IS NULL AND tb.source_lang IS NULL)
|
|
1622
|
+
)"""
|
|
1623
|
+
lang_params_reverse.extend([target_lang, target_lang])
|
|
1624
|
+
|
|
1625
|
+
# Project filter conditions
|
|
1626
|
+
project_conditions = ""
|
|
1627
|
+
project_params = []
|
|
1565
1628
|
if project_id:
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1629
|
+
project_conditions = " AND (t.project_id = ? OR t.project_id IS NULL)"
|
|
1630
|
+
project_params = [project_id]
|
|
1631
|
+
|
|
1632
|
+
# Min length conditions
|
|
1633
|
+
min_len_forward = ""
|
|
1634
|
+
min_len_reverse = ""
|
|
1569
1635
|
if min_length > 0:
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
#
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1636
|
+
min_len_forward = f" AND LENGTH(t.source_term) >= {min_length}"
|
|
1637
|
+
min_len_reverse = f" AND LENGTH(t.target_term) >= {min_length}"
|
|
1638
|
+
|
|
1639
|
+
# Build forward query
|
|
1640
|
+
forward_query = base_select_forward + lang_conditions_forward + project_conditions + min_len_forward
|
|
1641
|
+
forward_params.extend(lang_params_forward)
|
|
1642
|
+
forward_params.extend(project_params)
|
|
1643
|
+
|
|
1644
|
+
if bidirectional:
|
|
1645
|
+
# Build reverse query
|
|
1646
|
+
reverse_query = base_select_reverse + lang_conditions_reverse + project_conditions + min_len_reverse
|
|
1647
|
+
reverse_params.extend(lang_params_reverse)
|
|
1648
|
+
reverse_params.extend(project_params)
|
|
1649
|
+
|
|
1650
|
+
# Combine with UNION and sort
|
|
1651
|
+
query = f"""
|
|
1652
|
+
SELECT * FROM (
|
|
1653
|
+
{forward_query}
|
|
1654
|
+
UNION ALL
|
|
1655
|
+
{reverse_query}
|
|
1656
|
+
) combined
|
|
1657
|
+
ORDER BY COALESCE(ranking, -1) ASC, source_term ASC
|
|
1658
|
+
"""
|
|
1659
|
+
params = forward_params + reverse_params
|
|
1660
|
+
else:
|
|
1661
|
+
# Original forward-only behavior
|
|
1662
|
+
query = forward_query + " ORDER BY COALESCE(ranking, -1) ASC, source_term ASC"
|
|
1663
|
+
params = forward_params
|
|
1664
|
+
|
|
1577
1665
|
self.cursor.execute(query, params)
|
|
1578
1666
|
results = []
|
|
1667
|
+
seen_combinations = set() # Track (source_term, target_term, termbase_id) to avoid duplicates
|
|
1668
|
+
|
|
1579
1669
|
for row in self.cursor.fetchall():
|
|
1580
1670
|
result_dict = dict(row)
|
|
1671
|
+
|
|
1672
|
+
# Deduplicate: same term pair from same termbase should only appear once
|
|
1673
|
+
# Prefer 'source' match over 'target' match
|
|
1674
|
+
combo_key = (
|
|
1675
|
+
result_dict.get('source_term', '').lower(),
|
|
1676
|
+
result_dict.get('target_term', '').lower(),
|
|
1677
|
+
result_dict.get('termbase_id')
|
|
1678
|
+
)
|
|
1679
|
+
if combo_key in seen_combinations:
|
|
1680
|
+
continue
|
|
1681
|
+
seen_combinations.add(combo_key)
|
|
1682
|
+
|
|
1581
1683
|
# SQLite stores booleans as 0/1, explicitly convert to Python bool
|
|
1582
1684
|
if 'is_project_termbase' in result_dict:
|
|
1583
1685
|
result_dict['is_project_termbase'] = bool(result_dict['is_project_termbase'])
|
|
1584
|
-
|
|
1686
|
+
|
|
1585
1687
|
# Fetch target synonyms for this term and include them in the result
|
|
1586
1688
|
term_id = result_dict.get('id')
|
|
1689
|
+
match_direction = result_dict.get('match_direction', 'source')
|
|
1587
1690
|
if term_id:
|
|
1588
1691
|
try:
|
|
1692
|
+
# For reverse matches, fetch 'source' synonyms since they become targets
|
|
1693
|
+
synonym_lang = 'source' if match_direction == 'target' else 'target'
|
|
1589
1694
|
self.cursor.execute("""
|
|
1590
1695
|
SELECT synonym_text, forbidden FROM termbase_synonyms
|
|
1591
|
-
WHERE term_id = ? AND language =
|
|
1696
|
+
WHERE term_id = ? AND language = ?
|
|
1592
1697
|
ORDER BY display_order ASC
|
|
1593
|
-
""", (term_id,))
|
|
1698
|
+
""", (term_id, synonym_lang))
|
|
1594
1699
|
synonyms = []
|
|
1595
1700
|
for syn_row in self.cursor.fetchall():
|
|
1596
1701
|
syn_text = syn_row[0]
|
|
@@ -1600,7 +1705,7 @@ class DatabaseManager:
|
|
|
1600
1705
|
result_dict['target_synonyms'] = synonyms
|
|
1601
1706
|
except Exception:
|
|
1602
1707
|
result_dict['target_synonyms'] = []
|
|
1603
|
-
|
|
1708
|
+
|
|
1604
1709
|
results.append(result_dict)
|
|
1605
1710
|
return results
|
|
1606
1711
|
|
modules/mqxliff_handler.py
CHANGED
|
@@ -159,9 +159,78 @@ class MQXLIFFHandler:
|
|
|
159
159
|
|
|
160
160
|
segment = FormattedSegment(trans_unit_id, plain_text, formatted_xml)
|
|
161
161
|
segments.append(segment)
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
return segments
|
|
164
|
-
|
|
164
|
+
|
|
165
|
+
def extract_bilingual_segments(self) -> List[Dict]:
|
|
166
|
+
"""
|
|
167
|
+
Extract all source AND target segments from the MQXLIFF file.
|
|
168
|
+
Used for importing pretranslated mqxliff files.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of dicts with 'id', 'source', 'target', 'status' keys
|
|
172
|
+
"""
|
|
173
|
+
segments = []
|
|
174
|
+
|
|
175
|
+
if self.body_element is None:
|
|
176
|
+
return segments
|
|
177
|
+
|
|
178
|
+
# Find all trans-unit elements (with or without namespace)
|
|
179
|
+
trans_units = self.body_element.findall('.//xliff:trans-unit', self.NAMESPACES)
|
|
180
|
+
if not trans_units:
|
|
181
|
+
trans_units = self.body_element.findall('.//trans-unit')
|
|
182
|
+
|
|
183
|
+
for trans_unit in trans_units:
|
|
184
|
+
trans_unit_id = trans_unit.get('id', 'unknown')
|
|
185
|
+
|
|
186
|
+
# Skip auxiliary segments (like hyperlink URLs with mq:nosplitjoin="true")
|
|
187
|
+
nosplitjoin = trans_unit.get('{MQXliff}nosplitjoin', 'false')
|
|
188
|
+
if nosplitjoin == 'true':
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# Find source element
|
|
192
|
+
source_elem = trans_unit.find('xliff:source', self.NAMESPACES)
|
|
193
|
+
if source_elem is None:
|
|
194
|
+
source_elem = trans_unit.find('source')
|
|
195
|
+
|
|
196
|
+
# Find target element
|
|
197
|
+
target_elem = trans_unit.find('xliff:target', self.NAMESPACES)
|
|
198
|
+
if target_elem is None:
|
|
199
|
+
target_elem = trans_unit.find('target')
|
|
200
|
+
|
|
201
|
+
source_text = ""
|
|
202
|
+
target_text = ""
|
|
203
|
+
|
|
204
|
+
if source_elem is not None:
|
|
205
|
+
source_text = self._extract_plain_text(source_elem)
|
|
206
|
+
|
|
207
|
+
if target_elem is not None:
|
|
208
|
+
target_text = self._extract_plain_text(target_elem)
|
|
209
|
+
|
|
210
|
+
# Get memoQ status if available
|
|
211
|
+
mq_status = trans_unit.get('{MQXliff}status', '')
|
|
212
|
+
|
|
213
|
+
# Map memoQ status to internal status
|
|
214
|
+
# memoQ statuses: "NotStarted", "Editing", "Confirmed", "Reviewed", "Rejected", etc.
|
|
215
|
+
status = 'not_started'
|
|
216
|
+
if mq_status in ['Confirmed', 'ProofRead', 'Reviewed']:
|
|
217
|
+
status = 'confirmed'
|
|
218
|
+
elif mq_status == 'Editing':
|
|
219
|
+
status = 'translated'
|
|
220
|
+
elif target_text.strip():
|
|
221
|
+
# Has target but unknown status - mark as pre-translated
|
|
222
|
+
status = 'pre_translated'
|
|
223
|
+
|
|
224
|
+
segments.append({
|
|
225
|
+
'id': trans_unit_id,
|
|
226
|
+
'source': source_text,
|
|
227
|
+
'target': target_text,
|
|
228
|
+
'status': status,
|
|
229
|
+
'mq_status': mq_status
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return segments
|
|
233
|
+
|
|
165
234
|
def _extract_plain_text(self, element: ET.Element) -> str:
|
|
166
235
|
"""
|
|
167
236
|
Recursively extract plain text from an XML element, stripping all tags.
|
modules/tm_metadata_manager.py
CHANGED
|
@@ -344,19 +344,22 @@ class TMMetadataManager:
|
|
|
344
344
|
"""Check if a TM is active for a project (or global when project_id=0)"""
|
|
345
345
|
if project_id is None:
|
|
346
346
|
return False # If None (not 0), default to inactive
|
|
347
|
-
|
|
347
|
+
|
|
348
348
|
try:
|
|
349
349
|
cursor = self.db_manager.cursor
|
|
350
|
-
|
|
350
|
+
|
|
351
|
+
# Check if TM is active for this project OR globally (project_id=0)
|
|
351
352
|
cursor.execute("""
|
|
352
|
-
SELECT is_active FROM tm_activation
|
|
353
|
-
WHERE tm_id = ? AND project_id = ?
|
|
353
|
+
SELECT is_active FROM tm_activation
|
|
354
|
+
WHERE tm_id = ? AND (project_id = ? OR project_id = 0)
|
|
355
|
+
ORDER BY project_id DESC
|
|
354
356
|
""", (tm_db_id, project_id))
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
|
|
358
|
+
# Return True if any activation is active (project-specific takes priority due to ORDER BY)
|
|
359
|
+
for row in cursor.fetchall():
|
|
360
|
+
if bool(row[0]):
|
|
361
|
+
return True
|
|
362
|
+
|
|
360
363
|
# If no activation record exists, TM is inactive by default
|
|
361
364
|
return False
|
|
362
365
|
except Exception as e:
|
|
@@ -382,15 +385,16 @@ class TMMetadataManager:
|
|
|
382
385
|
|
|
383
386
|
try:
|
|
384
387
|
cursor = self.db_manager.cursor
|
|
385
|
-
|
|
388
|
+
|
|
386
389
|
# Only return TMs that have been explicitly activated (is_active = 1)
|
|
390
|
+
# Include both project-specific activations AND global activations (project_id=0)
|
|
387
391
|
cursor.execute("""
|
|
388
|
-
SELECT tm.tm_id
|
|
392
|
+
SELECT DISTINCT tm.tm_id
|
|
389
393
|
FROM translation_memories tm
|
|
390
394
|
INNER JOIN tm_activation ta ON tm.id = ta.tm_id
|
|
391
|
-
WHERE ta.project_id = ? AND ta.is_active = 1
|
|
395
|
+
WHERE (ta.project_id = ? OR ta.project_id = 0) AND ta.is_active = 1
|
|
392
396
|
""", (project_id,))
|
|
393
|
-
|
|
397
|
+
|
|
394
398
|
return [row[0] for row in cursor.fetchall()]
|
|
395
399
|
except Exception as e:
|
|
396
400
|
self.log(f"✗ Error fetching active tm_ids: {e}")
|
|
@@ -422,16 +426,17 @@ class TMMetadataManager:
|
|
|
422
426
|
|
|
423
427
|
try:
|
|
424
428
|
cursor = self.db_manager.cursor
|
|
425
|
-
|
|
429
|
+
|
|
426
430
|
# Return TMs where Write checkbox is enabled (read_only = 0)
|
|
427
|
-
# AND the TM has an activation record for this project
|
|
431
|
+
# AND the TM has an activation record for this project OR for global (project_id=0)
|
|
432
|
+
# This ensures TMs created when no project was loaded still work
|
|
428
433
|
cursor.execute("""
|
|
429
|
-
SELECT tm.tm_id
|
|
434
|
+
SELECT DISTINCT tm.tm_id
|
|
430
435
|
FROM translation_memories tm
|
|
431
436
|
INNER JOIN tm_activation ta ON tm.id = ta.tm_id
|
|
432
|
-
WHERE ta.project_id = ? AND tm.read_only = 0
|
|
437
|
+
WHERE (ta.project_id = ? OR ta.project_id = 0) AND tm.read_only = 0
|
|
433
438
|
""", (project_id,))
|
|
434
|
-
|
|
439
|
+
|
|
435
440
|
return [row[0] for row in cursor.fetchall()]
|
|
436
441
|
except Exception as e:
|
|
437
442
|
self.log(f"✗ Error fetching writable tm_ids: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supervertaler
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.178
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Supervertaler.py,sha256=
|
|
1
|
+
Supervertaler.py,sha256=24LuStpwkjEpgE9gA0ng57sIJgOTkXS15riRgt-eSRM,2324855
|
|
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=yNtaJNAKtICBBSc5iyhIufzDn25k7OqkOuFeojmWuM4,87319
|
|
10
10
|
modules/database_migrations.py,sha256=tndJ4wV_2JBfPggMgO1tQRwdfRFZ9zwvADllCZE9CCk,15663
|
|
11
11
|
modules/dejavurtf_handler.py,sha256=8NZPPYtHga40SZCypHjPoJPmZTvm9rD-eEUUab7mjtg,28156
|
|
12
12
|
modules/document_analyzer.py,sha256=t1rVvqLaTcpQTEja228C7zZnh8dXshK4wA9t1E9aGVk,19524
|
|
@@ -28,7 +28,7 @@ modules/llm_superbench_ui.py,sha256=lmzsL8lt0KzFw-z8De1zb49Emnv7f1dZv_DJmoQz0bQ,
|
|
|
28
28
|
modules/local_llm_setup.py,sha256=33y-D_LKzkn2w8ejyjeKaovf_An6xQ98mKISoqe-Qjc,42661
|
|
29
29
|
modules/model_update_dialog.py,sha256=kEg0FuO1N-uj6QY5ZIj-FqdiLQuPuAY48pbuwT0HUGI,13113
|
|
30
30
|
modules/model_version_checker.py,sha256=41g7gcWvyrKPYeobaOGCMZLwAHgQmFwVF8zokodKae8,12741
|
|
31
|
-
modules/mqxliff_handler.py,sha256
|
|
31
|
+
modules/mqxliff_handler.py,sha256=TVtrf7ieGoxfoLxy4v4S7by9YImKypw1EY0wFpZO3Lo,28792
|
|
32
32
|
modules/non_translatables_manager.py,sha256=izorabiX6rSQzuBIvnY67wmu5vd85SbzexXccbmwPs4,27465
|
|
33
33
|
modules/pdf_rescue_Qt.py,sha256=9W_M0Zms4miapQbrqm-viHNCpaW39GL9VaKKFCJxpnE,80479
|
|
34
34
|
modules/pdf_rescue_tkinter.py,sha256=a4R_OUnn7X5O_XMR1roybrdu1aXoGCwwO-mwYB2ZpOg,39606
|
|
@@ -63,7 +63,7 @@ modules/termview_widget.py,sha256=O3ah7g-4Lb_iUctxl9sMyxh8V3A5I5PFxmy9iIH2Kgk,53
|
|
|
63
63
|
modules/theme_manager.py,sha256=EOI_5pM2bXAadw08bbl92TLN-w28lbw4Zi1E8vQ-kM0,16694
|
|
64
64
|
modules/tm_editor_dialog.py,sha256=AzGwq4QW641uFJdF8DljLTRRp4FLoYX3Pe4rlTjQWNg,3517
|
|
65
65
|
modules/tm_manager_qt.py,sha256=h2bvXkRuboHf_RRz9-5FX35GVRlpXgRDWeXyj1QWtPs,54406
|
|
66
|
-
modules/tm_metadata_manager.py,sha256=
|
|
66
|
+
modules/tm_metadata_manager.py,sha256=NTsaI_YjQnVOpU_scAwK9uR1Tcl9pzKD1GwLVy7sx2g,23590
|
|
67
67
|
modules/tmx_editor.py,sha256=n0CtdZI8f1fPRWmCqz5Ysxbnp556Qj-6Y56a-YIz6pY,59239
|
|
68
68
|
modules/tmx_editor_qt.py,sha256=PxBIUw_06PHYTBHsd8hZzVJXW8T0A0ljfz1Wjjsa4yU,117022
|
|
69
69
|
modules/tmx_generator.py,sha256=pNkxwdMLvSRMMru0lkB1gvViIpg9BQy1EVhRbwoef3k,9426
|
|
@@ -77,9 +77,9 @@ modules/unified_prompt_manager_qt.py,sha256=U89UFGG-M7BLetoaLAlma0x-n8SIyx682DhS
|
|
|
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.178.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
|
|
81
|
+
supervertaler-1.9.178.dist-info/METADATA,sha256=xnzgsUbZYFuATYJ-szDvevswcXb94_u7lC33CAzW-1A,5725
|
|
82
|
+
supervertaler-1.9.178.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
83
|
+
supervertaler-1.9.178.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
|
|
84
|
+
supervertaler-1.9.178.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
|
|
85
|
+
supervertaler-1.9.178.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|