From 759263b63ff9aa3ba3f26af932964e0a3fc62134 Mon Sep 17 00:00:00 2001
From: zhom <2717306+zhom@users.noreply.github.com>
Date: Sat, 26 Sep 2026 10:33:41 +0400
Subject: [PATCH] fix: first browser download stuck at 0%
---
AGENTS.md | 1 +
src-tauri/src/api_client.rs | 12 +-
src-tauri/src/downloaded_browsers_registry.rs | 21 +--
src-tauri/src/downloader.rs | 60 ++++++--
src-tauri/src/extraction.rs | 2 +
src-tauri/src/geolocation.rs | 30 ++++
src-tauri/src/lib.rs | 6 +-
src-tauri/src/system_proxy.rs | 145 ++++++++++++++++++
src/components/welcome-dialog.tsx | 6 +
src/hooks/use-browser-download.ts | 14 +-
src/hooks/use-browser-setup.ts | 8 +-
src/i18n/locales/en.json | 3 +-
src/i18n/locales/es.json | 3 +-
src/i18n/locales/fr.json | 3 +-
src/i18n/locales/ja.json | 3 +-
src/i18n/locales/ko.json | 3 +-
src/i18n/locales/pt.json | 3 +-
src/i18n/locales/ru.json | 3 +-
src/i18n/locales/tr.json | 3 +-
src/i18n/locales/vi.json | 3 +-
src/i18n/locales/zh.json | 3 +-
src/lib/backend-errors.ts | 5 +
22 files changed, 291 insertions(+), 49 deletions(-)
create mode 100644 src-tauri/src/system_proxy.rs
diff --git a/AGENTS.md b/AGENTS.md
index 4c408b85..9947821e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -63,6 +63,7 @@ donutbrowser/
│ │ ├── downloader.rs # Browser binary downloader
│ │ ├── extraction.rs # Archive extraction (zip, tar, dmg, msi)
│ │ ├── settings_manager.rs # App settings persistence (atomic writes), tips + paid-welcome state
+│ │ ├── system_proxy.rs # The OS proxy Donut's own requests use; named in the error when it is unreachable
│ │ ├── vault.rs # Per-install key that seals local secrets; opens legacy build-password seals once
│ │ ├── cookie_manager.rs # Cookie import/export
│ │ ├── profile_importer.rs # Bulk profile import (Chromium-family detection, ZIP, batch)
diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs
index 0ee658ca..3107b534 100644
--- a/src-tauri/src/api_client.rs
+++ b/src-tauri/src/api_client.rs
@@ -266,6 +266,7 @@ impl ApiClient {
let url = "https://donutbrowser.com/wayfern.json";
let mut last_err = None;
+ let mut proxy_err = None;
let mut version_info: Option = None;
for attempt in 1..=3 {
@@ -291,6 +292,7 @@ impl ApiClient {
}
Err(e) => {
log::warn!("Wayfern fetch attempt {attempt}/3 failed: {e}");
+ proxy_err = crate::system_proxy::explain(&e);
last_err = Some(e.to_string());
}
}
@@ -301,10 +303,12 @@ impl ApiClient {
}
let version_info = version_info.ok_or_else(|| {
- format!(
- "Failed to fetch Wayfern version after 3 attempts: {}",
- last_err.unwrap_or_default()
- )
+ proxy_err.unwrap_or_else(|| {
+ format!(
+ "Failed to fetch Wayfern version after 3 attempts: {}",
+ last_err.unwrap_or_default()
+ )
+ })
})?;
log::info!("Fetched Wayfern version: {}", version_info.version);
diff --git a/src-tauri/src/downloaded_browsers_registry.rs b/src-tauri/src/downloaded_browsers_registry.rs
index 5b546e79..8c2d80cf 100644
--- a/src-tauri/src/downloaded_browsers_registry.rs
+++ b/src-tauri/src/downloaded_browsers_registry.rs
@@ -1567,6 +1567,12 @@ pub async fn ensure_active_browsers_downloaded(
Ok(info) => info.version,
Err(e) => {
log::warn!("Failed to resolve current {browser} version: {e}");
+ // The first-run screen listens for this. Without it the screen
+ // waits at 0% for a download that never starts (issue #625).
+ let _ = crate::events::emit(
+ "download-progress",
+ crate::downloader::DownloadProgress::failed(browser, "", Some(e.to_string())),
+ );
continue;
}
}
@@ -1621,6 +1627,10 @@ pub async fn ensure_active_browsers_downloaded(
log::warn!(
"Failed to auto-download {browser} {version} (attempt {attempt}/{MAX_ATTEMPTS}): {e}"
);
+ // A proxy setting stays wrong until the user changes it.
+ if crate::system_proxy::is_unreachable_error(&e.to_string()) {
+ break;
+ }
}
Err(_) => {
// The download future itself hung past the overall timeout and was dropped,
@@ -1632,16 +1642,7 @@ pub async fn ensure_active_browsers_downloaded(
ATTEMPT_TIMEOUT.as_secs()
);
crate::downloader::clear_download_state_for_browser(browser);
- let progress = crate::downloader::DownloadProgress {
- browser: (*browser).to_string(),
- version: version.clone(),
- downloaded_bytes: 0,
- total_bytes: None,
- percentage: 0.0,
- speed_bytes_per_sec: 0.0,
- eta_seconds: None,
- stage: "error".to_string(),
- };
+ let progress = crate::downloader::DownloadProgress::failed(browser, &version, None);
let _ = crate::events::emit("download-progress", &progress);
}
}
diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs
index 8030feb1..51cbd0e4 100644
--- a/src-tauri/src/downloader.rs
+++ b/src-tauri/src/downloader.rs
@@ -54,6 +54,26 @@ pub struct DownloadProgress {
pub speed_bytes_per_sec: f64,
pub eta_seconds: Option,
pub stage: String, // "downloading", "extracting", "verifying"
+ /// Why an "error" stage happened: a coded backend error or plain text.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub error: Option,
+}
+
+impl DownloadProgress {
+ /// The terminal event for a download that stopped, so the UI can show why.
+ pub fn failed(browser: &str, version: &str, error: Option) -> Self {
+ Self {
+ browser: browser.to_string(),
+ version: version.to_string(),
+ downloaded_bytes: 0,
+ total_bytes: None,
+ percentage: 0.0,
+ speed_bytes_per_sec: 0.0,
+ eta_seconds: None,
+ stage: "error".to_string(),
+ error,
+ }
+ }
}
pub struct Downloader {
@@ -268,6 +288,7 @@ impl Downloader {
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "verifying".to_string(),
+ error: None,
},
);
verify_archive_checksum(
@@ -317,7 +338,7 @@ impl Downloader {
}
Err(e) => {
log::warn!("Checksum sidecar request failed for {browser} {version}: {e}");
- return Err(unavailable());
+ return Err(crate::system_proxy::explain(&e).map_or_else(unavailable, Into::into));
}
};
@@ -416,6 +437,15 @@ impl Downloader {
break;
}
Err(e) => {
+ // Through a system proxy, a second connect failure in a row means
+ // the proxy is the problem. Five more attempts would only keep the
+ // bar at 0% for minutes before saying the same thing.
+ if attempt > 0 {
+ if let Some(proxy_error) = crate::system_proxy::explain(&e) {
+ log::warn!("Download request through the system proxy failed: {e}");
+ return Err(proxy_error.into());
+ }
+ }
let is_retryable = e.is_connect() || e.is_timeout() || e.is_request();
if is_retryable && attempt < max_send_retries {
let delay = 2u64.pow(attempt.min(4));
@@ -511,6 +541,7 @@ impl Downloader {
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "downloading".to_string(),
+ error: None,
},
);
@@ -594,6 +625,7 @@ impl Downloader {
speed_bytes_per_sec: speed,
eta_seconds: eta,
stage: "downloading".to_string(),
+ error: None,
},
);
last_update = now;
@@ -778,24 +810,18 @@ impl Downloader {
// Emit a terminal stage so the UI stops spinning. A user cancellation maps to
// "cancelled"; any other failure (network error, stall timeout, bad status)
// maps to "error" so the frontend can show a concrete error toast.
- let stage = if cancel_token.is_cancelled() {
- "cancelled"
+ let failure = contextualize("Failed to download browser", e);
+ let progress = if cancel_token.is_cancelled() {
+ DownloadProgress {
+ stage: "cancelled".to_string(),
+ ..DownloadProgress::failed(&browser_str, &version, None)
+ }
} else {
- "error"
- };
- let progress = DownloadProgress {
- browser: browser_str.clone(),
- version: version.clone(),
- downloaded_bytes: 0,
- total_bytes: None,
- percentage: 0.0,
- speed_bytes_per_sec: 0.0,
- eta_seconds: None,
- stage: stage.to_string(),
+ DownloadProgress::failed(&browser_str, &version, Some(failure.to_string()))
};
let _ = events::emit("download-progress", &progress);
- return Err(contextualize("Failed to download browser", e));
+ return Err(failure);
}
};
@@ -841,6 +867,7 @@ impl Downloader {
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "error".to_string(),
+ error: None,
};
let _ = events::emit("download-progress", &progress);
@@ -862,6 +889,7 @@ impl Downloader {
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "verifying".to_string(),
+ error: None,
};
let _ = events::emit("download-progress", &progress);
@@ -913,6 +941,7 @@ impl Downloader {
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "error".to_string(),
+ error: None,
};
let _ = events::emit("download-progress", &progress);
@@ -956,6 +985,7 @@ impl Downloader {
speed_bytes_per_sec: 0.0,
eta_seconds: Some(0.0),
stage: "completed".to_string(),
+ error: None,
};
let _ = events::emit("download-progress", &progress);
diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs
index f21a11ce..b02e8991 100644
--- a/src-tauri/src/extraction.rs
+++ b/src-tauri/src/extraction.rs
@@ -129,6 +129,7 @@ impl ExtractionReporter {
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "extracting".to_string(),
+ error: None,
};
let _ = events::emit("download-progress", &progress);
}
@@ -273,6 +274,7 @@ impl Extractor {
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "extracting".to_string(),
+ error: None,
};
let _ = events::emit("download-progress", &progress);
diff --git a/src-tauri/src/geolocation.rs b/src-tauri/src/geolocation.rs
index 22f58eb3..ad121780 100644
--- a/src-tauri/src/geolocation.rs
+++ b/src-tauri/src/geolocation.rs
@@ -317,6 +317,36 @@ pub fn lookup_exit_insight(ip: &str) -> ExitInsight {
insight
}
+/// City, country and country code for `ip`, read from the local City
+/// database. Proxy checks used to ask ip-api.com over plain HTTP, which showed
+/// that service and everyone on the path which exit address this machine was
+/// testing. `None` wherever the database has no answer.
+pub fn lookup_place(ip: &str) -> (Option, Option, Option) {
+ let Ok(ip_addr) = IpAddr::from_str(ip) else {
+ return (None, None, None);
+ };
+ let Some(path) = GeoIPDownloader::get_mmdb_file_path()
+ .ok()
+ .filter(|path| path.exists())
+ else {
+ return (None, None, None);
+ };
+ let Ok(reader) = Reader::open_readfile(&path) else {
+ return (None, None, None);
+ };
+ let Ok(Some(city)) = reader
+ .lookup(ip_addr)
+ .and_then(|lookup| lookup.decode::())
+ else {
+ return (None, None, None);
+ };
+ (
+ first_non_empty([city.city.names.english]),
+ first_non_empty([city.country.names.english]),
+ first_non_empty([city.country.iso_code]).map(|code| code.to_uppercase()),
+ )
+}
+
/// The autonomous system's organisation, from the ASN database that ships
/// alongside the city one. Absent on an install that has only ever fetched the
/// city database, which is why the caller treats `None` as "unknown".
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 5139ce10..fe6cf2a8 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -101,6 +101,7 @@ mod settings_manager;
pub mod socks5_local;
pub mod sync;
mod synchronizer;
+mod system_proxy;
pub mod traffic_stats;
mod wayfern_cdp;
mod wayfern_identity_storage;
@@ -1685,10 +1686,7 @@ pub async fn check_vpn_validity_core(
match ip_utils::fetch_public_ip(Some(&local_proxy_url)).await {
Ok(ip) => {
- let (city, country, country_code) =
- crate::proxy_manager::ProxyManager::get_ip_geolocation(&ip)
- .await
- .unwrap_or_default();
+ let (city, country, country_code) = crate::geolocation::lookup_place(&ip);
let insight = crate::geolocation::lookup_exit_insight(&ip);
result = Some(crate::proxy_manager::ProxyCheckResult {
diff --git a/src-tauri/src/system_proxy.rs b/src-tauri/src/system_proxy.rs
new file mode 100644
index 00000000..7fdbd3ca
--- /dev/null
+++ b/src-tauri/src/system_proxy.rs
@@ -0,0 +1,145 @@
+//! The proxy the operating system hands Donut's own HTTP clients.
+//!
+//! reqwest is built with `system-proxy`, so every client that does not call
+//! `no_proxy()` sends its requests through whatever proxy the environment or,
+//! on Windows, the WinINet settings name. A proxy left behind by an uninstalled
+//! VPN or debugging tool therefore breaks Donut's own downloads, and the user
+//! sees a transfer that never starts (issue #625). This module reads the same
+//! settings so a failed request can say which proxy it went through.
+
+/// The proxy used for `https` requests, as `host:port` or a URL. `None` when
+/// requests go out directly.
+pub fn https_proxy() -> Option {
+ env_https_proxy(|name| std::env::var(name).ok()).or_else(platform_https_proxy)
+}
+
+/// The error a caller returns when a request that went through `proxy` could
+/// not connect. The frontend turns it into a sentence that names the proxy.
+pub fn unreachable_error(proxy: &str) -> String {
+ serde_json::json!({
+ "code": "SYSTEM_PROXY_UNREACHABLE",
+ "params": { "proxy": proxy }
+ })
+ .to_string()
+}
+
+/// The coded error for a failed request, when the failure is the connection
+/// itself and a system proxy carried it. `None` for any other failure, which
+/// the caller reports as before.
+pub fn explain(error: &reqwest::Error) -> Option {
+ if !(error.is_connect() || error.is_timeout()) {
+ return None;
+ }
+ https_proxy().map(|proxy| unreachable_error(&proxy))
+}
+
+/// Whether `message` is the error `unreachable_error` builds. Retrying that one
+/// only repeats the wait, because the setting stays wrong until the user
+/// changes it.
+pub fn is_unreachable_error(message: &str) -> bool {
+ message.contains("\"SYSTEM_PROXY_UNREACHABLE\"")
+}
+
+/// The variables reqwest reads, in the order it reads them.
+fn env_https_proxy(read: impl Fn(&str) -> Option) -> Option {
+ ["HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"]
+ .into_iter()
+ .filter_map(read)
+ .map(|value| value.trim().to_string())
+ .find(|value| !value.is_empty())
+}
+
+#[cfg(target_os = "windows")]
+fn platform_https_proxy() -> Option {
+ use winreg::enums::HKEY_CURRENT_USER;
+ use winreg::RegKey;
+
+ let settings = RegKey::predef(HKEY_CURRENT_USER)
+ .open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Internet Settings")
+ .ok()?;
+ let enabled: u32 = settings.get_value("ProxyEnable").ok()?;
+ if enabled == 0 {
+ return None;
+ }
+ let server: String = settings.get_value("ProxyServer").ok()?;
+ parse_proxy_server(&server)
+}
+
+#[cfg(not(target_os = "windows"))]
+fn platform_https_proxy() -> Option {
+ None
+}
+
+/// Read the WinINet `ProxyServer` value. It is either one `host:port` for
+/// every scheme, or a list such as `http=a:80;https=b:443;socks=c:1080`.
+#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
+fn parse_proxy_server(value: &str) -> Option {
+ let value = value.trim();
+ if value.is_empty() {
+ return None;
+ }
+ if !value.contains('=') {
+ return Some(value.to_string());
+ }
+
+ let entry = |scheme: &str| {
+ value.split(';').find_map(|part| {
+ let (name, address) = part.split_once('=')?;
+ let address = address.trim();
+ (name.trim().eq_ignore_ascii_case(scheme) && !address.is_empty()).then(|| address.to_string())
+ })
+ };
+ entry("https").or_else(|| entry("http"))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn https_proxy_variables_win_over_all_proxy() {
+ let proxy = env_https_proxy(|name| match name {
+ "https_proxy" => Some("http://127.0.0.1:8080".to_string()),
+ "ALL_PROXY" => Some("socks5://127.0.0.1:1080".to_string()),
+ _ => None,
+ });
+ assert_eq!(proxy.as_deref(), Some("http://127.0.0.1:8080"));
+ }
+
+ #[test]
+ fn blank_variables_mean_no_proxy() {
+ assert_eq!(env_https_proxy(|_| Some(" ".to_string())), None);
+ assert_eq!(env_https_proxy(|_| None), None);
+ }
+
+ #[test]
+ fn a_single_proxy_server_serves_every_scheme() {
+ assert_eq!(
+ parse_proxy_server("127.0.0.1:8888").as_deref(),
+ Some("127.0.0.1:8888")
+ );
+ }
+
+ #[test]
+ fn a_per_scheme_list_uses_the_https_entry() {
+ assert_eq!(
+ parse_proxy_server("http=10.0.0.1:80;https=10.0.0.2:443;socks=10.0.0.3:1080").as_deref(),
+ Some("10.0.0.2:443")
+ );
+ assert_eq!(
+ parse_proxy_server("HTTP=10.0.0.1:80").as_deref(),
+ Some("10.0.0.1:80")
+ );
+ assert_eq!(parse_proxy_server("socks=10.0.0.3:1080"), None);
+ assert_eq!(parse_proxy_server(""), None);
+ }
+
+ #[test]
+ fn the_unreachable_error_is_recognised() {
+ let error = unreachable_error("127.0.0.1:8888");
+ assert!(is_unreachable_error(&error));
+ assert!(!is_unreachable_error("Download failed after 6 attempts"));
+ let parsed: serde_json::Value = serde_json::from_str(&error).unwrap();
+ assert_eq!(parsed["params"]["proxy"], "127.0.0.1:8888");
+ }
+}
diff --git a/src/components/welcome-dialog.tsx b/src/components/welcome-dialog.tsx
index 7558d18d..183c3546 100644
--- a/src/components/welcome-dialog.tsx
+++ b/src/components/welcome-dialog.tsx
@@ -28,6 +28,7 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { useBrowserSetup } from "@/hooks/use-browser-setup";
import { useInputModality } from "@/hooks/use-input-modality";
import { usePermissions } from "@/hooks/use-permissions";
+import { translateBackendError } from "@/lib/backend-errors";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import { getCurrentOS } from "@/lib/platform";
@@ -460,6 +461,11 @@ export function WelcomeDialog({
browser: browserName,
})}
+ {setup.error?.detail && (
+
+ {translateBackendError(t, setup.error.detail)}
+
+ )}
{/* No escape hatch here: a browser must finish downloading
diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts
index 1ca142ee..d4ff4f82 100644
--- a/src/hooks/use-browser-download.ts
+++ b/src/hooks/use-browser-download.ts
@@ -3,6 +3,7 @@ import type { Event as TauriEvent } from "@tauri-apps/api/event";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useRef, useState } from "react";
import i18n from "@/i18n";
+import { translateBackendError } from "@/lib/backend-errors";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import { isOnboardingActive } from "@/lib/onboarding-signal";
import {
@@ -38,6 +39,7 @@ interface DownloadProgress {
speed_bytes_per_sec: number;
eta_seconds?: number;
stage: string;
+ error?: string;
}
interface BrowserVersionsResult {
@@ -418,11 +420,13 @@ export function useBrowserDownload() {
),
{
id: `download-error-${progress.browser}-${progress.version}`,
- description: i18n.t(
- wasExtracting
- ? "browserDownload.toast.extractionFailedDescription"
- : "browserDownload.toast.downloadFailedDescription",
- ),
+ description: progress.error
+ ? translateBackendError(i18n.t, progress.error)
+ : i18n.t(
+ wasExtracting
+ ? "browserDownload.toast.extractionFailedDescription"
+ : "browserDownload.toast.downloadFailedDescription",
+ ),
},
);
}
diff --git a/src/hooks/use-browser-setup.ts b/src/hooks/use-browser-setup.ts
index 987d9995..1a7beec0 100644
--- a/src/hooks/use-browser-setup.ts
+++ b/src/hooks/use-browser-setup.ts
@@ -13,6 +13,7 @@ interface DownloadProgress {
speed_bytes_per_sec: number;
eta_seconds?: number | null;
stage: string;
+ error?: string;
}
export type SetupPhase = "downloading" | "extracting" | "ready" | "error";
@@ -25,6 +26,8 @@ export type SetupErrorStage =
export interface SetupError {
stage: SetupErrorStage;
+ /** The backend's reason, coded or plain, when it sent one. */
+ detail?: string;
}
// The backend reports real extraction percentages for most archive formats
@@ -216,7 +219,10 @@ export function useBrowserSetup(browser: string, active: boolean) {
doneRef.current = true;
finishExtraction();
setPhase("error");
- setError({ stage: toErrorStage(lastStageRef.current) });
+ setError({
+ stage: toErrorStage(lastStageRef.current),
+ detail: p.error,
+ });
break;
case "cancelled":
// Treat a cancellation like an error so the dialog can offer retry.
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index f128e879..4350ef4c 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -2350,7 +2350,8 @@
"syncDisplayUnavailable": "The display size could not be read, so the windows cannot be arranged.",
"syncDisplayTooSmall": "The display is too small for {{windows}} windows in that layout.",
"syncArrangeFailed": "No window could be moved.",
- "extensionPathInvalid": "That path is not allowed: it contains '..'."
+ "extensionPathInvalid": "That path is not allowed: it contains '..'.",
+ "systemProxyUnreachable": "Donut could not connect through the system proxy {{proxy}}. Fix or turn off the proxy in your system network settings, then try again."
},
"rail": {
"profiles": "Profiles",
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
index 55e91a16..ddbaed44 100644
--- a/src/i18n/locales/es.json
+++ b/src/i18n/locales/es.json
@@ -2360,7 +2360,8 @@
"syncDisplayUnavailable": "No se pudo leer el tamaño de la pantalla, así que las ventanas no se pueden ordenar.",
"syncDisplayTooSmall": "La pantalla es demasiado pequeña para {{windows}} ventanas en esa disposición.",
"syncArrangeFailed": "No se pudo mover ninguna ventana.",
- "extensionPathInvalid": "Esa ruta no está permitida: contiene '..'."
+ "extensionPathInvalid": "Esa ruta no está permitida: contiene '..'.",
+ "systemProxyUnreachable": "Donut no pudo conectarse a través del proxy del sistema {{proxy}}. Corrige o desactiva el proxy en la configuración de red del sistema e inténtalo de nuevo."
},
"rail": {
"profiles": "Perfiles",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index d3e16cf0..d32aa629 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -2360,7 +2360,8 @@
"syncDisplayUnavailable": "La taille de l'écran n'a pas pu être lue, les fenêtres ne peuvent donc pas être rangées.",
"syncDisplayTooSmall": "L'écran est trop petit pour {{windows}} fenêtres dans cette disposition.",
"syncArrangeFailed": "Aucune fenêtre n'a pu être déplacée.",
- "extensionPathInvalid": "Ce chemin n'est pas autorisé : il contient « .. »."
+ "extensionPathInvalid": "Ce chemin n'est pas autorisé : il contient « .. ».",
+ "systemProxyUnreachable": "Donut n'a pas pu se connecter via le proxy système {{proxy}}. Corrigez ou désactivez le proxy dans les paramètres réseau de votre système, puis réessayez."
},
"rail": {
"profiles": "Profils",
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index 37be8785..89dbc9fa 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -2350,7 +2350,8 @@
"syncDisplayUnavailable": "画面の大きさを読み取れなかったため、ウィンドウを並べられません。",
"syncDisplayTooSmall": "その配置で {{windows}} 個のウィンドウを並べるには画面が小さすぎます。",
"syncArrangeFailed": "動かせたウィンドウはありませんでした。",
- "extensionPathInvalid": "そのパスは使用できません。'..' が含まれています。"
+ "extensionPathInvalid": "そのパスは使用できません。'..' が含まれています。",
+ "systemProxyUnreachable": "システムプロキシ {{proxy}} 経由で接続できませんでした。システムのネットワーク設定でプロキシを修正するか無効にしてから、もう一度お試しください。"
},
"rail": {
"profiles": "プロファイル",
diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json
index c61a2790..754cb5d3 100644
--- a/src/i18n/locales/ko.json
+++ b/src/i18n/locales/ko.json
@@ -2350,7 +2350,8 @@
"syncDisplayUnavailable": "화면 크기를 읽지 못해 창을 정렬할 수 없습니다.",
"syncDisplayTooSmall": "그 배치로 창 {{windows}}개를 놓기에는 화면이 너무 작습니다.",
"syncArrangeFailed": "옮겨진 창이 없습니다.",
- "extensionPathInvalid": "그 경로는 사용할 수 없습니다. '..'이 포함되어 있습니다."
+ "extensionPathInvalid": "그 경로는 사용할 수 없습니다. '..'이 포함되어 있습니다.",
+ "systemProxyUnreachable": "시스템 프록시 {{proxy}}를 통해 연결하지 못했습니다. 시스템 네트워크 설정에서 프록시를 수정하거나 끈 다음 다시 시도하세요."
},
"rail": {
"profiles": "프로필",
diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json
index 37f41709..203252df 100644
--- a/src/i18n/locales/pt.json
+++ b/src/i18n/locales/pt.json
@@ -2360,7 +2360,8 @@
"syncDisplayUnavailable": "Não foi possível ler o tamanho do ecrã, por isso as janelas não podem ser organizadas.",
"syncDisplayTooSmall": "O ecrã é demasiado pequeno para {{windows}} janelas nessa disposição.",
"syncArrangeFailed": "Não foi possível mover nenhuma janela.",
- "extensionPathInvalid": "Esse caminho não é permitido: contém '..'."
+ "extensionPathInvalid": "Esse caminho não é permitido: contém '..'.",
+ "systemProxyUnreachable": "O Donut não conseguiu se conectar pelo proxy do sistema {{proxy}}. Corrija ou desative o proxy nas configurações de rede do sistema e tente novamente."
},
"rail": {
"profiles": "Perfis",
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index d7d6015a..fea689cd 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -2370,7 +2370,8 @@
"syncDisplayUnavailable": "Не удалось узнать размер экрана, поэтому окна не расставить.",
"syncDisplayTooSmall": "Экран слишком мал для {{windows}} окон в таком расположении.",
"syncArrangeFailed": "Ни одно окно не удалось переместить.",
- "extensionPathInvalid": "Этот путь недопустим: он содержит «..»."
+ "extensionPathInvalid": "Этот путь недопустим: он содержит «..».",
+ "systemProxyUnreachable": "Donut не удалось подключиться через системный прокси {{proxy}}. Исправьте или отключите прокси в сетевых настройках системы и повторите попытку."
},
"rail": {
"profiles": "Профили",
diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json
index 14957a1c..48ad6fd1 100644
--- a/src/i18n/locales/tr.json
+++ b/src/i18n/locales/tr.json
@@ -2350,7 +2350,8 @@
"syncDisplayUnavailable": "Ekran boyutu okunamadı, bu yüzden pencereler yerleştirilemiyor.",
"syncDisplayTooSmall": "Ekran, o düzende {{windows}} pencere için fazla küçük.",
"syncArrangeFailed": "Hiçbir pencere taşınamadı.",
- "extensionPathInvalid": "Bu yol kullanılamaz: '..' içeriyor."
+ "extensionPathInvalid": "Bu yol kullanılamaz: '..' içeriyor.",
+ "systemProxyUnreachable": "Donut, {{proxy}} sistem proxy'si üzerinden bağlanamadı. Sistem ağ ayarlarınızda proxy'yi düzeltin veya kapatın, ardından yeniden deneyin."
},
"rail": {
"profiles": "Profiller",
diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json
index cb6a933b..3a18b196 100644
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -2350,7 +2350,8 @@
"syncDisplayUnavailable": "Không đọc được kích thước màn hình nên không xếp được cửa sổ.",
"syncDisplayTooSmall": "Màn hình quá nhỏ cho {{windows}} cửa sổ theo cách xếp đó.",
"syncArrangeFailed": "Không cửa sổ nào được di chuyển.",
- "extensionPathInvalid": "Đường dẫn đó không được phép: nó chứa '..'."
+ "extensionPathInvalid": "Đường dẫn đó không được phép: nó chứa '..'.",
+ "systemProxyUnreachable": "Donut không thể kết nối qua proxy hệ thống {{proxy}}. Hãy sửa hoặc tắt proxy trong cài đặt mạng của hệ thống rồi thử lại."
},
"rail": {
"profiles": "Profile",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 2e06be48..71140898 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -2350,7 +2350,8 @@
"syncDisplayUnavailable": "无法读取显示器尺寸,因此不能排列窗口。",
"syncDisplayTooSmall": "显示器太小,无法以该布局排列 {{windows}} 个窗口。",
"syncArrangeFailed": "没有窗口被移动。",
- "extensionPathInvalid": "该路径不允许使用:它包含“..”。"
+ "extensionPathInvalid": "该路径不允许使用:它包含“..”。",
+ "systemProxyUnreachable": "Donut 无法通过系统代理 {{proxy}} 连接。请在系统网络设置中修正或关闭该代理,然后重试。"
},
"rail": {
"profiles": "配置文件",
diff --git a/src/lib/backend-errors.ts b/src/lib/backend-errors.ts
index 94ec20fe..8136e50f 100644
--- a/src/lib/backend-errors.ts
+++ b/src/lib/backend-errors.ts
@@ -64,6 +64,7 @@ export type BackendErrorCode =
| "UPDATE_CHECKSUM_MISMATCH"
| "BROWSER_CHECKSUM_UNAVAILABLE"
| "BROWSER_CHECKSUM_MISMATCH"
+ | "SYSTEM_PROXY_UNREACHABLE"
| "UPDATE_PROFILES_RUNNING"
| "UPDATE_PREPARATION_FAILED"
| "PROFILE_NAME_EXISTS"
@@ -441,6 +442,10 @@ export function translateBackendError(t: TFunction, err: unknown): string {
browser: parsed.params?.browser ?? "",
version: parsed.params?.version ?? "",
});
+ case "SYSTEM_PROXY_UNREACHABLE":
+ return t("backendErrors.systemProxyUnreachable", {
+ proxy: parsed.params?.proxy ?? "",
+ });
case "BROWSER_CHECKSUM_MISMATCH":
return t("backendErrors.browserChecksumMismatch", {
browser: parsed.params?.browser ?? "",