5dc00d6e70
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
261 lines
12 KiB
Python
261 lines
12 KiB
Python
import os
|
|
import time
|
|
import threading
|
|
import tkinter as tk
|
|
from tkinter import filedialog, messagebox
|
|
|
|
import customtkinter as ctk
|
|
from deep_translator import GoogleTranslator
|
|
from langdetect import detect, LangDetectException
|
|
|
|
# ==========================================
|
|
# НАСТРОЙКИ ПРИЛОЖЕНИЯ И ПЕРЕВОДЧИКА
|
|
# ==========================================
|
|
|
|
# Настройки внешнего вида CustomTkinter
|
|
ctk.set_appearance_mode("System") # Подстраивается под темную/светлую тему ОС
|
|
ctk.set_default_color_theme("blue") # Цветовой акцент
|
|
|
|
# Получаем языки
|
|
translator_api = GoogleTranslator()
|
|
LANGUAGES_DICT = translator_api.get_supported_languages(as_dict=True)
|
|
LANGUAGES_LIST =[lang.capitalize() for lang in LANGUAGES_DICT.keys()]
|
|
|
|
# Лимиты Google
|
|
API_TIMEOUT = 1.0
|
|
MAX_TEXT_LEN = 4800
|
|
|
|
# ==========================================
|
|
# БЭКЭНД: ЛОГИКА ПЕРЕВОДА
|
|
# ==========================================
|
|
|
|
def safe_translate(text, target_lang='ru'):
|
|
"""Переводит текст, обходя лимиты по символам."""
|
|
translator = GoogleTranslator(source='auto', 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_name(text):
|
|
"""Определяет язык и возвращает его красивое название."""
|
|
try:
|
|
# Убираем лишние знаки для точности определения
|
|
import re
|
|
clean_text = re.sub(r'[^\w\s]', '', text)
|
|
if not clean_text.strip():
|
|
return "Неизвестно"
|
|
|
|
lang_code = detect(clean_text)
|
|
for name, code in LANGUAGES_DICT.items():
|
|
if code.lower() == lang_code.lower():
|
|
return name.capitalize()
|
|
return "Неизвестно"
|
|
except LangDetectException:
|
|
return "Неизвестно"
|
|
|
|
# ==========================================
|
|
# ФРОНТЭНД: СОВРЕМЕННЫЙ UI
|
|
# ==========================================
|
|
|
|
class ModernTranslatorApp(ctk.CTk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.title("⚡ Быстрый переводчик")
|
|
self.geometry("950x550")
|
|
self.minsize(700, 450)
|
|
|
|
# Настройка сетки окна (2 колонки)
|
|
self.grid_columnconfigure(0, weight=1)
|
|
self.grid_columnconfigure(1, weight=1)
|
|
self.grid_rowconfigure(1, weight=1)
|
|
|
|
self.build_ui()
|
|
|
|
def build_ui(self):
|
|
# --------------------------------------------------
|
|
# ВЕРХНЯЯ ПАНЕЛЬ (Заголовки и выбор языка)
|
|
# --------------------------------------------------
|
|
# Левый заголовок (Исходный язык)
|
|
self.frame_top_left = ctk.CTkFrame(self, fg_color="transparent")
|
|
self.frame_top_left.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew")
|
|
|
|
self.lbl_source = ctk.CTkLabel(self.frame_top_left, text="Исходный текст", font=("Roboto", 16, "bold"))
|
|
self.lbl_source.pack(side="left")
|
|
|
|
self.lbl_detected = ctk.CTkLabel(self.frame_top_left, text="(Определен: Авто)", text_color=("gray50", "gray70"), font=("Roboto", 12))
|
|
self.lbl_detected.pack(side="left", padx=10)
|
|
|
|
# Правый заголовок (Выбор целевого языка)
|
|
self.frame_top_right = ctk.CTkFrame(self, fg_color="transparent")
|
|
self.frame_top_right.grid(row=0, column=1, padx=20, pady=(20, 10), sticky="ew")
|
|
|
|
ctk.CTkLabel(self.frame_top_right, text="Перевод на:", font=("Roboto", 16, "bold")).pack(side="left")
|
|
|
|
self.target_lang_cb = ctk.CTkComboBox(self.frame_top_right, values=LANGUAGES_LIST, width=200, state="readonly")
|
|
self.target_lang_cb.set("Russian") # По умолчанию русский
|
|
self.target_lang_cb.pack(side="left", padx=15)
|
|
|
|
# --------------------------------------------------
|
|
# ЦЕНТРАЛЬНАЯ ПАНЕЛЬ (Текстовые поля)
|
|
# --------------------------------------------------
|
|
# Поле ввода (Слева)
|
|
self.textbox_in = ctk.CTkTextbox(self, font=("Roboto", 14), wrap="word", corner_radius=10)
|
|
self.textbox_in.grid(row=1, column=0, padx=(20, 10), pady=0, sticky="nsew")
|
|
|
|
# Поле вывода (Справа)
|
|
self.textbox_out = ctk.CTkTextbox(self, font=("Roboto", 14), wrap="word", corner_radius=10, fg_color=("gray90", "gray16"))
|
|
self.textbox_out.grid(row=1, column=1, padx=(10, 20), pady=0, sticky="nsew")
|
|
self.textbox_out.configure(state="disabled") # Только для чтения
|
|
|
|
# Подсказка
|
|
self.textbox_in.insert("0.0", "Введите текст здесь...\n\n(Подсказка: нажмите Enter для перевода, Shift+Enter для новой строки)")
|
|
self.textbox_in.bind("<FocusIn>", self.clear_placeholder)
|
|
|
|
# Привязка клавиш
|
|
self.textbox_in.bind("<Return>", self.handle_enter)
|
|
self.textbox_in.bind("<Shift-Return>", self.handle_shift_enter)
|
|
|
|
# --------------------------------------------------
|
|
# НИЖНЯЯ ПАНЕЛЬ (Кнопки-иконки)
|
|
# --------------------------------------------------
|
|
self.frame_bottom_left = ctk.CTkFrame(self, fg_color="transparent")
|
|
self.frame_bottom_left.grid(row=2, column=0, padx=20, pady=15, sticky="ew")
|
|
|
|
self.frame_bottom_right = ctk.CTkFrame(self, fg_color="transparent")
|
|
self.frame_bottom_right.grid(row=2, column=1, padx=20, pady=15, sticky="ew")
|
|
|
|
# Кнопки слева
|
|
self.btn_clear = ctk.CTkButton(self.frame_bottom_left, text="🗑️ Очистить", width=120, fg_color=("gray75", "gray30"), hover_color=("gray65", "gray25"), text_color=("black", "white"), command=self.clear_all)
|
|
self.btn_clear.pack(side="left")
|
|
|
|
self.btn_translate = ctk.CTkButton(self.frame_bottom_left, text="Перевести ➔", width=140, font=("Roboto", 14, "bold"), command=self.run_translation)
|
|
self.btn_translate.pack(side="right")
|
|
|
|
# Кнопки справа
|
|
self.btn_save = ctk.CTkButton(self.frame_bottom_right, text="💾 Сохранить", width=120, fg_color=("gray75", "gray30"), hover_color=("gray65", "gray25"), text_color=("black", "white"), command=self.save_to_file)
|
|
self.btn_save.pack(side="right")
|
|
|
|
self.btn_copy = ctk.CTkButton(self.frame_bottom_right, text="📋 Скопировать", width=120, fg_color=("gray75", "gray30"), hover_color=("gray65", "gray25"), text_color=("black", "white"), command=self.copy_to_clipboard)
|
|
self.btn_copy.pack(side="right", padx=10)
|
|
|
|
# Переменная для контроля плейсхолдера
|
|
self.placeholder_cleared = False
|
|
|
|
# ==========================================
|
|
# ЛОГИКА ИНТЕРФЕЙСА
|
|
# ==========================================
|
|
|
|
def clear_placeholder(self, event):
|
|
"""Убирает подсказку при первом клике на текстовое поле."""
|
|
if not self.placeholder_cleared:
|
|
self.textbox_in.delete("0.0", "end")
|
|
self.placeholder_cleared = True
|
|
|
|
def handle_enter(self, event):
|
|
"""Срабатывает при нажатии Enter (запускает перевод)."""
|
|
self.run_translation()
|
|
return "break" # Блокирует создание новой строки
|
|
|
|
def handle_shift_enter(self, event):
|
|
"""Срабатывает при Shift+Enter (создает новую строку)."""
|
|
pass # Ничего не делаем, CustomTkinter сам перенесет строку
|
|
|
|
def clear_all(self):
|
|
"""Очищает оба поля."""
|
|
self.textbox_in.delete("0.0", "end")
|
|
self.textbox_out.configure(state="normal")
|
|
self.textbox_out.delete("0.0", "end")
|
|
self.textbox_out.configure(state="disabled")
|
|
self.lbl_detected.configure(text="(Определен: Авто)")
|
|
self.placeholder_cleared = True # Чтобы подсказка не появлялась снова
|
|
|
|
def save_to_file(self):
|
|
"""Сохраняет перевод в .txt файл."""
|
|
text = self.textbox_out.get("0.0", "end").strip()
|
|
if not text:
|
|
messagebox.showinfo("Пусто", "Нет переведенного текста для сохранения.")
|
|
return
|
|
|
|
filepath = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")], title="Сохранить перевод")
|
|
if filepath:
|
|
try:
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
f.write(text)
|
|
messagebox.showinfo("Успех", "Текст успешно сохранен!")
|
|
except Exception as e:
|
|
messagebox.showerror("Ошибка", f"Не удалось сохранить файл:\n{e}")
|
|
|
|
def copy_to_clipboard(self):
|
|
"""Копирует переведенный текст в буфер обмена."""
|
|
text = self.textbox_out.get("0.0", "end").strip()
|
|
if text:
|
|
self.clipboard_clear()
|
|
self.clipboard_append(text)
|
|
|
|
# Меняем текст кнопки на пару секунд для визуала
|
|
self.btn_copy.configure(text="✔️ Скопировано!")
|
|
self.after(2000, lambda: self.btn_copy.configure(text="📋 Скопировать"))
|
|
|
|
def run_translation(self):
|
|
"""Главная функция перевода (выполняется в фоне)."""
|
|
src_text = self.textbox_in.get("0.0", "end").strip()
|
|
|
|
# Если в окне остался плейсхолдер или пусто
|
|
if not src_text or not self.placeholder_cleared:
|
|
return
|
|
|
|
target_name = self.target_lang_cb.get().lower()
|
|
target_code = LANGUAGES_DICT.get(target_name, 'ru')
|
|
|
|
# Блокируем UI на время запроса
|
|
self.btn_translate.configure(state="disabled", text="⏳ Перевод...")
|
|
self.textbox_out.configure(state="normal")
|
|
self.textbox_out.delete("0.0", "end")
|
|
self.textbox_out.insert("0.0", "Запрос к серверу перевода...")
|
|
self.textbox_out.configure(state="disabled")
|
|
|
|
def background_task():
|
|
# 1. Определяем язык
|
|
det_name = detect_language_name(src_text)
|
|
|
|
# 2. Переводим текст
|
|
try:
|
|
res = safe_translate(src_text, target_lang=target_code)
|
|
except Exception as e:
|
|
res = f"⚠️ Ошибка перевода. Проверьте интернет или повторите позже.\n\nДетали: {e}"
|
|
|
|
# 3. Возвращаем результат в главный поток (UI)
|
|
def update_ui():
|
|
self.lbl_detected.configure(text=f"(Определен: {det_name})")
|
|
|
|
self.textbox_out.configure(state="normal")
|
|
self.textbox_out.delete("0.0", "end")
|
|
self.textbox_out.insert("0.0", res)
|
|
self.textbox_out.configure(state="disabled")
|
|
|
|
self.btn_translate.configure(state="normal", text="Перевести ➔")
|
|
|
|
self.after(0, update_ui)
|
|
|
|
# Запускаем поток
|
|
threading.Thread(target=background_task, daemon=True).start()
|
|
|
|
if __name__ == "__main__":
|
|
app = ModernTranslatorApp()
|
|
app.mainloop() |