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("", self.clear_placeholder) # Привязка клавиш self.textbox_in.bind("", self.handle_enter) self.textbox_in.bind("", 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()