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 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.176-beta"
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
- create_btn.clicked.connect(lambda: self._show_create_tm_dialog(tm_metadata_mgr, refresh_tm_list, project_id))
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
- t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
22358
- t.domain, t.notes, t.project, t.client, t.forbidden,
22359
- tb.is_project_termbase, tb.name as termbase_name,
22360
- COALESCE(ta.priority, tb.ranking) as ranking
22361
- FROM termbase_terms t
22362
- LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
22363
- LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
22364
- WHERE LOWER(t.source_term) LIKE ?
22365
- AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
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
- # Limit raw hits per word to keep batch worker light
22377
- query += " LIMIT 15"
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 <= 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 = 'target' AND forbidden = 0
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.extract_source_segments()
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.plain_text,
26664
- target="",
26665
- status=DEFAULT_STATUS.key,
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
- f"Successfully imported {len(segments)} segment(s) from memoQ XLIFF.\n\n"
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
- source_combo.addItems(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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
- source_combo.addItems(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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(["English", "Dutch", "German", "French", "Spanish", "Italian", "Portuguese", "Polish", "Chinese", "Japanese", "Korean", "Russian"])
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)
@@ -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 source terms
1485
-
1484
+ Search termbases for matching terms (bidirectional by default)
1485
+
1486
1486
  Args:
1487
- search_term: Source term to search for
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
- query = """
1505
- SELECT
1506
- t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
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
- # Matching patterns:
1529
- # 1. Exact match: source_term = search_term
1530
- # 2. Glossary term starts with search: source_term LIKE "search_term %"
1531
- # 3. Glossary term ends with search: source_term LIKE "% search_term"
1532
- # 4. Glossary term contains search: source_term LIKE "% search_term %"
1533
- # 5. Glossary term (stripped) = search_term: RTRIM(source_term) = search_term (handles "ca." = "ca")
1534
- # 6. Search starts with glossary term: search_term LIKE source_term || '%'
1535
- # 7. Search = glossary term stripped: search_term = RTRIM(source_term)
1536
- params = [
1537
- project_id if project_id else 0, # Use 0 if no project (won't match any activation records)
1538
- search_term,
1539
- f"{search_term} %",
1540
- f"% {search_term}",
1541
- f"% {search_term} %",
1542
- search_term, # For RTRIM comparison
1543
- search_term, # For reverse LIKE
1544
- search_term # For reverse RTRIM comparison
1545
- ]
1546
-
1547
- # Language filters - if term has no language, use termbase language for filtering
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
- query += """ AND (
1550
- t.source_lang = ? OR
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
- params.extend([source_lang, source_lang])
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
- query += """ AND (
1558
- t.target_lang = ? OR
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
- params.extend([target_lang, target_lang])
1563
-
1564
- # Project filter: match project-specific terms OR global terms (project_id IS NULL)
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
- query += " AND (t.project_id = ? OR t.project_id IS NULL)"
1567
- params.append(project_id)
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
- query += f" AND LENGTH(t.source_term) >= {min_length}"
1571
-
1572
- # Sort by ranking (lower number = higher priority)
1573
- # Project termbases (ranking IS NULL) appear first, then by ranking, then alphabetically
1574
- # Use COALESCE to treat NULL as -1 (highest priority)
1575
- query += " ORDER BY COALESCE(tb.ranking, -1) ASC, t.source_term ASC"
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 = 'target'
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
 
@@ -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.
@@ -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
- row = cursor.fetchone()
357
- if row:
358
- return bool(row[0])
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.176b0
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=ABqaS7k80j4w3EiJr52ogQVIJRAFFOD3NpaeDbzWX6k,2318994
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=FecvNLJa_mw0PZld8-LozUSSgtAq6wDZ8Mhq_u2aiA0,82563
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=-tUFubKsxkx6nW7aMDhuDaTBcVTPtVHV1HyzF0TbkVo,26205
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=_drzJ80q-mr1y9La8t0Cb8MXh91n3I-lEGDwSmoVI_4,23161
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.176b0.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
81
- supervertaler-1.9.176b0.dist-info/METADATA,sha256=NXnmveUpuH50y_2kAdkEavCbLiRpDYAUSIIc7YK0d-8,5727
82
- supervertaler-1.9.176b0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
83
- supervertaler-1.9.176b0.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
84
- supervertaler-1.9.176b0.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
85
- supervertaler-1.9.176b0.dist-info/RECORD,,
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,,