diff --git a/packaging/linux.spec b/packaging/linux.spec index 770cd91..f932308 100644 --- a/packaging/linux.spec +++ b/packaging/linux.spec @@ -12,6 +12,8 @@ block_cipher = None import customtkinter ctk_path = os.path.dirname(customtkinter.__file__) +_i18n_path = os.path.join(os.path.dirname(SPEC), os.pardir, 'ui', 'i18n') + # Collect gi (PyGObject) submodules and data so pystray._appindicator works gi_hiddenimports = collect_submodules('gi') gi_datas = collect_data_files('gi') @@ -26,7 +28,7 @@ a = Analysis( [os.path.join(os.path.dirname(SPEC), os.pardir, 'linux.py')], pathex=[], binaries=[], - datas=[(ctk_path, 'customtkinter/')] + gi_datas + typelib_datas, + datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')] + gi_datas + typelib_datas, hiddenimports=[ 'pystray._appindicator', 'PIL._tkinter_finder', diff --git a/packaging/macos.spec b/packaging/macos.spec index ab10d1c..05a5fab 100644 --- a/packaging/macos.spec +++ b/packaging/macos.spec @@ -5,11 +5,13 @@ import os block_cipher = None +_i18n_path = os.path.join(os.path.dirname(SPEC), os.pardir, 'ui', 'i18n') + a = Analysis( [os.path.join(os.path.dirname(SPEC), os.pardir, 'macos.py')], pathex=[], binaries=[], - datas=[], + datas=[(_i18n_path, 'ui/i18n')], hiddenimports=[ 'rumps', 'objc', diff --git a/packaging/windows.spec b/packaging/windows.spec index 2fcfa5f..2ecb0ac 100644 --- a/packaging/windows.spec +++ b/packaging/windows.spec @@ -9,11 +9,13 @@ block_cipher = None import customtkinter ctk_path = os.path.dirname(customtkinter.__file__) +_i18n_path = os.path.join(os.path.dirname(SPEC), os.pardir, 'ui', 'i18n') + a = Analysis( [os.path.join(os.path.dirname(SPEC), os.pardir, 'windows.py')], pathex=[], binaries=[], - datas=[(ctk_path, 'customtkinter/')], + datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')], hiddenimports=[ 'pystray._win32', 'PIL._tkinter_finder', diff --git a/proxy/config.py b/proxy/config.py index 4290950..e8c0680 100644 --- a/proxy/config.py +++ b/proxy/config.py @@ -200,13 +200,19 @@ def parse_dc_ip_list(dc_ip_list: List[str]) -> Dict[int, str]: dc_redirects: Dict[int, str] = {} for entry in dc_ip_list: if ':' not in entry: - raise ValueError( + err = ValueError( f"Invalid --dc-ip format {entry!r}, expected DC:IP") + err.entry = entry + err.kind = "format" + raise err dc_s, ip_s = entry.split(':', 1) try: dc_n = int(dc_s) _socket.inet_aton(ip_s) except (ValueError, OSError): - raise ValueError(f"Invalid --dc-ip {entry!r}") + err = ValueError(f"Invalid --dc-ip {entry!r}") + err.entry = entry + err.kind = "invalid" + raise err dc_redirects[dc_n] = ip_s return dc_redirects diff --git a/ui/ctk_tray_ui.py b/ui/ctk_tray_ui.py index 21a887c..0e4ea7f 100644 --- a/ui/ctk_tray_ui.py +++ b/ui/ctk_tray_ui.py @@ -809,6 +809,16 @@ def merge_adv_from_form( base[key] = default_config[key] +def _dc_validation_message(error: ValueError) -> str: + exc_entry = getattr(error, "entry", None) + if exc_entry is None: + return str(error) + kind = getattr(error, "kind", "invalid") + if kind == "format": + return t("validation.dc_format", entry=exc_entry) + return t("validation.dc_invalid", entry=exc_entry) + + def validate_config_form( widgets: TrayConfigFormWidgets, default_config: dict, @@ -838,14 +848,7 @@ def validate_config_form( try: parse_dc_ip_list(lines) except ValueError as 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 + return _dc_validation_message(e) secret_val = widgets.secret_var.get().strip() if len(secret_val) != 32: diff --git a/ui/i18n/__init__.py b/ui/i18n/__init__.py index c8ce11f..ecb91d1 100644 --- a/ui/i18n/__init__.py +++ b/ui/i18n/__init__.py @@ -26,7 +26,7 @@ class LocaleEnum(str, Enum): _LOCALES_DIR = Path(__file__).resolve().parent -_DEFAULT_LOCALE = LocaleEnum.russian +_DEFAULT_LOCALE = LocaleEnum.english _translations: Dict[str, str] = {} _current_lang: LocaleEnum = _DEFAULT_LOCALE @@ -137,10 +137,11 @@ def language_option_labels() -> List[Tuple[LocaleEnum, str]]: def language_label_for_config(value: LocaleInput) -> str: loc = LocaleEnum.parse(value) - for cfg_val, label in language_option_labels(): + labels = language_option_labels() + for cfg_val, label in labels: if cfg_val == loc: return label - return language_option_labels()[0][1] + return labels[0][1] if labels else _DEFAULT_LOCALE.value def refresh_language_option_maps() -> None: diff --git a/ui/i18n/en.json b/ui/i18n/en.json index fb05669..a2ca8c7 100644 --- a/ui/i18n/en.json +++ b/ui/i18n/en.json @@ -76,6 +76,7 @@ "connectivity.cfproxy_title": "CF Proxy", "connectivity.cfworker_title": "CF Worker", "connectivity.timeout": "timeout", + "connectivity.no_response": "no response", "connectivity.available": "{title}: available", "connectivity.unavailable": "{title}: unavailable", "connectivity.all_ok": "{title}: all working", diff --git a/ui/i18n/ru.json b/ui/i18n/ru.json index 3cb7a0d..ff758fb 100644 --- a/ui/i18n/ru.json +++ b/ui/i18n/ru.json @@ -76,6 +76,7 @@ "connectivity.cfproxy_title": "CF-прокси", "connectivity.cfworker_title": "CF Worker", "connectivity.timeout": "таймаут", + "connectivity.no_response": "нет ответа", "connectivity.available": "{title}: доступен", "connectivity.unavailable": "{title}: недоступен", "connectivity.all_ok": "{title}: всё работает", diff --git a/utils/tray_common.py b/utils/tray_common.py index 5de5b76..1582833 100644 --- a/utils/tray_common.py +++ b/utils/tray_common.py @@ -188,17 +188,18 @@ def _apply_ui_language(cfg: dict) -> None: def load_config() -> dict: ensure_dirs() + cfg: Optional[dict] = None if CONFIG_FILE.exists(): try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: data = json.load(f) for k, v in DEFAULT_CONFIG.items(): data.setdefault(k, v) - _apply_ui_language(data) - return data + cfg = data except Exception as exc: log.warning("Failed to load config: %s", repr(exc)) - cfg = dict(DEFAULT_CONFIG) + if cfg is None: + cfg = dict(DEFAULT_CONFIG) _apply_ui_language(cfg) return cfg