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 ?? "",