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.
- Supervertaler.py +47886 -0
- modules/__init__.py +10 -0
- modules/ai_actions.py +964 -0
- modules/ai_attachment_manager.py +343 -0
- modules/ai_file_viewer_dialog.py +210 -0
- modules/autofingers_engine.py +466 -0
- modules/cafetran_docx_handler.py +379 -0
- modules/config_manager.py +469 -0
- modules/database_manager.py +1878 -0
- modules/database_migrations.py +417 -0
- modules/dejavurtf_handler.py +779 -0
- modules/document_analyzer.py +427 -0
- modules/docx_handler.py +689 -0
- modules/encoding_repair.py +319 -0
- modules/encoding_repair_Qt.py +393 -0
- modules/encoding_repair_ui.py +481 -0
- modules/feature_manager.py +350 -0
- modules/figure_context_manager.py +340 -0
- modules/file_dialog_helper.py +148 -0
- modules/find_replace.py +164 -0
- modules/find_replace_qt.py +457 -0
- modules/glossary_manager.py +433 -0
- modules/image_extractor.py +188 -0
- modules/keyboard_shortcuts_widget.py +571 -0
- modules/llm_clients.py +1211 -0
- modules/llm_leaderboard.py +737 -0
- modules/llm_superbench_ui.py +1401 -0
- modules/local_llm_setup.py +1104 -0
- modules/model_update_dialog.py +381 -0
- modules/model_version_checker.py +373 -0
- modules/mqxliff_handler.py +638 -0
- modules/non_translatables_manager.py +743 -0
- modules/pdf_rescue_Qt.py +1822 -0
- modules/pdf_rescue_tkinter.py +909 -0
- modules/phrase_docx_handler.py +516 -0
- modules/project_home_panel.py +209 -0
- modules/prompt_assistant.py +357 -0
- modules/prompt_library.py +689 -0
- modules/prompt_library_migration.py +447 -0
- modules/quick_access_sidebar.py +282 -0
- modules/ribbon_widget.py +597 -0
- modules/sdlppx_handler.py +874 -0
- modules/setup_wizard.py +353 -0
- modules/shortcut_manager.py +932 -0
- modules/simple_segmenter.py +128 -0
- modules/spellcheck_manager.py +727 -0
- modules/statuses.py +207 -0
- modules/style_guide_manager.py +315 -0
- modules/superbench_ui.py +1319 -0
- modules/superbrowser.py +329 -0
- modules/supercleaner.py +600 -0
- modules/supercleaner_ui.py +444 -0
- modules/superdocs.py +19 -0
- modules/superdocs_viewer_qt.py +382 -0
- modules/superlookup.py +252 -0
- modules/tag_cleaner.py +260 -0
- modules/tag_manager.py +333 -0
- modules/term_extractor.py +270 -0
- modules/termbase_entry_editor.py +842 -0
- modules/termbase_import_export.py +488 -0
- modules/termbase_manager.py +1060 -0
- modules/termview_widget.py +1172 -0
- modules/theme_manager.py +499 -0
- modules/tm_editor_dialog.py +99 -0
- modules/tm_manager_qt.py +1280 -0
- modules/tm_metadata_manager.py +545 -0
- modules/tmx_editor.py +1461 -0
- modules/tmx_editor_qt.py +2784 -0
- modules/tmx_generator.py +284 -0
- modules/tracked_changes.py +900 -0
- modules/trados_docx_handler.py +430 -0
- modules/translation_memory.py +715 -0
- modules/translation_results_panel.py +2134 -0
- modules/translation_services.py +282 -0
- modules/unified_prompt_library.py +659 -0
- modules/unified_prompt_manager_qt.py +3951 -0
- modules/voice_commands.py +920 -0
- modules/voice_dictation.py +477 -0
- modules/voice_dictation_lite.py +249 -0
- supervertaler-1.9.153.dist-info/METADATA +896 -0
- supervertaler-1.9.153.dist-info/RECORD +85 -0
- supervertaler-1.9.153.dist-info/WHEEL +5 -0
- supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
- supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
- 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()
|