diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a58df64..94057b9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,8 +13,8 @@ android { minSdk = 26 targetSdk = 35 // versionCode = major*1_000_000 + minor*10_000 + patch*100 + build - versionCode = 4_024_001 - versionName = "4.2.4+1" + versionCode = 4_025_001 + versionName = "4.2.5+1" vectorDrawables.useSupportLibrary = true ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") diff --git a/dist/navis-release/update.json b/dist/navis-release/update.json index 7abc4a3..4a0134a 100644 --- a/dist/navis-release/update.json +++ b/dist/navis-release/update.json @@ -1,11 +1,11 @@ { - "version": "4.2.4", - "notes": "4.2.4: группировка серверов по хосту — один физический сервер с протоколами VLESS · Trojan · Hysteria2 вместо «сервер 2/3»; «оптимальный» среди разных хостов", + "version": "4.2.5", + "notes": "4.2.5: переключатель языка Русский / English (localStorage navis.lang, по умолчанию RU); все строки UI и динамические подписи серверов", "platform": "windows-amd64", "os": "windows", "arch": "amd64", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", - "sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046", + "sha256": "fb8bf0fb98662601235ca6e33ba51f9aaf2a1602f3177c602adb8023161bcd90", "mandatory": false, "platforms": { "android": { @@ -40,7 +40,7 @@ }, "windows-amd64": { "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", - "sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046", + "sha256": "fb8bf0fb98662601235ca6e33ba51f9aaf2a1602f3177c602adb8023161bcd90", "os": "windows", "arch": "amd64" } diff --git a/dist/navis-release/windows/EvilFox.exe b/dist/navis-release/windows/EvilFox.exe index 5f15617..3c3c0bb 100644 Binary files a/dist/navis-release/windows/EvilFox.exe and b/dist/navis-release/windows/EvilFox.exe differ diff --git a/dist/navis-release/windows/Navis.exe b/dist/navis-release/windows/Navis.exe index 5f15617..3c3c0bb 100644 Binary files a/dist/navis-release/windows/Navis.exe and b/dist/navis-release/windows/Navis.exe differ diff --git a/dist/navis-release/windows/evilfox-cli.exe b/dist/navis-release/windows/evilfox-cli.exe index c0bae2b..eea0495 100644 Binary files a/dist/navis-release/windows/evilfox-cli.exe and b/dist/navis-release/windows/evilfox-cli.exe differ diff --git a/dist/navis-release/windows/vpnclient.exe b/dist/navis-release/windows/vpnclient.exe index c0bae2b..eea0495 100644 Binary files a/dist/navis-release/windows/vpnclient.exe and b/dist/navis-release/windows/vpnclient.exe differ diff --git a/dist/update.json b/dist/update.json index 7abc4a3..4a0134a 100644 --- a/dist/update.json +++ b/dist/update.json @@ -1,11 +1,11 @@ { - "version": "4.2.4", - "notes": "4.2.4: группировка серверов по хосту — один физический сервер с протоколами VLESS · Trojan · Hysteria2 вместо «сервер 2/3»; «оптимальный» среди разных хостов", + "version": "4.2.5", + "notes": "4.2.5: переключатель языка Русский / English (localStorage navis.lang, по умолчанию RU); все строки UI и динамические подписи серверов", "platform": "windows-amd64", "os": "windows", "arch": "amd64", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", - "sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046", + "sha256": "fb8bf0fb98662601235ca6e33ba51f9aaf2a1602f3177c602adb8023161bcd90", "mandatory": false, "platforms": { "android": { @@ -40,7 +40,7 @@ }, "windows-amd64": { "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", - "sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046", + "sha256": "fb8bf0fb98662601235ca6e33ba51f9aaf2a1602f3177c602adb8023161bcd90", "os": "windows", "arch": "amd64" } diff --git a/internal/appui/index.html b/internal/appui/index.html index 0339cbd..a3fbb55 100644 --- a/internal/appui/index.html +++ b/internal/appui/index.html @@ -16,6 +16,13 @@ } catch (_) { document.documentElement.setAttribute("data-theme", "light"); } + try { + var lang = localStorage.getItem("navis.lang"); + if (lang !== "en" && lang !== "ru") lang = "ru"; + document.documentElement.setAttribute("lang", lang); + } catch (_) { + document.documentElement.setAttribute("lang", "ru"); + } })(); @@ -260,6 +267,62 @@ .theme-toggle svg { width: 18px; height: 18px; display: none; } html[data-theme="light"] .theme-toggle .icon-moon { display: block; } html[data-theme="dark"] .theme-toggle .icon-sun { display: block; } + .side-foot-controls { + display: flex; + align-items: center; + gap: 6px; + flex: 0 0 auto; + } + .lang-seg { + display: inline-flex; + border-radius: 10px; + border: 1px solid var(--line); + background: var(--theme-toggle-bg); + overflow: hidden; + height: 36px; + } + .lang-seg button { + border: 0; + background: transparent; + color: var(--muted); + font: inherit; + font-size: .68rem; + font-weight: 800; + letter-spacing: .04em; + padding: 0 9px; + cursor: pointer; + min-width: 32px; + transition: background .15s, color .15s; + } + .lang-seg button.active { + background: var(--side-active); + color: var(--side-active-fg); + } + .lang-seg button:hover:not(.active) { color: var(--ink); } + .lang-row-seg { + display: inline-flex; + border-radius: 10px; + border: 1px solid var(--line); + background: var(--surface-2); + overflow: hidden; + height: 32px; + } + .lang-row-seg button { + border: 0; + background: transparent; + color: var(--muted); + font: inherit; + font-size: .75rem; + font-weight: 800; + letter-spacing: .03em; + padding: 0 12px; + cursor: pointer; + transition: background .15s, color .15s; + } + .lang-row-seg button.active { + background: var(--side-active); + color: var(--side-active-fg); + } /* ==== Content ==== */ .content { @@ -1331,29 +1394,34 @@
EvilFox - + +
+ +
- Доступно обновление + Доступно обновление

- - + +
@@ -1380,7 +1449,7 @@
-
- +

Нажмите большую кнопку, чтобы включить защиту

- @@ -1433,75 +1502,75 @@
-

Мои серверы

+

Мои серверы

-
-

🔗 Добавить конфигурацию

+
+

🔗 Добавить конфигурацию

🔴 Конфигурация не добавлена

- -

Вставьте URL-ссылку для подключения

- + +

Вставьте URL-ссылку для подключения

+
-
🌍 Серверы
+
🌍 Серверы
- - + +
- Свойства сервера + Свойства сервера
-
Сервер
-
Пинг
-
Протокол
-
Статус
+
Сервер
+
Пинг
+
Протокол
+
Статус
- +
-

Настройки

+

Настройки

-
Режим трафика
+
Режим трафика
- - + +
-

Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с EvilFox.exe. Меняется в отключённом состоянии.

+

Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с EvilFox.exe. Меняется в отключённом состоянии.

- Системный прокси - В режиме VPN не используется — TUN и так захватывает всё + Системный прокси + В режиме VPN не используется — TUN и так захватывает всё
- Тёмная тема + Тёмная тема
- 🚀 Авто-лучший сервер - При подключении автоматически выбирается сервер с наименьшим пингом + Язык / Language +
+ + +
+
+ +
+ 🚀 Авто-лучший сервер + При подключении автоматически выбирается сервер с наименьшим пингом
- 🔒 Защита от утечек (Kill Switch) - При обрыве VPN блокировать весь интернет-трафик до ручного отключения или переподключения. Полноценно работает в режиме VPN; в режиме прокси защищены только программы, использующие системный прокси. + 🔒 Защита от утечек (Kill Switch) + При обрыве VPN блокировать весь интернет-трафик до ручного отключения или переподключения. Полноценно работает в режиме VPN; в режиме прокси защищены только программы, использующие системный прокси.
- +
- +
- +
@@ -1581,7 +1658,7 @@
- +
@@ -1594,28 +1671,28 @@
- Сервис · cores, обновления + Сервис · cores, обновления
- - + +
- +
- Расширенные настройки + Расширенные настройки
-

Статистика

+

Статистика

-
Статистика за сегодня
+
Статистика за сегодня
-
Использовано
+
Использовано
0 мин
-
Время в сети
+
Время в сети
0
-
Сессий
+
Сессий
-
Трафик за 7 дней
+
Трафик за 7 дней
-
Пока нет данных
+
Пока нет данных
-
Журнал подключения
+
Журнал подключения
Пусто — подключитесь, чтобы увидеть журнал.
- - + +
-

Помощь

+

Помощь

- Как подключиться? -

Добавьте конфигурацию на вкладке «Мои серверы», затем на вкладке «Защита» нажмите большую круглую кнопку. Приложение само выберет оптимальный сервер.

+ Как подключиться? +

Добавьте конфигурацию на вкладке «Мои серверы», затем на вкладке «Защита» нажмите большую круглую кнопку. Приложение само выберет оптимальный сервер.

- Где взять URL-ссылку? -

URL-ссылку для подключения можно получить на сайте evilfox.win (кнопка ниже) или у вашего провайдера. Полученную ссылку вставьте на вкладке «Мои серверы».

+ Где взять URL-ссылку? +

URL-ссылку для подключения можно получить на сайте evilfox.win (кнопка ниже) или у вашего провайдера. Полученную ссылку вставьте на вкладке «Мои серверы».

- Не подключается — что делать? -

Проверьте, что конфигурация добавлена и срок её действия не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Выбрать лучший»). Подробности — в журнале на вкладке «Статистика».

