|
| 1 | +import threading |
| 2 | +import tkinter as tk |
| 3 | +from tkinter import messagebox, filedialog |
| 4 | +from dataclasses import dataclass |
| 5 | +from typing import List |
| 6 | +import csv |
| 7 | +import os |
| 8 | + |
| 9 | +import ttkbootstrap as tb |
| 10 | +from ttkbootstrap.constants import * |
| 11 | +from googletrans import Translator, LANGUAGES |
| 12 | +from tkinterdnd2 import TkinterDnD, DND_FILES |
| 13 | + |
| 14 | +# ---------------- GLOBAL STATE ---------------- # |
| 15 | +all_translations: List["TranslationResult"] = [] |
| 16 | +translator = Translator() |
| 17 | + |
| 18 | +# ---------------- DATA STRUCTURE ---------------- # |
| 19 | +@dataclass |
| 20 | +class TranslationResult: |
| 21 | + original_text: str |
| 22 | + translated_text: str |
| 23 | + src_lang: str |
| 24 | + dest_lang: str |
| 25 | + |
| 26 | +# ---------------- HELPERS ---------------- # |
| 27 | +def cap(text: str) -> str: |
| 28 | + return text[:1].upper() + text[1:] if text else text |
| 29 | + |
| 30 | +def copy_to_clipboard(text: str): |
| 31 | + app.clipboard_clear() |
| 32 | + app.clipboard_append(text) |
| 33 | + messagebox.showinfo("Copied", "Translation copied to clipboard") |
| 34 | + |
| 35 | +# ---------------- DRAG & DROP ---------------- # |
| 36 | +def drop_text_file(event): |
| 37 | + files = app.tk.splitlist(event.data) |
| 38 | + for file in files: |
| 39 | + if file.lower().endswith(".txt"): |
| 40 | + try: |
| 41 | + with open(file, "r", encoding="utf-8") as f: |
| 42 | + content = f.read() |
| 43 | + text_entry.delete("1.0", END) |
| 44 | + text_entry.insert("1.0", content) |
| 45 | + except Exception as e: |
| 46 | + messagebox.showerror("File Error", str(e)) |
| 47 | + |
| 48 | +# ---------------- EXPORT ---------------- # |
| 49 | +def export_translations(fmt: str): |
| 50 | + if not all_translations: |
| 51 | + messagebox.showwarning("No Data", "No translations to export") |
| 52 | + return |
| 53 | + |
| 54 | + filetypes = [("Text File", "*.txt")] if fmt == "txt" else [("CSV File", "*.csv")] |
| 55 | + path = filedialog.asksaveasfilename(defaultextension=f".{fmt}", filetypes=filetypes) |
| 56 | + |
| 57 | + if not path: |
| 58 | + return |
| 59 | + |
| 60 | + try: |
| 61 | + if fmt == "txt": |
| 62 | + with open(path, "w", encoding="utf-8") as f: |
| 63 | + for r in all_translations: |
| 64 | + f.write(f"Source ({cap(LANGUAGES[r.src_lang])})\n") |
| 65 | + f.write(f"Original: {r.original_text}\n") |
| 66 | + f.write(f"Target ({cap(LANGUAGES[r.dest_lang])})\n") |
| 67 | + f.write(f"Translated: {r.translated_text}\n") |
| 68 | + f.write("-" * 60 + "\n") |
| 69 | + else: |
| 70 | + with open(path, "w", newline="", encoding="utf-8") as f: |
| 71 | + writer = csv.writer(f) |
| 72 | + writer.writerow(["Source Language", "Target Language", "Original", "Translated"]) |
| 73 | + for r in all_translations: |
| 74 | + writer.writerow([ |
| 75 | + cap(LANGUAGES[r.src_lang]), |
| 76 | + cap(LANGUAGES[r.dest_lang]), |
| 77 | + r.original_text, |
| 78 | + r.translated_text |
| 79 | + ]) |
| 80 | + |
| 81 | + messagebox.showinfo("Exported", "Translations exported successfully") |
| 82 | + except Exception as e: |
| 83 | + messagebox.showerror("Export Error", str(e)) |
| 84 | + |
| 85 | +# ---------------- TRANSLATION ---------------- # |
| 86 | +def fetch_translations(text, langs): |
| 87 | + results = [] |
| 88 | + try: |
| 89 | + detected = translator.detect(text).lang |
| 90 | + for lang in langs: |
| 91 | + t = translator.translate(text, src=detected, dest=lang) |
| 92 | + results.append( |
| 93 | + TranslationResult(text, t.text, detected, lang) |
| 94 | + ) |
| 95 | + except Exception as e: |
| 96 | + messagebox.showerror("Translation Error", str(e)) |
| 97 | + return results |
| 98 | + |
| 99 | +# ---------------- DISPLAY ---------------- # |
| 100 | +def display_results(): |
| 101 | + for w in results_frame.winfo_children(): |
| 102 | + w.destroy() |
| 103 | + |
| 104 | + for r in all_translations: |
| 105 | + card = tb.Frame(results_frame, padding=15) |
| 106 | + card.pack(fill=X, pady=8) |
| 107 | + |
| 108 | + tb.Label( |
| 109 | + card, |
| 110 | + text=f"Original ({cap(LANGUAGES[r.src_lang])})", |
| 111 | + font=("Segoe UI", 11) |
| 112 | + ).pack(anchor=W) |
| 113 | + |
| 114 | + tb.Label( |
| 115 | + card, |
| 116 | + text=r.original_text, |
| 117 | + wraplength=900 |
| 118 | + ).pack(anchor=W) |
| 119 | + |
| 120 | + row = tb.Frame(card) |
| 121 | + row.pack(fill=X, pady=5) |
| 122 | + |
| 123 | + tb.Label( |
| 124 | + row, |
| 125 | + text=f"{cap(LANGUAGES[r.dest_lang])}: {r.translated_text}", |
| 126 | + font=("Segoe UI", 14, "bold"), |
| 127 | + wraplength=800 |
| 128 | + ).pack(side=LEFT) |
| 129 | + |
| 130 | + tb.Button( |
| 131 | + row, |
| 132 | + text="📋 Copy", |
| 133 | + command=lambda t=r.translated_text: copy_to_clipboard(t) |
| 134 | + ).pack(side=LEFT, padx=10) |
| 135 | + |
| 136 | +# ---------------- SEARCH FILTER ---------------- # |
| 137 | +def filter_languages(*_): |
| 138 | + query = search_var.get().lower() |
| 139 | + lang_listbox.delete(0, END) |
| 140 | + for lang in all_language_names: |
| 141 | + if query in lang.lower(): |
| 142 | + lang_listbox.insert(END, lang) |
| 143 | + |
| 144 | +# ---------------- RUN TRANSLATION ---------------- # |
| 145 | +def perform_translation(): |
| 146 | + text = text_entry.get("1.0", END).strip() |
| 147 | + selections = lang_listbox.curselection() |
| 148 | + |
| 149 | + if not text or not selections: |
| 150 | + messagebox.showwarning("Input Required", "Enter text and select languages") |
| 151 | + return |
| 152 | + |
| 153 | + langs = [] |
| 154 | + for i in selections: |
| 155 | + name = lang_listbox.get(i) |
| 156 | + for code, lang in LANGUAGES.items(): |
| 157 | + if cap(lang) == name: |
| 158 | + langs.append(code) |
| 159 | + |
| 160 | + threading.Thread( |
| 161 | + target=run_translation, |
| 162 | + args=(text, langs), |
| 163 | + daemon=True |
| 164 | + ).start() |
| 165 | + |
| 166 | +def run_translation(text, langs): |
| 167 | + global all_translations |
| 168 | + all_translations = fetch_translations(text, langs) |
| 169 | + app.after(0, display_results) |
| 170 | + |
| 171 | +# ---------------- UI ---------------- # |
| 172 | +app = TkinterDnD.Tk() |
| 173 | +app.title("🌐 Machine Translation Tool") |
| 174 | +app.geometry("1000x780") |
| 175 | + |
| 176 | +style = tb.Style("flatly") |
| 177 | + |
| 178 | +top = tb.Frame(app, padding=15) |
| 179 | +top.pack(fill=X) |
| 180 | + |
| 181 | +tb.Label( |
| 182 | + top, |
| 183 | + text="🌐 Machine Translation Tool", |
| 184 | + font=("Segoe UI", 18, "bold") |
| 185 | +).pack(anchor=W) |
| 186 | + |
| 187 | +text_entry = tk.Text(top, height=5, font=("Segoe UI", 12)) |
| 188 | +text_entry.pack(fill=X, pady=8) |
| 189 | + |
| 190 | +# Drag & Drop |
| 191 | +text_entry.drop_target_register(DND_FILES) |
| 192 | +text_entry.dnd_bind("<<Drop>>", drop_text_file) |
| 193 | + |
| 194 | +tb.Label(top, text="Search Languages:", font=("Segoe UI", 11)).pack(anchor=W) |
| 195 | + |
| 196 | +search_var = tk.StringVar() |
| 197 | +search_var.trace_add("write", filter_languages) |
| 198 | + |
| 199 | +search_entry = tb.Entry(top, textvariable=search_var) |
| 200 | +search_entry.pack(fill=X, pady=5) |
| 201 | + |
| 202 | +lang_listbox = tk.Listbox(top, selectmode=MULTIPLE, height=8) |
| 203 | +lang_listbox.pack(fill=X) |
| 204 | + |
| 205 | +all_language_names = sorted(cap(v) for v in LANGUAGES.values()) |
| 206 | +for l in all_language_names: |
| 207 | + lang_listbox.insert(END, l) |
| 208 | + |
| 209 | +btn_row = tb.Frame(top) |
| 210 | +btn_row.pack(fill=X, pady=5) |
| 211 | + |
| 212 | +tb.Button(btn_row, text="Translate", bootstyle="success", command=perform_translation).pack(side=LEFT) |
| 213 | +tb.Button(btn_row, text="Export TXT", command=lambda: export_translations("txt")).pack(side=LEFT, padx=5) |
| 214 | + |
| 215 | +# ---------------- RESULTS (SCROLL ONLY) ---------------- # |
| 216 | +results_container = tb.Frame(app) |
| 217 | +results_container.pack(fill=BOTH, expand=True) |
| 218 | + |
| 219 | +canvas = tk.Canvas(results_container) |
| 220 | +scroll = tb.Scrollbar(results_container, command=canvas.yview) |
| 221 | +results_frame = tb.Frame(canvas) |
| 222 | + |
| 223 | +canvas.create_window((0, 0), window=results_frame, anchor="nw") |
| 224 | +canvas.configure(yscrollcommand=scroll.set) |
| 225 | + |
| 226 | +results_frame.bind( |
| 227 | + "<Configure>", |
| 228 | + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) |
| 229 | +) |
| 230 | + |
| 231 | +canvas.pack(side=LEFT, fill=BOTH, expand=True) |
| 232 | +scroll.pack(side=RIGHT, fill=Y) |
| 233 | + |
| 234 | +app.mainloop() |
0 commit comments