Столкнувшись с исключением, иногда не понимаешь: "divizion by zero" — что это? Начинающие разработчики часто не могут понять причину ошибки из-за неправильного понимания её а русском языке. Опытные разработчики также сталкиваются с неизвестными им исключениями, а часто лезть в переводчик для понимания ошибки не хочется. Сегодня я напишу модуль для быстрого перевода таких ошибок, и все непойманные исключения и предупреждения в Python будут выводиться на русском языке.

Для начала установим библиотеку, которая будет обращаться к Google Translator для перевода всех ошибок и предупреждений в Python:

pip install deep_translator

Также нам потребуется работать с цветами для вывода ошибок, поэтому нам потребуется установить colorama. Вот команда:

pip install colorama

Теперь приступим к написанию кода! Я разбил код модуля на несколько файлов, каждый из которых направлен на выполнение совей задачи. Вот код:

error_translator.py:

import sys
import warnings
import os
import json
import time
import re
from deep_translator import GoogleTranslator

SOURCE_LANG = 'auto'
TARGET_LANG = 'ru'
CACHE_DIR = os.path.join(os.path.dirname(__file__), 'error_cache')
CACHE_FILE = os.path.join(CACHE_DIR, 'translations.json')
MAX_CACHE_ENTRIES = 100000

translator = GoogleTranslator(source=SOURCE_LANG, target=TARGET_LANG)

