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("", run_translation) txt_in.bind("", lambda e: None) if __name__ == "__main__": root = tk.Tk() app = TranslatorApp(root) root.mainloop()