feature: i18n (#1025)

This commit is contained in:
Kirill
2026-06-23 15:41:49 +03:00
committed by GitHub
parent fed772049b
commit 85b5e7f22a
9 changed files with 810 additions and 309 deletions
+215 -185
View File
@@ -17,63 +17,16 @@ from ui.ctk_theme import (
main_content_frame,
)
from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets
from ui.i18n import (
label_from_language,
language_from_label,
language_option_labels,
set_language,
t,
)
log = logging.getLogger('tg-mtproto-proxy')
_TIP_HOST = (
"Адрес, на котором прокси принимает подключения.\n"
"Обычно 127.0.0.1 — локальная сеть, 0.0.0.0 - все интерфейсы"
)
_TIP_PORT = (
"Порт прокси. В Telegram Desktop в настройках прокси должен быть "
"указан тот же порт"
)
_TIP_SECRET = "Секретный ключ для авторизации клиентов"
_TIP_DC = (
"Соответствие номера датацентра Telegram (DC) и IP-адреса сервера.\n"
"Каждая строка: «номер:IP», например 4:149.154.167.220. "
"Прокси по этим правилам направляет трафик к нужным серверам Telegram\n\n"
"Если у вас не работают медиа и работает CF-прокси, то попробуйте убрать строку 2:149.154.167.220"
)
_TIP_VERBOSE = (
"Если включено, в файл логов пишется больше подробностей — "
"необходимо при поиске неполадок"
)
_TIP_BUF_KB = (
"Размер буфера приёма/передачи в килобайтах.\n"
"Больше значение — больше выделение памяти на сокет"
)
_TIP_POOL = (
"Сколько параллельных WebSocket-сессий к одному датацентру можно держать.\n"
"Увеличение может помочь при высокой нагрузке"
)
_TIP_LOG_MB = (
"Максимальный размер файла лога; при достижении лимита файл перезаписывается"
)
_TIP_AUTOSTART = (
"Запускать TG WS Proxy при входе в Windows. "
"Если вы переместите программу в другую папку, автозапуск сбросится"
)
_TIP_CHECK_UPDATES = "При запуске проверять наличие обновлений"
_TIP_CFPROXY = (
"Использовать Cloudflare прокси для недоступных датацентров"
)
_TIP_CFPROXY_DOMAIN = (
"Ваши собственные домены, проксируемые через Cloudflare, для WS-подключения.\n"
"Несколько доменов указывайте через запятую.\n"
"Если не указаны — выбираются автоматически из поддерживаемых доменов"
)
_TIP_CFPROXY_USER_DOMAIN_CB = (
"Указать свои домены вместо автоматического выбора"
)
_TIP_CFWORKER_DOMAIN = (
"Домены Cloudflare Worker (например, name.account.workers.dev).\n"
"Несколько доменов указывайте через запятую.\n"
"Прокси передает через них подключение к Telegram DC по IP"
)
_TIP_SAVE = "Сохранить настройки"
_TIP_CANCEL = "Закрыть окно без сохранения изменений"
_CFPROXY_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md"
_CFWORKER_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md"
_CFPROXY_TEST_DCS = [1, 2, 3, 4, 5, 203]
@@ -123,11 +76,11 @@ def _run_connectivity_test(cases: list) -> dict:
if "101" in first:
results[dc] = True
else:
results[dc] = first or "нет ответа"
results[dc] = first or t("connectivity.no_response")
ssock.close()
raw.close()
except _socket.timeout:
results[dc] = "таймаут"
results[dc] = t("connectivity.timeout")
except OSError as exc:
msg = str(exc)
results[dc] = msg[:60] if len(msg) > 60 else msg
@@ -183,30 +136,34 @@ def _show_connectivity_results(title_base: str, results: dict,
from tkinter import messagebox as _mb
ok = [dc for dc, v in results.items() if v is True]
total = len(_CFPROXY_TEST_DCS)
if auto_mode:
if domain:
title = f"{title_base}: доступен"
msg = f"\u2713 {title_base} работает. {len(ok)} из {len(_CFPROXY_TEST_DCS)} серверов доступны."
title = t("connectivity.available", title=title_base)
msg = t("connectivity.auto_ok", title=title_base, ok=len(ok), total=total)
else:
title = f"{title_base}: недоступен"
title = t("connectivity.unavailable", title=title_base)
msg = unavailable_message
else:
fail = [(dc, v) for dc, v in results.items() if v is not True]
if len(ok) == len(_CFPROXY_TEST_DCS):
title = f"{title_base}: всё работает"
msg = f"\u2713 Все {len(_CFPROXY_TEST_DCS)} серверов доступны через {domain}."
if len(ok) == total:
title = t("connectivity.all_ok", title=title_base)
msg = t("connectivity.all_ok_domain", total=total, domain=domain)
elif not ok:
title = f"{title_base}: недоступен"
msg = f"\u2717 Ни один сервер не отвечает через {domain}.\n\nОшибки:\n"
msg += "\n".join(f" {label_prefix}{dc}: {v}" for dc, v in fail)
else:
title = f"{title_base}: частично работает"
msg = (
f"Домен: {domain}\n\n"
f"\u2713 Работают: {', '.join(f'{label_prefix}{dc}' for dc in ok)}\n\n"
f"\u2717 Недоступны:\n"
+ "\n".join(f" {label_prefix}{dc}: {v}" for dc, v in fail)
title = t("connectivity.unavailable", title=title_base)
errors = "\n".join(
t("connectivity.error_line", prefix=label_prefix, dc=dc, error=v)
for dc, v in fail
)
msg = t("connectivity.none_ok", domain=domain, errors=errors)
else:
title = t("connectivity.partial", title=title_base)
ok_list = ", ".join(f"{label_prefix}{dc}" for dc in ok)
fail_list = "\n".join(
t("connectivity.error_line", prefix=label_prefix, dc=dc, error=v)
for dc, v in fail
)
msg = t("connectivity.partial_detail", domain=domain, ok_list=ok_list, fail_list=fail_list)
root = _tk.Tk()
root.withdraw()
@@ -232,26 +189,25 @@ def _show_multi_connectivity_results(title_base: str, per_domain: dict,
fail = [(dc, v) for dc, v in results.items() if v is not True]
if len(ok) == total:
any_ok = True
blocks.append(f"\u2713 {domain}: все {total} серверов доступны")
blocks.append(t("connectivity.multi_all_ok", domain=domain, total=total))
elif not ok:
all_ok = False
blocks.append(f"\u2717 {domain}: недоступен")
blocks.append(t("connectivity.multi_fail", domain=domain))
else:
all_ok = False
any_ok = True
ok_list = ", ".join(f"{label_prefix}{dc}" for dc in ok)
fail_list = ", ".join(f"{label_prefix}{dc}" for dc, _ in fail)
blocks.append(
f"~ {domain}: работают "
f"{', '.join(f'{label_prefix}{dc}' for dc in ok)}; "
f"недоступны "
f"{', '.join(f'{label_prefix}{dc}' for dc, _ in fail)}"
t("connectivity.multi_partial", domain=domain, ok_list=ok_list, fail_list=fail_list)
)
if all_ok:
title = f"{title_base}: всё работает"
title = t("connectivity.all_ok", title=title_base)
elif any_ok:
title = f"{title_base}: частично работает"
title = t("connectivity.partial", title=title_base)
else:
title = f"{title_base}: недоступен"
title = t("connectivity.unavailable", title=title_base)
msg = "\n\n".join(blocks)
root = _tk.Tk()
@@ -265,12 +221,32 @@ def _show_multi_connectivity_results(title_base: str, per_domain: dict,
_INNER_W = 396
_APPEARANCE_OPTIONS = ["Авто", "Светлая", "Тёмная"]
_APPEARANCE_FROM_CFG = {"auto": "Авто", "light": "Светлая", "dark": "Тёмная"}
_APPEARANCE_TO_CFG = {"Авто": "auto", "Светлая": "light", "Тёмная": "dark"}
_APPEARANCE_KEYS = ("auto", "light", "dark")
_APPEARANCE_TO_CTK = {"auto": "system", "light": "Light", "dark": "Dark"}
def _appearance_options() -> List[str]:
return [t(f"appearance.{key}") for key in _APPEARANCE_KEYS]
def _appearance_from_cfg(value: str) -> str:
if value in _APPEARANCE_KEYS:
return t(f"appearance.{value}")
return t("appearance.auto")
def _appearance_to_cfg(label: str) -> str:
for key in _APPEARANCE_KEYS:
if t(f"appearance.{key}") == label:
return key
return "auto"
def _sync_language_combobox(combo: Any, var: Any, cfg_value: str) -> None:
combo.configure(values=[label for _, label in language_option_labels()])
var.set(label_from_language(cfg_value))
def _entry(ctk, parent, theme, *, var=None, width=0, height=36, radius=10, **kw):
opts = dict(
font=(theme.ui_font_family, 13), corner_radius=radius,
@@ -374,6 +350,7 @@ class TrayConfigFormWidgets:
cfproxy_user_domain_var: Optional[Any] = None
cfproxy_worker_domain_var: Optional[Any] = None
appearance_var: Optional[Any] = None
language_var: Optional[Any] = None
def install_tray_config_form(
@@ -385,11 +362,15 @@ def install_tray_config_form(
*,
show_autostart: bool = False,
autostart_value: bool = False,
on_language_change: Optional[Callable[[], None]] = None,
) -> TrayConfigFormWidgets:
lang_cfg = cfg.get("language", default_config["language"])
set_language(lang_cfg)
header = ctk.CTkFrame(frame, fg_color="transparent")
header.pack(fill="x", pady=(0, 2))
ctk.CTkLabel(
header, text="Настройки",
header, text=t("settings.title"),
font=(theme.ui_font_family, 17, "bold"),
text_color=theme.text_primary, anchor="w",
).pack(side="left")
@@ -398,35 +379,16 @@ def install_tray_config_form(
font=(theme.ui_font_family, 12),
text_color=theme.text_secondary, anchor="e",
).pack(side="right", padx=(4, 0))
appearance_var = ctk.StringVar(
value=_APPEARANCE_FROM_CFG.get(cfg.get("appearance", "auto"), "Авто")
value=_appearance_from_cfg(cfg.get("appearance", "auto"))
)
def _on_appearance_change(choice: str) -> None:
cfg_val = _APPEARANCE_TO_CFG.get(choice, "auto")
cfg_val = _appearance_to_cfg(choice)
ctk.set_appearance_mode(_APPEARANCE_TO_CTK[cfg_val])
cfg["appearance"] = cfg_val
ctk.CTkComboBox(
header,
values=_APPEARANCE_OPTIONS,
variable=appearance_var,
width=102,
height=28,
font=(theme.ui_font_family, 12),
text_color=theme.text_secondary,
fg_color=theme.field_bg,
border_color=theme.field_border,
button_color=theme.field_border,
button_hover_color=theme.text_secondary,
dropdown_fg_color=theme.field_bg,
dropdown_text_color=theme.text_primary,
dropdown_hover_color=theme.field_border,
corner_radius=8,
state="readonly",
command=_on_appearance_change,
).pack(side="right")
ctk.CTkButton(
header, text="Donate ♥", width=90, height=28,
font=(theme.ui_font_family, 13, "bold"), corner_radius=8,
@@ -438,22 +400,88 @@ def install_tray_config_form(
),
).pack(side="right", padx=(0, 6))
conn = _config_section(ctk, frame, theme, "Подключение MTProto")
ui_inner = _config_section(ctk, frame, theme, t("section.interface"))
ui_row = ctk.CTkFrame(ui_inner, fg_color="transparent")
ui_row.pack(fill="x")
lang_col = ctk.CTkFrame(ui_row, fg_color="transparent")
lang_col.pack(side="left", fill="x", expand=True, padx=(0, 8))
theme_col = ctk.CTkFrame(ui_row, fg_color="transparent")
theme_col.pack(side="left", fill="x", expand=True, padx=(8, 0))
language_var = ctk.StringVar(value=label_from_language(lang_cfg))
_label(ctk, lang_col, theme, t("settings.language"), size=11).pack(
anchor="w", pady=(0, 2)
)
language_combo = ctk.CTkComboBox(
lang_col,
values=[label for _, label in language_option_labels()],
variable=language_var,
height=32,
font=(theme.ui_font_family, 12),
text_color=theme.text_primary,
fg_color=theme.bg,
border_color=theme.field_border,
button_color=theme.field_border,
button_hover_color=theme.text_secondary,
dropdown_fg_color=theme.field_bg,
dropdown_text_color=theme.text_primary,
dropdown_hover_color=theme.field_border,
corner_radius=8,
state="readonly",
)
language_combo.pack(fill="x")
_sync_language_combobox(language_combo, language_var, lang_cfg)
def _on_language_change(choice: str) -> None:
lang = language_from_label(choice)
set_language(lang)
_sync_language_combobox(language_combo, language_var, lang)
if on_language_change is not None:
on_language_change()
language_combo.configure(command=_on_language_change)
_label(ctk, theme_col, theme, t("settings.theme"), size=11).pack(
anchor="w", pady=(0, 2)
)
theme_combo = ctk.CTkComboBox(
theme_col,
values=_appearance_options(),
variable=appearance_var,
height=32,
font=(theme.ui_font_family, 12),
text_color=theme.text_primary,
fg_color=theme.bg,
border_color=theme.field_border,
button_color=theme.field_border,
button_hover_color=theme.text_secondary,
dropdown_fg_color=theme.field_bg,
dropdown_text_color=theme.text_primary,
dropdown_hover_color=theme.field_border,
corner_radius=8,
state="readonly",
command=_on_appearance_change,
)
theme_combo.pack(fill="x")
conn = _config_section(ctk, frame, theme, t("section.mtproto"))
host_row = ctk.CTkFrame(conn, fg_color="transparent")
host_row.pack(fill="x")
host_col, host_var = _labeled_entry(
ctk, host_row, theme, "IP-адрес",
ctk, host_row, theme, t("label.host"),
cfg.get("host", default_config["host"]),
tip=_TIP_HOST, width=160, pack_fill=True,
tip=t("tip.host"), width=160, pack_fill=True,
)
host_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
port_col, port_var = _labeled_entry(
ctk, host_row, theme, "Порт",
ctk, host_row, theme, t("label.port"),
cfg.get("port", default_config["port"]),
tip=_TIP_PORT, width=100,
tip=t("tip.port"), width=100,
)
port_col.pack(side="left")
@@ -461,9 +489,9 @@ def install_tray_config_form(
secret_row.pack(fill="x")
secret_col, secret_var = _labeled_entry(
ctk, secret_row, theme, "Secret",
ctk, secret_row, theme, t("label.secret"),
cfg.get("secret", default_config["secret"]),
tip=_TIP_SECRET, width=160, pack_fill=True,
tip=t("tip.secret"), width=160, pack_fill=True,
)
secret_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
@@ -478,8 +506,8 @@ def install_tray_config_form(
command=lambda: secret_var.set(os.urandom(16).hex()),
).pack()
dc_inner = _config_section(ctk, frame, theme, "Датацентры Telegram (DC → IP)")
dc_lbl = _label(ctk, dc_inner, theme, "По одному правилу на строку, формат: номер:IP", size=11)
dc_inner = _config_section(ctk, frame, theme, t("section.dc"))
dc_lbl = _label(ctk, dc_inner, theme, t("label.dc_hint"), size=11)
dc_lbl.pack(anchor="w", pady=(0, 4))
dc_textbox = ctk.CTkTextbox(
dc_inner, width=_INNER_W, height=88,
@@ -489,9 +517,9 @@ def install_tray_config_form(
)
dc_textbox.pack(fill="x")
dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", default_config["dc_ip"])))
attach_tooltip_to_widgets([dc_lbl, dc_textbox], _TIP_DC)
attach_tooltip_to_widgets([dc_lbl, dc_textbox], t("tip.dc"))
cf_inner = _config_section(ctk, frame, theme, "Cloudflare Proxy")
cf_inner = _config_section(ctk, frame, theme, t("section.cfproxy"))
cf_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
cf_row.pack(fill="x", pady=(0, 4))
@@ -499,9 +527,9 @@ def install_tray_config_form(
cfproxy_var = ctk.BooleanVar(
value=cfg.get("cfproxy", default_config.get("cfproxy", True))
)
cf_cb = _checkbox(ctk, cf_row, theme, "Включить CF-прокси", cfproxy_var)
cf_cb = _checkbox(ctk, cf_row, theme, t("label.cf_enable"), cfproxy_var)
cf_cb.pack(side="left", padx=(0, 16))
attach_ctk_tooltip(cf_cb, _TIP_CFPROXY)
attach_ctk_tooltip(cf_cb, t("tip.cfproxy"))
_cf_test_btn = [None]
@@ -512,7 +540,7 @@ def install_tray_config_form(
)
btn = _cf_test_btn[0]
if btn:
btn.configure(text="...", state="disabled")
btn.configure(text=t("button.test_loading"), state="disabled")
import threading as _threading
if user_domains:
def _worker():
@@ -522,14 +550,14 @@ def install_tray_config_form(
btn.after(
0,
lambda: _show_multi_connectivity_results(
"CF-прокси", per, label_prefix='kws',
t("connectivity.cfproxy_title"), per, label_prefix='kws',
),
)
except Exception as exc:
log.error("CF proxy test failed: %s", exc)
finally:
if btn:
btn.after(0, lambda: btn.configure(text="Тест", state="normal"))
btn.after(0, lambda: btn.configure(text=t("button.test"), state="normal"))
_threading.Thread(target=_worker, daemon=True).start()
else:
def _worker_auto():
@@ -539,23 +567,21 @@ def install_tray_config_form(
btn.after(
0,
lambda: _show_connectivity_results(
"CF-прокси", res,
t("connectivity.cfproxy_title"), res,
domain=ok_domain or '',
auto_mode=True,
unavailable_message=(
"\u2717 Ни один из автоматических CF-доменов не отвечает."
),
unavailable_message=t("connectivity.cf_auto_fail"),
),
)
except Exception as exc:
log.error("CF proxy auto-test failed: %s", exc)
finally:
if btn:
btn.after(0, lambda: btn.configure(text="Тест", state="normal"))
btn.after(0, lambda: btn.configure(text=t("button.test"), state="normal"))
_threading.Thread(target=_worker_auto, daemon=True).start()
_cf_test_widget = ctk.CTkButton(
cf_row, text="Тест", width=56, height=28,
cf_row, text=t("button.test"), width=56, height=28,
font=(theme.ui_font_family, 13), corner_radius=8,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff", border_width=1, border_color=theme.field_border,
@@ -571,9 +597,9 @@ def install_tray_config_form(
cfg.get("cfproxy_user_domain", default_config.get("cfproxy_user_domain", ""))
)
cf_custom_cb_var = ctk.BooleanVar(value=bool(saved_user_domains))
cf_custom_cb = _checkbox(ctk, cf_custom_row, theme, "Свой домен", cf_custom_cb_var)
cf_custom_cb = _checkbox(ctk, cf_custom_row, theme, t("label.cf_custom_domain"), cf_custom_cb_var)
cf_custom_cb.pack(side="left", padx=(0, 10))
attach_ctk_tooltip(cf_custom_cb, _TIP_CFPROXY_USER_DOMAIN_CB)
attach_ctk_tooltip(cf_custom_cb, t("tip.cfproxy_user_domain_cb"))
ctk.CTkButton(
cf_custom_row, text="?", width=28, height=32,
@@ -589,7 +615,7 @@ def install_tray_config_form(
height=32, radius=8,
)
cf_domain_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
attach_ctk_tooltip(cf_domain_entry, _TIP_CFPROXY_DOMAIN)
attach_ctk_tooltip(cf_domain_entry, t("tip.cfproxy_domain"))
def _sync_domain_entry(*_):
state = "normal" if cf_custom_cb_var.get() else "disabled"
@@ -600,11 +626,11 @@ def install_tray_config_form(
cf_custom_cb_var.trace_add("write", _sync_domain_entry)
_sync_domain_entry()
cf_worker_inner = _config_section(ctk, frame, theme, "Cloudflare Worker")
cf_worker_inner = _config_section(ctk, frame, theme, t("section.cfworker"))
cf_worker_row = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
cf_worker_row.pack(fill="x", pady=(0, 4))
cf_worker_lbl = _label(ctk, cf_worker_row, theme, "Cloudflare Worker домены (через запятую)", size=11)
cf_worker_lbl = _label(ctk, cf_worker_row, theme, t("label.cfworker_domains"), size=11)
cf_worker_lbl.pack(anchor="w", pady=(0, 2))
cf_worker_input = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
@@ -620,7 +646,7 @@ def install_tray_config_form(
height=32, radius=8,
)
cf_worker_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
attach_tooltip_to_widgets([cf_worker_lbl, cf_worker_entry], _TIP_CFWORKER_DOMAIN)
attach_tooltip_to_widgets([cf_worker_lbl, cf_worker_entry], t("tip.cfworker_domain"))
_cfworker_test_btn = [None]
@@ -636,7 +662,7 @@ def install_tray_config_form(
btn = _cfworker_test_btn[0]
if not domains or btn is None:
return
btn.configure(text="...", state="disabled")
btn.configure(text=t("button.test_loading"), state="disabled")
import threading as _threading
def _worker():
@@ -645,13 +671,13 @@ def install_tray_config_form(
btn.after(
0,
lambda: _show_multi_connectivity_results(
"CF Worker", per, label_prefix='DC',
t("connectivity.cfworker_title"), per, label_prefix='DC',
),
)
except Exception as exc:
log.error("CF worker test failed: %s", exc)
finally:
btn.after(0, lambda: btn.configure(text="Тест"))
btn.after(0, lambda: btn.configure(text=t("button.test")))
btn.after(0, _sync_cfworker_test_button)
_threading.Thread(target=_worker, daemon=True).start()
@@ -665,7 +691,7 @@ def install_tray_config_form(
).pack(side="right")
_cfworker_test_widget = ctk.CTkButton(
cf_worker_input, text="Тест", width=56, height=32,
cf_worker_input, text=t("button.test"), width=56, height=32,
font=(theme.ui_font_family, 13), corner_radius=8,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff", border_width=1, border_color=theme.field_border,
@@ -676,20 +702,20 @@ def install_tray_config_form(
cfproxy_worker_domain_var.trace_add("write", _sync_cfworker_test_button)
_sync_cfworker_test_button()
log_inner = _config_section(ctk, frame, theme, "Логи и производительность")
log_inner = _config_section(ctk, frame, theme, t("section.logs"))
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
verbose_cb = _checkbox(ctk, log_inner, theme, "Подробное логирование (verbose)", verbose_var)
verbose_cb = _checkbox(ctk, log_inner, theme, t("label.verbose"), verbose_var)
verbose_cb.pack(anchor="w", pady=(0, 6))
attach_ctk_tooltip(verbose_cb, _TIP_VERBOSE)
attach_ctk_tooltip(verbose_cb, t("tip.verbose"))
adv_frame = ctk.CTkFrame(log_inner, fg_color="transparent")
adv_frame.pack(fill="x")
adv_rows = [
("Буфер, КБ (по умолчанию 256)", "buf_kb", _TIP_BUF_KB),
("Пул WebSocket-сессий (по умолчанию 4)", "pool_size", _TIP_POOL),
("Макс. размер лога, МБ (по умолчанию 5)", "log_max_mb", _TIP_LOG_MB),
(t("label.buf_kb"), "buf_kb", t("tip.buf_kb")),
(t("label.pool_size"), "pool_size", t("tip.pool")),
(t("label.log_max_mb"), "log_max_mb", t("tip.log_mb")),
]
for label_text, key, tip in adv_rows:
col = ctk.CTkFrame(adv_frame, fg_color="transparent")
@@ -706,38 +732,32 @@ def install_tray_config_form(
adv_entries = list(adv_frame.winfo_children())
adv_keys = ("buf_kb", "pool_size", "log_max_mb")
upd_inner = _config_section(ctk, frame, theme, "Обновления")
upd_inner = _config_section(ctk, frame, theme, t("section.updates"))
st = get_status()
check_updates_var = ctk.BooleanVar(
value=bool(cfg.get("check_updates", default_config.get("check_updates", True)))
)
upd_cb = _checkbox(ctk, upd_inner, theme, "Проверять обновления при запуске", check_updates_var)
upd_cb = _checkbox(ctk, upd_inner, theme, t("label.check_updates"), check_updates_var)
upd_cb.pack(anchor="w", pady=(0, 6))
attach_ctk_tooltip(upd_cb, _TIP_CHECK_UPDATES)
attach_ctk_tooltip(upd_cb, t("tip.check_updates"))
if st.get("error"):
upd_status = "Не удалось связаться с GitHub. Проверьте сеть."
upd_status = t("updates.status_error")
elif not st.get("checked"):
upd_status = "Статус появится после фоновой проверки при запуске."
upd_status = t("updates.status_pending")
elif st.get("has_update") and st.get("latest"):
upd_status = (
f"На GitHub доступна версия {st['latest']} "
f"(у вас {__version__})."
)
upd_status = t("updates.status_available", latest=st["latest"], current=__version__)
elif st.get("ahead_of_release") and st.get("latest"):
upd_status = (
f"У вас {__version__} — новее последнего релиза на GitHub "
f"({st['latest']})."
)
upd_status = t("updates.status_ahead", current=__version__, latest=st["latest"])
else:
upd_status = "Установлена последняя известная версия с GitHub."
upd_status = t("updates.status_latest")
_label(ctk, upd_inner, theme, upd_status, size=11,
justify="left", wraplength=_INNER_W).pack(anchor="w", pady=(0, 8))
rel_url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
ctk.CTkButton(
upd_inner, text="Открыть страницу релиза", height=32,
upd_inner, text=t("button.open_release"), height=32,
font=(theme.ui_font_family, 13), corner_radius=8,
fg_color=theme.field_bg, hover_color=theme.field_border,
text_color=theme.text_primary, border_width=1,
@@ -747,17 +767,17 @@ def install_tray_config_form(
autostart_var = None
if show_autostart:
sys_inner = _config_section(ctk, frame, theme, "Запуск Windows", bottom_spacer=4)
sys_inner = _config_section(ctk, frame, theme, t("section.windows_startup"), bottom_spacer=4)
autostart_var = ctk.BooleanVar(value=autostart_value)
as_cb = _checkbox(ctk, sys_inner, theme, "Автозапуск при включении компьютера", autostart_var)
as_cb = _checkbox(ctk, sys_inner, theme, t("label.autostart"), autostart_var)
as_cb.pack(anchor="w", pady=(0, 4))
as_hint = _label(
ctk, sys_inner, theme,
"Если переместить программу в другую папку, запись автозапуска может сброситься.",
t("label.autostart_hint"),
size=11, justify="left", wraplength=_INNER_W,
)
as_hint.pack(anchor="w")
attach_tooltip_to_widgets([as_cb, as_hint], _TIP_AUTOSTART)
attach_tooltip_to_widgets([as_cb, as_hint], t("tip.autostart"))
return TrayConfigFormWidgets(
host_var=host_var, port_var=port_var, secret_var=secret_var,
@@ -768,6 +788,7 @@ def install_tray_config_form(
cfproxy_user_domain_var=cfproxy_user_domain_var,
cfproxy_worker_domain_var=cfproxy_worker_domain_var,
appearance_var=appearance_var,
language_var=language_var,
)
@@ -800,14 +821,14 @@ def validate_config_form(
try:
_sock.inet_aton(host_val)
except OSError:
return "Некорректный IP-адрес."
return t("validation.bad_host")
try:
port_val = int(widgets.port_var.get().strip())
if not (1 <= port_val <= 65535):
raise ValueError
except ValueError:
return "Порт должен быть числом 1-65535"
return t("validation.bad_port")
lines = [
line.strip()
@@ -817,15 +838,22 @@ def validate_config_form(
try:
parse_dc_ip_list(lines)
except ValueError as e:
return str(e)
msg = str(e)
if "expected DC:IP" in msg:
entry = msg.split("format ", 1)[-1].rstrip(")")
return t("validation.dc_format", entry=entry.strip("'"))
if msg.startswith("Invalid --dc-ip "):
entry = msg.split(" ", 2)[-1]
return t("validation.dc_invalid", entry=entry)
return msg
secret_val = widgets.secret_var.get().strip()
if len(secret_val) != 32:
return "Secret должен содержать ровно 32 hex-символа (16 байт)."
return t("validation.bad_secret_len")
try:
bytes.fromhex(secret_val)
except ValueError:
return "Secret должен состоять только из hex-символов (0-9, a-f)."
return t("validation.bad_secret_hex")
new_cfg: Dict[str, Any] = {
"host": host_val,
@@ -851,7 +879,9 @@ def validate_config_form(
if widgets.cfproxy_worker_domain_var is not None:
new_cfg["cfproxy_worker_domain"] = coerce_domain_list(widgets.cfproxy_worker_domain_var.get())
if widgets.appearance_var is not None:
new_cfg["appearance"] = _APPEARANCE_TO_CFG.get(widgets.appearance_var.get(), "auto")
new_cfg["appearance"] = _appearance_to_cfg(widgets.appearance_var.get())
if widgets.language_var is not None:
new_cfg["language"] = language_from_label(widgets.language_var.get()).value
return new_cfg
@@ -872,22 +902,22 @@ def install_tray_config_buttons(
btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
btn_frame.pack(fill="x", pady=(0, 0))
save_btn = ctk.CTkButton(
btn_frame, text="Сохранить", height=38,
btn_frame, text=t("button.save"), height=38,
font=(theme.ui_font_family, 14, "bold"), corner_radius=10,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff",
command=on_save)
save_btn.pack(side="left", fill="x", expand=True, padx=(0, 8))
attach_ctk_tooltip(save_btn, _TIP_SAVE)
attach_ctk_tooltip(save_btn, t("tip.save"))
cancel_btn = ctk.CTkButton(
btn_frame, text="Отмена", height=38,
btn_frame, text=t("button.cancel"), height=38,
font=(theme.ui_font_family, 14), corner_radius=10,
fg_color=theme.field_bg, hover_color=theme.field_border,
text_color=theme.text_primary, border_width=1,
border_color=theme.field_border,
command=on_cancel)
cancel_btn.pack(side="right", fill="x", expand=True)
attach_ctk_tooltip(cancel_btn, _TIP_CANCEL)
attach_ctk_tooltip(cancel_btn, t("tip.cancel"))
def populate_first_run_window(
@@ -912,19 +942,19 @@ def populate_first_run_window(
width=4, height=32, corner_radius=2)
accent_bar.pack(side="left", padx=(0, 12))
ctk.CTkLabel(title_frame, text="Прокси запущен и работает в системном трее",
ctk.CTkLabel(title_frame, text=t("first_run.title"),
font=(theme.ui_font_family, 17, "bold"),
text_color=theme.text_primary).pack(side="left")
sections = [
("Как подключить Telegram Desktop:", True),
(" Автоматически:", True),
(" ПКМ по иконке в трее → «Открыть в Telegram»", False),
(f" Или скопировать ссылку, отправить её себе в TG и нажать по ней: {tg_url}", False),
("\n Вручную:", True),
(" Настройки → Продвинутые → Тип подключения → Прокси", False),
(f" MTProto → {link_host} : {port}", False),
(f" Secret: dd{secret}", False),
(t("first_run.how_to"), True),
(t("first_run.auto"), True),
(t("first_run.auto_hint"), False),
(t("first_run.auto_link", url=tg_url), False),
("\n" + t("first_run.manual"), True),
(t("first_run.manual_path"), False),
(t("first_run.manual_mtproto", host=link_host, port=port), False),
(t("first_run.manual_secret", secret=secret), False),
]
textbox = ctk.CTkTextbox(
@@ -956,13 +986,13 @@ def populate_first_run_window(
corner_radius=0).pack(fill="x", pady=(0, 12))
auto_var = ctk.BooleanVar(value=True)
_checkbox(ctk, frame, theme, "Открыть прокси в Telegram сейчас",
_checkbox(ctk, frame, theme, t("first_run.open_now"),
auto_var).pack(anchor="w", pady=(0, 16))
def on_ok():
on_done(auto_var.get())
ctk.CTkButton(frame, text="Начать", width=180, height=42,
ctk.CTkButton(frame, text=t("button.start"), width=180, height=42,
font=(theme.ui_font_family, 15, "bold"), corner_radius=10,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff",