supervertaler 1.9.153__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.

Files changed (85) hide show
  1. Supervertaler.py +47886 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1878 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +932 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +329 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +252 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +333 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1172 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +715 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +659 -0
  76. modules/unified_prompt_manager_qt.py +3951 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.153.dist-info/METADATA +896 -0
  81. supervertaler-1.9.153.dist-info/RECORD +85 -0
  82. supervertaler-1.9.153.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.153.dist-info/top_level.txt +2 -0
modules/tmx_editor.py ADDED
@@ -0,0 +1,1461 @@
1
+ """
2
+ TMX Editor Module - Professional Translation Memory Editor
3
+
4
+ A standalone, nimble TMX editor inspired by Heartsome TMX Editor 8.
5
+ Can run independently or integrate with Supervertaler.
6
+
7
+ Key Features (inspired by Heartsome):
8
+ - Dual-language grid editor (source/target columns)
9
+ - Fast filtering by language, content, status
10
+ - In-place editing with validation
11
+ - TMX file validation and repair
12
+ - Header metadata editing
13
+ - Large file support with pagination
14
+ - Import/Export multiple formats
15
+ - Multi-language support (view any language pair)
16
+
17
+ Architecture:
18
+ - Standalone mode: Run this file directly
19
+ - Integrated mode: Called from Supervertaler as a module
20
+
21
+ Designer: Michael Beijer
22
+ Based on concepts from: Heartsome TMX Editor 8 (Java/Eclipse RCP)
23
+ License: MIT - Open Source and Free
24
+ """
25
+
26
+ import xml.etree.ElementTree as ET
27
+ from datetime import datetime
28
+ import os
29
+ import re
30
+ from typing import List, Dict, Optional, Tuple
31
+ from dataclasses import dataclass, field
32
+
33
+
34
+ @dataclass
35
+ class TmxSegment:
36
+ """Translation unit variant (segment in one language)"""
37
+ lang: str
38
+ text: str
39
+ creation_date: str = ""
40
+ creation_id: str = ""
41
+ change_date: str = ""
42
+ change_id: str = ""
43
+
44
+
45
+ @dataclass
46
+ class TmxTranslationUnit:
47
+ """Translation unit (TU) containing multiple language variants"""
48
+ tu_id: int
49
+ segments: Dict[str, TmxSegment] = field(default_factory=dict)
50
+ creation_date: str = ""
51
+ creation_id: str = ""
52
+ change_date: str = ""
53
+ change_id: str = ""
54
+ srclang: str = ""
55
+
56
+ def get_segment(self, lang: str) -> Optional[TmxSegment]:
57
+ """Get segment for specific language"""
58
+ return self.segments.get(lang)
59
+
60
+ def set_segment(self, lang: str, text: str):
61
+ """Set or update segment for specific language"""
62
+ if lang in self.segments:
63
+ self.segments[lang].text = text
64
+ self.segments[lang].change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
65
+ else:
66
+ self.segments[lang] = TmxSegment(lang=lang, text=text,
67
+ creation_date=datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))
68
+
69
+
70
+ @dataclass
71
+ class TmxHeader:
72
+ """TMX file header information"""
73
+ creation_tool: str = "Supervertaler TMX Editor"
74
+ creation_tool_version: str = "1.0"
75
+ segtype: str = "sentence"
76
+ o_tmf: str = "unknown"
77
+ adminlang: str = "en-US"
78
+ srclang: str = "en-US"
79
+ datatype: str = "unknown"
80
+ creation_date: str = ""
81
+ creation_id: str = ""
82
+ change_date: str = ""
83
+ change_id: str = ""
84
+
85
+ def __post_init__(self):
86
+ if not self.creation_date:
87
+ self.creation_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
88
+
89
+
90
+ class TmxFile:
91
+ """TMX file data model"""
92
+
93
+ def __init__(self):
94
+ self.header = TmxHeader()
95
+ self.translation_units: List[TmxTranslationUnit] = []
96
+ self.languages: List[str] = []
97
+ self.file_path: Optional[str] = None
98
+ self.is_modified: bool = False
99
+ self.version: str = "1.4"
100
+
101
+ def add_translation_unit(self, tu: TmxTranslationUnit):
102
+ """Add a translation unit and update language list"""
103
+ self.translation_units.append(tu)
104
+ for lang in tu.segments.keys():
105
+ if lang not in self.languages:
106
+ self.languages.append(lang)
107
+ self.is_modified = True
108
+
109
+ def get_languages(self) -> List[str]:
110
+ """Get list of all languages in the TMX file"""
111
+ return sorted(self.languages)
112
+
113
+ def get_tu_by_id(self, tu_id: int) -> Optional[TmxTranslationUnit]:
114
+ """Get translation unit by ID"""
115
+ for tu in self.translation_units:
116
+ if tu.tu_id == tu_id:
117
+ return tu
118
+ return None
119
+
120
+ def get_tu_count(self) -> int:
121
+ """Get total number of translation units"""
122
+ return len(self.translation_units)
123
+
124
+
125
+ class TmxParser:
126
+ """TMX file parser and writer"""
127
+
128
+ @staticmethod
129
+ def parse_file(file_path: str) -> TmxFile:
130
+ """Parse TMX file and return TmxFile object"""
131
+ tmx = TmxFile()
132
+ tmx.file_path = file_path
133
+
134
+ try:
135
+ tree = ET.parse(file_path)
136
+ root = tree.getroot()
137
+
138
+ # Parse header
139
+ header_elem = root.find('header')
140
+ if header_elem is not None:
141
+ tmx.header = TmxHeader(
142
+ creation_tool=header_elem.get('creationtool', 'unknown'),
143
+ creation_tool_version=header_elem.get('creationtoolversion', '1.0'),
144
+ segtype=header_elem.get('segtype', 'sentence'),
145
+ o_tmf=header_elem.get('o-tmf', 'unknown'),
146
+ adminlang=header_elem.get('adminlang', 'en-US'),
147
+ srclang=header_elem.get('srclang', 'en-US'),
148
+ datatype=header_elem.get('datatype', 'unknown'),
149
+ creation_date=header_elem.get('creationdate', ''),
150
+ creation_id=header_elem.get('creationid', ''),
151
+ change_date=header_elem.get('changedate', ''),
152
+ change_id=header_elem.get('changeid', '')
153
+ )
154
+
155
+ # Get TMX version
156
+ tmx.version = root.get('version', '1.4')
157
+
158
+ # Parse translation units
159
+ body = root.find('body')
160
+ if body is not None:
161
+ for idx, tu_elem in enumerate(body.findall('tu'), start=1):
162
+ tu = TmxTranslationUnit(
163
+ tu_id=idx,
164
+ creation_date=tu_elem.get('creationdate', ''),
165
+ creation_id=tu_elem.get('creationid', ''),
166
+ change_date=tu_elem.get('changedate', ''),
167
+ change_id=tu_elem.get('changeid', ''),
168
+ srclang=tu_elem.get('srclang', '')
169
+ )
170
+
171
+ # Parse segments (tuvs)
172
+ for tuv_elem in tu_elem.findall('tuv'):
173
+ lang = tuv_elem.get('{http://www.w3.org/XML/1998/namespace}lang',
174
+ tuv_elem.get('lang', 'unknown'))
175
+
176
+ seg_elem = tuv_elem.find('seg')
177
+ if seg_elem is not None:
178
+ # Get text content including any inline tags
179
+ text = TmxParser._get_element_text(seg_elem)
180
+
181
+ segment = TmxSegment(
182
+ lang=lang,
183
+ text=text,
184
+ creation_date=tuv_elem.get('creationdate', ''),
185
+ creation_id=tuv_elem.get('creationid', ''),
186
+ change_date=tuv_elem.get('changedate', ''),
187
+ change_id=tuv_elem.get('changeid', '')
188
+ )
189
+ tu.segments[lang] = segment
190
+
191
+ if lang not in tmx.languages:
192
+ tmx.languages.append(lang)
193
+
194
+ tmx.translation_units.append(tu)
195
+
196
+ tmx.is_modified = False
197
+ return tmx
198
+
199
+ except Exception as e:
200
+ raise Exception(f"Failed to parse TMX file: {str(e)}")
201
+
202
+ @staticmethod
203
+ def _get_element_text(element) -> str:
204
+ """Get all text content from element including tail text of children"""
205
+ text_parts = []
206
+ if element.text:
207
+ text_parts.append(element.text)
208
+ for child in element:
209
+ # Add child tag representation
210
+ text_parts.append(ET.tostring(child, encoding='unicode', method='html'))
211
+ if element.tail:
212
+ text_parts.append(element.tail)
213
+ return ''.join(text_parts).strip()
214
+
215
+ @staticmethod
216
+ def save_file(tmx: TmxFile, file_path: Optional[str] = None) -> bool:
217
+ """Save TMX file"""
218
+ if file_path is None:
219
+ file_path = tmx.file_path
220
+
221
+ if not file_path:
222
+ raise ValueError("No file path specified")
223
+
224
+ try:
225
+ # Create root element
226
+ root = ET.Element('tmx', version=tmx.version)
227
+
228
+ # Create header
229
+ header = ET.SubElement(root, 'header',
230
+ creationtool=tmx.header.creation_tool,
231
+ creationtoolversion=tmx.header.creation_tool_version,
232
+ segtype=tmx.header.segtype,
233
+ **{'o-tmf': tmx.header.o_tmf},
234
+ adminlang=tmx.header.adminlang,
235
+ srclang=tmx.header.srclang,
236
+ datatype=tmx.header.datatype)
237
+
238
+ if tmx.header.creation_date:
239
+ header.set('creationdate', tmx.header.creation_date)
240
+ if tmx.header.creation_id:
241
+ header.set('creationid', tmx.header.creation_id)
242
+ if tmx.header.change_date:
243
+ header.set('changedate', tmx.header.change_date)
244
+ if tmx.header.change_id:
245
+ header.set('changeid', tmx.header.change_id)
246
+
247
+ # Create body
248
+ body = ET.SubElement(root, 'body')
249
+
250
+ # Add translation units
251
+ for tu_data in tmx.translation_units:
252
+ tu = ET.SubElement(body, 'tu')
253
+
254
+ if tu_data.creation_date:
255
+ tu.set('creationdate', tu_data.creation_date)
256
+ if tu_data.creation_id:
257
+ tu.set('creationid', tu_data.creation_id)
258
+ if tu_data.change_date:
259
+ tu.set('changedate', tu_data.change_date)
260
+ if tu_data.change_id:
261
+ tu.set('changeid', tu_data.change_id)
262
+ if tu_data.srclang:
263
+ tu.set('srclang', tu_data.srclang)
264
+
265
+ # Add segments (sorted by language for consistency)
266
+ for lang in sorted(tu_data.segments.keys()):
267
+ segment = tu_data.segments[lang]
268
+ tuv = ET.SubElement(tu, 'tuv')
269
+ tuv.set('{http://www.w3.org/XML/1998/namespace}lang', lang)
270
+
271
+ if segment.creation_date:
272
+ tuv.set('creationdate', segment.creation_date)
273
+ if segment.creation_id:
274
+ tuv.set('creationid', segment.creation_id)
275
+ if segment.change_date:
276
+ tuv.set('changedate', segment.change_date)
277
+ if segment.change_id:
278
+ tuv.set('changeid', segment.change_id)
279
+
280
+ seg = ET.SubElement(tuv, 'seg')
281
+ seg.text = segment.text
282
+
283
+ # Write to file with pretty formatting
284
+ tree = ET.ElementTree(root)
285
+ ET.register_namespace('xml', 'http://www.w3.org/XML/1998/namespace')
286
+
287
+ # Pretty print
288
+ TmxParser._indent(root)
289
+
290
+ with open(file_path, 'wb') as f:
291
+ f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
292
+ f.write(b'<!DOCTYPE tmx SYSTEM "tmx14.dtd">\n')
293
+ tree.write(f, encoding='utf-8', xml_declaration=False)
294
+
295
+ tmx.file_path = file_path
296
+ tmx.is_modified = False
297
+ return True
298
+
299
+ except Exception as e:
300
+ raise Exception(f"Failed to save TMX file: {str(e)}")
301
+
302
+ @staticmethod
303
+ def _indent(elem, level=0):
304
+ """Add pretty-printing indentation to XML"""
305
+ i = "\n" + level * " "
306
+ if len(elem):
307
+ if not elem.text or not elem.text.strip():
308
+ elem.text = i + " "
309
+ if not elem.tail or not elem.tail.strip():
310
+ elem.tail = i
311
+ for child in elem:
312
+ TmxParser._indent(child, level + 1)
313
+ if not child.tail or not child.tail.strip():
314
+ child.tail = i
315
+ else:
316
+ if level and (not elem.tail or not elem.tail.strip()):
317
+ elem.tail = i
318
+
319
+
320
+ class TmxEditorUI:
321
+ """TMX Editor user interface"""
322
+
323
+ def __init__(self, parent=None, standalone=True):
324
+ """
325
+ Initialize TMX Editor UI
326
+
327
+ Args:
328
+ parent: Parent widget (None for standalone window)
329
+ standalone: If True, creates own window. If False, embeds in parent
330
+ """
331
+ self.tmx_file: Optional[TmxFile] = None
332
+ self.current_page = 0
333
+ self.items_per_page = 50
334
+ self.filtered_tus: List[TmxTranslationUnit] = []
335
+ self.src_lang = ""
336
+ self.tgt_lang = ""
337
+ self.filter_source = ""
338
+ self.filter_target = ""
339
+ self.standalone = standalone
340
+
341
+ if standalone:
342
+ # Create standalone window
343
+ self.root = tk.Tk()
344
+ self.root.title("TMX Editor - Supervertaler")
345
+ self.root.geometry("1200x700")
346
+ self.container = self.root
347
+ else:
348
+ # Embed in parent widget
349
+ self.root = parent
350
+ self.container = tk.Frame(parent)
351
+ self.container.pack(fill='both', expand=True)
352
+
353
+ self.create_ui()
354
+
355
+ if standalone:
356
+ # Bind close event
357
+ self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
358
+
359
+ def create_ui(self):
360
+ """Create the user interface"""
361
+ # Menu bar (only for standalone)
362
+ if self.standalone:
363
+ self.create_menu_bar()
364
+
365
+ # Toolbar
366
+ self.create_toolbar()
367
+
368
+ # Main content area
369
+ content_frame = tk.Frame(self.container)
370
+ content_frame.pack(fill='both', expand=True, padx=5, pady=5)
371
+
372
+ # Language selector panel
373
+ self.create_language_panel(content_frame)
374
+
375
+ # Filter panel
376
+ self.create_filter_panel(content_frame)
377
+
378
+ # Integrated edit panel (above the grid)
379
+ self.create_edit_panel(content_frame)
380
+
381
+ # Grid editor
382
+ self.create_grid_editor(content_frame)
383
+
384
+ # Pagination controls
385
+ self.create_pagination_controls(content_frame)
386
+
387
+ # Status bar
388
+ self.create_status_bar()
389
+
390
+ def create_menu_bar(self):
391
+ """Create menu bar for standalone mode"""
392
+ menubar = tk.Menu(self.root)
393
+ self.root.config(menu=menubar)
394
+
395
+ # File menu
396
+ file_menu = tk.Menu(menubar, tearoff=0)
397
+ menubar.add_cascade(label="File", menu=file_menu)
398
+ file_menu.add_command(label="New TMX", command=self.new_tmx)
399
+ file_menu.add_command(label="Open TMX...", command=self.open_tmx)
400
+ file_menu.add_command(label="Save", command=self.save_tmx, accelerator="Ctrl+S")
401
+ file_menu.add_command(label="Save As...", command=self.save_tmx_as)
402
+ file_menu.add_separator()
403
+ file_menu.add_command(label="Exit", command=self.on_closing)
404
+
405
+ # Edit menu
406
+ edit_menu = tk.Menu(menubar, tearoff=0)
407
+ menubar.add_cascade(label="Edit", menu=edit_menu)
408
+ edit_menu.add_command(label="Add Translation Unit", command=self.add_translation_unit)
409
+ edit_menu.add_command(label="Delete Selected", command=self.delete_selected_tu)
410
+ edit_menu.add_separator()
411
+ edit_menu.add_command(label="Find/Replace...", command=self.show_find_replace)
412
+
413
+ # View menu
414
+ view_menu = tk.Menu(menubar, tearoff=0)
415
+ menubar.add_cascade(label="View", menu=view_menu)
416
+ view_menu.add_command(label="TMX Header...", command=self.edit_header)
417
+ view_menu.add_command(label="Statistics", command=self.show_statistics)
418
+
419
+ # Tools menu
420
+ tools_menu = tk.Menu(menubar, tearoff=0)
421
+ menubar.add_cascade(label="Tools", menu=tools_menu)
422
+ tools_menu.add_command(label="Validate TMX", command=self.validate_tmx)
423
+ tools_menu.add_command(label="Export to...", command=self.export_tmx)
424
+
425
+ # Keyboard shortcuts
426
+ self.root.bind('<Control-s>', lambda e: self.save_tmx())
427
+ self.root.bind('<Control-o>', lambda e: self.open_tmx())
428
+ self.root.bind('<Control-n>', lambda e: self.new_tmx())
429
+
430
+ def create_toolbar(self):
431
+ """Create toolbar with common actions"""
432
+ toolbar = tk.Frame(self.container, bg='#f0f0f0', relief='raised', bd=1)
433
+ toolbar.pack(side='top', fill='x', padx=2, pady=2)
434
+
435
+ # File operations
436
+ tk.Button(toolbar, text="📁 New", command=self.new_tmx,
437
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
438
+ tk.Button(toolbar, text="📂 Open", command=self.open_tmx,
439
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
440
+ tk.Button(toolbar, text="💾 Save", command=self.save_tmx,
441
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
442
+
443
+ tk.Frame(toolbar, width=2, bg='#ccc').pack(side='left', fill='y', padx=5, pady=2)
444
+
445
+ # Edit operations
446
+ tk.Button(toolbar, text="➕ Add TU", command=self.add_translation_unit,
447
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
448
+ tk.Button(toolbar, text="❌ Delete", command=self.delete_selected_tu,
449
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
450
+
451
+ tk.Frame(toolbar, width=2, bg='#ccc').pack(side='left', fill='y', padx=5, pady=2)
452
+
453
+ # View operations
454
+ tk.Button(toolbar, text="ℹ️ Header", command=self.edit_header,
455
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
456
+ tk.Button(toolbar, text="📊 Stats", command=self.show_statistics,
457
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
458
+ tk.Button(toolbar, text="✓ Validate", command=self.validate_tmx,
459
+ relief='flat', padx=10, pady=5).pack(side='left', padx=2)
460
+
461
+ def create_language_panel(self, parent):
462
+ """Create language selection panel"""
463
+ lang_frame = tk.Frame(parent, bg='#e8f4f8', relief='ridge', bd=1)
464
+ lang_frame.pack(side='top', fill='x', pady=(0, 5))
465
+
466
+ tk.Label(lang_frame, text="📖 Language Pair:", bg='#e8f4f8',
467
+ font=('Segoe UI', 9, 'bold')).pack(side='left', padx=10, pady=5)
468
+
469
+ tk.Label(lang_frame, text="Source:", bg='#e8f4f8').pack(side='left', padx=(10, 2))
470
+ self.src_lang_combo = ttk.Combobox(lang_frame, width=15, state='readonly')
471
+ self.src_lang_combo.pack(side='left', padx=(0, 10))
472
+ self.src_lang_combo.bind('<<ComboboxSelected>>', self.on_language_changed)
473
+
474
+ tk.Label(lang_frame, text="→", bg='#e8f4f8', font=('Segoe UI', 12)).pack(side='left', padx=5)
475
+
476
+ tk.Label(lang_frame, text="Target:", bg='#e8f4f8').pack(side='left', padx=(0, 2))
477
+ self.tgt_lang_combo = ttk.Combobox(lang_frame, width=15, state='readonly')
478
+ self.tgt_lang_combo.pack(side='left', padx=(0, 10))
479
+ self.tgt_lang_combo.bind('<<ComboboxSelected>>', self.on_language_changed)
480
+
481
+ # Show all languages button
482
+ tk.Button(lang_frame, text="🌐 All Languages", command=self.show_all_languages,
483
+ relief='flat', bg='#4CAF50', fg='white', padx=10, pady=3).pack(side='right', padx=10)
484
+
485
+ def create_filter_panel(self, parent):
486
+ """Create filter panel"""
487
+ filter_frame = tk.Frame(parent, bg='#fff3cd', relief='ridge', bd=1)
488
+ filter_frame.pack(side='top', fill='x', pady=(0, 5))
489
+
490
+ tk.Label(filter_frame, text="🔍 Filter:", bg='#fff3cd',
491
+ font=('Segoe UI', 9, 'bold')).pack(side='left', padx=10, pady=5)
492
+
493
+ tk.Label(filter_frame, text="Source:", bg='#fff3cd').pack(side='left', padx=(10, 2))
494
+ self.filter_source_entry = tk.Entry(filter_frame, width=25)
495
+ self.filter_source_entry.pack(side='left', padx=(0, 10))
496
+ self.filter_source_entry.bind('<Return>', lambda e: self.apply_filters())
497
+
498
+ tk.Label(filter_frame, text="Target:", bg='#fff3cd').pack(side='left', padx=(0, 2))
499
+ self.filter_target_entry = tk.Entry(filter_frame, width=25)
500
+ self.filter_target_entry.pack(side='left', padx=(0, 10))
501
+ self.filter_target_entry.bind('<Return>', lambda e: self.apply_filters())
502
+
503
+ tk.Button(filter_frame, text="Apply Filter", command=self.apply_filters,
504
+ relief='flat', bg='#ff9800', fg='white', padx=10, pady=3).pack(side='left', padx=5)
505
+ tk.Button(filter_frame, text="Clear", command=self.clear_filters,
506
+ relief='flat', bg='#9e9e9e', fg='white', padx=10, pady=3).pack(side='left', padx=2)
507
+
508
+ def create_edit_panel(self, parent):
509
+ """Create integrated edit panel above the grid"""
510
+ edit_frame = tk.Frame(parent, bg='#e8f4f8', relief='ridge', bd=2)
511
+ edit_frame.pack(side='top', fill='x', pady=(0, 5))
512
+
513
+ # Header
514
+ header = tk.Frame(edit_frame, bg='#e8f4f8')
515
+ header.pack(fill='x', padx=5, pady=5)
516
+
517
+ tk.Label(header, text="✏️ Edit Translation Unit", bg='#e8f4f8',
518
+ font=('Segoe UI', 10, 'bold')).pack(side='left')
519
+
520
+ self.edit_id_label = tk.Label(header, text="(Double-click a segment to edit)",
521
+ bg='#e8f4f8', fg='#666', font=('Segoe UI', 9))
522
+ self.edit_id_label.pack(side='left', padx=10)
523
+
524
+ # Edit area
525
+ edit_content = tk.Frame(edit_frame, bg='#e8f4f8')
526
+ edit_content.pack(fill='both', expand=True, padx=10, pady=(0, 10))
527
+
528
+ # Source column
529
+ src_frame = tk.Frame(edit_content, bg='#e8f4f8')
530
+ src_frame.pack(side='left', fill='both', expand=True, padx=(0, 5))
531
+
532
+ tk.Label(src_frame, text="Source:", bg='#e8f4f8',
533
+ font=('Segoe UI', 9, 'bold')).pack(anchor='w')
534
+ self.edit_src_lang_label = tk.Label(src_frame, text="", bg='#e8f4f8',
535
+ fg='#666', font=('Segoe UI', 8))
536
+ self.edit_src_lang_label.pack(anchor='w')
537
+
538
+ self.edit_source_text = tk.Text(src_frame, height=4, wrap='word',
539
+ font=('Segoe UI', 9), state='disabled')
540
+ self.edit_source_text.pack(fill='both', expand=True)
541
+
542
+ # Target column
543
+ tgt_frame = tk.Frame(edit_content, bg='#e8f4f8')
544
+ tgt_frame.pack(side='left', fill='both', expand=True, padx=(5, 0))
545
+
546
+ tk.Label(tgt_frame, text="Target:", bg='#e8f4f8',
547
+ font=('Segoe UI', 9, 'bold')).pack(anchor='w')
548
+ self.edit_tgt_lang_label = tk.Label(tgt_frame, text="", bg='#e8f4f8',
549
+ fg='#666', font=('Segoe UI', 8))
550
+ self.edit_tgt_lang_label.pack(anchor='w')
551
+
552
+ self.edit_target_text = tk.Text(tgt_frame, height=4, wrap='word',
553
+ font=('Segoe UI', 9), state='disabled')
554
+ self.edit_target_text.pack(fill='both', expand=True)
555
+
556
+ # Buttons
557
+ btn_frame = tk.Frame(edit_frame, bg='#e8f4f8')
558
+ btn_frame.pack(fill='x', padx=10, pady=(0, 10))
559
+
560
+ self.save_edit_btn = tk.Button(btn_frame, text="💾 Save Changes",
561
+ command=self.save_integrated_edit,
562
+ bg='#4CAF50', fg='white', padx=15, pady=5,
563
+ state='disabled')
564
+ self.save_edit_btn.pack(side='left', padx=(0, 5))
565
+
566
+ self.cancel_edit_btn = tk.Button(btn_frame, text="❌ Cancel",
567
+ command=self.cancel_integrated_edit,
568
+ bg='#f44336', fg='white', padx=15, pady=5,
569
+ state='disabled')
570
+ self.cancel_edit_btn.pack(side='left')
571
+
572
+ # Store currently edited TU
573
+ self.current_edit_tu = None
574
+
575
+ def create_grid_editor(self, parent):
576
+ """Create grid editor for translation units using Treeview (supports selection & resizing)"""
577
+ # Container with scrollbar
578
+ grid_container = tk.Frame(parent)
579
+ grid_container.pack(fill='both', expand=True)
580
+
581
+ # Create Treeview with resizable columns
582
+ columns = ('ID', 'Source', 'Target')
583
+ self.tree = ttk.Treeview(grid_container, columns=columns, show='headings',
584
+ selectmode='browse', height=20)
585
+
586
+ # Configure columns (resizable by user)
587
+ self.tree.heading('ID', text='ID')
588
+ self.tree.heading('Source', text=f'Source ({self.src_lang if hasattr(self, "src_lang") and self.src_lang else ""})')
589
+ self.tree.heading('Target', text=f'Target ({self.tgt_lang if hasattr(self, "tgt_lang") and self.tgt_lang else ""})')
590
+
591
+ self.tree.column('ID', width=60, anchor='center', stretch=False)
592
+ self.tree.column('Source', width=500, anchor='w', stretch=True)
593
+ self.tree.column('Target', width=500, anchor='w', stretch=True)
594
+
595
+ # Scrollbars
596
+ vsb = ttk.Scrollbar(grid_container, orient='vertical', command=self.tree.yview)
597
+ hsb = ttk.Scrollbar(grid_container, orient='horizontal', command=self.tree.xview)
598
+ self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
599
+
600
+ # Grid layout
601
+ self.tree.grid(row=0, column=0, sticky='nsew')
602
+ vsb.grid(row=0, column=1, sticky='ns')
603
+ hsb.grid(row=1, column=0, sticky='ew')
604
+
605
+ grid_container.grid_rowconfigure(0, weight=1)
606
+ grid_container.grid_columnconfigure(0, weight=1)
607
+
608
+ # Configure tag for search match highlighting (background color for matching rows)
609
+ self.tree.tag_configure('match', background='#fffacd') # Light yellow for matching rows
610
+
611
+ # Double-click to edit
612
+ self.tree.bind('<Double-Button-1>', self.on_tree_double_click)
613
+
614
+ # Single-click to select and show in edit panel
615
+ self.tree.bind('<<TreeviewSelect>>', self.on_tree_select)
616
+
617
+ # Store TU map
618
+ self.tu_item_map = {} # Maps tree item ID to TU object
619
+
620
+ # Context menu
621
+ self.create_context_menu()
622
+
623
+ def create_pagination_controls(self, parent):
624
+ """Create pagination controls"""
625
+ page_frame = tk.Frame(parent, bg='#f0f0f0', relief='raised', bd=1)
626
+ page_frame.pack(side='bottom', fill='x', pady=(5, 0))
627
+
628
+ self.page_label = tk.Label(page_frame, text="Page 0 of 0 (0 TUs)",
629
+ bg='#f0f0f0', font=('Segoe UI', 9))
630
+ self.page_label.pack(side='left', padx=10, pady=5)
631
+
632
+ # Navigation buttons
633
+ nav_frame = tk.Frame(page_frame, bg='#f0f0f0')
634
+ nav_frame.pack(side='right', padx=10)
635
+
636
+ tk.Button(nav_frame, text="⏮️ First", command=self.first_page,
637
+ relief='flat', padx=8, pady=3).pack(side='left', padx=2)
638
+ tk.Button(nav_frame, text="◀️ Prev", command=self.prev_page,
639
+ relief='flat', padx=8, pady=3).pack(side='left', padx=2)
640
+ tk.Button(nav_frame, text="Next ▶️", command=self.next_page,
641
+ relief='flat', padx=8, pady=3).pack(side='left', padx=2)
642
+ tk.Button(nav_frame, text="Last ⏭️", command=self.last_page,
643
+ relief='flat', padx=8, pady=3).pack(side='left', padx=2)
644
+
645
+ def create_status_bar(self):
646
+ """Create status bar"""
647
+ self.status_bar = tk.Label(self.container, text="Ready", bd=1, relief='sunken',
648
+ anchor='w', bg='#e0e0e0')
649
+ self.status_bar.pack(side='bottom', fill='x')
650
+
651
+ def create_context_menu(self):
652
+ """Create right-click context menu"""
653
+ self.context_menu = tk.Menu(self.tree, tearoff=0)
654
+ self.context_menu.add_command(label="Edit", command=self.edit_selected_tu)
655
+ self.context_menu.add_separator()
656
+ self.context_menu.add_command(label="Refresh", command=self.refresh_current_page)
657
+
658
+ self.tree.bind('<Button-3>', self.show_context_menu)
659
+
660
+ def show_context_menu(self, event):
661
+ """Show context menu on right-click"""
662
+ # Select item under cursor
663
+ item = self.tree.identify_row(event.y)
664
+ if item:
665
+ self.tree.selection_set(item)
666
+ self.context_menu.post(event.x_root, event.y_root)
667
+
668
+ # ===== File Operations =====
669
+
670
+ def new_tmx(self):
671
+ """Create new TMX file"""
672
+ if self.tmx_file and self.tmx_file.is_modified:
673
+ if not messagebox.askyesno("Unsaved Changes",
674
+ "Current file has unsaved changes. Continue?"):
675
+ return
676
+
677
+ # Prompt for languages
678
+ dialog = tk.Toplevel(self.root if self.standalone else self.root.winfo_toplevel())
679
+ dialog.title("New TMX File")
680
+ dialog.geometry("400x250")
681
+ dialog.transient(self.root if self.standalone else self.root.winfo_toplevel())
682
+
683
+ tk.Label(dialog, text="Create New TMX File", font=('Segoe UI', 12, 'bold')).pack(pady=10)
684
+
685
+ form_frame = tk.Frame(dialog)
686
+ form_frame.pack(pady=10, padx=20, fill='both', expand=True)
687
+
688
+ tk.Label(form_frame, text="Source Language:").grid(row=0, column=0, sticky='w', pady=5)
689
+ src_entry = tk.Entry(form_frame, width=20)
690
+ src_entry.grid(row=0, column=1, pady=5, padx=10)
691
+ src_entry.insert(0, "en-US")
692
+
693
+ tk.Label(form_frame, text="Target Language:").grid(row=1, column=0, sticky='w', pady=5)
694
+ tgt_entry = tk.Entry(form_frame, width=20)
695
+ tgt_entry.grid(row=1, column=1, pady=5, padx=10)
696
+ tgt_entry.insert(0, "nl-NL")
697
+
698
+ tk.Label(form_frame, text="Creator ID:").grid(row=2, column=0, sticky='w', pady=5)
699
+ creator_entry = tk.Entry(form_frame, width=20)
700
+ creator_entry.grid(row=2, column=1, pady=5, padx=10)
701
+ creator_entry.insert(0, os.getlogin() if hasattr(os, 'getlogin') else "user")
702
+
703
+ def create():
704
+ src = src_entry.get().strip()
705
+ tgt = tgt_entry.get().strip()
706
+ creator = creator_entry.get().strip()
707
+
708
+ if not src or not tgt:
709
+ messagebox.showerror("Error", "Please enter both source and target languages")
710
+ return
711
+
712
+ self.tmx_file = TmxFile()
713
+ self.tmx_file.header.srclang = src
714
+ self.tmx_file.header.creation_id = creator
715
+ self.tmx_file.header.change_id = creator
716
+ self.tmx_file.languages = [src, tgt]
717
+
718
+ # Add one empty translation unit
719
+ tu = TmxTranslationUnit(tu_id=1, creation_id=creator)
720
+ tu.set_segment(src, "")
721
+ tu.set_segment(tgt, "")
722
+ self.tmx_file.add_translation_unit(tu)
723
+
724
+ self.src_lang = src
725
+ self.tgt_lang = tgt
726
+
727
+ self.refresh_ui()
728
+ dialog.destroy()
729
+ self.set_status(f"Created new TMX file: {src} → {tgt}")
730
+
731
+ btn_frame = tk.Frame(dialog)
732
+ btn_frame.pack(pady=10)
733
+ tk.Button(btn_frame, text="Create", command=create, bg='#4CAF50', fg='white',
734
+ padx=20, pady=5).pack(side='left', padx=5)
735
+ tk.Button(btn_frame, text="Cancel", command=dialog.destroy,
736
+ padx=20, pady=5).pack(side='left', padx=5)
737
+
738
+ def open_tmx(self):
739
+ """Open TMX file"""
740
+ if self.tmx_file and self.tmx_file.is_modified:
741
+ if not messagebox.askyesno("Unsaved Changes",
742
+ "Current file has unsaved changes. Continue?"):
743
+ return
744
+
745
+ file_path = filedialog.askopenfilename(
746
+ title="Open TMX File",
747
+ filetypes=[("TMX files", "*.tmx"), ("All files", "*.*")]
748
+ )
749
+
750
+ if file_path:
751
+ try:
752
+ self.tmx_file = TmxParser.parse_file(file_path)
753
+
754
+ # Set default languages (first two in file)
755
+ langs = self.tmx_file.get_languages()
756
+ if len(langs) >= 2:
757
+ self.src_lang = langs[0]
758
+ self.tgt_lang = langs[1]
759
+ elif len(langs) == 1:
760
+ self.src_lang = langs[0]
761
+ self.tgt_lang = langs[0]
762
+
763
+ self.refresh_ui()
764
+ self.set_status(f"Opened: {os.path.basename(file_path)} ({self.tmx_file.get_tu_count()} TUs)")
765
+
766
+ except Exception as e:
767
+ messagebox.showerror("Error", f"Failed to open TMX file:\n{str(e)}")
768
+
769
+ def save_tmx(self):
770
+ """Save TMX file"""
771
+ if not self.tmx_file:
772
+ return
773
+
774
+ if not self.tmx_file.file_path:
775
+ self.save_tmx_as()
776
+ return
777
+
778
+ try:
779
+ # Update change date
780
+ self.tmx_file.header.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
781
+
782
+ TmxParser.save_file(self.tmx_file)
783
+ self.set_status(f"Saved: {os.path.basename(self.tmx_file.file_path)}")
784
+
785
+ except Exception as e:
786
+ messagebox.showerror("Error", f"Failed to save TMX file:\n{str(e)}")
787
+
788
+ def save_tmx_as(self):
789
+ """Save TMX file with new name"""
790
+ if not self.tmx_file:
791
+ return
792
+
793
+ file_path = filedialog.asksaveasfilename(
794
+ title="Save TMX File As",
795
+ defaultextension=".tmx",
796
+ filetypes=[("TMX files", "*.tmx"), ("All files", "*.*")]
797
+ )
798
+
799
+ if file_path:
800
+ try:
801
+ # Update change date
802
+ self.tmx_file.header.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
803
+
804
+ TmxParser.save_file(self.tmx_file, file_path)
805
+ self.set_status(f"Saved as: {os.path.basename(file_path)}")
806
+
807
+ except Exception as e:
808
+ messagebox.showerror("Error", f"Failed to save TMX file:\n{str(e)}")
809
+
810
+ # ===== Edit Operations =====
811
+
812
+ def add_translation_unit(self):
813
+ """Add new translation unit"""
814
+ if not self.tmx_file:
815
+ messagebox.showwarning("Warning", "Please create or open a TMX file first")
816
+ return
817
+
818
+ if not self.src_lang or not self.tgt_lang:
819
+ messagebox.showwarning("Warning", "Please select source and target languages")
820
+ return
821
+
822
+ # Create new TU
823
+ new_id = self.tmx_file.get_tu_count() + 1
824
+ tu = TmxTranslationUnit(tu_id=new_id,
825
+ creation_id=self.tmx_file.header.creation_id)
826
+ tu.set_segment(self.src_lang, "")
827
+ tu.set_segment(self.tgt_lang, "")
828
+
829
+ self.tmx_file.add_translation_unit(tu)
830
+ self.apply_filters() # Refresh view
831
+ self.set_status(f"Added TU #{new_id}")
832
+
833
+ def delete_selected_tu(self):
834
+ """Delete translation unit (placeholder)"""
835
+ messagebox.showinfo("Info", "To delete a TU, double-click to edit it.\nDeletion feature coming soon.")
836
+
837
+ def edit_selected_tu(self):
838
+ """Edit selected translation unit (placeholder)"""
839
+ messagebox.showinfo("Info", "Double-click on a TU to edit it.")
840
+
841
+ def open_edit_dialog(self, tu: TmxTranslationUnit, tree_item=None):
842
+ """Open dialog to edit translation unit"""
843
+ dialog = tk.Toplevel(self.root if self.standalone else self.root.winfo_toplevel())
844
+ dialog.title(f"Edit TU #{tu.tu_id}")
845
+ dialog.geometry("800x400")
846
+ dialog.transient(self.root if self.standalone else self.root.winfo_toplevel())
847
+
848
+ # Source text
849
+ tk.Label(dialog, text=f"Source ({self.src_lang}):", font=('Segoe UI', 10, 'bold')).pack(pady=(10, 2))
850
+ src_text = tk.Text(dialog, height=8, wrap='word', font=('Segoe UI', 10))
851
+ src_text.pack(fill='both', expand=True, padx=10, pady=5)
852
+
853
+ src_seg = tu.get_segment(self.src_lang)
854
+ if src_seg:
855
+ src_text.insert('1.0', src_seg.text)
856
+
857
+ # Target text
858
+ tk.Label(dialog, text=f"Target ({self.tgt_lang}):", font=('Segoe UI', 10, 'bold')).pack(pady=(5, 2))
859
+ tgt_text = tk.Text(dialog, height=8, wrap='word', font=('Segoe UI', 10))
860
+ tgt_text.pack(fill='both', expand=True, padx=10, pady=5)
861
+
862
+ tgt_seg = tu.get_segment(self.tgt_lang)
863
+ if tgt_seg:
864
+ tgt_text.insert('1.0', tgt_seg.text)
865
+
866
+ def save_changes():
867
+ # Update segments
868
+ tu.set_segment(self.src_lang, src_text.get('1.0', 'end-1c'))
869
+ tu.set_segment(self.tgt_lang, tgt_text.get('1.0', 'end-1c'))
870
+ tu.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
871
+
872
+ self.tmx_file.is_modified = True
873
+
874
+ # Refresh display
875
+ self.refresh_current_page()
876
+
877
+ dialog.destroy()
878
+ self.set_status(f"Updated TU #{tu.tu_id}")
879
+
880
+ # Buttons
881
+ btn_frame = tk.Frame(dialog)
882
+ btn_frame.pack(pady=10)
883
+ tk.Button(btn_frame, text="Save", command=save_changes, bg='#4CAF50', fg='white',
884
+ padx=20, pady=5).pack(side='left', padx=5)
885
+ tk.Button(btn_frame, text="Cancel", command=dialog.destroy,
886
+ padx=20, pady=5).pack(side='left', padx=5)
887
+
888
+ # Buttons
889
+ btn_frame = tk.Frame(dialog)
890
+ btn_frame.pack(pady=10)
891
+ tk.Button(btn_frame, text="Save", command=save_changes, bg='#4CAF50', fg='white',
892
+ padx=20, pady=5).pack(side='left', padx=5)
893
+ tk.Button(btn_frame, text="Cancel", command=dialog.destroy,
894
+ padx=20, pady=5).pack(side='left', padx=5)
895
+
896
+ def copy_source_to_target(self):
897
+ """Copy source text to target (placeholder)"""
898
+ messagebox.showinfo("Info", "Double-click a TU to edit it manually.")
899
+
900
+ # ===== View Operations =====
901
+
902
+ def on_language_changed(self, event=None):
903
+ """Handle language selection change"""
904
+ self.src_lang = self.src_lang_combo.get()
905
+ self.tgt_lang = self.tgt_lang_combo.get()
906
+ self.apply_filters()
907
+
908
+ def show_all_languages(self):
909
+ """Show dialog with all languages in TMX"""
910
+ if not self.tmx_file:
911
+ return
912
+
913
+ dialog = tk.Toplevel(self.root if self.standalone else self.root.winfo_toplevel())
914
+ dialog.title("All Languages")
915
+ dialog.geometry("400x400")
916
+
917
+ tk.Label(dialog, text="Languages in this TMX file:",
918
+ font=('Segoe UI', 11, 'bold')).pack(pady=10)
919
+
920
+ # List of languages
921
+ lang_frame = tk.Frame(dialog)
922
+ lang_frame.pack(fill='both', expand=True, padx=20, pady=10)
923
+
924
+ listbox = tk.Listbox(lang_frame, font=('Segoe UI', 10))
925
+ scrollbar = tk.Scrollbar(lang_frame, orient='vertical', command=listbox.yview)
926
+ listbox.config(yscrollcommand=scrollbar.set)
927
+
928
+ for lang in self.tmx_file.get_languages():
929
+ listbox.insert('end', lang)
930
+
931
+ listbox.pack(side='left', fill='both', expand=True)
932
+ scrollbar.pack(side='right', fill='y')
933
+
934
+ tk.Button(dialog, text="Close", command=dialog.destroy,
935
+ padx=20, pady=5).pack(pady=10)
936
+
937
+ def edit_header(self):
938
+ """Edit TMX header metadata"""
939
+ if not self.tmx_file:
940
+ messagebox.showwarning("Warning", "Please create or open a TMX file first")
941
+ return
942
+
943
+ dialog = tk.Toplevel(self.root if self.standalone else self.root.winfo_toplevel())
944
+ dialog.title("TMX Header Metadata")
945
+ dialog.geometry("500x500")
946
+
947
+ tk.Label(dialog, text="TMX Header Information",
948
+ font=('Segoe UI', 12, 'bold')).pack(pady=10)
949
+
950
+ # Form
951
+ form_frame = tk.Frame(dialog)
952
+ form_frame.pack(fill='both', expand=True, padx=20, pady=10)
953
+
954
+ fields = {}
955
+ row = 0
956
+
957
+ for field_name, field_label in [
958
+ ('creation_tool', 'Creation Tool'),
959
+ ('creation_tool_version', 'Tool Version'),
960
+ ('segtype', 'Segment Type'),
961
+ ('o_tmf', 'O-TMF'),
962
+ ('adminlang', 'Admin Language'),
963
+ ('srclang', 'Source Language'),
964
+ ('datatype', 'Data Type'),
965
+ ('creation_id', 'Creator ID'),
966
+ ('change_id', 'Last Modified By')
967
+ ]:
968
+ tk.Label(form_frame, text=f"{field_label}:").grid(row=row, column=0, sticky='w', pady=5)
969
+ entry = tk.Entry(form_frame, width=30)
970
+ entry.grid(row=row, column=1, pady=5, padx=10, sticky='ew')
971
+ entry.insert(0, getattr(self.tmx_file.header, field_name, ''))
972
+ fields[field_name] = entry
973
+ row += 1
974
+
975
+ form_frame.grid_columnconfigure(1, weight=1)
976
+
977
+ # Read-only dates
978
+ tk.Label(form_frame, text="Creation Date:").grid(row=row, column=0, sticky='w', pady=5)
979
+ tk.Label(form_frame, text=self.tmx_file.header.creation_date,
980
+ fg='#666').grid(row=row, column=1, sticky='w', pady=5, padx=10)
981
+ row += 1
982
+
983
+ tk.Label(form_frame, text="Last Modified:").grid(row=row, column=0, sticky='w', pady=5)
984
+ tk.Label(form_frame, text=self.tmx_file.header.change_date,
985
+ fg='#666').grid(row=row, column=1, sticky='w', pady=5, padx=10)
986
+
987
+ def save_header():
988
+ for field_name, entry in fields.items():
989
+ setattr(self.tmx_file.header, field_name, entry.get())
990
+
991
+ self.tmx_file.header.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
992
+ self.tmx_file.is_modified = True
993
+
994
+ dialog.destroy()
995
+ self.set_status("Header updated")
996
+
997
+ # Buttons
998
+ btn_frame = tk.Frame(dialog)
999
+ btn_frame.pack(pady=10)
1000
+ tk.Button(btn_frame, text="Save", command=save_header, bg='#4CAF50', fg='white',
1001
+ padx=20, pady=5).pack(side='left', padx=5)
1002
+ tk.Button(btn_frame, text="Cancel", command=dialog.destroy,
1003
+ padx=20, pady=5).pack(side='left', padx=5)
1004
+
1005
+ def show_statistics(self):
1006
+ """Show TMX file statistics"""
1007
+ if not self.tmx_file:
1008
+ return
1009
+
1010
+ total_tus = self.tmx_file.get_tu_count()
1011
+ languages = self.tmx_file.get_languages()
1012
+
1013
+ # Count segments per language
1014
+ lang_counts = {lang: 0 for lang in languages}
1015
+ total_chars = {lang: 0 for lang in languages}
1016
+
1017
+ for tu in self.tmx_file.translation_units:
1018
+ for lang, segment in tu.segments.items():
1019
+ lang_counts[lang] += 1
1020
+ total_chars[lang] += len(segment.text)
1021
+
1022
+ # Build statistics message
1023
+ stats = f"TMX File Statistics\n\n"
1024
+ stats += f"Total Translation Units: {total_tus}\n"
1025
+ stats += f"Languages: {len(languages)}\n\n"
1026
+ stats += "Segments per Language:\n"
1027
+
1028
+ for lang in sorted(languages):
1029
+ avg_chars = total_chars[lang] / lang_counts[lang] if lang_counts[lang] > 0 else 0
1030
+ stats += f" {lang}: {lang_counts[lang]} segments (avg {avg_chars:.1f} chars)\n"
1031
+
1032
+ messagebox.showinfo("Statistics", stats)
1033
+
1034
+ # ===== Filter Operations =====
1035
+
1036
+ def apply_filters(self):
1037
+ """Apply filters and refresh grid"""
1038
+ if not self.tmx_file:
1039
+ return
1040
+
1041
+ self.filter_source = self.filter_source_entry.get().lower()
1042
+ self.filter_target = self.filter_target_entry.get().lower()
1043
+
1044
+ # Filter TUs
1045
+ self.filtered_tus = []
1046
+ for tu in self.tmx_file.translation_units:
1047
+ src_seg = tu.get_segment(self.src_lang)
1048
+ tgt_seg = tu.get_segment(self.tgt_lang)
1049
+
1050
+ src_text = src_seg.text.lower() if src_seg else ""
1051
+ tgt_text = tgt_seg.text.lower() if tgt_seg else ""
1052
+
1053
+ # Apply filters
1054
+ if self.filter_source and self.filter_source not in src_text:
1055
+ continue
1056
+ if self.filter_target and self.filter_target not in tgt_text:
1057
+ continue
1058
+
1059
+ self.filtered_tus.append(tu)
1060
+
1061
+ self.current_page = 0
1062
+ self.refresh_current_page()
1063
+
1064
+ def clear_filters(self):
1065
+ """Clear all filters"""
1066
+ self.filter_source_entry.delete(0, 'end')
1067
+ self.filter_target_entry.delete(0, 'end')
1068
+ self.filter_source = ""
1069
+ self.filter_target = ""
1070
+ self.apply_filters()
1071
+
1072
+ # ===== Helper Methods =====
1073
+
1074
+ def _highlight_text_in_range(self, text_widget, start_index, end_index, search_term):
1075
+ """Highlight all occurrences of search_term in the given range (from concordance search)"""
1076
+ if not search_term:
1077
+ return
1078
+
1079
+ search_term_lower = search_term.lower()
1080
+ start_line = int(start_index.split('.')[0])
1081
+ end_line = int(end_index.split('.')[0])
1082
+
1083
+ for line_num in range(start_line, end_line + 1):
1084
+ line_text = text_widget.get(f"{line_num}.0", f"{line_num}.end")
1085
+ line_text_lower = line_text.lower()
1086
+
1087
+ # Find all occurrences in this line
1088
+ start_pos = 0
1089
+ while True:
1090
+ pos = line_text_lower.find(search_term_lower, start_pos)
1091
+ if pos == -1:
1092
+ break
1093
+
1094
+ # Apply highlight tag
1095
+ highlight_start = f"{line_num}.{pos}"
1096
+ highlight_end = f"{line_num}.{pos + len(search_term_lower)}"
1097
+ text_widget.tag_add('highlight', highlight_start, highlight_end)
1098
+
1099
+ start_pos = pos + len(search_term_lower)
1100
+
1101
+ def highlight_search_term_in_text(self, text, search_term):
1102
+ """Highlight search term in text using Unicode bold characters
1103
+
1104
+ Args:
1105
+ text: Text to search in
1106
+ search_term: Term to highlight
1107
+
1108
+ Returns:
1109
+ Text with search term converted to Unicode bold
1110
+ """
1111
+ if not search_term or not text:
1112
+ return text
1113
+
1114
+ # Case-insensitive search
1115
+ search_lower = search_term.lower()
1116
+ text_lower = text.lower()
1117
+
1118
+ # Find all occurrences
1119
+ result = []
1120
+ last_pos = 0
1121
+
1122
+ pos = text_lower.find(search_lower)
1123
+ while pos != -1:
1124
+ # Add text before match
1125
+ result.append(text[last_pos:pos])
1126
+
1127
+ # Convert match to Unicode bold
1128
+ match_text = text[pos:pos + len(search_term)]
1129
+ bold_text = self._to_unicode_bold(match_text)
1130
+ result.append(bold_text)
1131
+
1132
+ last_pos = pos + len(search_term)
1133
+ pos = text_lower.find(search_lower, last_pos)
1134
+
1135
+ # Add remaining text
1136
+ result.append(text[last_pos:])
1137
+
1138
+ return ''.join(result)
1139
+
1140
+ def _to_unicode_bold(self, text):
1141
+ """Convert text to Unicode bold characters
1142
+
1143
+ Unicode has Mathematical Bold characters that display as bold.
1144
+ This works in Treeview where normal formatting doesn't.
1145
+ """
1146
+ # Unicode bold character mappings
1147
+ bold_map = {
1148
+ # Uppercase A-Z: U+1D400 to U+1D419
1149
+ **{chr(ord('A') + i): chr(0x1D400 + i) for i in range(26)},
1150
+ # Lowercase a-z: U+1D41A to U+1D433
1151
+ **{chr(ord('a') + i): chr(0x1D41A + i) for i in range(26)},
1152
+ # Digits 0-9: U+1D7CE to U+1D7D7
1153
+ **{chr(ord('0') + i): chr(0x1D7CE + i) for i in range(10)},
1154
+ }
1155
+
1156
+ # Convert each character
1157
+ return ''.join(bold_map.get(c, c) for c in text)
1158
+
1159
+ # ===== Pagination =====
1160
+
1161
+ def refresh_current_page(self):
1162
+ """Refresh current page in Treeview grid"""
1163
+ if not self.tmx_file:
1164
+ return
1165
+
1166
+ # Clear tree
1167
+ for item in self.tree.get_children():
1168
+ self.tree.delete(item)
1169
+ self.tu_item_map.clear()
1170
+
1171
+ # Update column headers with language codes
1172
+ self.tree.heading('Source', text=f'Source ({self.src_lang})')
1173
+ self.tree.heading('Target', text=f'Target ({self.tgt_lang})')
1174
+
1175
+ # Calculate page range
1176
+ total_items = len(self.filtered_tus)
1177
+ total_pages = (total_items + self.items_per_page - 1) // self.items_per_page
1178
+
1179
+ if total_pages == 0:
1180
+ self.page_label.config(text="No items")
1181
+ return
1182
+
1183
+ start_idx = self.current_page * self.items_per_page
1184
+ end_idx = min(start_idx + self.items_per_page, total_items)
1185
+
1186
+ # Add items to tree
1187
+ for tu in self.filtered_tus[start_idx:end_idx]:
1188
+ src_seg = tu.get_segment(self.src_lang)
1189
+ tgt_seg = tu.get_segment(self.tgt_lang)
1190
+
1191
+ src_text = src_seg.text if src_seg else ""
1192
+ tgt_text = tgt_seg.text if tgt_seg else ""
1193
+
1194
+ # Clean up text for display (remove newlines)
1195
+ src_display = src_text.replace('\n', ' ').replace('\r', '')
1196
+ tgt_display = tgt_text.replace('\n', ' ').replace('\r', '')
1197
+
1198
+ # Highlight search terms with markers
1199
+ if self.filter_source:
1200
+ src_display = self.highlight_search_term_in_text(src_display, self.filter_source)
1201
+ if self.filter_target:
1202
+ tgt_display = self.highlight_search_term_in_text(tgt_display, self.filter_target)
1203
+
1204
+ # Check if this row matches search (for light background highlighting)
1205
+ tags = ()
1206
+ if self.filter_source and self.filter_source.lower() in src_text.lower():
1207
+ tags = ('match',)
1208
+ elif self.filter_target and self.filter_target.lower() in tgt_text.lower():
1209
+ tags = ('match',)
1210
+
1211
+ # Insert into tree
1212
+ item_id = self.tree.insert('', 'end', values=(tu.tu_id, src_display, tgt_display), tags=tags)
1213
+
1214
+ # Store TU reference
1215
+ self.tu_item_map[item_id] = tu
1216
+
1217
+ # Update page label
1218
+ self.page_label.config(text=f"Page {self.current_page + 1} of {total_pages} ({total_items} TUs)")
1219
+
1220
+ def on_tree_select(self, event):
1221
+ """Handle tree selection - load into edit panel"""
1222
+ selected = self.tree.selection()
1223
+ if selected:
1224
+ item_id = selected[0]
1225
+ if item_id in self.tu_item_map:
1226
+ tu = self.tu_item_map[item_id]
1227
+ self.load_tu_into_edit_panel(tu)
1228
+
1229
+ def on_tree_double_click(self, event):
1230
+ """Handle double-click on tree - load into edit panel and focus"""
1231
+ selected = self.tree.selection()
1232
+ if selected:
1233
+ item_id = selected[0]
1234
+ if item_id in self.tu_item_map:
1235
+ tu = self.tu_item_map[item_id]
1236
+ self.load_tu_into_edit_panel(tu)
1237
+ # Focus on target text for editing
1238
+ self.edit_target_text.focus_set()
1239
+
1240
+ def edit_selected_tu(self):
1241
+ """Edit selected TU from context menu"""
1242
+ selected = self.tree.selection()
1243
+ if selected:
1244
+ item_id = selected[0]
1245
+ if item_id in self.tu_item_map:
1246
+ tu = self.tu_item_map[item_id]
1247
+ self.load_tu_into_edit_panel(tu)
1248
+ self.edit_target_text.focus_set()
1249
+
1250
+ def load_tu_into_edit_panel(self, tu: TmxTranslationUnit):
1251
+ """Load a TU into the integrated edit panel"""
1252
+ self.current_edit_tu = tu
1253
+
1254
+ # Update labels
1255
+ self.edit_id_label.config(text=f"Editing TU #{tu.tu_id}")
1256
+ self.edit_src_lang_label.config(text=f"({self.src_lang})")
1257
+ self.edit_tgt_lang_label.config(text=f"({self.tgt_lang})")
1258
+
1259
+ # Load text
1260
+ self.edit_source_text.config(state='normal')
1261
+ self.edit_source_text.delete('1.0', 'end')
1262
+ src_seg = tu.get_segment(self.src_lang)
1263
+ if src_seg:
1264
+ self.edit_source_text.insert('1.0', src_seg.text)
1265
+
1266
+ self.edit_target_text.config(state='normal')
1267
+ self.edit_target_text.delete('1.0', 'end')
1268
+ tgt_seg = tu.get_segment(self.tgt_lang)
1269
+ if tgt_seg:
1270
+ self.edit_target_text.insert('1.0', tgt_seg.text)
1271
+
1272
+ # Enable buttons
1273
+ self.save_edit_btn.config(state='normal')
1274
+ self.cancel_edit_btn.config(state='normal')
1275
+
1276
+ self.set_status(f"Editing TU #{tu.tu_id}")
1277
+
1278
+ def save_integrated_edit(self):
1279
+ """Save changes from integrated edit panel"""
1280
+ if not self.current_edit_tu:
1281
+ return
1282
+
1283
+ # Get updated text
1284
+ src_text = self.edit_source_text.get('1.0', 'end-1c')
1285
+ tgt_text = self.edit_target_text.get('1.0', 'end-1c')
1286
+
1287
+ # Update TU
1288
+ self.current_edit_tu.set_segment(self.src_lang, src_text)
1289
+ self.current_edit_tu.set_segment(self.tgt_lang, tgt_text)
1290
+ self.current_edit_tu.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
1291
+
1292
+ self.tmx_file.is_modified = True
1293
+
1294
+ # Refresh display
1295
+ self.refresh_current_page()
1296
+
1297
+ self.set_status(f"Saved changes to TU #{self.current_edit_tu.tu_id}")
1298
+
1299
+ # Clear edit panel
1300
+ self.cancel_integrated_edit()
1301
+
1302
+ def cancel_integrated_edit(self):
1303
+ """Cancel editing and clear the integrated edit panel"""
1304
+ self.current_edit_tu = None
1305
+
1306
+ # Clear text
1307
+ self.edit_source_text.config(state='normal')
1308
+ self.edit_source_text.delete('1.0', 'end')
1309
+ self.edit_source_text.config(state='disabled')
1310
+
1311
+ self.edit_target_text.config(state='normal')
1312
+ self.edit_target_text.delete('1.0', 'end')
1313
+ self.edit_target_text.config(state='disabled')
1314
+
1315
+ # Reset labels
1316
+ self.edit_id_label.config(text="(Double-click a segment to edit)")
1317
+ self.edit_src_lang_label.config(text="")
1318
+ self.edit_tgt_lang_label.config(text="")
1319
+
1320
+ # Disable buttons
1321
+ self.save_edit_btn.config(state='disabled')
1322
+ self.cancel_edit_btn.config(state='disabled')
1323
+
1324
+ self.set_status("Ready")
1325
+
1326
+ def first_page(self):
1327
+ """Go to first page"""
1328
+ self.current_page = 0
1329
+ self.refresh_current_page()
1330
+
1331
+ def prev_page(self):
1332
+ """Go to previous page"""
1333
+ if self.current_page > 0:
1334
+ self.current_page -= 1
1335
+ self.refresh_current_page()
1336
+
1337
+ def next_page(self):
1338
+ """Go to next page"""
1339
+ total_items = len(self.filtered_tus)
1340
+ total_pages = (total_items + self.items_per_page - 1) // self.items_per_page
1341
+
1342
+ if self.current_page < total_pages - 1:
1343
+ self.current_page += 1
1344
+ self.refresh_current_page()
1345
+
1346
+ def last_page(self):
1347
+ """Go to last page"""
1348
+ total_items = len(self.filtered_tus)
1349
+ total_pages = (total_items + self.items_per_page - 1) // self.items_per_page
1350
+
1351
+ if total_pages > 0:
1352
+ self.current_page = total_pages - 1
1353
+ self.refresh_current_page()
1354
+
1355
+ # ===== Tools =====
1356
+
1357
+ def validate_tmx(self):
1358
+ """Validate TMX file structure"""
1359
+ if not self.tmx_file:
1360
+ messagebox.showwarning("Warning", "Please create or open a TMX file first")
1361
+ return
1362
+
1363
+ issues = []
1364
+
1365
+ # Check header
1366
+ if not self.tmx_file.header.srclang:
1367
+ issues.append("Missing source language in header")
1368
+
1369
+ # Check translation units
1370
+ for tu in self.tmx_file.translation_units:
1371
+ if not tu.segments:
1372
+ issues.append(f"TU #{tu.tu_id}: No segments")
1373
+ continue
1374
+
1375
+ # Check for empty segments
1376
+ for lang, seg in tu.segments.items():
1377
+ if not seg.text.strip():
1378
+ issues.append(f"TU #{tu.tu_id}: Empty segment for {lang}")
1379
+
1380
+ if issues:
1381
+ issues_text = "\n".join(issues[:20]) # Show first 20 issues
1382
+ if len(issues) > 20:
1383
+ issues_text += f"\n... and {len(issues) - 20} more issues"
1384
+
1385
+ messagebox.showwarning("Validation Issues",
1386
+ f"Found {len(issues)} issue(s):\n\n{issues_text}")
1387
+ else:
1388
+ messagebox.showinfo("Validation", "✓ No issues found. TMX file is valid!")
1389
+
1390
+ def show_find_replace(self):
1391
+ """Show find/replace dialog"""
1392
+ messagebox.showinfo("Find/Replace", "Find/Replace feature coming soon!")
1393
+
1394
+ def export_tmx(self):
1395
+ """Export TMX to other formats"""
1396
+ messagebox.showinfo("Export", "Export feature coming soon!")
1397
+
1398
+ # ===== UI Helpers =====
1399
+
1400
+ def refresh_ui(self):
1401
+ """Refresh entire UI after loading file"""
1402
+ if not self.tmx_file:
1403
+ return
1404
+
1405
+ # Update language combos
1406
+ languages = self.tmx_file.get_languages()
1407
+ self.src_lang_combo['values'] = languages
1408
+ self.tgt_lang_combo['values'] = languages
1409
+
1410
+ if self.src_lang in languages:
1411
+ self.src_lang_combo.set(self.src_lang)
1412
+ elif languages:
1413
+ self.src_lang_combo.set(languages[0])
1414
+ self.src_lang = languages[0]
1415
+
1416
+ if self.tgt_lang in languages:
1417
+ self.tgt_lang_combo.set(self.tgt_lang)
1418
+ elif len(languages) > 1:
1419
+ self.tgt_lang_combo.set(languages[1])
1420
+ self.tgt_lang = languages[1]
1421
+ elif languages:
1422
+ self.tgt_lang_combo.set(languages[0])
1423
+ self.tgt_lang = languages[0]
1424
+
1425
+ # Apply filters (will refresh grid)
1426
+ self.apply_filters()
1427
+
1428
+ def set_status(self, message: str):
1429
+ """Set status bar message"""
1430
+ self.status_bar.config(text=message)
1431
+
1432
+ def on_closing(self):
1433
+ """Handle window closing"""
1434
+ if self.tmx_file and self.tmx_file.is_modified:
1435
+ response = messagebox.askyesnocancel("Unsaved Changes",
1436
+ "Save changes before closing?")
1437
+ if response is None: # Cancel
1438
+ return
1439
+ elif response: # Yes
1440
+ self.save_tmx()
1441
+
1442
+ if self.standalone:
1443
+ self.root.quit()
1444
+ self.root.destroy()
1445
+
1446
+ def run(self):
1447
+ """Run the application (standalone mode only)"""
1448
+ if self.standalone:
1449
+ self.root.mainloop()
1450
+
1451
+
1452
+ # ===== Standalone Entry Point =====
1453
+
1454
+ def main():
1455
+ """Main entry point for standalone execution"""
1456
+ app = TmxEditorUI(standalone=True)
1457
+ app.run()
1458
+
1459
+
1460
+ if __name__ == '__main__':
1461
+ main()