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 @@
- Защита
+ Защита
- Мои серверы
+ Мои серверы
- Настройки
+ Настройки
- Статистика
+ Статистика
- Помощь
+ Помощь
-
Доступно обновление
+
Доступно обновление
- Обновить
- Пропустить эту версию
+ Обновить
+ Пропустить эту версию
@@ -1380,7 +1449,7 @@
-
+
@@ -1392,12 +1461,12 @@
- VPN · весь трафик
+ VPN · весь трафик
Нажмите большую кнопку, чтобы включить защиту
-
+
·
Нет сервера
@@ -1421,9 +1490,9 @@
-
⭐ Рекомендуемый
+
⭐ Рекомендуемый
🦊 EvilFox VPN
-
Оптимальные серверы рядом с вами · защита без настройки
+
Оптимальные серверы рядом с вами · защита без настройки
🎁 3 дня бесплатного доступа
🔥 Подключиться
@@ -1433,75 +1502,75 @@
- Мои серверы
+ Мои серверы
-
- 🔗 Добавить конфигурацию
+
+ 🔗 Добавить конфигурацию
🔴 Конфигурация не добавлена
- Добавить конфигурацию
- Вставьте URL-ссылку для подключения
- Где взять URL-ссылку?
+ Добавить конфигурацию
+ Вставьте URL-ссылку для подключения
+ Где взять URL-ссылку?
- Информация об аккаунте
+ Информация об аккаунте
- Действует до
+ Действует до
- Трафик
+ Трафик
- Устройства
+ Устройства
- Свойства сервера
+ Свойства сервера
-
Сервер —
-
Пинг —
-
Протокол —
-
Статус —
+
Сервер —
+
Пинг —
+
Протокол —
+
Статус —
- Сохранить профиль
+ Сохранить профиль
- Настройки
+ Настройки
-
Режим трафика
+
Режим трафика
- Режим прокси
- Режим VPN
+ Режим прокси
+ Режим VPN
-
Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с EvilFox.exe. Меняется в отключённом состоянии.
+
Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с EvilFox.exe. Меняется в отключённом состоянии.
- Системный прокси
- В режиме VPN не используется — TUN и так захватывает всё
+ Системный прокси
+ В режиме VPN не используется — TUN и так захватывает всё
@@ -1510,7 +1579,7 @@
- Тёмная тема
+ Тёмная тема
@@ -1518,8 +1587,16 @@
-
🚀 Авто-лучший сервер
- При подключении автоматически выбирается сервер с наименьшим пингом
+ Язык / Language
+
+ RU
+ EN
+
+
+
+
+ 🚀 Авто-лучший сервер
+ При подключении автоматически выбирается сервер с наименьшим пингом
@@ -1528,8 +1605,8 @@
- 🔒 Защита от утечек (Kill Switch)
- При обрыве VPN блокировать весь интернет-трафик до ручного отключения или переподключения. Полноценно работает в режиме VPN; в режиме прокси защищены только программы, использующие системный прокси.
+ 🔒 Защита от утечек (Kill Switch)
+ При обрыве VPN блокировать весь интернет-трафик до ручного отключения или переподключения. Полноценно работает в режиме VPN; в режиме прокси защищены только программы, использующие системный прокси.
@@ -1557,23 +1634,23 @@
Brutal up
-
+
Brutal down
-
+
Obfs
- нет
+ нет
salamander
gecko
Obfs password
-
+
SNI
@@ -1581,7 +1658,7 @@
pinSHA256
-
+
Hop interval
@@ -1594,28 +1671,28 @@
- Сервис · cores, обновления
+ Сервис · cores, обновления
- Проверить обновление
- Установить cores
+ Проверить обновление
+ Установить cores
- Обновления этой сборки — через Microsoft Store.
+ Обновления этой сборки — через Microsoft Store.
- Расширенные настройки
+ Расширенные настройки
-
Параметры прокси
+
Параметры прокси
HTTP: —
SOCKS: —
-
Локальные адреса прокси на этом компьютере — нужны только для ручной настройки отдельных программ.
+
Локальные адреса прокси на этом компьютере — нужны только для ручной настройки отдельных программ.
- Статистика
+ Статистика
@@ -1626,74 +1703,74 @@
-
Статистика за сегодня
+
Статистика за сегодня
—
-
Использовано
+
Использовано
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.
+ Перейти на сайт
https://evilfox.win/
-
- 🌐 Полезные ссылки
+
- Выйти
+ Выйти
@@ -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": {