def _load_cache():
    if not os.path.exists(CACHE_FILE):
        return {}
    try:
        with open(CACHE_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    except:
        return {}

def _save_cache(cache):
    os.makedirs(CACHE_DIR, exist_ok=True)
    if len(cache) > MAX_CACHE_ENTRIES:
        sorted_items = sorted(cache.items(), key=lambda x: x[1].get('timestamp', 0))
        cache = dict(sorted_items[-MAX_CACHE_ENTRIES:])
    with open(CACHE_FILE, 'w', encoding='utf-8') as f:
        json.dump(cache, f, ensure_ascii=False, indent=2)

def cached_translate(text):
    if not text:
        return text
    cache = _load_cache()
    if text in cache:
        return cache[text]['translation']
    try:
        translated = translator.translate(text)
    except Exception:
        return text
    cache[text] = {'translation': translated, 'timestamp': time.time()}
    _save_cache(cache)
    return translated

def translate_exc_message(msg):
    cleaned = re.sub(r'0x[0-9a-fA-F]+', '...', msg)
    return cached_translate(cleaned)

original_excepthook = sys.excepthook

def translating_excepthook(exc_type, exc_value, exc_tb):
    if exc_value is not None:
        translated = translate_exc_message(str(exc_value))
        try:
            new_exc = exc_type(translated)
        except Exception:
            new_exc = exc_value
    else:
        new_exc = exc_value
    original_excepthook(exc_type, new_exc, exc_tb)

original_showwarning = warnings.showwarning

def translating_showwarning(message, category, filename, lineno, file=None, line=None):
    translated = translate_exc_message(str(message))
    original_showwarning(translated, category, filename, lineno, file, line)

sys.excepthook = translating_excepthook
warnings.showwarning = translating_showwarning

run.py:

import sys
import subprocess
import re
import os
from pathlib import Path
import colorama

colorama.init()

sys.path.insert(0, str(Path(__file__).parent))
from error_translator import cached_translate

sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')

RED = colorama.Fore.RED
RESET = colorama.Style.RESET_ALL

error_header_pattern = re.compile(
    r'^(Traceback \(most recent call last\):|Exception in|During handling of|The above exception was)'
)

in_error_block = False
pending_warning_source = False

def translate_line(line):
    global in_error_block, pending_warning_source

    if pending_warning_source:
        pending_warning_source = False
        if line.startswith('  ') or line.startswith('\t'):
            return RED + line.rstrip('\n') + RESET + '\n'
        else:
            in_error_block = False
            return line

    if error_header_pattern.match(line):
        in_error_block = True
        return RED + line.rstrip('\n') + RESET + '\n'

    if not in_error_block and line.startswith('  File '):
        in_error_block = True
        return RED + line.rstrip('\n') + RESET + '\n'

    warning_match = re.search(r'(:\d+:)\s*(\w+Warning):\s*(.*)', line)
    if warning_match:
        if not in_error_block:
            in_error_block = True
        pending_warning_source = True
        prefix = line[:warning_match.start(1)]
        lineno = warning_match.group(1)
        category = warning_match.group(2)
        msg = warning_match.group(3)
        if msg:
            translated_msg = cached_translate(msg)
            return f"{RED}{prefix}{lineno} {category}: {translated_msg}{RESET}\n"
        else:
            return RED + line.rstrip('\n') + RESET + '\n'

    if in_error_block:
        if line.startswith('  File ') or line.startswith('    ') or line.startswith('\t'):
            return RED + line.rstrip('\n') + RESET + '\n'

        m = re.match(r'^(\s*(?:\w+\.)*\w*(?:Error|Warning|Exception)):\s*(.*)', line)
        if m:
            exc_part, msg = m.groups()
            if msg:
                translated = f"{RED}{exc_part}: {cached_translate(msg)}{RESET}\n"
            else:
                translated = RED + line.rstrip('\n') + RESET + '\n'
            in_error_block = False
            return translated

        if line.strip() == '':
            return RED + '\n'

        in_error_block = False
        return line

    return line

def run_target(target_script):
    env = os.environ.copy()
    env['PYTHONUNBUFFERED'] = '1'

    p = subprocess.Popen(
        [sys.executable, target_script],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        encoding='utf-8',
        errors='replace',
        env=env,
        bufsize=1
    )

    for line in p.stdout:
        sys.stdout.write(translate_line(line))
        sys.stdout.flush()

    p.wait()
    sys.exit(p.returncode)

translate_errors.py:

TARGET = "test.py"; from run import run_target; run_target(TARGET)

Работает это так: при запуске модуля translate_errors.py он запускает наш основной скрипт, название которого мы указали в константе TARGET в отдельном процессе. После выполнения скрипта программиста наш модуль проверяет каждую строчку вывода. Как только модуль натыкается на ошибку, он отправляет её в Google для перевода. Переведённый текст ошибки встаёт на место старого английского, после чего программа выводит переведённую на русский ошибку в консоль.

Пример вывода исключения в Python на русском языке
Пример вывода исключения в Python на русском языке

Не стоит забывать, что это работает не только с ошибками, но и с предупреждениями. Создадим скрипт test_warning.py и вставим приведённый ниже код, а затем запускаем translate_errors.py:

import warnings

warnings.warn("This is a test warning")
Переведённое предупреждение
Переведённое предупреждение

В модуле также предусмотрено автоматические кэширование всех переведённых ошибок на диск (ограничение: до 100000 записей, однако его можно изменять в коде модуля). Для того, чтобы переводчик работал, нужно стабильное интернет-соединение. Если интернет будет отсутствовать, перевод выполняться не будет (за исключением случаев, когда ошибка уже записана на диск и берётся из кэшированных записей)!

Если что-то не будет работать, пишите в комментарии — разберёмся. Ниже можно пройти опрос. Он поможет найти баги в работе проекта и улучшить его для всех. По итогам опроса мы обновим код, исправив все баги и выполним предложения по улучшению. Надеюсь, эта статья была полезной!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
В какой цвет красятся ошибки после перевода?
100%В красный, сохранение цвета работает3
0%В белый, цвет не сохранился (предоставлю в комментариях текст ошибки, которая не покрасилась для дальнейшего разбора)0
Проголосовали 3 пользователя. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
При использовании графических библиотек (PyGame, Tkinter, MatplotLib) как вела себя программа?
0%Все ошибки перевелись, результат удовлетворил0
0%Некоторые ошибки не перевелись (покажу в комментариях)0
Никто еще не голосовал. Воздержался 1 пользователь.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Имеются ли проблемы с кодировкой при выводе?
0%Нет, ошибки выводятся с правильной кодировкой0
0%Да, текст нечитаемый, кодировка не работает0
Никто еще не голосовал. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как модуль справляется с переводом предупреждений в Python?
0%Предупреждения переводятся, цвет и перевод в порядке0
0%Предупреждения не переводятся, цвет красный, в порядке0
0%Предупреждения переводятся, однако красятся в белый цвет0
0%Предупреждения не переводятся и красятся в белый цвет0
Никто еще не голосовал. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Готовы ли вы использовать данный проект для повседневного использования?
50%Да, готов2
0%Уже использую0
50%Нет, не готов2
0%Нет, потому что проект содержит много багов (расскажу о них в комментариях)0
Проголосовали 4 пользователя. Воздержавшихся нет.