492 lines
21 KiB
Python
492 lines
21 KiB
Python
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import tokenize
|
|||
|
|
import io
|
|||
|
|
import time
|
|||
|
|
import shutil
|
|||
|
|
import threading
|
|||
|
|
import tkinter as tk
|
|||
|
|
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
|||
|
|
|
|||
|
|
from deep_translator import GoogleTranslator
|
|||
|
|
from langdetect import detect, LangDetectException
|
|||
|
|
|
|||
|
|
# Получаем все поддерживаемые языки от Google Translate
|
|||
|
|
LANGUAGES_DICT = GoogleTranslator().get_supported_languages(as_dict=True)
|
|||
|
|
LANGUAGES_LIST =[lang.capitalize() for lang in LANGUAGES_DICT.keys()]
|
|||
|
|
|
|||
|
|
# Настройки для обхода банов
|
|||
|
|
API_TIMEOUT = 1.5
|
|||
|
|
MAX_TEXT_LEN = 4800
|
|||
|
|
|
|||
|
|
# ==========================================
|
|||
|
|
# БЭКЭНД: ФУНКЦИИ ПЕРЕВОДА И ПАРСИНГА
|
|||
|
|
# ==========================================
|
|||
|
|
|
|||
|
|
def fix_placeholders(text):
|
|||
|
|
"""Восстанавливает переменные форматирования кода после перевода."""
|
|||
|
|
if not text: return text
|
|||
|
|
text = re.sub(r'%\s+([a-zA-Z])', r'%\1', text)
|
|||
|
|
text = re.sub(r'%\s+([0-9]+)', r'%\1', text)
|
|||
|
|
text = re.sub(r'\{\s*(.*?)\s*\}', r'{\1}', text)
|
|||
|
|
return text
|
|||
|
|
|
|||
|
|
def safe_translate(text, target_lang='ru', source_lang='auto'):
|
|||
|
|
"""Переводит текст с разбивкой на части для обхода лимитов Google."""
|
|||
|
|
translator = GoogleTranslator(source=source_lang, target=target_lang)
|
|||
|
|
|
|||
|
|
if len(text) <= MAX_TEXT_LEN:
|
|||
|
|
return translator.translate(text)
|
|||
|
|
|
|||
|
|
chunks =[]
|
|||
|
|
current_chunk = ""
|
|||
|
|
for line in text.splitlines(keepends=True):
|
|||
|
|
if len(current_chunk) + len(line) < MAX_TEXT_LEN:
|
|||
|
|
current_chunk += line
|
|||
|
|
else:
|
|||
|
|
if current_chunk.strip():
|
|||
|
|
chunks.append(translator.translate(current_chunk))
|
|||
|
|
time.sleep(API_TIMEOUT)
|
|||
|
|
current_chunk = line
|
|||
|
|
if current_chunk.strip():
|
|||
|
|
chunks.append(translator.translate(current_chunk))
|
|||
|
|
|
|||
|
|
return "".join(chunks)
|
|||
|
|
|
|||
|
|
def detect_language(text):
|
|||
|
|
"""Определяет язык текста локально (без запросов к API)."""
|
|||
|
|
try:
|
|||
|
|
clean_text = re.sub(r'[^\w\s]', '', text)
|
|||
|
|
if not clean_text.strip(): return None
|
|||
|
|
return detect(clean_text)
|
|||
|
|
except LangDetectException:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def should_translate(content, config):
|
|||
|
|
"""
|
|||
|
|
ГЛАВНЫЙ ФИЛЬТР: Решает, является ли текст системным кодом или интерфейсом.
|
|||
|
|
"""
|
|||
|
|
clean_content = content.strip()
|
|||
|
|
|
|||
|
|
if not clean_content or not any(c.isalpha() for c in clean_content):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
if config.get('skip_technical', True):
|
|||
|
|
# Если в строке НЕТ пробелов и НЕТ иероглифов/кириллицы -> это тех. строка
|
|||
|
|
if " " not in clean_content and not re.search(r'[А-Яа-яЁё\u4e00-\u9fff\u3040-\u30ff]', clean_content):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
if config['translate_only']:
|
|||
|
|
lang_code = detect_language(clean_content)
|
|||
|
|
if lang_code and lang_code.lower() in config['selected_codes']:
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def process_file(filepath, config, update_log_cb):
|
|||
|
|
"""Универсальная функция парсинга."""
|
|||
|
|
try:
|
|||
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|||
|
|
source = f.read()
|
|||
|
|
except UnicodeDecodeError:
|
|||
|
|
return "error_utf8"
|
|||
|
|
|
|||
|
|
replacements =[]
|
|||
|
|
|
|||
|
|
if filepath.endswith('.py'):
|
|||
|
|
tokens = tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline)
|
|||
|
|
for tok in tokens:
|
|||
|
|
if tok.type in (tokenize.STRING, tokenize.COMMENT):
|
|||
|
|
if tok.type == tokenize.COMMENT:
|
|||
|
|
m = re.match(r'^(#\s*)(.*)', tok.string, re.DOTALL)
|
|||
|
|
if m:
|
|||
|
|
prefix, content = m.groups()
|
|||
|
|
if should_translate(content, config):
|
|||
|
|
translated = safe_translate(content, target_lang='ru')
|
|||
|
|
translated = fix_placeholders(translated)
|
|||
|
|
replacements.append((tok.start, tok.end, prefix + translated))
|
|||
|
|
time.sleep(API_TIMEOUT)
|
|||
|
|
elif tok.type == tokenize.STRING:
|
|||
|
|
m = re.match(r'^([a-zA-Z]*)("{1,3}|\'{1,3})(.*)(\2)$', tok.string, re.DOTALL)
|
|||
|
|
if m:
|
|||
|
|
prefix, quote, content, _ = m.groups()
|
|||
|
|
if should_translate(content, config):
|
|||
|
|
translated = safe_translate(content, target_lang='ru')
|
|||
|
|
translated = fix_placeholders(translated)
|
|||
|
|
if 'r' not in prefix.lower() and len(quote) == 1:
|
|||
|
|
if quote == '"': translated = translated.replace('"', '\\"')
|
|||
|
|
elif quote == "'": translated = translated.replace("'", "\\'")
|
|||
|
|
replacements.append((tok.start, tok.end, f"{prefix}{quote}{translated}{quote}"))
|
|||
|
|
time.sleep(API_TIMEOUT)
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
pattern = re.compile(
|
|||
|
|
r'(/\*.*?\*/)|'
|
|||
|
|
r'(//.*?$)|'
|
|||
|
|
r'("(?:\\.|[^"\\])*")|'
|
|||
|
|
r"('(?:\\.|[^\'\\])*')|"
|
|||
|
|
r'(`(?:\\.|[^`\\])*`)',
|
|||
|
|
re.DOTALL | re.MULTILINE
|
|||
|
|
)
|
|||
|
|
for match in pattern.finditer(source):
|
|||
|
|
text = match.group(0)
|
|||
|
|
is_comment_multi = text.startswith('/*')
|
|||
|
|
is_comment_single = text.startswith('//')
|
|||
|
|
|
|||
|
|
if is_comment_multi: content = text[2:-2]
|
|||
|
|
elif is_comment_single: content = text[2:]
|
|||
|
|
else: content = text[1:-1]
|
|||
|
|
|
|||
|
|
if should_translate(content, config):
|
|||
|
|
translated = safe_translate(content, target_lang='ru')
|
|||
|
|
translated = fix_placeholders(translated)
|
|||
|
|
|
|||
|
|
if is_comment_multi: new_text = f"/*{translated}*/"
|
|||
|
|
elif is_comment_single: new_text = f"//{translated}"
|
|||
|
|
else:
|
|||
|
|
quote = text[0]
|
|||
|
|
if quote == '"': translated = translated.replace('"', '\\"')
|
|||
|
|
elif quote == "'": translated = translated.replace("'", "\\'")
|
|||
|
|
elif quote == "`": translated = translated.replace("`", "\\`")
|
|||
|
|
new_text = f"{quote}{translated}{quote}"
|
|||
|
|
|
|||
|
|
replacements.append((match.start(), match.end(), new_text))
|
|||
|
|
time.sleep(API_TIMEOUT)
|
|||
|
|
|
|||
|
|
if not replacements:
|
|||
|
|
return "skipped"
|
|||
|
|
|
|||
|
|
if config['backup_enabled']:
|
|||
|
|
shutil.copy2(filepath, filepath + ".bak")
|
|||
|
|
|
|||
|
|
if filepath.endswith('.py'):
|
|||
|
|
lines = source.splitlines(keepends=True)
|
|||
|
|
for start, end, new_text in reversed(replacements):
|
|||
|
|
start_row, start_col = start[0] - 1, start[1]
|
|||
|
|
end_row, end_col = end[0] - 1, end[1]
|
|||
|
|
if start_row == end_row:
|
|||
|
|
lines[start_row] = lines[start_row][:start_col] + new_text + lines[start_row][end_col:]
|
|||
|
|
else:
|
|||
|
|
lines[start_row] = lines[start_row][:start_col] + new_text
|
|||
|
|
for r in range(start_row + 1, end_row): lines[r] = ""
|
|||
|
|
lines[end_row] = lines[end_row][end_col:]
|
|||
|
|
final_source = "".join(lines)
|
|||
|
|
else:
|
|||
|
|
for start, end, new_text in reversed(replacements):
|
|||
|
|
source = source[:start] + new_text + source[end:]
|
|||
|
|
final_source = source
|
|||
|
|
|
|||
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(final_source)
|
|||
|
|
|
|||
|
|
return "translated"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ==========================================
|
|||
|
|
# ФРОНТЭНД: ГРАФИЧЕСКИЙ ИНТЕРФЕЙС (TKINTER)
|
|||
|
|
# ==========================================
|
|||
|
|
|
|||
|
|
class TranslatorApp:
|
|||
|
|
def __init__(self, root):
|
|||
|
|
self.root = root
|
|||
|
|
self.root.title("PRO Локализатор Кода (Безопасный режим)")
|
|||
|
|
self.root.geometry("750x700")
|
|||
|
|
self.root.minsize(650, 650)
|
|||
|
|
|
|||
|
|
style = ttk.Style()
|
|||
|
|
if 'clam' in style.theme_names():
|
|||
|
|
style.theme_use('clam')
|
|||
|
|
|
|||
|
|
self.build_ui()
|
|||
|
|
|
|||
|
|
def build_ui(self):
|
|||
|
|
# --- ВЕРХНЯЯ ПАНЕЛЬ ---
|
|||
|
|
top_frame = ttk.Frame(self.root, padding=10)
|
|||
|
|
top_frame.pack(fill=tk.X)
|
|||
|
|
|
|||
|
|
ttk.Label(top_frame, text="Настройки безопасного перевода", font=("Arial", 12, "bold")).pack(side=tk.LEFT)
|
|||
|
|
ttk.Button(top_frame, text="⚡ Быстрый перевод", command=self.open_quick_translate).pack(side=tk.RIGHT)
|
|||
|
|
|
|||
|
|
# --- ГЛАВНАЯ ПАНЕЛЬ ---
|
|||
|
|
main_frame = ttk.LabelFrame(self.root, padding=15)
|
|||
|
|
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|||
|
|
|
|||
|
|
# 1. Выбор папки
|
|||
|
|
f_folder = ttk.Frame(main_frame)
|
|||
|
|
f_folder.pack(fill=tk.X, pady=5)
|
|||
|
|
ttk.Label(f_folder, text="Папка с кодом:").pack(side=tk.LEFT, padx=(0,5))
|
|||
|
|
self.folder_var = tk.StringVar()
|
|||
|
|
ttk.Entry(f_folder, textvariable=self.folder_var, state='readonly').pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|||
|
|
ttk.Button(f_folder, text="Обзор...", command=self.browse_folder).pack(side=tk.LEFT, padx=(5,0))
|
|||
|
|
|
|||
|
|
# 2. Расширения файлов
|
|||
|
|
f_ext = ttk.Frame(main_frame)
|
|||
|
|
f_ext.pack(fill=tk.X, pady=5)
|
|||
|
|
ttk.Label(f_ext, text="Расширения файлов:").pack(side=tk.LEFT, padx=(0,5))
|
|||
|
|
self.ext_var = tk.StringVar(value=".py, .qml, .js")
|
|||
|
|
ttk.Entry(f_ext, textvariable=self.ext_var).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|||
|
|
ttk.Label(f_ext, text="(через запятую)", foreground="gray").pack(side=tk.LEFT, padx=5)
|
|||
|
|
|
|||
|
|
# 3. Чекбоксы безопасности
|
|||
|
|
self.backup_var = tk.BooleanVar(value=True)
|
|||
|
|
ttk.Checkbutton(main_frame, text="Создавать .bak файлы оригиналов перед переводом", variable=self.backup_var).pack(anchor=tk.W, pady=(10,0))
|
|||
|
|
|
|||
|
|
self.skip_tech_var = tk.BooleanVar(value=True)
|
|||
|
|
ttk.Checkbutton(main_frame, text="🛡️ Умный фильтр: НЕ переводить технические строки (ключи, пути, camelCase)", variable=self.skip_tech_var).pack(anchor=tk.W, pady=5)
|
|||
|
|
|
|||
|
|
# 4. Настройки языков источника
|
|||
|
|
f_lang = ttk.Frame(main_frame)
|
|||
|
|
f_lang.pack(fill=tk.X, pady=5)
|
|||
|
|
|
|||
|
|
self.translate_only_var = tk.BooleanVar(value=False)
|
|||
|
|
cb_only = ttk.Checkbutton(f_lang, text="Переводить ТОЛЬКО с выбранных языков (иначе переведет всё)",
|
|||
|
|
variable=self.translate_only_var, command=self.toggle_lang_list)
|
|||
|
|
cb_only.pack(anchor=tk.W)
|
|||
|
|
|
|||
|
|
self.langs_listbox = tk.Listbox(f_lang, selectmode=tk.MULTIPLE, height=5, exportselection=False)
|
|||
|
|
self.langs_listbox.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=5)
|
|||
|
|
scrollbar = ttk.Scrollbar(f_lang, orient="vertical", command=self.langs_listbox.yview)
|
|||
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
|
|||
|
|
self.langs_listbox.config(yscrollcommand=scrollbar.set)
|
|||
|
|
|
|||
|
|
for lang in LANGUAGES_LIST:
|
|||
|
|
self.langs_listbox.insert(tk.END, lang)
|
|||
|
|
self.langs_listbox.config(state=tk.DISABLED)
|
|||
|
|
|
|||
|
|
# Прогресс
|
|||
|
|
self.progress_var = tk.DoubleVar()
|
|||
|
|
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
|
|||
|
|
self.progress_bar.pack(fill=tk.X, pady=15)
|
|||
|
|
|
|||
|
|
self.lbl_stats = ttk.Label(main_frame, text="Ожидание...")
|
|||
|
|
self.lbl_stats.pack(anchor=tk.W)
|
|||
|
|
|
|||
|
|
# Кнопка старта
|
|||
|
|
self.btn_start = ttk.Button(main_frame, text="▶ Начать массовый перевод", command=self.start_translation)
|
|||
|
|
self.btn_start.pack(fill=tk.X, pady=10)
|
|||
|
|
|
|||
|
|
# Логи
|
|||
|
|
self.txt_log = scrolledtext.ScrolledText(main_frame, height=8, state=tk.DISABLED, bg="#f4f4f4")
|
|||
|
|
self.txt_log.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
|
|||
|
|
def toggle_lang_list(self):
|
|||
|
|
state = tk.NORMAL if self.translate_only_var.get() else tk.DISABLED
|
|||
|
|
self.langs_listbox.config(state=state)
|
|||
|
|
|
|||
|
|
def browse_folder(self):
|
|||
|
|
folder = filedialog.askdirectory()
|
|||
|
|
if folder: self.folder_var.set(folder)
|
|||
|
|
|
|||
|
|
def log(self, text):
|
|||
|
|
def append():
|
|||
|
|
self.txt_log.config(state=tk.NORMAL)
|
|||
|
|
self.txt_log.insert(tk.END, text + "\n")
|
|||
|
|
self.txt_log.see(tk.END)
|
|||
|
|
self.txt_log.config(state=tk.DISABLED)
|
|||
|
|
self.root.after(0, append)
|
|||
|
|
|
|||
|
|
def start_translation(self):
|
|||
|
|
target_dir = self.folder_var.get()
|
|||
|
|
if not target_dir: return messagebox.showwarning("Внимание", "Укажите папку проекта!")
|
|||
|
|
|
|||
|
|
ext_str = self.ext_var.get()
|
|||
|
|
exts = tuple(e.strip().lower() for e in ext_str.split(',') if e.strip())
|
|||
|
|
if not exts: return messagebox.showwarning("Внимание", "Укажите хотя бы одно расширение!")
|
|||
|
|
|
|||
|
|
selected_langs =[]
|
|||
|
|
if self.translate_only_var.get():
|
|||
|
|
indices = self.langs_listbox.curselection()
|
|||
|
|
if not indices:
|
|||
|
|
return messagebox.showwarning("Внимание", "Вы не выбрали ни одного языка в списке!")
|
|||
|
|
for i in indices:
|
|||
|
|
lang_name = self.langs_listbox.get(i).lower()
|
|||
|
|
lang_code = LANGUAGES_DICT.get(lang_name)
|
|||
|
|
if lang_code: selected_langs.append(lang_code.lower())
|
|||
|
|
|
|||
|
|
config = {
|
|||
|
|
'target_dir': target_dir,
|
|||
|
|
'exts': exts,
|
|||
|
|
'backup_enabled': self.backup_var.get(),
|
|||
|
|
'skip_technical': self.skip_tech_var.get(),
|
|||
|
|
'translate_only': self.translate_only_var.get(),
|
|||
|
|
'selected_codes': selected_langs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
self.btn_start.config(state=tk.DISABLED)
|
|||
|
|
self.txt_log.config(state=tk.NORMAL)
|
|||
|
|
self.txt_log.delete(1.0, tk.END)
|
|||
|
|
self.txt_log.config(state=tk.DISABLED)
|
|||
|
|
|
|||
|
|
threading.Thread(target=self.worker, args=(config,), daemon=True).start()
|
|||
|
|
|
|||
|
|
def worker(self, config):
|
|||
|
|
self.log("[i] Сканирование файлов...")
|
|||
|
|
target_files =[]
|
|||
|
|
for root, _, files in os.walk(config['target_dir']):
|
|||
|
|
for file in files:
|
|||
|
|
if file.lower().endswith(config['exts']):
|
|||
|
|
target_files.append(os.path.join(root, file))
|
|||
|
|
|
|||
|
|
total = len(target_files)
|
|||
|
|
if total == 0:
|
|||
|
|
self.log("[!] Файлы с указанными расширениями не найдены.")
|
|||
|
|
self.root.after(0, lambda: self.btn_start.config(state=tk.NORMAL))
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
translated_c = skipped_c = err_c = 0
|
|||
|
|
|
|||
|
|
for i, filepath in enumerate(target_files, 1):
|
|||
|
|
short_p = os.path.relpath(filepath, config['target_dir'])
|
|||
|
|
|
|||
|
|
self.root.after(0, lambda v=i, t=total: self.progress_var.set(v / t * 100))
|
|||
|
|
self.root.after(0, lambda s=short_p: self.lbl_stats.config(text=f"Обработка: {s}"))
|
|||
|
|
|
|||
|
|
res = process_file(filepath, config, self.log)
|
|||
|
|
if res == "translated":
|
|||
|
|
translated_c += 1
|
|||
|
|
self.log(f"[+] Переведено: {short_p}")
|
|||
|
|
elif res == "skipped":
|
|||
|
|
skipped_c += 1
|
|||
|
|
else:
|
|||
|
|
err_c += 1
|
|||
|
|
self.log(f"[!] Ошибка кодировки: {short_p}")
|
|||
|
|
|
|||
|
|
self.log("="*40)
|
|||
|
|
self.log(f"ГОТОВО! Переведено: {translated_c} | Пропущено: {skipped_c} | Ошибок: {err_c}")
|
|||
|
|
self.root.after(0, lambda: self.lbl_stats.config(text="Работа завершена."))
|
|||
|
|
self.root.after(0, lambda: self.btn_start.config(state=tk.NORMAL))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ==========================================
|
|||
|
|
# ОКНО БЫСТРОГО ПЕРЕВОДА
|
|||
|
|
# ==========================================
|
|||
|
|
def open_quick_translate(self):
|
|||
|
|
qt_win = tk.Toplevel(self.root)
|
|||
|
|
qt_win.title("⚡ Быстрый перевод")
|
|||
|
|
qt_win.geometry("850x550")
|
|||
|
|
qt_win.minsize(650, 450)
|
|||
|
|
|
|||
|
|
# --- Верхняя панель языков ---
|
|||
|
|
top_f = ttk.Frame(qt_win, padding=10)
|
|||
|
|
top_f.pack(fill=tk.X)
|
|||
|
|
|
|||
|
|
ttk.Label(top_f, text="Целевой язык:").pack(side=tk.LEFT)
|
|||
|
|
target_lang_cb = ttk.Combobox(top_f, values=LANGUAGES_LIST, state="readonly", width=20)
|
|||
|
|
target_lang_cb.set("Russian")
|
|||
|
|
target_lang_cb.pack(side=tk.LEFT, padx=10)
|
|||
|
|
|
|||
|
|
self.lbl_detected = ttk.Label(top_f, text="Исходный: Автоопределение", foreground="blue")
|
|||
|
|
self.lbl_detected.pack(side=tk.RIGHT)
|
|||
|
|
|
|||
|
|
# --- Разделитель текстовых панелей ---
|
|||
|
|
paned = ttk.PanedWindow(qt_win, orient=tk.HORIZONTAL)
|
|||
|
|
paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|||
|
|
|
|||
|
|
left_f = ttk.Frame(paned)
|
|||
|
|
right_f = ttk.Frame(paned)
|
|||
|
|
paned.add(left_f, weight=1)
|
|||
|
|
paned.add(right_f, weight=1)
|
|||
|
|
|
|||
|
|
# -------------------------------------
|
|||
|
|
# ЛЕВАЯ ЧАСТЬ (ВВОД + ИКОНКА ОЧИСТКИ)
|
|||
|
|
# -------------------------------------
|
|||
|
|
left_header = ttk.Frame(left_f)
|
|||
|
|
left_header.pack(fill=tk.X, pady=(0, 2))
|
|||
|
|
|
|||
|
|
ttk.Label(left_header, text="Вставьте текст (Enter - перевод):").pack(side=tk.LEFT)
|
|||
|
|
|
|||
|
|
def clear_fields():
|
|||
|
|
txt_in.delete(1.0, tk.END)
|
|||
|
|
txt_out.config(state=tk.NORMAL)
|
|||
|
|
txt_out.delete(1.0, tk.END)
|
|||
|
|
txt_out.config(state=tk.DISABLED)
|
|||
|
|
|
|||
|
|
# Кнопка-иконка Очистки (Корзина)
|
|||
|
|
btn_clear = ttk.Button(left_header, text="🗑️ Очистить", command=clear_fields)
|
|||
|
|
btn_clear.pack(side=tk.RIGHT)
|
|||
|
|
|
|||
|
|
txt_in = scrolledtext.ScrolledText(left_f, wrap=tk.WORD, font=("Arial", 11))
|
|||
|
|
txt_in.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
|
|||
|
|
# -------------------------------------
|
|||
|
|
# ПРАВАЯ ЧАСТЬ (ВЫВОД + ИКОНКА СОХРАНЕНИЯ)
|
|||
|
|
# -------------------------------------
|
|||
|
|
right_header = ttk.Frame(right_f)
|
|||
|
|
right_header.pack(fill=tk.X, pady=(0, 2))
|
|||
|
|
|
|||
|
|
ttk.Label(right_header, text="Перевод:").pack(side=tk.LEFT)
|
|||
|
|
|
|||
|
|
def save_to_file():
|
|||
|
|
text = txt_out.get(1.0, tk.END).strip()
|
|||
|
|
if not text: return
|
|||
|
|
filepath = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")])
|
|||
|
|
if filepath:
|
|||
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(text)
|
|||
|
|
messagebox.showinfo("Сохранено", "Текст успешно сохранен в файл!")
|
|||
|
|
|
|||
|
|
# Кнопка-иконка Сохранения (Дискета)
|
|||
|
|
btn_save = ttk.Button(right_header, text="💾 Сохранить", command=save_to_file)
|
|||
|
|
btn_save.pack(side=tk.RIGHT)
|
|||
|
|
|
|||
|
|
txt_out = scrolledtext.ScrolledText(right_f, wrap=tk.WORD, font=("Arial", 11), bg="#f9f9f9")
|
|||
|
|
txt_out.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
|
|||
|
|
# --- Нижняя панель (Только кнопка старта перевода) ---
|
|||
|
|
bot_f = ttk.Frame(qt_win, padding=10)
|
|||
|
|
bot_f.pack(fill=tk.X)
|
|||
|
|
|
|||
|
|
def run_translation(event=None):
|
|||
|
|
src_text = txt_in.get(1.0, tk.END).strip()
|
|||
|
|
if not src_text:
|
|||
|
|
return "break" if event else None
|
|||
|
|
|
|||
|
|
target_name = target_lang_cb.get().lower()
|
|||
|
|
target_code = LANGUAGES_DICT.get(target_name, 'ru')
|
|||
|
|
|
|||
|
|
btn_trans.config(state=tk.DISABLED, text="Перевод...")
|
|||
|
|
txt_out.config(state=tk.NORMAL)
|
|||
|
|
txt_out.delete(1.0, tk.END)
|
|||
|
|
txt_out.insert(tk.END, "Запрос к серверу...")
|
|||
|
|
txt_out.config(state=tk.DISABLED)
|
|||
|
|
|
|||
|
|
def task():
|
|||
|
|
det_code = detect_language(src_text)
|
|||
|
|
det_name = "Неизвестно"
|
|||
|
|
if det_code:
|
|||
|
|
for name, code in LANGUAGES_DICT.items():
|
|||
|
|
if code.lower() == det_code.lower():
|
|||
|
|
det_name = name.capitalize()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
res = safe_translate(src_text, target_lang=target_code, source_lang='auto')
|
|||
|
|
except Exception as e:
|
|||
|
|
res = f"Ошибка: {e}"
|
|||
|
|
|
|||
|
|
def update_ui():
|
|||
|
|
self.lbl_detected.config(text=f"Определен язык: {det_name}")
|
|||
|
|
txt_out.config(state=tk.NORMAL)
|
|||
|
|
txt_out.delete(1.0, tk.END)
|
|||
|
|
txt_out.insert(tk.END, res)
|
|||
|
|
txt_out.config(state=tk.DISABLED)
|
|||
|
|
btn_trans.config(state=tk.NORMAL, text="Перевести ➔")
|
|||
|
|
qt_win.after(0, update_ui)
|
|||
|
|
|
|||
|
|
threading.Thread(target=task, daemon=True).start()
|
|||
|
|
|
|||
|
|
if event:
|
|||
|
|
return "break"
|
|||
|
|
|
|||
|
|
btn_trans = ttk.Button(bot_f, text="Перевести ➔", command=run_translation)
|
|||
|
|
btn_trans.pack(side=tk.LEFT)
|
|||
|
|
|
|||
|
|
# Биндинги клавиш для поля ввода
|
|||
|
|
txt_in.bind("<Return>", run_translation)
|
|||
|
|
txt_in.bind("<Shift-Return>", lambda e: None)
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
root = tk.Tk()
|
|||
|
|
app = TranslatorApp(root)
|
|||
|
|
root.mainloop()
|