refactor: cleanup, korean translation

This commit is contained in:
zhom
2026-05-23 14:05:00 +04:00
parent 375530e358
commit 69da467ce0
31 changed files with 2573 additions and 307 deletions
+1 -1
View File
@@ -1174,7 +1174,7 @@ export default function Home() {
failed_count: payload.failed_count ?? 0,
phase: payload.phase,
},
{ id: toastId },
{ id: toastId, profileId: payload.profile_id },
);
}
});
@@ -42,7 +42,7 @@ export function DeleteConfirmationDialog({
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
+29 -8
View File
@@ -73,6 +73,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import type { Extension, ExtensionGroup } from "@/types";
import { DeleteConfirmationDialog } from "./delete-confirmation-dialog";
@@ -308,7 +309,11 @@ export function ExtensionManagementDialog({
);
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showErrorToast(
parseBackendError(err)
? translateBackendError(t, err)
: t("proxies.management.updateSyncFailed"),
);
} finally {
setIsTogglingExtSync((prev) => ({ ...prev, [ext.id]: false }));
}
@@ -331,7 +336,11 @@ export function ExtensionManagementDialog({
);
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showErrorToast(
parseBackendError(err)
? translateBackendError(t, err)
: t("proxies.management.updateSyncFailed"),
);
} finally {
setIsTogglingGroupSync((prev) => ({ ...prev, [group.id]: false }));
}
@@ -589,9 +598,15 @@ export function ExtensionManagementDialog({
}),
),
);
const failed = results.filter((r) => r.status === "rejected").length;
if (failed > 0) {
showErrorToast(t("proxies.management.updateSyncFailed"));
const firstRejection = results.find((r) => r.status === "rejected") as
| PromiseRejectedResult
| undefined;
if (firstRejection) {
showErrorToast(
parseBackendError(firstRejection.reason)
? translateBackendError(t, firstRejection.reason)
: t("proxies.management.updateSyncFailed"),
);
} else {
showSuccessToast(
targetEnabled
@@ -614,9 +629,15 @@ export function ExtensionManagementDialog({
}),
),
);
const failed = results.filter((r) => r.status === "rejected").length;
if (failed > 0) {
showErrorToast(t("proxies.management.updateSyncFailed"));
const firstRejection = results.find((r) => r.status === "rejected") as
| PromiseRejectedResult
| undefined;
if (firstRejection) {
showErrorToast(
parseBackendError(firstRejection.reason)
? translateBackendError(t, firstRejection.reason)
: t("proxies.management.updateSyncFailed"),
);
} else {
showSuccessToast(
targetEnabled
+12 -5
View File
@@ -57,6 +57,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import type { GroupWithCount, ProfileGroup } from "@/types";
import { RippleButton } from "./ui/ripple";
@@ -262,8 +263,8 @@ export function GroupManagementDialog({
} catch (error) {
console.error("Failed to toggle sync:", error);
showErrorToast(
error instanceof Error
? error.message
parseBackendError(error)
? translateBackendError(t, error)
: t("proxies.management.updateSyncFailed"),
);
} finally {
@@ -529,9 +530,15 @@ export function GroupManagementDialog({
}),
),
);
const failed = results.filter((r) => r.status === "rejected").length;
if (failed > 0) {
showErrorToast(t("proxies.management.updateSyncFailed"));
const firstRejection = results.find((r) => r.status === "rejected") as
| PromiseRejectedResult
| undefined;
if (firstRejection) {
showErrorToast(
parseBackendError(firstRejection.reason)
? translateBackendError(t, firstRejection.reason)
: t("proxies.management.updateSyncFailed"),
);
} else {
showSuccessToast(
targetEnabled
+15 -2
View File
@@ -120,6 +120,7 @@ export function IntegrationsDialog({
const [isMcpStarting, setIsMcpStarting] = useState(false);
const [agents, setAgents] = useState<McpAgentInfo[]>([]);
const [busyAgentIds, setBusyAgentIds] = useState<Set<string>>(new Set());
const [apiPortDraft, setApiPortDraft] = useState<string>("10108");
const { termsAccepted } = useWayfernTerms();
@@ -127,6 +128,7 @@ export function IntegrationsDialog({
try {
const loaded = await invoke<AppSettings>("get_app_settings");
setSettings(loaded);
setApiPortDraft(String(loaded.api_port ?? ""));
} catch (e) {
console.error("Failed to load settings:", e);
}
@@ -370,13 +372,24 @@ export function IntegrationsDialog({
<div className="flex items-center gap-2">
<Input
type="number"
value={settings.api_port}
value={apiPortDraft}
onChange={(e) => {
setApiPortDraft(e.target.value);
const val = Number.parseInt(e.target.value, 10);
if (!Number.isNaN(val)) {
if (
!Number.isNaN(val) &&
val >= 1 &&
val <= 65535
) {
setSettings({ ...settings, api_port: val });
}
}}
onBlur={() => {
const val = Number.parseInt(apiPortDraft, 10);
if (Number.isNaN(val) || val < 1 || val > 65535) {
setApiPortDraft(String(settings.api_port));
}
}}
className="w-24 font-mono"
min={1}
max={65535}
+1 -1
View File
@@ -12,7 +12,7 @@ type Props = ButtonProps & {
export const LoadingButton = ({ isLoading, className, ...props }: Props) => {
return (
<UIButton
className={cn("grid place-items-center", className)}
className={cn("inline-flex items-center justify-center", className)}
{...props}
disabled={props.disabled || isLoading}
>
+24 -5
View File
@@ -582,8 +582,9 @@ function ProfileInfoLayout({
const deleteAction = findAction("delete");
const fingerprintAction = findAction("fingerprint");
const cookiesAction =
findAction("manage cookies") ?? findAction("copy cookies");
const cookiesManageAction = findAction("manage cookies");
const cookiesCopyAction = findAction("copy cookies");
const cookiesAction = cookiesManageAction ?? cookiesCopyAction;
const extensionAction = findAction("extension");
const syncAction = findAction("sync");
const _launchHookAction = findAction("hook") ?? findAction("launch hook");
@@ -905,6 +906,7 @@ function ProfileInfoLayout({
profile={profile}
isRunning={isRunning}
isDisabled={isDisabled}
onCopyCookies={cookiesCopyAction?.onClick}
t={t}
/>
)}
@@ -1435,11 +1437,14 @@ function ExtensionsSectionInline({
function CookiesSectionInline({
profile,
isRunning,
isDisabled,
onCopyCookies,
t,
}: {
profile: BrowserProfile;
isRunning: boolean;
isDisabled: boolean;
onCopyCookies?: () => void;
t: (key: string, options?: Record<string, unknown>) => string;
}) {
type CookieStats = {
@@ -1483,9 +1488,23 @@ function CookiesSectionInline({
return (
<div className="flex flex-col gap-3 min-h-0 flex-1">
<div className="flex items-center gap-2 text-sm font-semibold">
<LuCookie className="size-4" />
{t("profileInfo.sections.cookies")}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-sm font-semibold">
<LuCookie className="size-4" />
{t("profileInfo.sections.cookies")}
</div>
{onCopyCookies && (
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5"
disabled={isDisabled}
onClick={onCopyCookies}
>
<LuCopy className="size-3.5" />
{t("profiles.actions.copyCookies")}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{t("profileInfo.sectionDesc.cookies")}
+26 -13
View File
@@ -67,6 +67,7 @@ import {
} from "@/components/ui/tooltip";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { useVpnEvents } from "@/hooks/use-vpn-events";
import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { cn } from "@/lib/utils";
import type { ProxyCheckResult, StoredProxy, VpnConfig } from "@/types";
@@ -394,8 +395,8 @@ export function ProxyManagementDialog({
} catch (error) {
console.error("Failed to toggle sync:", error);
showErrorToast(
error instanceof Error
? error.message
parseBackendError(error)
? translateBackendError(t, error)
: t("proxies.management.updateSyncFailed"),
);
} finally {
@@ -458,8 +459,8 @@ export function ProxyManagementDialog({
} catch (error) {
console.error("Failed to toggle VPN sync:", error);
showErrorToast(
error instanceof Error
? error.message
parseBackendError(error)
? translateBackendError(t, error)
: t("proxies.management.updateSyncFailed"),
);
} finally {
@@ -1010,9 +1011,15 @@ export function ProxyManagementDialog({
}),
),
);
const failed = results.filter((r) => r.status === "rejected").length;
if (failed > 0) {
showErrorToast(t("proxies.management.updateSyncFailed"));
const firstRejection = results.find((r) => r.status === "rejected") as
| PromiseRejectedResult
| undefined;
if (firstRejection) {
showErrorToast(
parseBackendError(firstRejection.reason)
? translateBackendError(t, firstRejection.reason)
: t("proxies.management.updateSyncFailed"),
);
} else {
showSuccessToast(
targetEnabled
@@ -1039,9 +1046,15 @@ export function ProxyManagementDialog({
}),
),
);
const failed = results.filter((r) => r.status === "rejected").length;
if (failed > 0) {
showErrorToast(t("vpns.management.updateSyncFailed"));
const firstRejection = results.find((r) => r.status === "rejected") as
| PromiseRejectedResult
| undefined;
if (firstRejection) {
showErrorToast(
parseBackendError(firstRejection.reason)
? translateBackendError(t, firstRejection.reason)
: t("proxies.management.updateSyncFailed"),
);
} else {
showSuccessToast(
targetEnabled
@@ -1055,7 +1068,7 @@ export function ProxyManagementDialog({
return (
<>
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
<DialogContent className="max-w-[min(95vw,1600px)] max-h-[90vh] flex flex-col">
<DialogContent className="max-w-4xl max-h-[85vh] flex flex-col">
{!subPage && (
<DialogHeader>
<DialogTitle>{t("proxies.management.title")}</DialogTitle>
@@ -1170,7 +1183,7 @@ export function ProxyManagementDialog({
} as React.CSSProperties
}
>
<Table className="min-w-max">
<Table className="w-full">
<TableHeader className="sticky top-0 z-10 bg-background">
{proxiesTable.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
@@ -1251,7 +1264,7 @@ export function ProxyManagementDialog({
} as React.CSSProperties
}
>
<Table className="min-w-max">
<Table className="w-full">
<TableHeader className="sticky top-0 z-10 bg-background">
{vpnsTable.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
+1
View File
@@ -464,6 +464,7 @@ export function SettingsDialog({
| "fr"
| "zh"
| "ja"
| "ko"
| "ru"),
);
setOriginalLanguage(selectedLanguage);
+69 -20
View File
@@ -1,9 +1,12 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FiWifi } from "react-icons/fi";
import { LuLayers, LuPuzzle, LuShield, LuUsers } from "react-icons/lu";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -19,6 +22,8 @@ interface UnsyncedEntityCounts {
proxies: number;
groups: number;
vpns: number;
extensions: number;
extension_groups: number;
}
interface SyncAllDialogProps {
@@ -67,27 +72,55 @@ export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) {
}
}, [onClose, t]);
const totalCount =
(counts?.proxies ?? 0) + (counts?.groups ?? 0) + (counts?.vpns ?? 0);
const items = useMemo(() => {
if (!counts) return [];
return [
{
key: "proxies",
count: counts.proxies,
label: t("syncAll.labels.proxies"),
Icon: FiWifi,
},
{
key: "vpns",
count: counts.vpns,
label: t("syncAll.labels.vpns"),
Icon: LuShield,
},
{
key: "groups",
count: counts.groups,
label: t("syncAll.labels.groups"),
Icon: LuUsers,
},
{
key: "extensions",
count: counts.extensions,
label: t("syncAll.labels.extensions"),
Icon: LuPuzzle,
},
{
key: "extensionGroups",
count: counts.extension_groups,
label: t("syncAll.labels.extensionGroups"),
Icon: LuLayers,
},
].filter((item) => item.count > 0);
}, [counts, t]);
// Don't show if there's nothing to sync
const totalCount = items.reduce((sum, item) => sum + item.count, 0);
// Don't render anything when there's nothing to sync — the parent
// mounts this dialog eagerly after login, so silent-close is correct.
if (!isLoading && totalCount === 0) {
return null;
}
const parts: string[] = [];
if (counts?.proxies && counts.proxies > 0) {
parts.push(t("syncAll.proxies", { count: counts.proxies }));
}
if (counts?.groups && counts.groups > 0) {
parts.push(t("syncAll.groups", { count: counts.groups }));
}
if (counts?.vpns && counts.vpns > 0) {
parts.push(t("syncAll.vpns", { count: counts.vpns }));
}
return (
<Dialog open={isOpen && totalCount > 0} onOpenChange={onClose}>
<Dialog
open={isOpen && (isLoading || totalCount > 0)}
onOpenChange={onClose}
>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("syncAll.title")}</DialogTitle>
@@ -99,10 +132,26 @@ export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) {
<div className="size-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="py-4">
<p className="text-sm text-muted-foreground">
{t("syncAll.itemsList", { items: parts.join(", ") })}
</p>
<div className="grid grid-cols-2 gap-2 py-2">
{items.map(({ key, count, label, Icon }) => (
<div
key={key}
className="flex items-center gap-3 rounded-lg border border-border/60 bg-card/50 p-3 transition-colors hover:bg-card"
>
<div className="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<Icon className="size-4" />
</div>
<div className="min-w-0 flex-1 text-sm font-medium truncate">
{label}
</div>
<Badge
variant="secondary"
className="shrink-0 tabular-nums px-2"
>
{count}
</Badge>
</div>
))}
</div>
)}
+7 -9
View File
@@ -11,19 +11,18 @@ const MotionThumb = motion.create(SwitchPrimitive.Thumb);
type AnimatedSwitchProps = React.ComponentProps<typeof SwitchPrimitive.Root>;
/**
* Toggle switch with a thumb that slides between the off (left) and on
* (right) positions and squashes wider while pressed. Animated via Framer
* Motion — no layout shift when the parent's width changes, and the
* pressed state is purely visual so external onCheckedChange semantics
* stay identical to a Radix Switch.
* Switch whose thumb actually slides between off and on. The Root flips
* its flex alignment on `data-state=checked`, which moves the Thumb's
* layout box; Framer Motion's `layout` prop tweens between the two
* positions. The thumb also squashes wider while pressed.
*/
function AnimatedSwitch({ className, ...props }: AnimatedSwitchProps) {
return (
<SwitchPrimitive.Root
data-slot="animated-switch"
className={cn(
"peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent",
"bg-input data-[state=checked]:bg-primary",
"peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center justify-start rounded-full border border-transparent px-[2px]",
"bg-input data-[state=checked]:bg-primary data-[state=checked]:justify-end",
"transition-colors duration-200 ease-out",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:cursor-not-allowed disabled:opacity-50",
@@ -39,8 +38,7 @@ function AnimatedSwitch({ className, ...props }: AnimatedSwitchProps) {
)}
layout
transition={{ type: "spring", stiffness: 700, damping: 32, mass: 0.5 }}
whileTap={{ width: 22 }}
style={{ marginLeft: 2, marginRight: 2 }}
whileTap={{ width: 20 }}
/>
</SwitchPrimitive.Root>
);
+3
View File
@@ -5,6 +5,7 @@ import en from "./locales/en.json";
import es from "./locales/es.json";
import fr from "./locales/fr.json";
import ja from "./locales/ja.json";
import ko from "./locales/ko.json";
import pt from "./locales/pt.json";
import ru from "./locales/ru.json";
import zh from "./locales/zh.json";
@@ -16,6 +17,7 @@ export const SUPPORTED_LANGUAGES = [
{ code: "fr", name: "French", nativeName: "Français" },
{ code: "zh", name: "Chinese", nativeName: "中文" },
{ code: "ja", name: "Japanese", nativeName: "日本語" },
{ code: "ko", name: "Korean", nativeName: "한국어" },
{ code: "ru", name: "Russian", nativeName: "Русский" },
] as const;
@@ -61,6 +63,7 @@ const resources = {
fr: { translation: fr },
zh: { translation: zh },
ja: { translation: ja },
ko: { translation: ko },
ru: { translation: ru },
};
+16 -8
View File
@@ -1070,16 +1070,16 @@
"syncAll": {
"title": "Enable Sync for Existing Items",
"description": "You have items that are not being synced. Would you like to enable sync for all of them?",
"itemsList": "Items not synced: {{items}}",
"proxies": "{{count}} proxy",
"proxies_plural": "{{count}} proxies",
"groups": "{{count}} group",
"groups_plural": "{{count}} groups",
"vpns": "{{count}} VPN",
"vpns_plural": "{{count}} VPNs",
"enableAll": "Enable All",
"skip": "Skip",
"success": "Sync enabled for all items"
"success": "Sync enabled for all items",
"labels": {
"proxies": "Proxies",
"vpns": "VPNs",
"groups": "Groups",
"extensions": "Extensions",
"extensionGroups": "Extension Groups"
}
},
"crossOs": {
"viewOnly": "This profile was created on {{os}} and is not supported on this system",
@@ -1788,6 +1788,14 @@
"profileLocked": "Profile is locked. Enter the password first.",
"invalidProfileId": "Invalid profile id",
"passwordTooShort": "Password must be at least {{min}} characters",
"proxyNotFound": "Proxy not found",
"groupNotFound": "Group not found",
"vpnNotFound": "VPN not found",
"extensionNotFound": "Extension not found",
"extensionGroupNotFound": "Extension group not found",
"cannotModifyCloudManagedProxy": "Cannot modify sync for a cloud-managed proxy",
"syncLockedByProfile": "Sync cannot be disabled while this is used by synced profiles",
"syncNotConfigured": "Sync is not configured. Sign in or configure a self-hosted server first.",
"internal": "Something went wrong: {{detail}}",
"invalidLaunchHookUrl": "Invalid launch hook URL. Use a full http:// or https:// URL.",
"cookieDbLocked": "Could not read cookies — the database is locked. Close the browser and try again.",
+16 -8
View File
@@ -1070,16 +1070,16 @@
"syncAll": {
"title": "Activar sincronización para elementos existentes",
"description": "Tienes elementos que no se están sincronizando. ¿Te gustaría activar la sincronización para todos?",
"itemsList": "Elementos no sincronizados: {{items}}",
"proxies": "{{count}} proxy",
"proxies_plural": "{{count}} proxies",
"groups": "{{count}} grupo",
"groups_plural": "{{count}} grupos",
"vpns": "{{count}} VPN",
"vpns_plural": "{{count}} VPNs",
"enableAll": "Activar todos",
"skip": "Omitir",
"success": "Sincronización activada para todos los elementos"
"success": "Sincronización activada para todos los elementos",
"labels": {
"proxies": "Proxies",
"vpns": "VPN",
"groups": "Grupos",
"extensions": "Extensiones",
"extensionGroups": "Grupos de extensiones"
}
},
"crossOs": {
"viewOnly": "Este perfil fue creado en {{os}} y no es compatible con este sistema",
@@ -1788,6 +1788,14 @@
"profileLocked": "El perfil está bloqueado. Introduce la contraseña primero.",
"invalidProfileId": "ID de perfil no válido",
"passwordTooShort": "La contraseña debe tener al menos {{min}} caracteres",
"proxyNotFound": "Proxy no encontrado",
"groupNotFound": "Grupo no encontrado",
"vpnNotFound": "VPN no encontrada",
"extensionNotFound": "Extensión no encontrada",
"extensionGroupNotFound": "Grupo de extensiones no encontrado",
"cannotModifyCloudManagedProxy": "No se puede modificar la sincronización de un proxy gestionado en la nube",
"syncLockedByProfile": "No se puede desactivar la sincronización mientras se usa en perfiles sincronizados",
"syncNotConfigured": "La sincronización no está configurada. Inicia sesión o configura un servidor propio.",
"internal": "Algo salió mal: {{detail}}",
"invalidLaunchHookUrl": "URL del hook de inicio no válida. Usa una URL completa http:// o https://.",
"cookieDbLocked": "No se pudieron leer las cookies — la base de datos está bloqueada. Cierra el navegador e inténtalo de nuevo.",
+16 -8
View File
@@ -1070,16 +1070,16 @@
"syncAll": {
"title": "Activer la synchronisation pour les éléments existants",
"description": "Vous avez des éléments qui ne sont pas synchronisés. Voulez-vous activer la synchronisation pour tous ?",
"itemsList": "Éléments non synchronisés : {{items}}",
"proxies": "{{count}} proxy",
"proxies_plural": "{{count}} proxies",
"groups": "{{count}} groupe",
"groups_plural": "{{count}} groupes",
"vpns": "{{count}} VPN",
"vpns_plural": "{{count}} VPNs",
"enableAll": "Tout activer",
"skip": "Ignorer",
"success": "Synchronisation activée pour tous les éléments"
"success": "Synchronisation activée pour tous les éléments",
"labels": {
"proxies": "Proxies",
"vpns": "VPN",
"groups": "Groupes",
"extensions": "Extensions",
"extensionGroups": "Groupes d'extensions"
}
},
"crossOs": {
"viewOnly": "Ce profil a été créé sur {{os}} et n'est pas pris en charge sur ce système",
@@ -1788,6 +1788,14 @@
"profileLocked": "Le profil est verrouillé. Entrez d'abord le mot de passe.",
"invalidProfileId": "Identifiant de profil non valide",
"passwordTooShort": "Le mot de passe doit comporter au moins {{min}} caractères",
"proxyNotFound": "Proxy introuvable",
"groupNotFound": "Groupe introuvable",
"vpnNotFound": "VPN introuvable",
"extensionNotFound": "Extension introuvable",
"extensionGroupNotFound": "Groupe d'extensions introuvable",
"cannotModifyCloudManagedProxy": "Impossible de modifier la synchronisation d'un proxy géré dans le cloud",
"syncLockedByProfile": "La synchronisation ne peut pas être désactivée tant qu'elle est utilisée par des profils synchronisés",
"syncNotConfigured": "La synchronisation n'est pas configurée. Connectez-vous ou configurez un serveur auto-hébergé.",
"internal": "Une erreur s'est produite : {{detail}}",
"invalidLaunchHookUrl": "URL du hook de lancement invalide. Utilisez une URL http:// ou https:// complète.",
"cookieDbLocked": "Impossible de lire les cookies — la base de données est verrouillée. Fermez le navigateur et réessayez.",
+16 -8
View File
@@ -1070,16 +1070,16 @@
"syncAll": {
"title": "既存アイテムの同期を有効にする",
"description": "同期されていないアイテムがあります。すべての同期を有効にしますか?",
"itemsList": "未同期アイテム: {{items}}",
"proxies": "{{count}}個のプロキシ",
"proxies_plural": "{{count}}個のプロキシ",
"groups": "{{count}}個のグループ",
"groups_plural": "{{count}}個のグループ",
"vpns": "{{count}}個のVPN",
"vpns_plural": "{{count}}個のVPN",
"enableAll": "すべて有効にする",
"skip": "スキップ",
"success": "すべてのアイテムの同期が有効になりました"
"success": "すべてのアイテムの同期が有効になりました",
"labels": {
"proxies": "プロキシ",
"vpns": "VPN",
"groups": "グループ",
"extensions": "拡張機能",
"extensionGroups": "拡張機能グループ"
}
},
"crossOs": {
"viewOnly": "このプロファイルは{{os}}で作成されたもので、このシステムではサポートされていません",
@@ -1788,6 +1788,14 @@
"profileLocked": "プロファイルはロックされています。先にパスワードを入力してください。",
"invalidProfileId": "無効なプロファイルIDです",
"passwordTooShort": "パスワードは {{min}} 文字以上必要です",
"proxyNotFound": "プロキシが見つかりません",
"groupNotFound": "グループが見つかりません",
"vpnNotFound": "VPNが見つかりません",
"extensionNotFound": "拡張機能が見つかりません",
"extensionGroupNotFound": "拡張機能グループが見つかりません",
"cannotModifyCloudManagedProxy": "クラウド管理のプロキシの同期は変更できません",
"syncLockedByProfile": "同期済みプロファイルで使用中のため、同期を無効にできません",
"syncNotConfigured": "同期が設定されていません。サインインするか、セルフホストサーバーを設定してください。",
"internal": "問題が発生しました: {{detail}}",
"invalidLaunchHookUrl": "起動フックURLが無効です。完全な http:// または https:// URL を使用してください。",
"cookieDbLocked": "Cookie を読み取れません — データベースがロックされています。ブラウザを閉じてから再試行してください。",
File diff suppressed because it is too large Load Diff
+16 -8
View File
@@ -1070,16 +1070,16 @@
"syncAll": {
"title": "Ativar sincronização para itens existentes",
"description": "Você tem itens que não estão sendo sincronizados. Gostaria de ativar a sincronização para todos?",
"itemsList": "Itens não sincronizados: {{items}}",
"proxies": "{{count}} proxy",
"proxies_plural": "{{count}} proxies",
"groups": "{{count}} grupo",
"groups_plural": "{{count}} grupos",
"vpns": "{{count}} VPN",
"vpns_plural": "{{count}} VPNs",
"enableAll": "Ativar todos",
"skip": "Pular",
"success": "Sincronização ativada para todos os itens"
"success": "Sincronização ativada para todos os itens",
"labels": {
"proxies": "Proxies",
"vpns": "VPNs",
"groups": "Grupos",
"extensions": "Extensões",
"extensionGroups": "Grupos de extensões"
}
},
"crossOs": {
"viewOnly": "Este perfil foi criado em {{os}} e não é compatível com este sistema",
@@ -1788,6 +1788,14 @@
"profileLocked": "O perfil está bloqueado. Digite a senha primeiro.",
"invalidProfileId": "ID de perfil inválido",
"passwordTooShort": "A senha deve ter pelo menos {{min}} caracteres",
"proxyNotFound": "Proxy não encontrado",
"groupNotFound": "Grupo não encontrado",
"vpnNotFound": "VPN não encontrada",
"extensionNotFound": "Extensão não encontrada",
"extensionGroupNotFound": "Grupo de extensões não encontrado",
"cannotModifyCloudManagedProxy": "Não é possível modificar a sincronização de um proxy gerenciado na nuvem",
"syncLockedByProfile": "A sincronização não pode ser desativada enquanto estiver em uso por perfis sincronizados",
"syncNotConfigured": "A sincronização não está configurada. Faça login ou configure um servidor auto-hospedado.",
"internal": "Algo deu errado: {{detail}}",
"invalidLaunchHookUrl": "URL do hook de inicialização inválida. Use uma URL completa http:// ou https://.",
"cookieDbLocked": "Não foi possível ler os cookies — o banco de dados está bloqueado. Feche o navegador e tente novamente.",
+16 -8
View File
@@ -1070,16 +1070,16 @@
"syncAll": {
"title": "Включить синхронизацию для существующих элементов",
"description": "У вас есть элементы, которые не синхронизируются. Хотите включить синхронизацию для всех?",
"itemsList": "Несинхронизированные элементы: {{items}}",
"proxies": "{{count}} прокси",
"proxies_plural": "{{count}} прокси",
"groups": "{{count}} группа",
"groups_plural": "{{count}} групп",
"vpns": "{{count}} VPN",
"vpns_plural": "{{count}} VPN",
"enableAll": "Включить все",
"skip": "Пропустить",
"success": "Синхронизация включена для всех элементов"
"success": "Синхронизация включена для всех элементов",
"labels": {
"proxies": "Прокси",
"vpns": "VPN",
"groups": "Группы",
"extensions": "Расширения",
"extensionGroups": "Группы расширений"
}
},
"crossOs": {
"viewOnly": "Этот профиль был создан на {{os}} и не поддерживается в этой системе",
@@ -1788,6 +1788,14 @@
"profileLocked": "Профиль заблокирован. Сначала введите пароль.",
"invalidProfileId": "Недействительный идентификатор профиля",
"passwordTooShort": "Пароль должен быть не короче {{min}} символов",
"proxyNotFound": "Прокси не найден",
"groupNotFound": "Группа не найдена",
"vpnNotFound": "VPN не найден",
"extensionNotFound": "Расширение не найдено",
"extensionGroupNotFound": "Группа расширений не найдена",
"cannotModifyCloudManagedProxy": "Невозможно изменить синхронизацию для облачного прокси",
"syncLockedByProfile": "Невозможно отключить синхронизацию, пока используется синхронизированными профилями",
"syncNotConfigured": "Синхронизация не настроена. Войдите или настройте собственный сервер.",
"internal": "Что-то пошло не так: {{detail}}",
"invalidLaunchHookUrl": "Неверный URL хука запуска. Используйте полный URL http:// или https://.",
"cookieDbLocked": "Не удалось прочитать куки — база данных заблокирована. Закройте браузер и попробуйте снова.",
+16 -8
View File
@@ -1070,16 +1070,16 @@
"syncAll": {
"title": "为现有项目启用同步",
"description": "您有未同步的项目。是否要为所有项目启用同步?",
"itemsList": "未同步项目: {{items}}",
"proxies": "{{count}} 个代理",
"proxies_plural": "{{count}} 个代理",
"groups": "{{count}} 个分组",
"groups_plural": "{{count}} 个分组",
"vpns": "{{count}} 个 VPN",
"vpns_plural": "{{count}} 个 VPN",
"enableAll": "全部启用",
"skip": "跳过",
"success": "已为所有项目启用同步"
"success": "已为所有项目启用同步",
"labels": {
"proxies": "代理",
"vpns": "VPN",
"groups": "分组",
"extensions": "扩展",
"extensionGroups": "扩展分组"
}
},
"crossOs": {
"viewOnly": "此配置文件在 {{os}} 上创建,不受此系统支持",
@@ -1788,6 +1788,14 @@
"profileLocked": "配置文件已锁定。请先输入密码。",
"invalidProfileId": "配置文件 ID 无效",
"passwordTooShort": "密码至少需要 {{min}} 个字符",
"proxyNotFound": "未找到代理",
"groupNotFound": "未找到分组",
"vpnNotFound": "未找到 VPN",
"extensionNotFound": "未找到扩展",
"extensionGroupNotFound": "未找到扩展分组",
"cannotModifyCloudManagedProxy": "无法修改云管理代理的同步",
"syncLockedByProfile": "在被已同步的配置文件使用时无法禁用同步",
"syncNotConfigured": "同步未配置。请先登录或配置自托管服务器。",
"internal": "出现问题:{{detail}}",
"invalidLaunchHookUrl": "启动钩子 URL 无效。请使用完整的 http:// 或 https:// URL。",
"cookieDbLocked": "无法读取 Cookie — 数据库已锁定。请关闭浏览器后重试。",
+24
View File
@@ -20,6 +20,14 @@ export type BackendErrorCode =
| "COOKIE_DB_LOCKED"
| "COOKIE_DB_UNAVAILABLE"
| "SELF_HOSTED_REQUIRES_LOGOUT"
| "PROXY_NOT_FOUND"
| "GROUP_NOT_FOUND"
| "VPN_NOT_FOUND"
| "EXTENSION_NOT_FOUND"
| "EXTENSION_GROUP_NOT_FOUND"
| "CANNOT_MODIFY_CLOUD_MANAGED_PROXY"
| "SYNC_LOCKED_BY_PROFILE"
| "SYNC_NOT_CONFIGURED"
| "INTERNAL_ERROR";
export interface BackendError {
@@ -96,6 +104,22 @@ export function translateBackendError(t: TFunction, err: unknown): string {
return t("backendErrors.cookieDbUnavailable");
case "SELF_HOSTED_REQUIRES_LOGOUT":
return t("backendErrors.selfHostedRequiresLogout");
case "PROXY_NOT_FOUND":
return t("backendErrors.proxyNotFound");
case "GROUP_NOT_FOUND":
return t("backendErrors.groupNotFound");
case "VPN_NOT_FOUND":
return t("backendErrors.vpnNotFound");
case "EXTENSION_NOT_FOUND":
return t("backendErrors.extensionNotFound");
case "EXTENSION_GROUP_NOT_FOUND":
return t("backendErrors.extensionGroupNotFound");
case "CANNOT_MODIFY_CLOUD_MANAGED_PROXY":
return t("backendErrors.cannotModifyCloudManagedProxy");
case "SYNC_LOCKED_BY_PROFILE":
return t("backendErrors.syncLockedByProfile");
case "SYNC_NOT_CONFIGURED":
return t("backendErrors.syncNotConfigured");
case "INTERNAL_ERROR":
return t("backendErrors.internal", {
detail: parsed.params?.detail ?? "",
+11 -1
View File
@@ -1,3 +1,4 @@
import { invoke } from "@tauri-apps/api/core";
import React from "react";
import { type ExternalToast, toast as sonnerToast } from "sonner";
import { UnifiedToast } from "@/components/custom-toast";
@@ -259,7 +260,7 @@ export function showSyncProgressToast(
failed_count: number;
phase: string;
},
options?: { id?: string },
options?: { id?: string; profileId?: string },
) {
return showToast({
type: "sync-progress",
@@ -268,6 +269,15 @@ export function showSyncProgressToast(
id: options?.id,
duration: Number.POSITIVE_INFINITY,
onCancel: () => {
if (options?.profileId) {
// Fire-and-forget — backend flips the cancel flag for the in-flight
// upload/download loops to drain.
void invoke("cancel_profile_sync", {
profileId: options.profileId,
}).catch((err: unknown) => {
console.error("Failed to cancel sync:", err);
});
}
if (options?.id) {
dismissToast(options.id);
}