Initial commit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dinlo
2026-05-31 18:45:49 +08:00
commit 5dc00d6e70
21 changed files with 103910 additions and 0 deletions
+261
View File
@@ -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()