Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user