+ Не подключается — что делать? +

Проверьте, что конфигурация добавлена и срок её действия не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Выбрать лучший»). Подробности — в журнале на вкладке «Статистика».

EvilFox

версия…

-

VPN/прокси-клиент: Naive · Hysteria 2 · AWG · Xray

+

VPN/прокси-клиент: Naive · Hysteria 2 · AWG · Xray

- +
-
-

Нужна конфигурация?

-

URL-ссылку для подключения можно получить на evilfox.win. Техподдержка — в Telegram.

- +
+

Нужна конфигурация?

+

URL-ссылку для подключения можно получить на evilfox.win. Техподдержка — в Telegram.

+
-
-

🌐 Полезные ссылки

+
+

🌐 Полезные ссылки

- +
@@ -1703,20 +1780,20 @@ @@ -1750,6 +1827,7 @@ })(); const $ = (id) => document.getElementById(id); const meta = $("meta"); + // meta initial text set by applyTranslations / paint const btn = $("toggleBtn"); const coreBtn = $("coreBtn"); const saveBtn = $("saveBtn"); @@ -1788,10 +1866,341 @@ const logsRefreshBtn = $("logsRefreshBtn"); const logsClearBtn = $("logsClearBtn"); const THEME_KEY = "navis.theme"; + const LANG_KEY = "navis.lang"; const SKIP_UPDATE_KEY = "navis.skipUpdateVersion"; let storePackaged = false; let currentMode = "proxy"; let currentPage = "guard"; + let currentLang = "ru"; + + const I18N = { + ru: { + nav_guard: "Защита", nav_profiles: "Мои серверы", nav_settings: "Настройки", + nav_stats: "Статистика", nav_help: "Помощь", + theme_toggle_title: "Тема", theme_toggle_aria: "Переключить тему", + theme_light: "Светлая тема", theme_dark: "Тёмная тема", + language: "Язык / Language", + update_available: "Доступно обновление", update_btn: "Обновить", skip_update: "Пропустить эту версию", + power_aria: "Подключиться или отключиться", + vpn_chip: "VPN · весь трафик", + cur_server_title: "Открыть список серверов", + prov_badge: "⭐ Рекомендуемый", + prov_sub: "Оптимальные серверы рядом с вами · защита без настройки", + offer_text: "🎁 3 дня бесплатного доступа", + page_profiles: "Мои серверы", add_config_aria: "Добавить конфигурацию", + add_config_title: "🔗 Добавить конфигурацию", add_config_btn: "Добавить конфигурацию", + add_config_hint: "Вставьте URL-ссылку для подключения", where_url: "Где взять URL-ссылку?", + servers_title: "🌍 Серверы", ping_btn: "Пинг", best_btn: "Выбрать лучший", + best_btn_title: "Выбрать сервер с наименьшим пингом", + account_info: "Информация об аккаунте", expires_label: "Действует до", + traffic_label: "Трафик", devices_label: "Устройства", + server_props: "Свойства сервера", prop_server: "Сервер", prop_ping: "Пинг", + prop_proto: "Протокол", prop_status: "Статус", save_profile: "Сохранить профиль", + page_settings: "Настройки", traffic_mode: "Режим трафика", + mode_proxy: "Режим прокси", mode_vpn: "Режим VPN", + mode_hint: "Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с EvilFox.exe. Меняется в отключённом состоянии.", + sysproxy: "Системный прокси", sysproxy_sub: "В режиме VPN не используется — TUN и так захватывает всё", + dark_theme: "Тёмная тема", + auto_best: "🚀 Авто-лучший сервер", + auto_best_sub: "При подключении автоматически выбирается сервер с наименьшим пингом", + kill_switch: "🔒 Защита от утечек (Kill Switch)", + kill_switch_sub: "При обрыве VPN блокировать весь интернет-трафик до ручного отключения или переподключения. Полноценно работает в режиме VPN; в режиме прокси защищены только программы, использующие системный прокси.", + obfs_none: "нет", ph_empty_bbr: "пусто = BBR", ph_password: "пароль", ph_optional: "опционально", + svc_cores: "Сервис · cores, обновления", check_update: "Проверить обновление", + install_cores: "Установить cores", store_updates: "Обновления этой сборки — через Microsoft Store.", + adv_settings: "Расширенные настройки", proxy_params: "Параметры прокси", + proxy_params_hint: "Локальные адреса прокси на этом компьютере — нужны только для ручной настройки отдельных программ.", + page_stats: "Статистика", stats_today: "Статистика за сегодня", + stat_used: "Использовано", stat_online: "Время в сети", stat_sessions: "Сессий", + stats_week: "Трафик за 7 дней", no_data: "Пока нет данных", no_data_short: "нет данных", + conn_log: "Журнал подключения", refresh: "Обновить", clear_screen: "Очистить экран", + page_help: "Помощь", + faq_how_q: "Как подключиться?", + faq_how_a: "Добавьте конфигурацию на вкладке «Мои серверы», затем на вкладке «Защита» нажмите большую круглую кнопку. Приложение само выберет оптимальный сервер.", + faq_url_q: "Где взять URL-ссылку?", + faq_url_a: "URL-ссылку для подключения можно получить на сайте evilfox.win (кнопка ниже) или у вашего провайдера. Полученную ссылку вставьте на вкладке «Мои серверы».", + faq_fail_q: "Не подключается — что делать?", + faq_fail_a: "Проверьте, что конфигурация добавлена и срок её действия не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Выбрать лучший»). Подробности — в журнале на вкладке «Статистика».", + about_stack: "VPN/прокси-клиент: Naive · Hysteria 2 · AWG · Xray", + shop_aria: "Получить конфигурацию", need_config: "Нужна конфигурация?", + need_config_p: "URL-ссылку для подключения можно получить на evilfox.win. Техподдержка — в Telegram.", + go_site: "Перейти на сайт", links_aria: "Полезные ссылки", useful_links: "🌐 Полезные ссылки", + quit: "Выйти", loading: "Загрузка…", + new_profile: "Новый профиль", new_name: "Название", new_link: "Ссылка", + cancel: "Отмена", create: "Создать", + region_unknown: "регион неизвестен", + meta_quit: "Выход…", ks_on: "Kill Switch включён: при обрыве VPN трафик будет заблокирован", + ks_off: "Kill Switch выключен", + unit_sec: "сек", unit_min: "мин", unit_h: "ч", unit_gb: "ГБ", unit_tb: "ТБ", unit_mb: "МБ", + proto_generic: "протокол", + title_optimal: "{region} — оптимальный сервер", + title_server_optimal: "Сервер {n} — оптимальный", + title_server_n: "{region} — сервер {n}", title_server: "Сервер {n}", + server_generic: "Сервер", no_server: "Нет сервера", + no_server_hint: "добавьте конфигурацию на вкладке «Мои серверы»", + pick_other_server: "нажмите, чтобы выбрать другой сервер", + ping_not_measured: "пинг не измерен", + servers_one: "сервер", servers_few: "сервера", servers_many: "серверов", + no_servers_empty: "Нет серверов — добавьте конфигурацию выше", + badge_active: "АКТИВЕН", proto_title: "Протокол {label}", + ping_this_server: "Проверить пинг этого сервера", ping_fail: "Не удалось измерить пинг", + connecting_to: "Подключение к «{name}»…", connected_to: "Защищённое соединение: {name}", + traffic_avail: "Доступно {left}", traffic_avail_of: "Доступно {left} из {total}", + pct_used: "{pct}% использовано", traffic_used_unlim: "Использовано {used} · Безлимит", + expired_on: "Срок доступа истёк {date}", burns_today: "⏳ Сгорает сегодня (до 23:59)", + active_until: "Активен до {date}", expired: "Истёк", today: "Сегодня", + days_left: "осталось {n} дн.", of_pair: "{a} из {b}", unlim: "Безлимит", + version_label: "Версия {v}", update_available_v: "Доступно обновление {v}", + update_notes_fallback: "У вас {v}. Обновление только по кнопке.", + status_blocked: "⛔ Соединение прервано — трафик заблокирован", + status_on: "🟢 Защита активна", status_off: "🔴 Отключено", + power_connect: "ПОДКЛЮЧИТЬСЯ", power_disconnect: "ОТКЛЮЧИТЬСЯ", + btn_connect: "Подключить", btn_disconnect: "Отключить", + config_added: "🟢 Конфигурация добавлена", config_missing: "🔴 Конфигурация не добавлена", + proxy_line: "Прокси", + hint_ks: "Kill Switch активен: интернет заблокирован до отключения или переподключения", + detail_ks: "⛔ Kill Switch: соединение прервано, трафик заблокирован. Нажмите «Отключиться» или переподключитесь.", + hint_encrypted: "Трафик зашифрован", detail_cores: "Сначала установите cores в Настройках («Сервис»)", + hint_cores: "Нужны cores для выбранного протокола", + hint_vpn: "Режим VPN: защита всего трафика, включая игры и приложения", + hint_idle: "Нажмите, чтобы включить защиту", + prov_connected: "✅ Подключено", prov_connect: "🔥 Подключиться", + unavailable: "недоступен", not_measured: "не измерен", + prop_active: "🟢 Активен", prop_inactive: "⚪ Не активен", + meta_ping_auto: "Пинг и автоподключение…", meta_ping_best: "Пинг и выбор лучшего…", + meta_best: "Лучший: {name} · {ms} ms", meta_connected_short: "подключено", + meta_disconnecting: "Отключение…", meta_disconnected: "Отключено", + meta_connecting: "Подключение…", meta_connected: "Защищённое соединение установлено", + meta_saved: "Профиль сохранён", meta_cores_dl: "Скачивание official cores…", + meta_cores_ok: "Cores готовы: {path}", + confirm_delete: "Удалить сервер «{name}»?", meta_deleted: "Профиль удалён", + err_need_name: "Укажите название профиля", meta_created: "Профиль создан", + meta_opening_site: "Открываю evilfox.win…", meta_opened: "Открыто в браузере", + meta_open_fail: "Не удалось открыть ссылку", + meta_opening_named: "Открываю {name}…", + svc_site: "Сайт", svc_support: "Поддержка: Telegram", + meta_need_url: "Вставьте URL-ссылку для подключения", + meta_adding_config: "Добавление конфигурации…", + meta_picking_best: "подбираю оптимальный сервер…", + import_ok: "Конфигурация добавлена · серверов: {n}", + import_skipped: "пропущено {n}", import_ok_raw: "Конфигурация добавлена: {r}", + meta_mode_vpn: "Режим VPN включён: весь трафик пойдёт через туннель после подключения", + meta_mode_proxy: "Режим прокси включён", + log_empty: "Пусто — подключитесь, чтобы увидеть журнал.", + log_fail: "Не удалось получить логи", + meta_pinging: "Пинг серверов…", + meta_ping_result: "Пинг: {ok}/{total} доступны", + meta_checking_upd: "Проверка обновлений…", + meta_upd_avail: "Доступна версия {v}", meta_upd_err: "Обновление: {e}", + meta_upd_current: "У вас актуальная версия {v}", + meta_upd_install: "Скачивание и установка обновления…", meta_restart: "Перезапуск…", + meta_upd_skipped: "Версия {v} пропущена. Следующую предложим." + }, + en: { + nav_guard: "Protection", nav_profiles: "My servers", nav_settings: "Settings", + nav_stats: "Statistics", nav_help: "Help", + theme_toggle_title: "Theme", theme_toggle_aria: "Toggle theme", + theme_light: "Light theme", theme_dark: "Dark theme", + language: "Language / Язык", + update_available: "Update available", update_btn: "Update", skip_update: "Skip this version", + power_aria: "Connect or disconnect", + vpn_chip: "VPN · all traffic", + cur_server_title: "Open server list", + prov_badge: "⭐ Recommended", + prov_sub: "Optimal servers near you · protection with no setup", + offer_text: "🎁 3 days free access", + page_profiles: "My servers", add_config_aria: "Add configuration", + add_config_title: "🔗 Add configuration", add_config_btn: "Add configuration", + add_config_hint: "Paste a connection URL", where_url: "Where do I get a URL?", + servers_title: "🌍 Servers", ping_btn: "Ping", best_btn: "Pick best", + best_btn_title: "Select the server with the lowest ping", + account_info: "Account info", expires_label: "Valid until", + traffic_label: "Traffic", devices_label: "Devices", + server_props: "Server properties", prop_server: "Server", prop_ping: "Ping", + prop_proto: "Protocol", prop_status: "Status", save_profile: "Save profile", + page_settings: "Settings", traffic_mode: "Traffic mode", + mode_proxy: "Proxy mode", mode_vpn: "VPN mode", + mode_hint: "Proxy — traffic goes through a local proxy (and the Windows system proxy). VPN — all traffic, including games and apps, via a TUN adapter; requires administrator rights and wintun.dll next to EvilFox.exe. Change only while disconnected.", + sysproxy: "System proxy", sysproxy_sub: "Not used in VPN mode — TUN already captures everything", + dark_theme: "Dark theme", + auto_best: "🚀 Auto best server", + auto_best_sub: "On connect, automatically pick the server with the lowest ping", + kill_switch: "🔒 Leak protection (Kill Switch)", + kill_switch_sub: "If the VPN drops, block all internet traffic until you disconnect or reconnect. Fully effective in VPN mode; in proxy mode only apps that use the system proxy are protected.", + obfs_none: "none", ph_empty_bbr: "empty = BBR", ph_password: "password", ph_optional: "optional", + svc_cores: "Service · cores, updates", check_update: "Check for updates", + install_cores: "Install cores", store_updates: "Updates for this build come from the Microsoft Store.", + adv_settings: "Advanced settings", proxy_params: "Proxy parameters", + proxy_params_hint: "Local proxy addresses on this computer — only needed for manual setup of individual apps.", + page_stats: "Statistics", stats_today: "Today's stats", + stat_used: "Used", stat_online: "Time online", stat_sessions: "Sessions", + stats_week: "Traffic last 7 days", no_data: "No data yet", no_data_short: "no data", + conn_log: "Connection log", refresh: "Refresh", clear_screen: "Clear screen", + page_help: "Help", + faq_how_q: "How do I connect?", + faq_how_a: "Add a configuration on the My servers tab, then on Protection press the large round button. The app will pick an optimal server.", + faq_url_q: "Where do I get a URL?", + faq_url_a: "You can get a connection URL at evilfox.win (button below) or from your provider. Paste it on the My servers tab.", + faq_fail_q: "Won't connect — what should I do?", + faq_fail_a: "Check that a configuration is added and has not expired (My servers → Account info). Try another server (Pick best). Details are in the log on the Statistics tab.", + about_stack: "VPN/proxy client: Naive · Hysteria 2 · AWG · Xray", + shop_aria: "Get configuration", need_config: "Need a configuration?", + need_config_p: "You can get a connection URL at evilfox.win. Support is on Telegram.", + go_site: "Go to website", links_aria: "Useful links", useful_links: "🌐 Useful links", + quit: "Quit", loading: "Loading…", + new_profile: "New profile", new_name: "Name", new_link: "Link", + cancel: "Cancel", create: "Create", + region_unknown: "unknown region", + meta_quit: "Quitting…", ks_on: "Kill Switch on: if VPN drops, traffic will be blocked", + ks_off: "Kill Switch off", + unit_sec: "sec", unit_min: "min", unit_h: "h", unit_gb: "GB", unit_tb: "TB", unit_mb: "MB", + proto_generic: "protocol", + title_optimal: "{region} — optimal server", + title_server_optimal: "Server {n} — optimal", + title_server_n: "{region} — server {n}", title_server: "Server {n}", + server_generic: "Server", no_server: "No server", + no_server_hint: "add a configuration on the My servers tab", + pick_other_server: "tap to choose another server", + ping_not_measured: "ping not measured", + servers_one: "server", servers_few: "servers", servers_many: "servers", + no_servers_empty: "No servers — add a configuration above", + badge_active: "ACTIVE", proto_title: "Protocol {label}", + ping_this_server: "Ping this server", ping_fail: "Could not measure ping", + connecting_to: "Connecting to “{name}”…", connected_to: "Secure connection: {name}", + traffic_avail: "Available {left}", traffic_avail_of: "Available {left} of {total}", + pct_used: "{pct}% used", traffic_used_unlim: "Used {used} · Unlimited", + expired_on: "Access expired {date}", burns_today: "⏳ Expires today (by 23:59)", + active_until: "Active until {date}", expired: "Expired", today: "Today", + days_left: "{n} days left", of_pair: "{a} of {b}", unlim: "Unlimited", + version_label: "Version {v}", update_available_v: "Update available {v}", + update_notes_fallback: "You have {v}. Update only via the button.", + status_blocked: "⛔ Connection interrupted — traffic blocked", + status_on: "🟢 Protection on", status_off: "🔴 Disconnected", + power_connect: "CONNECT", power_disconnect: "DISCONNECT", + btn_connect: "Connect", btn_disconnect: "Disconnect", + config_added: "🟢 Configuration added", config_missing: "🔴 No configuration", + proxy_line: "Proxy", + hint_ks: "Kill Switch active: internet blocked until disconnect or reconnect", + detail_ks: "⛔ Kill Switch: connection interrupted, traffic blocked. Press Disconnect or reconnect.", + hint_encrypted: "Traffic encrypted", detail_cores: "Install cores first in Settings (Service)", + hint_cores: "Cores required for the selected protocol", + hint_vpn: "VPN mode: protect all traffic, including games and apps", + hint_idle: "Press to enable protection", + prov_connected: "✅ Connected", prov_connect: "🔥 Connect", + unavailable: "unreachable", not_measured: "not measured", + prop_active: "🟢 Active", prop_inactive: "⚪ Inactive", + meta_ping_auto: "Pinging and auto-connecting…", meta_ping_best: "Pinging and picking best…", + meta_best: "Best: {name} · {ms} ms", meta_connected_short: "connected", + meta_disconnecting: "Disconnecting…", meta_disconnected: "Disconnected", + meta_connecting: "Connecting…", meta_connected: "Secure connection established", + meta_saved: "Profile saved", meta_cores_dl: "Downloading official cores…", + meta_cores_ok: "Cores ready: {path}", + confirm_delete: "Delete server “{name}”?", meta_deleted: "Profile deleted", + err_need_name: "Enter a profile name", meta_created: "Profile created", + meta_opening_site: "Opening evilfox.win…", meta_opened: "Opened in browser", + meta_open_fail: "Could not open link", + meta_opening_named: "Opening {name}…", + svc_site: "Website", svc_support: "Support: Telegram", + meta_need_url: "Paste a connection URL", + meta_adding_config: "Adding configuration…", + meta_picking_best: "picking optimal server…", + import_ok: "Configuration added · servers: {n}", + import_skipped: "skipped {n}", import_ok_raw: "Configuration added: {r}", + meta_mode_vpn: "VPN mode on: all traffic will go through the tunnel after connect", + meta_mode_proxy: "Proxy mode on", + log_empty: "Empty — connect to see the log.", + log_fail: "Could not load logs", + meta_pinging: "Pinging servers…", + meta_ping_result: "Ping: {ok}/{total} reachable", + meta_checking_upd: "Checking for updates…", + meta_upd_avail: "Version {v} available", meta_upd_err: "Update: {e}", + meta_upd_current: "You have the latest version {v}", + meta_upd_install: "Downloading and installing update…", meta_restart: "Restarting…", + meta_upd_skipped: "Version {v} skipped. We'll offer the next one." + } + }; + function t(key, vars) { + const dict = I18N[currentLang] || I18N.ru; + let s = (dict && dict[key] !== undefined) ? dict[key] : ((I18N.ru && I18N.ru[key]) || key); + if (vars) { + Object.keys(vars).forEach((k) => { + s = String(s).split("{" + k + "}").join(String(vars[k])); + }); + } + return s; + } + function readLang() { + try { + const v = localStorage.getItem(LANG_KEY); + if (v === "en" || v === "ru") return v; + } catch (_) {} + return "ru"; + } + function syncLangSeg() { + document.querySelectorAll("#langSeg button[data-lang], #langSegSettings button[data-lang]").forEach((b) => { + b.classList.toggle("active", b.getAttribute("data-lang") === currentLang); + }); + } + function applyTranslations() { + document.documentElement.setAttribute("lang", currentLang === "en" ? "en" : "ru"); + document.querySelectorAll("[data-i18n]").forEach((el) => { + const key = el.getAttribute("data-i18n"); + if (key) el.textContent = t(key); + }); + document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { + const key = el.getAttribute("data-i18n-placeholder"); + if (key) el.setAttribute("placeholder", t(key)); + }); + document.querySelectorAll("[data-i18n-title]").forEach((el) => { + const key = el.getAttribute("data-i18n-title"); + if (key) el.title = t(key); + }); + document.querySelectorAll("[data-i18n-aria]").forEach((el) => { + const key = el.getAttribute("data-i18n-aria"); + if (key) el.setAttribute("aria-label", t(key)); + }); + if ($("provGift")) $("provGift").textContent = t("offer_text"); + if (meta && !meta.textContent) meta.textContent = t("loading"); + else if (meta && (meta.textContent === "Загрузка…" || meta.textContent === "Loading…")) meta.textContent = t("loading"); + if ($("statOnline") && ($("statOnline").textContent === "0 мин" || $("statOnline").textContent === "0 min")) { + $("statOnline").textContent = "0 " + t("unit_min"); + } + if (logView) { + const logPlaceholderRu = "Пусто — подключитесь, чтобы увидеть журнал."; + const logPlaceholderEn = "Empty — connect to see the log."; + if (!logView.textContent.trim() || + logView.textContent === logPlaceholderRu || + logView.textContent === logPlaceholderEn) { + logView.textContent = t("log_empty"); + } + } + syncLangSeg(); + if (typeof applyTheme === "function") { + applyTheme(document.documentElement.getAttribute("data-theme") || readTheme()); + } + } + function setLang(lang) { + currentLang = (lang === "en") ? "en" : "ru"; + try { localStorage.setItem(LANG_KEY, currentLang); } catch (_) {} + applyTranslations(); + if (typeof renderRecommendedServices === "function") renderRecommendedServices(); + // Re-paint live status / server labels from Go state (uses t() + countryName). + if (typeof refresh === "function") { + refresh().catch(() => {}); + } else if (typeof rebuildFriendlyNames === "function") { + rebuildFriendlyNames(); + if (typeof renderServerList === "function") renderServerList(profile && profile.value); + if (typeof renderCurServer === "function") renderCurServer(profile && profile.value); + if (typeof renderProvConnect === "function") renderProvConnect(); + if (typeof renderProfProps === "function") renderProfProps(); + if (typeof renderStats === "function") renderStats(); + } + } + currentLang = readLang(); + function pluralServers(n) { + if (currentLang === "en") return n === 1 ? t("servers_one") : t("servers_many"); + return pluralRu(n, t("servers_one"), t("servers_few"), t("servers_many")); + } // Русские названия стран для человекочитаемых имён серверов (ISO alpha-2). const COUNTRY_RU = { @@ -1811,21 +2220,39 @@ ["румыния", "RO"], ["румыня", "RO"], ["руминия", "RO"], ["румыниа", "RO"], ["romania", "RO"], ["bucharest", "RO"], ["бухарест", "RO"] ]; + const COUNTRY_EN = { + AT: "Austria", AU: "Australia", AE: "UAE", AR: "Argentina", BE: "Belgium", + BG: "Bulgaria", BR: "Brazil", CA: "Canada", CH: "Switzerland", CZ: "Czechia", + DE: "Germany", DK: "Denmark", EE: "Estonia", ES: "Spain", FI: "Finland", + FR: "France", GB: "United Kingdom", GR: "Greece", HK: "Hong Kong", HU: "Hungary", + IE: "Ireland", IL: "Israel", IN: "India", IT: "Italy", JP: "Japan", + KR: "South Korea", KZ: "Kazakhstan", LT: "Lithuania", LU: "Luxembourg", LV: "Latvia", + MD: "Moldova", MX: "Mexico", NL: "Netherlands", NO: "Norway", PL: "Poland", + PT: "Portugal", RO: "Romania", RS: "Serbia", RU: "Russia", SE: "Sweden", + SG: "Singapore", SK: "Slovakia", TR: "Turkey", UA: "Ukraine", US: "USA", ZA: "South Africa" + }; function countryRu(cc) { const c = String(cc || "").toUpperCase(); return COUNTRY_RU[c] || ""; } + function countryName(cc) { + const c = String(cc || "").toUpperCase(); + if (currentLang === "en") return COUNTRY_EN[c] || COUNTRY_RU[c] || ""; + return COUNTRY_RU[c] || ""; + } function isoFromNameText(text) { const lower = String(text || "").toLowerCase(); if (!lower) return ""; for (let i = 0; i < COUNTRY_NAME_ALIASES.length; i++) { if (lower.indexOf(COUNTRY_NAME_ALIASES[i][0]) >= 0) return COUNTRY_NAME_ALIASES[i][1]; } - // Прямое совпадение канонического русского названия. + // Прямое совпадение канонического русского / английского названия. const keys = Object.keys(COUNTRY_RU); for (let i = 0; i < keys.length; i++) { const ru = COUNTRY_RU[keys[i]].toLowerCase(); if (ru && lower.indexOf(ru) >= 0) return keys[i]; + const en = (COUNTRY_EN[keys[i]] || "").toLowerCase(); + if (en && lower.indexOf(en) >= 0) return keys[i]; } return ""; } @@ -1981,7 +2408,7 @@ el.textContent = cc || "·"; el.classList.add("unknown"); } - el.title = cc ? (countryRu(cc) || cc) : "регион неизвестен"; + el.title = cc ? (countryName(cc) || cc) : t("region_unknown"); } function pluralRu(n, one, few, many) { const m10 = n % 10, m100 = n % 100; @@ -2018,7 +2445,7 @@ document.documentElement.setAttribute("data-theme", theme); try { localStorage.setItem(THEME_KEY, theme); } catch (_) {} if (themeToggle) { - themeToggle.title = theme === "dark" ? "Светлая тема" : "Тёмная тема"; + themeToggle.title = theme === "dark" ? t("theme_light") : t("theme_dark"); themeToggle.setAttribute("aria-label", themeToggle.title); } if (themeSwitch) themeSwitch.checked = theme === "dark"; @@ -2034,6 +2461,16 @@ themeSwitch.addEventListener("change", () => applyTheme(themeSwitch.checked ? "dark" : "light")); } + function wireLangSeg(root) { + if (!root) return; + root.querySelectorAll("button[data-lang]").forEach((b) => { + b.addEventListener("click", () => setLang(b.getAttribute("data-lang"))); + }); + } + wireLangSeg($("langSeg")); + wireLangSeg($("langSegSettings")); + applyTranslations(); + function skippedVersion() { try { return localStorage.getItem(SKIP_UPDATE_KEY) || ""; } catch (_) { return ""; } } @@ -2049,7 +2486,7 @@ quitBtn.style.display = "block"; quitBtn.addEventListener("click", () => { withBusy(async () => { - setMeta("Выход…"); + setMeta(t("meta_quit")); try { await quit(); } catch (_) {} }); }); @@ -2062,16 +2499,16 @@ const SHOP_URL = "https://evilfox.win/"; // Текст оффера — правьте здесь (показывается на карточке EvilFox на вкладке «Защита»). // Правила Store: никаких цен и упоминаний оплаты внутри приложения. - const OFFER_TEXT = "🎁 3 дня бесплатного доступа"; + function offerText() { return t("offer_text"); } // Полезные ссылки (Помощь). Хост каждой ссылки — в allowlist openURL // (internal/apphost/app.go → allowedLinkHosts). const RECOMMENDED_SERVICES = [ - { icon: "🌐", name: "Сайт", url: "https://evilfox.win/" }, - { icon: "✈️", name: "Поддержка: Telegram", url: "https://t.me/evilfox100Bot" } + { icon: "🌐", nameKey: "svc_site", url: "https://evilfox.win/" }, + { icon: "✈️", nameKey: "svc_support", url: "https://t.me/evilfox100Bot" } ]; const AUTO_BEST_KEY = "navis.autoBest"; // Оффер на карточке EvilFox рендерим из константы, чтобы правка была в одном месте. - if ($("provGift")) $("provGift").textContent = OFFER_TEXT; + if ($("provGift")) $("provGift").textContent = offerText(); try { const saved = localStorage.getItem(AUTO_BEST_KEY); @@ -2087,9 +2524,7 @@ if (killSwitchSyncing) return; try { await setKillSwitch(!!killSwitch.checked); - setMeta(killSwitch.checked - ? "Kill Switch включён: при обрыве VPN трафик будет заблокирован" - : "Kill Switch выключен", "ok"); + setMeta(killSwitch.checked ? t("ks_on") : t("ks_off"), "ok"); } catch (e) { killSwitch.checked = !killSwitch.checked; setMeta(String(e), "err"); @@ -2159,12 +2594,12 @@ wasConnected = connectedNow; } function fmtOnline(sec) { - if (!sec || sec < 60) return (sec || 0) + " сек"; + if (!sec || sec < 60) return (sec || 0) + " " + t("unit_sec"); const min = Math.floor(sec / 60); - if (min < 60) return min + " мин"; - return Math.floor(min / 60) + " ч " + (min % 60) + " мин"; + if (min < 60) return min + " " + t("unit_min"); + return Math.floor(min / 60) + " " + t("unit_h") + " " + (min % 60) + " " + t("unit_min"); } - const DAY_RU = ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"]; + function dayLabels() { return currentLang === "en" ? ["Su","Mo","Tu","We","Th","Fr","Sa"] : ["Вс","Пн","Вт","Ср","Чт","Пт","Сб"]; } function renderStats() { const daily = loadDaily(); const today = daily[dayKey()] || {}; @@ -2181,14 +2616,14 @@ const d = new Date(); d.setDate(d.getDate() - i); const rec = daily[dayKey(d)] || {}; - days.push({ label: DAY_RU[d.getDay()], t: rec.t || 0 }); + days.push({ label: dayLabels()[d.getDay()], t: rec.t || 0 }); if ((rec.t || 0) > maxT) maxT = rec.t; } chart.innerHTML = ""; if (!maxT) { const empty = document.createElement("div"); empty.className = "week-empty"; - empty.textContent = "Пока нет данных"; + empty.textContent = t("no_data"); chart.appendChild(empty); return; } @@ -2198,7 +2633,7 @@ const bar = document.createElement("div"); bar.className = "week-bar"; bar.style.height = Math.max(2, Math.round(d.t / maxT * 100)) + "%"; - bar.title = d.t ? fmtTraffic(d.t) : "нет данных"; + bar.title = d.t ? fmtTraffic(d.t) : t("no_data_short"); const label = document.createElement("div"); label.className = "week-day"; label.textContent = d.label; @@ -2290,7 +2725,7 @@ if (link.startsWith("hysteria2://") || link.startsWith("hy2://")) return "hysteria2"; if (link.startsWith("awg://") || link.includes("[interface]")) return "awg"; if (link.startsWith("naive") || link.startsWith("https://") || link.startsWith("quic://")) return "naive"; - return "протокол"; + return t("proto_generic"); } function msClass(ms, ok) { @@ -2449,14 +2884,14 @@ hostGroups.forEach((g, i) => { if (g.bestMs < bestMsVal) { bestMsVal = g.bestMs; bestIdx = i; } }); - const region = iso === "__none__" ? "" : countryRu(iso); + const region = iso === "__none__" ? "" : countryName(iso); hostGroups.forEach((g, i) => { const n = i + 1; let title; if (i === bestIdx) { - title = region ? (region + " — оптимальный сервер") : ("Сервер " + n + " — оптимальный"); + title = region ? t("title_optimal", { region: region }) : t("title_server_optimal", { n: n }); } else { - title = region ? (region + " — сервер " + n) : ("Сервер " + n); + title = region ? t("title_server_n", { region: region, n: n }) : t("title_server", { n: n }); } const protos = uniqueProtos(g.list); const groupNames = g.list.map((x) => x.name); @@ -2479,9 +2914,9 @@ // Фолбэк: не показываем сырые опечатки панели — только каноническое имя по ISO. const p = profiles.find((x) => x.name === name); const iso = profileISO(p) || isoFromNameText(name); - const region = countryRu(iso); + const region = countryName(iso); if (region) return region; - return "Сервер"; + return t("server_generic"); } function fillProfiles(list, active) { @@ -2539,8 +2974,8 @@ const p = profiles.find((x) => x.name === (activeName || profile.value)); if (!p) { setFlagEl($("curFlag"), null); - $("curName").textContent = "Нет сервера"; - $("curHost").textContent = "добавьте конфигурацию на вкладке «Мои серверы»"; + $("curName").textContent = t("no_server"); + $("curHost").textContent = t("no_server_hint"); $("curProto").textContent = "—"; $("curPing").textContent = "…"; $("curPing").className = "ms"; @@ -2548,7 +2983,7 @@ } setFlagEl($("curFlag"), p); $("curName").textContent = friendlyTitle(p.name); - $("curHost").textContent = "нажмите, чтобы выбрать другой сервер"; + $("curHost").textContent = t("pick_other_server"); $("curProto").textContent = protoPretty(p.protocol); const f = friendlyMap[p.name]; const members = (f && f.groupNames) @@ -2568,7 +3003,7 @@ // Пинг ещё не измерялся — показываем «—», а не пустоту. el.className = "ms"; el.textContent = "—"; - el.title = "пинг не измерен"; + el.title = t("ping_not_measured"); } } @@ -2580,13 +3015,13 @@ const srvCount = $("srvCount"); if (srvCount) { srvCount.textContent = list.length - ? "· " + list.length + " " + pluralRu(list.length, "сервер", "сервера", "серверов") + ? "· " + list.length + " " + pluralServers(list.length) : ""; } if (!list.length) { const empty = document.createElement("div"); empty.className = "server-empty"; - empty.textContent = "Нет серверов — добавьте конфигурацию выше"; + empty.textContent = t("no_servers_empty"); serverList.appendChild(empty); return; } @@ -2614,7 +3049,7 @@ if (isActive) { const badge = document.createElement("span"); badge.className = "badge-active"; - badge.textContent = "АКТИВЕН"; + badge.textContent = t("badge_active"); nameRow.appendChild(badge); } metaCol.appendChild(nameRow); @@ -2626,7 +3061,7 @@ const tag = document.createElement("span"); tag.className = "proto-tag" + (isActive && active === pr.name ? " selected" : ""); tag.textContent = pr.label; - tag.title = "Протокол " + pr.label; + tag.title = t("proto_title", { label: pr.label }); tag.setAttribute("role", "button"); tag.tabIndex = 0; tag.addEventListener("click", (e) => { @@ -2665,7 +3100,7 @@ } else { ms.className = "ms"; ms.textContent = "—"; - ms.title = "пинг не измерен"; + ms.title = t("ping_not_measured"); } right.appendChild(ms); // [📊] — пинг всех протоколов группы (лучший показывается в строке). @@ -2673,7 +3108,7 @@ pingOne.type = "button"; pingOne.className = "row-ping"; pingOne.textContent = "📊"; - pingOne.title = "Проверить пинг этого сервера"; + pingOne.title = t("ping_this_server"); pingOne.addEventListener("click", (e) => { e.stopPropagation(); runPingGroup(g.members, pingOne, ms); @@ -2705,7 +3140,7 @@ renderCurServer(profile.value); } catch (e) { msEl.textContent = prevText; - setMeta("Не удалось измерить пинг: " + String(e), "err"); + setMeta(t("ping_fail") + ": " + String(e), "err"); btnEl.disabled = false; } } @@ -2726,29 +3161,29 @@ async function onServerConnect(name) { if (busy) return; await withBusy(async () => { - setMeta("Подключение к «" + friendlyTitle(name) + "»…"); + setMeta(t("connecting_to", { name: friendlyTitle(name) })); await connectProfile(name); - setMeta("Защищённое соединение: " + friendlyTitle(name), "ok"); + setMeta(t("connected_to", { name: friendlyTitle(name) }), "ok"); }); } function fmtTraffic(bytes) { - if (!bytes || bytes <= 0) return "0 ГБ"; + if (!bytes || bytes <= 0) return "0 " + t("unit_gb"); const gb = bytes / (1024 * 1024 * 1024); if (gb >= 1024) { const tb = gb / 1024; - return (tb >= 100 ? Math.round(tb) : tb.toFixed(1)) + " ТБ"; + return (tb >= 100 ? Math.round(tb) : tb.toFixed(1)) + " " + t("unit_tb"); } - if (gb >= 100) return Math.round(gb) + " ГБ"; - if (gb >= 1) return gb.toFixed(1) + " ГБ"; + if (gb >= 100) return Math.round(gb) + " " + t("unit_gb"); + if (gb >= 1) return gb.toFixed(1) + " " + t("unit_gb"); const mb = bytes / (1024 * 1024); - return (mb >= 1 ? Math.round(mb) : 1) + " МБ"; + return (mb >= 1 ? Math.round(mb) : 1) + " " + t("unit_mb"); } function fmtDate(unix) { const d = new Date(unix * 1000); try { - return d.toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" }); + return d.toLocaleDateString(currentLang === "en" ? "en-US" : "ru-RU", { day: "numeric", month: "long", year: "numeric" }); } catch (_) { return d.toISOString().slice(0, 10); } @@ -2792,13 +3227,13 @@ const usedPct = Math.min(100, Math.round(used / info.total * 100)); // «из Y ГБ» показываем только когда лимит уже начали расходовать. label.textContent = used > 0 - ? "Доступно " + fmtTraffic(left) + " из " + fmtTraffic(info.total) - : "Доступно " + fmtTraffic(left); - pct.textContent = usedPct + "% использовано"; + ? t("traffic_avail_of", { left: fmtTraffic(left), total: fmtTraffic(info.total) }) + : t("traffic_avail", { left: fmtTraffic(left) }); + pct.textContent = t("pct_used", { pct: usedPct }); fill.style.width = usedPct + "%"; fill.classList.toggle("warn", usedPct >= 85); } else { - label.textContent = "Использовано " + fmtTraffic(used) + " · Безлимит"; + label.textContent = t("traffic_used_unlim", { used: fmtTraffic(used) }); pct.textContent = ""; fill.style.width = "100%"; fill.classList.remove("warn"); @@ -2811,11 +3246,11 @@ const expDate = new Date(info.expire * 1000); const now = new Date(); if (expDate.getTime() < now.getTime() && !isSameCalendarDay(expDate, now)) { - expText = "Срок доступа истёк " + fmtDateShort(info.expire); + expText = t("expired_on", { date: fmtDateShort(info.expire) }); } else if (isSameCalendarDay(expDate, now)) { - expText = "⏳ Сгорает сегодня (до 23:59)"; + expText = t("burns_today"); } else { - expText = "Активен до " + fmtDateShort(info.expire); + expText = t("active_until", { date: fmtDateShort(info.expire) }); } } exp.hidden = !expText; @@ -2846,13 +3281,13 @@ const daysLeft = Math.floor((d.getTime() - Date.now()) / 86400000); const v = $("subExpireVal"); if (daysLeft < 0) { - v.textContent = text + " · Истёк"; + v.textContent = text + " · " + t("expired"); v.classList.add("warn"); } else if (daysLeft === 0) { - v.textContent = text + " · Сегодня"; + v.textContent = text + " · " + t("today"); v.classList.add("warn"); } else if (daysLeft <= 14) { - v.textContent = text + " · осталось " + daysLeft + " дн."; + v.textContent = text + " · " + t("days_left", { n: daysLeft }); v.classList.toggle("warn", daysLeft <= 3); } else { v.textContent = text; @@ -2864,21 +3299,21 @@ if (hasTraffic) { const used = (info.upload || 0) + (info.download || 0); $("subTrafficVal").textContent = info.total > 0 - ? (fmtTraffic(used) + " из " + fmtTraffic(info.total)) - : (fmtTraffic(used) + " · Безлимит"); + ? t("of_pair", { a: fmtTraffic(used), b: fmtTraffic(info.total) }) + : (fmtTraffic(used) + " · " + t("unlim")); } devicesRow.hidden = !hasDevices; if (hasDevices) { const n = info.devices || 0; - $("subDevicesVal").textContent = info.device_limit > 0 ? (n + " из " + info.device_limit) : String(n); + $("subDevicesVal").textContent = info.device_limit > 0 ? t("of_pair", { a: n, b: info.device_limit }) : String(n); } } function renderUpdate(u, version) { // Версия — только в «Помощь»; в сайдбаре бренд без номера. if (verLabel) verLabel.textContent = "EvilFox"; - if (aboutVer) aboutVer.textContent = "Версия " + (version || "?"); + if (aboutVer) aboutVer.textContent = t("version_label", { v: version || "?" }); if (badgeVer) { badgeVer.hidden = true; badgeVer.textContent = ""; @@ -2905,8 +3340,8 @@ } if (u.available) { updateBanner.classList.add("show"); - $("updateTitle").textContent = "Доступно обновление " + latest; - $("updateNotes").textContent = u.notes || ("У вас " + (u.current || version || "?") + ". Обновление только по кнопке."); + $("updateTitle").textContent = t("update_available_v", { v: latest }); + $("updateNotes").textContent = u.notes || t("update_notes_fallback", { v: u.current || version || "?" }); updateBanner.dataset.latest = latest; } else { updateBanner.classList.remove("show"); @@ -2922,13 +3357,13 @@ hero.classList.toggle("on", connected); btn.classList.toggle("on", connected || ksActive); if (ksActive) { - statusText.textContent = "⛔ Соединение прервано — трафик заблокирован"; - if (powerLabel) powerLabel.textContent = "ОТКЛЮЧИТЬСЯ"; + statusText.textContent = t("status_blocked"); + if (powerLabel) powerLabel.textContent = t("power_disconnect"); } else { - statusText.textContent = connected ? "🟢 Защита активна" : "🔴 Отключено"; - if (powerLabel) powerLabel.textContent = connected ? "ОТКЛЮЧИТЬСЯ" : "ПОДКЛЮЧИТЬСЯ"; + statusText.textContent = connected ? t("status_on") : t("status_off"); + if (powerLabel) powerLabel.textContent = connected ? t("power_disconnect") : t("power_connect"); } - btn.title = (connected || ksActive) ? "Отключить" : "Подключить"; + btn.title = (connected || ksActive) ? t("btn_disconnect") : t("btn_connect"); if (killSwitch && document.activeElement !== killSwitch) { killSwitchSyncing = true; killSwitch.checked = !!state.kill_switch; @@ -2939,7 +3374,7 @@ const subStatus = $("subStatus"); if (subStatus) { const hasSub = !!(state.subscription_url && String(state.subscription_url).trim()); - subStatus.textContent = hasSub ? "🟢 Конфигурация добавлена" : "🔴 Конфигурация не добавлена"; + subStatus.textContent = hasSub ? t("config_added") : t("config_missing"); } const lock = connected || busy; @@ -2984,7 +3419,7 @@ // Чип протокола в hero убран (дублировал карточку активного сервера) — guard на случай возврата. if (protoChip) { protoChip.textContent = label; - protoChip.classList.toggle("active", !!state.connected || label !== "протокол"); + protoChip.classList.toggle("active", !!state.connected || label !== t("proto_generic")); } const hy2 = (label === "hysteria2" || label === "hy2"); if ($("hy2Box")) $("hy2Box").style.display = hy2 ? "" : "none"; @@ -3001,7 +3436,7 @@ if (state.http_proxy) parts.push("HTTP " + state.http_proxy); if (state.socks_proxy) parts.push("SOCKS " + state.socks_proxy); logsProxyLine.hidden = !parts.length; - logsProxyLine.textContent = parts.length ? ("Прокси: " + parts.join(" · ")) : ""; + logsProxyLine.textContent = parts.length ? (t("proxy_line") + ": " + parts.join(" · ")) : ""; } renderProvConnect(); renderProfProps(); @@ -3010,19 +3445,17 @@ // «Готово к подключению» больше нет (статус виден на большой кнопке). let detail = "", detailKind = ""; if (ksActive) { - heroHint.textContent = "Kill Switch активен: интернет заблокирован до отключения или переподключения"; - detail = "⛔ Kill Switch: соединение прервано, трафик заблокирован. Нажмите «Отключиться» или переподключитесь."; + heroHint.textContent = t("hint_ks"); + detail = t("detail_ks"); detailKind = "err"; } else if (connected) { - heroHint.textContent = "Трафик зашифрован"; + heroHint.textContent = t("hint_encrypted"); } else if (state.core_ready === false) { - detail = "Сначала установите cores в Настройках («Сервис»)"; + detail = t("detail_cores"); detailKind = "err"; - heroHint.textContent = "Нужны cores для выбранного протокола"; + heroHint.textContent = t("hint_cores"); } else { - heroHint.textContent = currentMode === "vpn" - ? "Режим VPN: защита всего трафика, включая игры и приложения" - : "Нажмите, чтобы включить защиту"; + heroHint.textContent = currentMode === "vpn" ? t("hint_vpn") : t("hint_idle"); } if (!busy && Date.now() > metaHoldUntil) { setMeta(detail, detailKind); @@ -3036,10 +3469,10 @@ const b = $("provConnectBtn"); if (!b) return; if (connected) { - b.textContent = "✅ Подключено"; + b.textContent = t("prov_connected"); b.disabled = true; } else { - b.textContent = "🔥 Подключиться"; + b.textContent = t("prov_connect"); b.disabled = busy; } } @@ -3060,13 +3493,13 @@ const pr = pingMap[p.name]; if (pingEl) { if (pr && pr.ok) pingEl.textContent = pr.ms + " ms"; - else if (pr && pr.error) pingEl.textContent = "недоступен"; - else pingEl.textContent = "не измерен"; + else if (pr && pr.error) pingEl.textContent = t("unavailable"); + else pingEl.textContent = t("not_measured"); } if (protoEl) { protoEl.textContent = p.protocol ? protoPretty(p.protocol) : "—"; } - if (stEl) stEl.textContent = connected ? "🟢 Активен" : "⚪ Не активен"; + if (stEl) stEl.textContent = connected ? t("prop_active") : t("prop_inactive"); } async function refresh(opts) { @@ -3102,13 +3535,13 @@ } async function runBest(autoConnect) { - setMeta(autoConnect ? "Пинг и автоподключение…" : "Пинг и выбор лучшего…"); + setMeta(autoConnect ? t("meta_ping_auto") : t("meta_ping_best")); const res = await pingBest(!!autoConnect); rememberPings(res.pings || []); renderServerList(res.best_name || profile.value); renderCurServer(res.best_name || profile.value); if (res.best_name) { - setMeta("Лучший: " + friendlyTitle(res.best_name) + " · " + res.best_ms + " ms" + (res.connected ? " · подключено" : ""), "ok"); + setMeta(t("meta_best", { name: friendlyTitle(res.best_name), ms: res.best_ms }) + (res.connected ? " · " + t("meta_connected_short") : ""), "ok"); } return res; } @@ -3124,14 +3557,14 @@ if (connected || ksActive) { // ksActive: отключение снимает блокировку Kill Switch и // восстанавливает обычную сеть. - setMeta("Отключение…"); + setMeta(t("meta_disconnecting")); await disconnect(); - setMeta("Отключено", "ok"); + setMeta(t("meta_disconnected"), "ok"); } else if (autoBest.checked && profiles.length > 1) { // «Авто-лучший сервер»: пинг всех и подключение к самому быстрому. await runBest(true); } else { - setMeta("Подключение…"); + setMeta(t("meta_connecting")); // Профиль уже выбран; скрытые поля name/proxy гидратируются из state. // Не перезаписываем proxy пустой строкой, если форма ещё не заполнена. const n = (nameInput && nameInput.value.trim()) || profile.value; @@ -3141,7 +3574,7 @@ await persistHy2IfNeeded(); } await connect(); - setMeta("Защищённое соединение установлено", "ok"); + setMeta(t("meta_connected"), "ok"); } } catch (e) { setMeta(String(e), "err"); } })); @@ -3152,25 +3585,25 @@ const px = (proxy && proxy.value.trim()) || ""; await saveProfile(n, px, !!sysproxy.checked); await persistHy2IfNeeded(); - setMeta("Профиль сохранён", "ok"); + setMeta(t("meta_saved"), "ok"); } catch (e) { setMeta(String(e), "err"); } })); if (coreBtn) coreBtn.addEventListener("click", () => withBusy(async () => { try { - setMeta("Скачивание official cores…"); + setMeta(t("meta_cores_dl")); const path = await installCore(); - setMeta("Cores готовы: " + path, "ok"); + setMeta(t("meta_cores_ok", { path: path }), "ok"); } catch (e) { setMeta(String(e), "err"); } })); if (delBtn) delBtn.addEventListener("click", () => withBusy(async () => { try { - if (!confirm("Удалить сервер «" + friendlyTitle(profile.value) + "»?")) return; + if (!confirm(t("confirm_delete", { name: friendlyTitle(profile.value) }))) return; await deleteProfile(profile.value); formHydrated = false; dirty = false; - setMeta("Профиль удалён", "ok"); + setMeta(t("meta_deleted"), "ok"); } catch (e) { setMeta(String(e), "err"); } })); @@ -3188,12 +3621,12 @@ try { const n = $("newName").value.trim(); const p = $("newProxy").value.trim(); - if (!n) throw "Укажите название профиля"; + if (!n) throw t("err_need_name"); await createProfile(n, p, !!sysproxy.checked); modal.classList.remove("open"); formHydrated = false; dirty = false; - setMeta("Профиль создан", "ok"); + setMeta(t("meta_created"), "ok"); } catch (e) { setMeta(String(e), "err"); } })); @@ -3203,20 +3636,21 @@ e.stopPropagation(); } try { - setMeta("Открываю evilfox.win…"); + setMeta(t("meta_opening_site")); await openURL(SHOP_URL); - setMeta("Открыто в браузере", "ok"); + setMeta(t("meta_opened"), "ok"); } catch (err) { - setMeta("Не удалось открыть ссылку: " + String(err), "err"); + setMeta(t("meta_open_fail") + ": " + String(err), "err"); } } shopBtn.addEventListener("click", openShop); shopLink.addEventListener("click", openShop); // «Рекомендуемые сервисы» → «Полезные ссылки» (см. RECOMMENDED_SERVICES). - (function renderRecommendedServices() { + function renderRecommendedServices() { const grid = $("svcGrid"); if (!grid) return; + grid.innerHTML = ""; RECOMMENDED_SERVICES.forEach((s) => { const b = document.createElement("button"); b.type = "button"; @@ -3226,43 +3660,44 @@ ic.className = "ic"; ic.textContent = s.icon || "•"; const nm = document.createElement("span"); - nm.textContent = s.name; + nm.textContent = t(s.nameKey); b.append(ic, nm); b.addEventListener("click", async () => { try { - setMeta("Открываю " + s.name + "…"); + setMeta(t("meta_opening_named", { name: t(s.nameKey) })); await openURL(s.url); - setMeta("Открыто в браузере", "ok"); + setMeta(t("meta_opened"), "ok"); } catch (err) { - setMeta("Не удалось открыть ссылку: " + String(err), "err"); + setMeta(t("meta_open_fail") + ": " + String(err), "err"); } }); grid.appendChild(b); }); - })(); + } + renderRecommendedServices(); async function runImportSubscription() { const url = subUrl.value.trim(); if (!url) { - setMeta("Вставьте URL-ссылку для подключения", "err"); + setMeta(t("meta_need_url"), "err"); return; } - setMeta("Добавление конфигурации…"); + setMeta(t("meta_adding_config")); const r = await importSubscription(url); formHydrated = false; dirty = false; subUrl.value = ""; - setMeta(importSummary(r) + " · подбираю оптимальный сервер…", "ok"); + setMeta(importSummary(r) + " · " + t("meta_picking_best"), "ok"); await runBest(!!autoBest.checked); } function importSummary(r) { if (r && typeof r === "object") { - let s = "Конфигурация добавлена · серверов: " + (r.imported || 0); - if (r.skipped > 0) s += " (пропущено " + r.skipped + ")"; + let s = t("import_ok", { n: r.imported || 0 }); + if (r.skipped > 0) s += " (" + t("import_skipped", { n: r.skipped }) + ")"; return s; } - return "Конфигурация добавлена: " + r; + return t("import_ok_raw", { r: r }); } // «Где взять URL-ссылку?» — открывает evilfox.win в браузере. @@ -3292,9 +3727,7 @@ await withBusy(async () => { try { await setMode(mode); - setMeta(mode === "vpn" - ? "Режим VPN включён: весь трафик пойдёт через туннель после подключения" - : "Режим прокси включён", "ok"); + setMeta(mode === "vpn" ? t("meta_mode_vpn") : t("meta_mode_proxy"), "ok"); } catch (e) { setMeta(String(e), "err"); } }); } @@ -3304,10 +3737,10 @@ async function refreshLogs() { try { const text = await getLogs(); - logView.textContent = (text && text.trim()) ? text : "Пусто — подключитесь, чтобы увидеть журнал."; + logView.textContent = (text && text.trim()) ? text : t("log_empty"); logView.scrollTop = logView.scrollHeight; } catch (e) { - logView.textContent = "Не удалось получить логи: " + String(e); + logView.textContent = t("log_fail") + ": " + String(e); } } if (logsRefreshBtn) logsRefreshBtn.addEventListener("click", () => refreshLogs()); @@ -3315,13 +3748,13 @@ pingBtn.addEventListener("click", () => withBusy(async () => { try { - setMeta("Пинг серверов…"); + setMeta(t("meta_pinging")); const rows = await pingServers(); rememberPings(rows); renderServerList(profile.value); renderCurServer(profile.value); const ok = (rows || []).filter((r) => r.ok).length; - setMeta("Пинг: " + ok + "/" + (rows || []).length + " доступны", ok ? "ok" : "err"); + setMeta(t("meta_ping_result", { ok: ok, total: (rows || []).length }), ok ? "ok" : "err"); } catch (e) { setMeta(String(e), "err"); } })); @@ -3333,12 +3766,12 @@ async function runUpdateCheck() { try { - setMeta("Проверка обновлений…"); + setMeta(t("meta_checking_upd")); const st = await checkUpdate(); renderUpdate(st, st.current); - if (st.available) setMeta("Доступна версия " + st.latest, "ok"); - else if (st.error) setMeta("Обновление: " + st.error, "err"); - else setMeta("У вас актуальная версия " + st.current, "ok"); + if (st.available) setMeta(t("meta_upd_avail", { v: st.latest }), "ok"); + else if (st.error) setMeta(t("meta_upd_err", { e: st.error }), "err"); + else setMeta(t("meta_upd_current", { v: st.current }), "ok"); } catch (e) { setMeta(String(e), "err"); } } updCheckBtn.addEventListener("click", () => withBusy(runUpdateCheck)); @@ -3346,9 +3779,9 @@ updateBtn.addEventListener("click", () => withBusy(async () => { try { - setMeta("Скачивание и установка обновления…"); + setMeta(t("meta_upd_install")); const msg = await applyUpdate(); - setMeta(String(msg || "Перезапуск…"), "ok"); + setMeta(String(msg || t("meta_restart")), "ok"); } catch (e) { setMeta(String(e), "err"); } })); @@ -3356,7 +3789,7 @@ const latest = updateBanner.dataset.latest || ""; if (latest) setSkippedVersion(latest); updateBanner.classList.remove("show"); - setMeta("Версия " + (latest || "?") + " пропущена. Следующую предложим.", "ok"); + setMeta(t("meta_upd_skipped", { v: latest || "?" }), "ok"); }); (async () => { diff --git a/internal/update/update.go b/internal/update/update.go index d2f85b9..4eed290 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -18,7 +18,7 @@ import ( // CurrentVersion is the product/semver used for update eligibility (feed "version"). // Keep major.minor.patch only — no build suffix here. -const CurrentVersion = "4.2.4" +const CurrentVersion = "4.2.5" // BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part, // macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build. diff --git a/packaging/msix/AppxManifest.xml b/packaging/msix/AppxManifest.xml index c176209..894af25 100644 --- a/packaging/msix/AppxManifest.xml +++ b/packaging/msix/AppxManifest.xml @@ -33,7 +33,7 @@ diff --git a/server/update.json b/server/update.json index 7abc4a3..4a0134a 100644 --- a/server/update.json +++ b/server/update.json @@ -1,11 +1,11 @@ { - "version": "4.2.4", - "notes": "4.2.4: группировка серверов по хосту — один физический сервер с протоколами VLESS · Trojan · Hysteria2 вместо «сервер 2/3»; «оптимальный» среди разных хостов", + "version": "4.2.5", + "notes": "4.2.5: переключатель языка Русский / English (localStorage navis.lang, по умолчанию RU); все строки UI и динамические подписи серверов", "platform": "windows-amd64", "os": "windows", "arch": "amd64", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", - "sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046", + "sha256": "fb8bf0fb98662601235ca6e33ba51f9aaf2a1602f3177c602adb8023161bcd90", "mandatory": false, "platforms": { "android": { @@ -40,7 +40,7 @@ }, "windows-amd64": { "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", - "sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046", + "sha256": "fb8bf0fb98662601235ca6e33ba51f9aaf2a1602f3177c602adb8023161bcd90", "os": "windows", "arch": "amd64" } diff --git a/versioninfo.json b/versioninfo.json index e0eb139..a5b024e 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -1,7 +1,7 @@ { "FixedFileInfo": { - "FileVersion": { "Major": 4, "Minor": 2, "Patch": 4, "Build": 1 }, - "ProductVersion": { "Major": 4, "Minor": 2, "Patch": 4, "Build": 1 }, + "FileVersion": { "Major": 4, "Minor": 2, "Patch": 5, "Build": 1 }, + "ProductVersion": { "Major": 4, "Minor": 2, "Patch": 5, "Build": 1 }, "FileFlagsMask": "3f", "FileFlags": "00", "FileOS": "40004", @@ -11,12 +11,12 @@ "StringFileInfo": { "CompanyName": "EvilFox", "FileDescription": "EvilFox — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)", - "FileVersion": "4.2.4.1", + "FileVersion": "4.2.5.1", "InternalName": "EvilFox", "LegalCopyright": "Copyright (c) EvilFox", "OriginalFilename": "EvilFox.exe", "ProductName": "EvilFox", - "ProductVersion": "4.2.4.1", + "ProductVersion": "4.2.5.1", "Comments": "Open-source VPN/proxy client. https://evilfox.win/" }, "VarFileInfo": {