mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-30 15:48:19 +02:00
chore: copy
This commit is contained in:
@@ -67,6 +67,8 @@ donutbrowser/
|
||||
- Adding a new string means adding the key to ALL seven locale files in `src/i18n/locales/` (en, es, fr, ja, pt, ru, zh) — not just `en.json`. The English version alone is incomplete work.
|
||||
- Reuse existing keys (`common.buttons.*`, `common.labels.*`, `createProfile.*`, etc.) before creating new namespaces. Check `en.json` first.
|
||||
- Strings excluded from this rule: `console.log/warn/error`, dev-only debug labels, internal IDs, CSS class names, type names. If unsure whether a string renders to the user, assume it does and translate it.
|
||||
- **Never use `t(key, "fallback")` with a default-value second argument.** The 2-arg form is forbidden — every key must exist in every locale file before the call site lands. Fallbacks mask missing translations: a key missing from `ru.json` will silently render the English fallback to Russian users, so the bug never surfaces in CI or review. Only call `t("namespace.key")`. If a translation is missing for any locale, that's a bug to fix at the JSON, not a hole to paper over at the call site.
|
||||
- Empty-string values in non-English locales are also forbidden — a locale either has the right translation or it has the same content as English; never `""`. If a particular language doesn't need a particular phrase (e.g. a suffix that doesn't grammatically apply), refactor the JSX to use a single interpolated key (`t("foo.bar", { name })` with `"...{{name}}..."` in each locale) instead of splitting prefix/suffix.
|
||||
|
||||
## Singletons
|
||||
|
||||
|
||||
@@ -1232,7 +1232,7 @@ pub fn run() {
|
||||
#[allow(unused_variables)]
|
||||
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||
.title("Donut Browser")
|
||||
.inner_size(800.0, 500.0)
|
||||
.inner_size(840.0, 500.0)
|
||||
.resizable(false)
|
||||
.fullscreen(false)
|
||||
.center()
|
||||
|
||||
+1
-1
@@ -1070,7 +1070,7 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<div className="grid items-center justify-items-center min-h-screen gap-8 font-(family-name:--font-geist-sans) bg-background">
|
||||
<main className="flex flex-col items-center w-full max-w-3xl">
|
||||
<main className="flex flex-col items-center w-full max-w-4xl px-3">
|
||||
<div className="w-full">
|
||||
<HomeHeader
|
||||
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaExternalLinkAlt, FaTimes } from "react-icons/fa";
|
||||
import { LuCheckCheck } from "react-icons/lu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -19,6 +20,7 @@ export function AppUpdateToast({
|
||||
onDismiss,
|
||||
updateReady = false,
|
||||
}: AppUpdateToastProps) {
|
||||
const { t } = useTranslation();
|
||||
const handleRestartClick = async () => {
|
||||
await onRestart();
|
||||
};
|
||||
@@ -43,10 +45,10 @@ export function AppUpdateToast({
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{updateReady
|
||||
? "Update ready, restart to apply"
|
||||
? t("appUpdate.toast.updateReady")
|
||||
: updateInfo.repo_update
|
||||
? "Update available via package manager"
|
||||
: "Manual download required"}
|
||||
: t("appUpdate.toast.manualDownloadRequired")}
|
||||
</span>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{updateInfo.current_version} → {updateInfo.new_version}
|
||||
@@ -71,7 +73,7 @@ export function AppUpdateToast({
|
||||
className="flex gap-2 items-center text-xs"
|
||||
>
|
||||
<LuCheckCheck className="w-3 h-3" />
|
||||
Restart Now
|
||||
{t("appUpdate.toast.restartNow")}
|
||||
</RippleButton>
|
||||
) : (
|
||||
!updateInfo.repo_update &&
|
||||
@@ -82,7 +84,7 @@ export function AppUpdateToast({
|
||||
className="flex gap-2 items-center text-xs"
|
||||
>
|
||||
<FaExternalLinkAlt className="w-3 h-3" />
|
||||
View Release
|
||||
{t("appUpdate.toast.viewRelease")}
|
||||
</RippleButton>
|
||||
)
|
||||
)}
|
||||
@@ -92,7 +94,7 @@ export function AppUpdateToast({
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
>
|
||||
Later
|
||||
{t("appUpdate.toast.later")}
|
||||
</RippleButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -953,7 +953,7 @@ export function CreateProfileDialog({
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-4 h-4 rounded-full border-2 animate-spin border-muted/40 border-t-primary" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fetching available versions...
|
||||
{t("createProfile.version.fetching")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -294,7 +294,9 @@ export function UnifiedToast(props: ToastProps) {
|
||||
"completed_files" in progress && (
|
||||
<div className="mt-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{progress.phase === "uploading" ? "Uploading" : "Downloading"}{" "}
|
||||
{progress.phase === "uploading"
|
||||
? t("appUpdate.toast.uploading")
|
||||
: t("appUpdate.toast.downloading")}{" "}
|
||||
{progress.completed_files}/{progress.total_files} files
|
||||
{" \u2022 "}
|
||||
{formatBytesCompact(progress.completed_bytes)} /{" "}
|
||||
@@ -349,17 +351,17 @@ export function UnifiedToast(props: ToastProps) {
|
||||
<>
|
||||
{stage === "extracting" && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Extracting browser files... Please do not close the app.
|
||||
{t("browserDownload.toast.extracting")}
|
||||
</p>
|
||||
)}
|
||||
{stage === "verifying" && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Verifying browser files...
|
||||
{t("browserDownload.toast.verifying")}
|
||||
</p>
|
||||
)}
|
||||
{stage === "downloading (twilight rolling release)" && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Downloading rolling release build...
|
||||
{t("browserDownload.toast.downloadingRolling")}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -55,12 +55,12 @@ export function ExtensionGroupAssignmentDialog({
|
||||
} catch (err) {
|
||||
console.error("Failed to load extension groups:", err);
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to load extension groups",
|
||||
err instanceof Error ? err.message : t("extensions.loadGroupsFailed"),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const handleAssign = useCallback(async () => {
|
||||
setIsAssigning(true);
|
||||
@@ -79,7 +79,7 @@ export function ExtensionGroupAssignmentDialog({
|
||||
} catch (err) {
|
||||
console.error("Failed to assign extension group:", err);
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to assign extension group";
|
||||
err instanceof Error ? err.message : t("extensions.assignGroupFailed");
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
|
||||
@@ -50,36 +50,43 @@ type SyncStatus = "disabled" | "syncing" | "synced" | "error" | "waiting";
|
||||
function getSyncStatusDot(
|
||||
item: { sync_enabled?: boolean; last_sync?: number },
|
||||
liveStatus: SyncStatus | undefined,
|
||||
t: (key: string, options?: Record<string, unknown>) => string,
|
||||
): { color: string; tooltip: string; animate: boolean } {
|
||||
const status = liveStatus ?? (item.sync_enabled ? "synced" : "disabled");
|
||||
|
||||
switch (status) {
|
||||
case "syncing":
|
||||
return { color: "bg-warning", tooltip: "Syncing...", animate: true };
|
||||
return {
|
||||
color: "bg-warning",
|
||||
tooltip: t("profileTable.syncTooltipSyncing"),
|
||||
animate: true,
|
||||
};
|
||||
case "synced":
|
||||
return {
|
||||
color: "bg-success",
|
||||
tooltip: item.last_sync
|
||||
? `Synced ${new Date(item.last_sync * 1000).toLocaleString()}`
|
||||
: "Synced",
|
||||
? t("profileTable.syncTooltipSyncedAt", {
|
||||
time: new Date(item.last_sync * 1000).toLocaleString(),
|
||||
})
|
||||
: t("profileTable.syncTooltipSynced"),
|
||||
animate: false,
|
||||
};
|
||||
case "waiting":
|
||||
return {
|
||||
color: "bg-warning",
|
||||
tooltip: "Waiting to sync",
|
||||
tooltip: t("profileTable.syncTooltipWaiting"),
|
||||
animate: false,
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
color: "bg-destructive",
|
||||
tooltip: "Sync error",
|
||||
tooltip: t("profileTable.syncTooltipError"),
|
||||
animate: false,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: "bg-muted-foreground",
|
||||
tooltip: "Not synced",
|
||||
tooltip: t("profileTable.syncTooltipNotSynced"),
|
||||
animate: false,
|
||||
};
|
||||
}
|
||||
@@ -674,6 +681,7 @@ export function ExtensionManagementDialog({
|
||||
const syncDot = getSyncStatusDot(
|
||||
ext,
|
||||
extSyncStatus[ext.id],
|
||||
t,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
@@ -840,6 +848,7 @@ export function ExtensionManagementDialog({
|
||||
const groupSyncDot = getSyncStatusDot(
|
||||
group,
|
||||
extSyncStatus[group.id],
|
||||
t,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -995,7 +1004,7 @@ export function ExtensionManagementDialog({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent className="max-w-lg max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("extensions.editGroup")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -1003,87 +1012,89 @@ export function ExtensionManagementDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("common.labels.name")}</Label>
|
||||
<Input
|
||||
value={editGroupName}
|
||||
onChange={(e) => {
|
||||
setEditGroupName(e.target.value);
|
||||
}}
|
||||
placeholder={t("extensions.groupNamePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{extensions.filter((e) => !editGroupExtensionIds.includes(e.id))
|
||||
.length > 0 && (
|
||||
<ScrollArea className="overflow-y-auto flex-1 -mx-6 px-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("extensions.addToGroup")}</Label>
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(extId) => {
|
||||
setEditGroupExtensionIds((prev) => [...prev, extId]);
|
||||
<Label>{t("common.labels.name")}</Label>
|
||||
<Input
|
||||
value={editGroupName}
|
||||
onChange={(e) => {
|
||||
setEditGroupName(e.target.value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("extensions.addToGroup")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{extensions
|
||||
.filter((e) => !editGroupExtensionIds.includes(e.id))
|
||||
.map((ext) => (
|
||||
<SelectItem key={ext.id} value={ext.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderExtensionIcon(ext, "sm")}
|
||||
{ext.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
placeholder={t("extensions.groupNamePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("extensions.groupExtensions")}</Label>
|
||||
{editGroupExtensionIds.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground py-2">
|
||||
{t("extensions.noExtensionsInGroup")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 max-h-[200px] overflow-y-auto">
|
||||
{editGroupExtensionIds.map((extId) => {
|
||||
const ext = extensions.find((e) => e.id === extId);
|
||||
if (!ext) return null;
|
||||
return (
|
||||
<div
|
||||
key={extId}
|
||||
className="flex items-center gap-2 rounded-md border px-2 py-1.5"
|
||||
>
|
||||
{renderExtensionIcon(ext, "sm")}
|
||||
<span className="text-sm flex-1 truncate min-w-0">
|
||||
{ext.name}
|
||||
</span>
|
||||
{renderCompatIcons(ext.browser_compatibility)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 shrink-0"
|
||||
onClick={() => {
|
||||
setEditGroupExtensionIds((prev) =>
|
||||
prev.filter((id) => id !== extId),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<LuTrash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{extensions.filter((e) => !editGroupExtensionIds.includes(e.id))
|
||||
.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>{t("extensions.addToGroup")}</Label>
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(extId) => {
|
||||
setEditGroupExtensionIds((prev) => [...prev, extId]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("extensions.addToGroup")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{extensions
|
||||
.filter((e) => !editGroupExtensionIds.includes(e.id))
|
||||
.map((ext) => (
|
||||
<SelectItem key={ext.id} value={ext.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderExtensionIcon(ext, "sm")}
|
||||
{ext.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("extensions.groupExtensions")}</Label>
|
||||
{editGroupExtensionIds.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground py-2">
|
||||
{t("extensions.noExtensionsInGroup")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 max-h-[200px] overflow-y-auto">
|
||||
{editGroupExtensionIds.map((extId) => {
|
||||
const ext = extensions.find((e) => e.id === extId);
|
||||
if (!ext) return null;
|
||||
return (
|
||||
<div
|
||||
key={extId}
|
||||
className="flex items-center gap-2 rounded-md border px-2 py-1.5"
|
||||
>
|
||||
{renderExtensionIcon(ext, "sm")}
|
||||
<span className="text-sm flex-1 truncate min-w-0">
|
||||
{ext.name}
|
||||
</span>
|
||||
{renderCompatIcons(ext.browser_compatibility)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 shrink-0"
|
||||
onClick={() => {
|
||||
setEditGroupExtensionIds((prev) =>
|
||||
prev.filter((id) => id !== extId),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<LuTrash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
@@ -1117,7 +1128,7 @@ export function ExtensionManagementDialog({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent className="max-w-lg max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("extensions.editExtension")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -1125,123 +1136,127 @@ export function ExtensionManagementDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{editingExtension && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("common.labels.name")}</Label>
|
||||
<Input
|
||||
value={editExtensionName}
|
||||
onChange={(e) => {
|
||||
setEditExtensionName(e.target.value);
|
||||
}}
|
||||
placeholder={t("extensions.namePlaceholder")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void handleUpdateExtension();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ScrollArea className="overflow-y-auto flex-1 -mx-6 px-6">
|
||||
{editingExtension && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("common.labels.name")}</Label>
|
||||
<Input
|
||||
value={editExtensionName}
|
||||
onChange={(e) => {
|
||||
setEditExtensionName(e.target.value);
|
||||
}}
|
||||
placeholder={t("extensions.namePlaceholder")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void handleUpdateExtension();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Metadata from manifest.json */}
|
||||
<div className="rounded-md border p-3 space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
{t("extensions.metadata")}
|
||||
</Label>
|
||||
<div className="grid grid-cols-[auto,1fr] gap-x-3 gap-y-1.5 text-sm">
|
||||
{editingExtension.version && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.version")}
|
||||
</span>
|
||||
<span>{editingExtension.version}</span>
|
||||
</>
|
||||
)}
|
||||
{editingExtension.author && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.author")}
|
||||
</span>
|
||||
<span>{editingExtension.author}</span>
|
||||
</>
|
||||
)}
|
||||
{editingExtension.description && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("common.labels.description")}
|
||||
</span>
|
||||
<span className="line-clamp-3">
|
||||
{editingExtension.description}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.compatibility.label")}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{renderCompatIcons(editingExtension.browser_compatibility)}
|
||||
</div>
|
||||
<span className="text-muted-foreground">
|
||||
{t("common.labels.type")}
|
||||
</span>
|
||||
<span>.{editingExtension.file_type}</span>
|
||||
{editingExtension.homepage_url && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.homepage")}
|
||||
</span>
|
||||
<a
|
||||
href={editingExtension.homepage_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline flex items-center gap-1 truncate"
|
||||
>
|
||||
<span className="truncate">
|
||||
{editingExtension.homepage_url}
|
||||
{/* Metadata from manifest.json */}
|
||||
<div className="rounded-md border p-3 space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
{t("extensions.metadata")}
|
||||
</Label>
|
||||
<div className="grid grid-cols-[auto,1fr] gap-x-3 gap-y-1.5 text-sm">
|
||||
{editingExtension.version && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.version")}
|
||||
</span>
|
||||
<LuExternalLink className="w-3 h-3 shrink-0" />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{!editingExtension.version &&
|
||||
!editingExtension.author &&
|
||||
!editingExtension.description &&
|
||||
!editingExtension.homepage_url && (
|
||||
<span className="col-span-2 text-muted-foreground text-xs">
|
||||
{t("extensions.noMetadata")}
|
||||
<span>{editingExtension.version}</span>
|
||||
</>
|
||||
)}
|
||||
{editingExtension.author && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.author")}
|
||||
</span>
|
||||
<span>{editingExtension.author}</span>
|
||||
</>
|
||||
)}
|
||||
{editingExtension.description && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("common.labels.description")}
|
||||
</span>
|
||||
<span className="line-clamp-3">
|
||||
{editingExtension.description}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.compatibility.label")}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{renderCompatIcons(
|
||||
editingExtension.browser_compatibility,
|
||||
)}
|
||||
</div>
|
||||
<span className="text-muted-foreground">
|
||||
{t("common.labels.type")}
|
||||
</span>
|
||||
<span>.{editingExtension.file_type}</span>
|
||||
{editingExtension.homepage_url && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("extensions.homepage")}
|
||||
</span>
|
||||
<a
|
||||
href={editingExtension.homepage_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline flex items-center gap-1 truncate"
|
||||
>
|
||||
<span className="truncate">
|
||||
{editingExtension.homepage_url}
|
||||
</span>
|
||||
<LuExternalLink className="w-3 h-3 shrink-0" />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{!editingExtension.version &&
|
||||
!editingExtension.author &&
|
||||
!editingExtension.description &&
|
||||
!editingExtension.homepage_url && (
|
||||
<span className="col-span-2 text-muted-foreground text-xs">
|
||||
{t("extensions.noMetadata")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Re-upload */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t("extensions.reupload")}</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<RippleButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
document.getElementById("ext-edit-file-input")?.click()
|
||||
}
|
||||
>
|
||||
<LuUpload className="w-3 h-3 mr-1" />
|
||||
{t("extensions.selectFile")}
|
||||
</RippleButton>
|
||||
<input
|
||||
id="ext-edit-file-input"
|
||||
type="file"
|
||||
accept=".xpi,.crx,.zip"
|
||||
className="hidden"
|
||||
onChange={handleEditFileSelect}
|
||||
/>
|
||||
{pendingUpdateFile && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||
{pendingUpdateFile.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Re-upload */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t("extensions.reupload")}</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<RippleButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
document.getElementById("ext-edit-file-input")?.click()
|
||||
}
|
||||
>
|
||||
<LuUpload className="w-3 h-3 mr-1" />
|
||||
{t("extensions.selectFile")}
|
||||
</RippleButton>
|
||||
<input
|
||||
id="ext-edit-file-input"
|
||||
type="file"
|
||||
accept=".xpi,.crx,.zip"
|
||||
className="hidden"
|
||||
onChange={handleEditFileSelect}
|
||||
/>
|
||||
{pendingUpdateFile && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||
{pendingUpdateFile.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@@ -139,7 +139,7 @@ export function GroupBadges({
|
||||
return (
|
||||
<div className="flex gap-2 mb-4">
|
||||
<div className="flex items-center gap-2 px-4.5 py-1.5 text-xs">
|
||||
Loading groups...
|
||||
{t("groups.loading")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -283,7 +283,7 @@ export function GroupManagementDialog({
|
||||
{/* Groups list */}
|
||||
{isLoading ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("common.loading")}
|
||||
{t("common.buttons.loading")}
|
||||
</div>
|
||||
) : groups.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -543,9 +543,9 @@ export function ImportProfileDialog({
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{t("importProfile.importedAsPrefix")}{" "}
|
||||
<strong>{getBrowserDisplayName(currentMappedBrowser)}</strong>{" "}
|
||||
{t("importProfile.importedAsSuffix")}
|
||||
{t("importProfile.importedAs", {
|
||||
browser: getBrowserDisplayName(currentMappedBrowser),
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
|
||||
@@ -536,7 +536,11 @@ const TagsCell = React.memo<{
|
||||
onChange={(opts) => void handleChange(opts)}
|
||||
creatable
|
||||
selectFirstItem={false}
|
||||
placeholder={effectiveTags.length === 0 ? "Add tags" : ""}
|
||||
placeholder={
|
||||
effectiveTags.length === 0
|
||||
? translate("profileTable.addTagsPlaceholder")
|
||||
: ""
|
||||
}
|
||||
className={cn(
|
||||
"bg-transparent border-0! focus-within:ring-0!",
|
||||
"[&_div:first-child]:border-0! [&_div:first-child]:ring-0! [&_div:first-child]:focus-within:ring-0!",
|
||||
@@ -1846,6 +1850,7 @@ export function ProfilesDataTable({
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
size: 100,
|
||||
cell: ({ row, table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
const profile = row.original;
|
||||
@@ -1964,7 +1969,7 @@ export function ProfilesDataTable({
|
||||
size="sm"
|
||||
disabled={!canLaunch || isLaunching || isStopping}
|
||||
className={cn(
|
||||
"min-w-[70px] h-7",
|
||||
"min-w-[80px] h-7 px-3",
|
||||
!canLaunch && "opacity-50 cursor-not-allowed",
|
||||
canLaunch && "cursor-pointer",
|
||||
isFollower && "border-accent",
|
||||
@@ -1980,9 +1985,9 @@ export function ProfilesDataTable({
|
||||
<div className="w-3 h-3 rounded-full border border-current animate-spin border-t-transparent" />
|
||||
</div>
|
||||
) : isRunning ? (
|
||||
"Stop"
|
||||
meta.t("profiles.actions.stop")
|
||||
) : (
|
||||
"Launch"
|
||||
meta.t("profiles.actions.launch")
|
||||
)}
|
||||
</RippleButton>
|
||||
</span>
|
||||
@@ -1999,7 +2004,9 @@ export function ProfilesDataTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
size: 130,
|
||||
header: ({ column, table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -2008,7 +2015,7 @@ export function ProfilesDataTable({
|
||||
}}
|
||||
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
|
||||
>
|
||||
Name
|
||||
{meta.t("common.labels.name")}
|
||||
{column.getIsSorted() === "asc" ? (
|
||||
<LuChevronUp className="ml-2 w-4 h-4" />
|
||||
) : column.getIsSorted() === "desc" ? (
|
||||
@@ -2137,7 +2144,11 @@ export function ProfilesDataTable({
|
||||
},
|
||||
{
|
||||
id: "tags",
|
||||
header: "Tags",
|
||||
size: 110,
|
||||
header: ({ table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
return meta.t("profileTable.tagsHeader");
|
||||
},
|
||||
cell: ({ row, table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
const profile = row.original;
|
||||
@@ -2166,7 +2177,11 @@ export function ProfilesDataTable({
|
||||
},
|
||||
{
|
||||
id: "note",
|
||||
header: "Note",
|
||||
size: 110,
|
||||
header: ({ table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
return meta.t("profileTable.noteHeader");
|
||||
},
|
||||
cell: ({ row, table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
const profile = row.original;
|
||||
@@ -2193,7 +2208,11 @@ export function ProfilesDataTable({
|
||||
},
|
||||
{
|
||||
id: "proxy",
|
||||
header: "Proxy / VPN",
|
||||
size: 130,
|
||||
header: ({ table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
return meta.t("profiles.table.proxy");
|
||||
},
|
||||
cell: ({ row, table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
const profile = row.original;
|
||||
@@ -2231,7 +2250,7 @@ export function ProfilesDataTable({
|
||||
? effectiveVpn.name
|
||||
: effectiveProxy
|
||||
? effectiveProxy.name
|
||||
: "Not Selected";
|
||||
: meta.t("profiles.table.notSelected");
|
||||
const vpnBadge = effectiveVpn ? "WG" : null;
|
||||
const tooltipText = hasAssignment ? displayName : null;
|
||||
const isSelectorOpen = meta.openProxySelectorFor === profile.id;
|
||||
@@ -2372,7 +2391,7 @@ export function ProfilesDataTable({
|
||||
))}
|
||||
</CommandGroup>
|
||||
{meta.vpnConfigs.length > 0 && (
|
||||
<CommandGroup heading="VPNs">
|
||||
<CommandGroup heading={t("profileTable.vpnsHeading")}>
|
||||
{meta.vpnConfigs.map((vpn) => (
|
||||
<CommandItem
|
||||
key={vpn.id}
|
||||
@@ -2405,7 +2424,9 @@ export function ProfilesDataTable({
|
||||
)}
|
||||
{meta.canCreateLocationProxy &&
|
||||
meta.countries.length > 0 && (
|
||||
<CommandGroup heading="Create by country">
|
||||
<CommandGroup
|
||||
heading={t("profileTable.createByCountryHeading")}
|
||||
>
|
||||
{meta.countries
|
||||
.filter(
|
||||
(c) =>
|
||||
@@ -2569,7 +2590,7 @@ export function ProfilesDataTable({
|
||||
platform === "macos" ? "h-[340px]" : "h-[280px]",
|
||||
)}
|
||||
>
|
||||
<Table className="overflow-visible">
|
||||
<Table className="overflow-visible table-fixed">
|
||||
<TableHeader className="overflow-visible">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="overflow-visible">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"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 { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -50,7 +50,15 @@ export function ProfileSelectorDialog({
|
||||
}: ProfileSelectorDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
// Use the centralized profile events hook
|
||||
const { profiles, runningProfiles: hookRunningProfiles } = useProfileEvents();
|
||||
const { profiles: rawProfiles, runningProfiles: hookRunningProfiles } =
|
||||
useProfileEvents();
|
||||
const profiles = useMemo(
|
||||
() =>
|
||||
[...rawProfiles].sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
|
||||
),
|
||||
[rawProfiles],
|
||||
);
|
||||
|
||||
// Use external runningProfiles if provided, otherwise use hook's runningProfiles
|
||||
const runningProfiles = externalRunningProfiles ?? hookRunningProfiles;
|
||||
@@ -148,11 +156,7 @@ export function ProfileSelectorDialog({
|
||||
if (runningAvailableProfile) {
|
||||
setSelectedProfile(runningAvailableProfile.name);
|
||||
} else {
|
||||
// Sort profiles by name and select first
|
||||
const sortedProfiles = [...profiles].sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
setSelectedProfile(sortedProfiles[0].name);
|
||||
setSelectedProfile(profiles[0].name);
|
||||
}
|
||||
}
|
||||
}, [isOpen, profiles, selectedProfile, runningProfiles]);
|
||||
|
||||
@@ -166,7 +166,7 @@ export function ProfileSyncDialog({
|
||||
}, [profile, hasConfig, onSyncConfigOpen, onClose, t]);
|
||||
|
||||
const formatLastSync = (timestamp?: number) => {
|
||||
if (!timestamp) return t("common.labels.never", "Never");
|
||||
if (!timestamp) return t("common.labels.never");
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
@@ -177,7 +177,7 @@ export function ProfileSyncDialog({
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("sync.mode.title", "Profile Sync")}</DialogTitle>
|
||||
<DialogTitle>{t("sync.mode.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("sync.mode.description", {
|
||||
name: profile.name,
|
||||
@@ -194,9 +194,7 @@ export function ProfileSyncDialog({
|
||||
<div className="grid gap-4 py-4">
|
||||
{!hasConfig && (
|
||||
<div className="p-3 text-sm rounded-md bg-muted">
|
||||
<p className="mb-2">
|
||||
{t("sync.mode.notConfigured", "Sync service not configured.")}
|
||||
</p>
|
||||
<p className="mb-2">{t("sync.mode.notConfigured")}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -205,7 +203,7 @@ export function ProfileSyncDialog({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t("sync.mode.configureService", "Configure Sync Service")}
|
||||
{t("sync.mode.configureService")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -222,13 +220,10 @@ export function ProfileSyncDialog({
|
||||
<RadioGroupItem value="Disabled" id="sync-disabled" />
|
||||
<Label htmlFor="sync-disabled" className="cursor-pointer">
|
||||
<span className="font-medium">
|
||||
{t("sync.mode.disabled", "Disabled")}
|
||||
{t("sync.mode.disabled")}
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"sync.mode.disabledDescription",
|
||||
"No sync for this profile",
|
||||
)}
|
||||
{t("sync.mode.disabledDescription")}
|
||||
</p>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -237,13 +232,10 @@ export function ProfileSyncDialog({
|
||||
<RadioGroupItem value="Regular" id="sync-regular" />
|
||||
<Label htmlFor="sync-regular" className="cursor-pointer">
|
||||
<span className="font-medium">
|
||||
{t("sync.mode.regular", "Regular Sync")}
|
||||
{t("sync.mode.regular")}
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"sync.mode.regularDescription",
|
||||
"Fast sync, unencrypted",
|
||||
)}
|
||||
{t("sync.mode.regularDescription")}
|
||||
</p>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -263,18 +255,12 @@ export function ProfileSyncDialog({
|
||||
}
|
||||
>
|
||||
<span className="font-medium">
|
||||
{t("sync.mode.encrypted", "E2E Encrypted Sync")}
|
||||
{t("sync.mode.encrypted")}
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{canUseEncryption
|
||||
? t(
|
||||
"sync.mode.encryptedDescription",
|
||||
"Encrypted before upload. Server never sees plaintext data.",
|
||||
)
|
||||
: t(
|
||||
"settings.encryption.requiresProOrOwner",
|
||||
"Profile encryption is available for Pro users and team owners.",
|
||||
)}
|
||||
? t("sync.mode.encryptedDescription")
|
||||
: t("settings.encryption.requiresProOrOwner")}
|
||||
</p>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -284,15 +270,12 @@ export function ProfileSyncDialog({
|
||||
!hasE2ePassword &&
|
||||
userChangedMode && (
|
||||
<div className="p-3 text-sm rounded-md bg-destructive/10 text-destructive">
|
||||
{t(
|
||||
"sync.mode.noPasswordWarning",
|
||||
"E2E password not set. Please set a password in Settings.",
|
||||
)}
|
||||
{t("sync.mode.noPasswordWarning")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("sync.mode.lastSynced", "Last Synced")}</Label>
|
||||
<Label>{t("sync.mode.lastSynced")}</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Badge variant="outline">
|
||||
{formatLastSync(profile.last_sync)}
|
||||
@@ -319,7 +302,7 @@ export function ProfileSyncDialog({
|
||||
</Button>
|
||||
{hasConfig && isSyncEnabled(profile) && (
|
||||
<LoadingButton onClick={handleSyncNow} isLoading={isSyncing}>
|
||||
{t("sync.mode.syncNow", "Sync Now")}
|
||||
{t("sync.mode.syncNow")}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
@@ -392,7 +392,7 @@ export function ProxyManagementDialog({
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
|
||||
<DialogContent className="max-w-[min(95vw,1600px)] max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("proxies.management.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -411,7 +411,7 @@ export function ProxyManagementDialog({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="proxies">
|
||||
<TabsContent value="proxies" className="mt-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
@@ -460,196 +460,188 @@ export function ProxyManagementDialog({
|
||||
{t("proxies.management.noneCreated")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-md">
|
||||
<ScrollArea className="h-[240px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("common.labels.name")}</TableHead>
|
||||
<TableHead className="w-20">
|
||||
{t("proxies.management.usage")}
|
||||
</TableHead>
|
||||
<TableHead className="w-24">
|
||||
{t("proxies.management.syncCol")}
|
||||
</TableHead>
|
||||
<TableHead className="w-24">
|
||||
{t("common.labels.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{storedProxies.map((proxy) => {
|
||||
const syncDot = getSyncStatusDot(
|
||||
proxy,
|
||||
proxySyncStatus[proxy.id],
|
||||
t,
|
||||
proxySyncErrors[proxy.id],
|
||||
);
|
||||
return (
|
||||
<TableRow key={proxy.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
|
||||
syncDot.animate
|
||||
? "animate-pulse"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{syncDot.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{proxy.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{proxyUsage[proxy.id] ?? 0}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="border rounded-md max-h-[240px] overflow-auto">
|
||||
<Table className="min-w-max">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("common.labels.name")}</TableHead>
|
||||
<TableHead className="whitespace-nowrap w-px">
|
||||
{t("proxies.management.usage")}
|
||||
</TableHead>
|
||||
<TableHead className="whitespace-nowrap w-px">
|
||||
{t("proxies.management.syncCol")}
|
||||
</TableHead>
|
||||
<TableHead className="whitespace-nowrap w-px">
|
||||
{t("common.labels.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{storedProxies.map((proxy) => {
|
||||
const syncDot = getSyncStatusDot(
|
||||
proxy,
|
||||
proxySyncStatus[proxy.id],
|
||||
t,
|
||||
proxySyncErrors[proxy.id],
|
||||
);
|
||||
return (
|
||||
<TableRow key={proxy.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={proxy.sync_enabled}
|
||||
onCheckedChange={() =>
|
||||
void handleToggleSync(proxy)
|
||||
}
|
||||
disabled={
|
||||
isTogglingSync[proxy.id] ||
|
||||
proxyInUse[proxy.id]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
|
||||
syncDot.animate
|
||||
? "animate-pulse"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{proxyInUse[proxy.id] ? (
|
||||
<p>
|
||||
{t(
|
||||
"proxies.management.syncCannotDisable",
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{proxy.sync_enabled
|
||||
? t(
|
||||
"proxies.management.disableSync",
|
||||
)
|
||||
: t(
|
||||
"proxies.management.enableSync",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<p>{syncDot.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<ProxyCheckButton
|
||||
proxy={proxy}
|
||||
profileId={proxy.id}
|
||||
checkingProfileId={checkingProxyId}
|
||||
cachedResult={
|
||||
proxyCheckResults[proxy.id]
|
||||
}
|
||||
setCheckingProfileId={
|
||||
setCheckingProxyId
|
||||
}
|
||||
onCheckComplete={(result) => {
|
||||
setProxyCheckResults((prev) => ({
|
||||
...prev,
|
||||
[proxy.id]: result,
|
||||
}));
|
||||
}}
|
||||
onCheckFailed={(result) => {
|
||||
setProxyCheckResults((prev) => ({
|
||||
...prev,
|
||||
[proxy.id]: result,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{proxy.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{proxyUsage[proxy.id] ?? 0}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={proxy.sync_enabled}
|
||||
onCheckedChange={() =>
|
||||
void handleToggleSync(proxy)
|
||||
}
|
||||
disabled={
|
||||
isTogglingSync[proxy.id] ||
|
||||
proxyInUse[proxy.id]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{proxyInUse[proxy.id] ? (
|
||||
<p>
|
||||
{t(
|
||||
"proxies.management.syncCannotDisable",
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{proxy.sync_enabled
|
||||
? t(
|
||||
"proxies.management.disableSync",
|
||||
)
|
||||
: t(
|
||||
"proxies.management.enableSync",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<ProxyCheckButton
|
||||
proxy={proxy}
|
||||
profileId={proxy.id}
|
||||
checkingProfileId={checkingProxyId}
|
||||
cachedResult={proxyCheckResults[proxy.id]}
|
||||
setCheckingProfileId={setCheckingProxyId}
|
||||
onCheckComplete={(result) => {
|
||||
setProxyCheckResults((prev) => ({
|
||||
...prev,
|
||||
[proxy.id]: result,
|
||||
}));
|
||||
}}
|
||||
onCheckFailed={(result) => {
|
||||
setProxyCheckResults((prev) => ({
|
||||
...prev,
|
||||
[proxy.id]: result,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleEditProxy(proxy);
|
||||
}}
|
||||
>
|
||||
<LuPencil className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{t("proxies.management.editProxy")}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleEditProxy(proxy);
|
||||
handleDeleteProxy(proxy);
|
||||
}}
|
||||
disabled={
|
||||
(proxyUsage[proxy.id] ?? 0) > 0
|
||||
}
|
||||
>
|
||||
<LuPencil className="w-4 h-4" />
|
||||
<LuTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{(proxyUsage[proxy.id] ?? 0) > 0 ? (
|
||||
<p>
|
||||
{t("proxies.management.editProxy")}
|
||||
{(proxyUsage[proxy.id] ?? 0) === 1
|
||||
? t(
|
||||
"proxies.management.cannotDelete_one",
|
||||
{
|
||||
count: proxyUsage[proxy.id],
|
||||
},
|
||||
)
|
||||
: t(
|
||||
"proxies.management.cannotDelete_other",
|
||||
{
|
||||
count: proxyUsage[proxy.id],
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleDeleteProxy(proxy);
|
||||
}}
|
||||
disabled={
|
||||
(proxyUsage[proxy.id] ?? 0) > 0
|
||||
}
|
||||
>
|
||||
<LuTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{(proxyUsage[proxy.id] ?? 0) > 0 ? (
|
||||
<p>
|
||||
{(proxyUsage[proxy.id] ?? 0) === 1
|
||||
? t(
|
||||
"proxies.management.cannotDelete_one",
|
||||
{
|
||||
count:
|
||||
proxyUsage[proxy.id],
|
||||
},
|
||||
)
|
||||
: t(
|
||||
"proxies.management.cannotDelete_other",
|
||||
{
|
||||
count:
|
||||
proxyUsage[proxy.id],
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{t(
|
||||
"proxies.management.deleteProxy",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<p>
|
||||
{t(
|
||||
"proxies.management.deleteProxy",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vpns">
|
||||
<TabsContent value="vpns" className="mt-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
@@ -684,169 +676,167 @@ export function ProxyManagementDialog({
|
||||
{t("vpns.management.noneCreated")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-md">
|
||||
<ScrollArea className="h-[240px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("common.labels.name")}</TableHead>
|
||||
<TableHead className="w-16">
|
||||
{t("common.labels.type")}
|
||||
</TableHead>
|
||||
<TableHead className="w-20">
|
||||
{t("proxies.management.usage")}
|
||||
</TableHead>
|
||||
<TableHead className="w-24">
|
||||
{t("proxies.management.syncCol")}
|
||||
</TableHead>
|
||||
<TableHead className="w-24">
|
||||
{t("common.labels.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{vpnConfigs.map((vpn) => {
|
||||
const syncDot = getSyncStatusDot(
|
||||
vpn,
|
||||
vpnSyncStatus[vpn.id],
|
||||
t,
|
||||
vpnSyncErrors[vpn.id],
|
||||
);
|
||||
return (
|
||||
<TableRow key={vpn.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
|
||||
syncDot.animate
|
||||
? "animate-pulse"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{syncDot.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{vpn.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">WG</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{vpnUsage[vpn.id] ?? 0}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="border rounded-md max-h-[240px] overflow-auto">
|
||||
<Table className="min-w-max">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("common.labels.name")}</TableHead>
|
||||
<TableHead className="whitespace-nowrap w-px">
|
||||
{t("common.labels.type")}
|
||||
</TableHead>
|
||||
<TableHead className="whitespace-nowrap w-px">
|
||||
{t("proxies.management.usage")}
|
||||
</TableHead>
|
||||
<TableHead className="whitespace-nowrap w-px">
|
||||
{t("proxies.management.syncCol")}
|
||||
</TableHead>
|
||||
<TableHead className="whitespace-nowrap w-px">
|
||||
{t("common.labels.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{vpnConfigs.map((vpn) => {
|
||||
const syncDot = getSyncStatusDot(
|
||||
vpn,
|
||||
vpnSyncStatus[vpn.id],
|
||||
t,
|
||||
vpnSyncErrors[vpn.id],
|
||||
);
|
||||
return (
|
||||
<TableRow key={vpn.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={vpn.sync_enabled}
|
||||
onCheckedChange={() =>
|
||||
void handleToggleVpnSync(vpn)
|
||||
}
|
||||
disabled={
|
||||
isTogglingVpnSync[vpn.id] ||
|
||||
vpnInUse[vpn.id]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
|
||||
syncDot.animate
|
||||
? "animate-pulse"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{vpnInUse[vpn.id] ? (
|
||||
<p>
|
||||
{t(
|
||||
"vpns.management.syncCannotDisable",
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{vpn.sync_enabled
|
||||
? t(
|
||||
"proxies.management.disableSync",
|
||||
)
|
||||
: t(
|
||||
"proxies.management.enableSync",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<p>{syncDot.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<VpnCheckButton
|
||||
vpnId={vpn.id}
|
||||
vpnName={vpn.name}
|
||||
checkingVpnId={checkingVpnId}
|
||||
setCheckingVpnId={setCheckingVpnId}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{vpn.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">WG</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{vpnUsage[vpn.id] ?? 0}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={vpn.sync_enabled}
|
||||
onCheckedChange={() =>
|
||||
void handleToggleVpnSync(vpn)
|
||||
}
|
||||
disabled={
|
||||
isTogglingVpnSync[vpn.id] ||
|
||||
vpnInUse[vpn.id]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{vpnInUse[vpn.id] ? (
|
||||
<p>
|
||||
{t(
|
||||
"vpns.management.syncCannotDisable",
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{vpn.sync_enabled
|
||||
? t(
|
||||
"proxies.management.disableSync",
|
||||
)
|
||||
: t(
|
||||
"proxies.management.enableSync",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<VpnCheckButton
|
||||
vpnId={vpn.id}
|
||||
vpnName={vpn.name}
|
||||
checkingVpnId={checkingVpnId}
|
||||
setCheckingVpnId={setCheckingVpnId}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleEditVpn(vpn);
|
||||
}}
|
||||
>
|
||||
<LuPencil className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t("vpns.management.editVpn")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleEditVpn(vpn);
|
||||
handleDeleteVpn(vpn);
|
||||
}}
|
||||
disabled={
|
||||
(vpnUsage[vpn.id] ?? 0) > 0
|
||||
}
|
||||
>
|
||||
<LuPencil className="w-4 h-4" />
|
||||
<LuTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t("vpns.management.editVpn")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleDeleteVpn(vpn);
|
||||
}}
|
||||
disabled={
|
||||
(vpnUsage[vpn.id] ?? 0) > 0
|
||||
}
|
||||
>
|
||||
<LuTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{(vpnUsage[vpn.id] ?? 0) > 0 ? (
|
||||
<p>
|
||||
{(vpnUsage[vpn.id] ?? 0) === 1
|
||||
? t(
|
||||
"vpns.management.cannotDelete_one",
|
||||
{ count: vpnUsage[vpn.id] },
|
||||
)
|
||||
: t(
|
||||
"vpns.management.cannotDelete_other",
|
||||
{ count: vpnUsage[vpn.id] },
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{t("vpns.management.deleteVpn")}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{(vpnUsage[vpn.id] ?? 0) > 0 ? (
|
||||
<p>
|
||||
{(vpnUsage[vpn.id] ?? 0) === 1
|
||||
? t(
|
||||
"vpns.management.cannotDelete_one",
|
||||
{ count: vpnUsage[vpn.id] },
|
||||
)
|
||||
: t(
|
||||
"vpns.management.cannotDelete_other",
|
||||
{ count: vpnUsage[vpn.id] },
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{t("vpns.management.deleteVpn")}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -811,7 +811,7 @@ export function SettingsDialog({
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose your preferred language for the application interface.
|
||||
{t("settings.language.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -820,10 +820,12 @@ export function SettingsDialog({
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label className="text-base font-medium">
|
||||
Default Browser
|
||||
{t("settings.defaultBrowser.title")}
|
||||
</Label>
|
||||
<Badge variant={isDefaultBrowser ? "default" : "secondary"}>
|
||||
{isDefaultBrowser ? "Active" : "Inactive"}
|
||||
{isDefaultBrowser
|
||||
? t("common.status.active")
|
||||
: t("common.status.inactive")}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -839,13 +841,12 @@ export function SettingsDialog({
|
||||
className="w-full"
|
||||
>
|
||||
{isDefaultBrowser
|
||||
? "Already Default Browser"
|
||||
: "Set as Default Browser"}
|
||||
? t("settings.defaultBrowser.alreadyDefault")
|
||||
: t("settings.defaultBrowser.setAsDefault")}
|
||||
</LoadingButton>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When set as default, Donut Browser will handle web links and
|
||||
allow you to choose which profile to use.
|
||||
{t("settings.defaultBrowser.description")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -854,12 +855,12 @@ export function SettingsDialog({
|
||||
{isMacOS && (
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">
|
||||
System Permissions
|
||||
{t("settings.permissions.title")}
|
||||
</Label>
|
||||
|
||||
{isLoadingPermissions ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading permissions...
|
||||
{t("settings.permissions.loading")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -928,7 +929,7 @@ export function SettingsDialog({
|
||||
className="w-full"
|
||||
onClick={onIntegrationsOpen}
|
||||
>
|
||||
Open Integrations Settings
|
||||
{t("integrations.openSettings")}
|
||||
</RippleButton>
|
||||
</div>
|
||||
|
||||
@@ -952,33 +953,24 @@ export function SettingsDialog({
|
||||
{/* Sync Encryption Section */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">
|
||||
{t("settings.encryption.title", "Sync Encryption")}
|
||||
{t("settings.encryption.title")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"settings.encryption.description",
|
||||
"Set a password to enable E2E encrypted sync. If you lose this password, encrypted profiles cannot be recovered.",
|
||||
)}
|
||||
{t("settings.encryption.description")}
|
||||
</p>
|
||||
|
||||
{!canUseEncryption ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"settings.encryption.requiresProOrOwner",
|
||||
"Profile encryption is available for Pro users and team owners.",
|
||||
)}
|
||||
{t("settings.encryption.requiresProOrOwner")}
|
||||
</p>
|
||||
) : hasE2ePassword ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="default">
|
||||
{t("settings.encryption.passwordSet", "Active")}
|
||||
{t("settings.encryption.passwordSet")}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"settings.encryption.passwordSetDescription",
|
||||
"E2E encryption password is set",
|
||||
)}
|
||||
{t("settings.encryption.passwordSetDescription")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -992,10 +984,7 @@ export function SettingsDialog({
|
||||
setE2eError("");
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
"settings.encryption.changePassword",
|
||||
"Change Password",
|
||||
)}
|
||||
{t("settings.encryption.changePassword")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -1004,21 +993,13 @@ export function SettingsDialog({
|
||||
try {
|
||||
await invoke("delete_e2e_password");
|
||||
setHasE2ePassword(false);
|
||||
showSuccessToast(
|
||||
t(
|
||||
"settings.encryption.removed",
|
||||
"Encryption password removed",
|
||||
),
|
||||
);
|
||||
showSuccessToast(t("settings.encryption.removed"));
|
||||
} catch (error) {
|
||||
showErrorToast(String(error));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
"settings.encryption.removePassword",
|
||||
"Remove Password",
|
||||
)}
|
||||
{t("settings.encryption.removePassword")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1026,10 +1007,7 @@ export function SettingsDialog({
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t(
|
||||
"settings.encryption.passwordPlaceholder",
|
||||
"Password (min 8 characters)",
|
||||
)}
|
||||
placeholder={t("settings.encryption.passwordPlaceholder")}
|
||||
value={e2ePassword}
|
||||
onChange={(e) => {
|
||||
setE2ePassword(e.target.value);
|
||||
@@ -1038,10 +1016,7 @@ export function SettingsDialog({
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t(
|
||||
"settings.encryption.confirmPlaceholder",
|
||||
"Confirm password",
|
||||
)}
|
||||
placeholder={t("settings.encryption.confirmPlaceholder")}
|
||||
value={e2ePasswordConfirm}
|
||||
onChange={(e) => {
|
||||
setE2ePasswordConfirm(e.target.value);
|
||||
@@ -1057,21 +1032,11 @@ export function SettingsDialog({
|
||||
isLoading={isSavingE2e}
|
||||
onClick={async () => {
|
||||
if (e2ePassword.length < 8) {
|
||||
setE2eError(
|
||||
t(
|
||||
"settings.encryption.passwordTooShort",
|
||||
"Password must be at least 8 characters",
|
||||
),
|
||||
);
|
||||
setE2eError(t("settings.encryption.passwordTooShort"));
|
||||
return;
|
||||
}
|
||||
if (e2ePassword !== e2ePasswordConfirm) {
|
||||
setE2eError(
|
||||
t(
|
||||
"settings.encryption.passwordMismatch",
|
||||
"Passwords do not match",
|
||||
),
|
||||
);
|
||||
setE2eError(t("settings.encryption.passwordMismatch"));
|
||||
return;
|
||||
}
|
||||
setIsSavingE2e(true);
|
||||
@@ -1083,10 +1048,7 @@ export function SettingsDialog({
|
||||
setE2ePassword("");
|
||||
setE2ePasswordConfirm("");
|
||||
showSuccessToast(
|
||||
t(
|
||||
"settings.encryption.passwordSaved",
|
||||
"Encryption password set",
|
||||
),
|
||||
t("settings.encryption.passwordSaved"),
|
||||
);
|
||||
} catch (error) {
|
||||
showErrorToast(String(error));
|
||||
@@ -1095,7 +1057,7 @@ export function SettingsDialog({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("settings.encryption.setPassword", "Set Password")}
|
||||
{t("settings.encryption.setPassword")}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
)}
|
||||
@@ -1172,13 +1134,11 @@ export function SettingsDialog({
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Clear All Version Cache
|
||||
{t("settings.advanced.clearCache")}
|
||||
</LoadingButton>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Clear all cached browser version data and refresh all browser
|
||||
versions from their sources. This will force a fresh download of
|
||||
version information for all browsers.
|
||||
{t("settings.advanced.clearCacheDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1194,7 +1154,7 @@ export function SettingsDialog({
|
||||
|
||||
<DialogFooter className="shrink-0">
|
||||
<RippleButton variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("common.buttons.cancel")}
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isSaving}
|
||||
@@ -1205,7 +1165,7 @@ export function SettingsDialog({
|
||||
}}
|
||||
disabled={isLoading || !hasChanges}
|
||||
>
|
||||
Save Settings
|
||||
{t("common.buttons.saveSettings")}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -452,7 +452,11 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
|
||||
setShowToken(!showToken);
|
||||
}}
|
||||
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
|
||||
aria-label={showToken ? "Hide token" : "Show token"}
|
||||
aria-label={
|
||||
showToken
|
||||
? t("common.aria.hideToken")
|
||||
: t("common.aria.showToken")
|
||||
}
|
||||
>
|
||||
{showToken ? (
|
||||
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuCheck, LuCopy } from "react-icons/lu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { showSuccessToast } from "@/lib/toast-utils";
|
||||
@@ -26,6 +27,7 @@ export function CopyToClipboard({
|
||||
className,
|
||||
successMessage = "Copied to clipboard",
|
||||
}: CopyToClipboardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
@@ -47,9 +49,11 @@ export function CopyToClipboard({
|
||||
size={size}
|
||||
className={`relative ${className ?? ""}`}
|
||||
onClick={copyToClipboard}
|
||||
aria-label={copied ? "Copied" : "Copy to clipboard"}
|
||||
aria-label={copied ? t("common.aria.copied") : t("common.aria.copy")}
|
||||
>
|
||||
<span className="sr-only">{copied ? "Copied" : "Copy"}</span>
|
||||
<span className="sr-only">
|
||||
{copied ? t("common.srOnly.copied") : t("common.srOnly.copy")}
|
||||
</span>
|
||||
<LuCopy
|
||||
className={`h-4 w-4 transition-all duration-300 ${
|
||||
copied ? "scale-0" : "scale-100"
|
||||
|
||||
@@ -160,7 +160,7 @@ function DialogContent({
|
||||
}}
|
||||
transition={transition}
|
||||
className={cn(
|
||||
"bg-background fixed top-[50%] left-[50%] z-10000 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg",
|
||||
"bg-background fixed top-[50%] left-[50%] z-10000 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
+47
-14
@@ -60,7 +60,8 @@
|
||||
"optional": "Optional",
|
||||
"required": "Required",
|
||||
"unknownProfile": "Unknown",
|
||||
"mode": "Mode"
|
||||
"mode": "Mode",
|
||||
"never": "Never"
|
||||
},
|
||||
"time": {
|
||||
"days": "days",
|
||||
@@ -72,7 +73,11 @@
|
||||
"aria": {
|
||||
"selectAll": "Select all",
|
||||
"selectRow": "Select row",
|
||||
"selectProfile": "Select profile"
|
||||
"selectProfile": "Select profile",
|
||||
"copy": "Copy to clipboard",
|
||||
"copied": "Copied",
|
||||
"showToken": "Show token",
|
||||
"hideToken": "Hide token"
|
||||
},
|
||||
"keys": {
|
||||
"escape": "Escape"
|
||||
@@ -87,7 +92,11 @@
|
||||
"title": "Command Palette",
|
||||
"description": "Search for a command to run..."
|
||||
},
|
||||
"noResults": "No results found."
|
||||
"noResults": "No results found.",
|
||||
"srOnly": {
|
||||
"copy": "Copy",
|
||||
"copied": "Copied"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -196,7 +205,8 @@
|
||||
"group": "Group",
|
||||
"proxy": "Proxy / VPN",
|
||||
"lastLaunch": "Last Launch",
|
||||
"empty": "No profiles found."
|
||||
"empty": "No profiles found.",
|
||||
"notSelected": "Not Selected"
|
||||
},
|
||||
"actions": {
|
||||
"launch": "Launch",
|
||||
@@ -488,7 +498,8 @@
|
||||
"deleteGroupAndProfiles": "Delete Group & Profiles",
|
||||
"loadProfilesFailed": "Failed to load profiles",
|
||||
"unknownGroup": "Unknown Group",
|
||||
"profileGroupsAriaLabel": "Profile groups"
|
||||
"profileGroupsAriaLabel": "Profile groups",
|
||||
"loading": "Loading groups..."
|
||||
},
|
||||
"sync": {
|
||||
"mode": {
|
||||
@@ -631,7 +642,8 @@
|
||||
"mcpAcceptTermsFirst": "(Accept Wayfern terms in Settings first)",
|
||||
"mcpStarted": "MCP server started on port {{port}}",
|
||||
"mcpStopped": "MCP server stopped",
|
||||
"mcpToggleFailed": "Failed to toggle MCP server"
|
||||
"mcpToggleFailed": "Failed to toggle MCP server",
|
||||
"openSettings": "Open Integrations Settings"
|
||||
},
|
||||
"import": {
|
||||
"title": "Import Profile",
|
||||
@@ -711,6 +723,10 @@
|
||||
"webrtc": "Block WebRTC",
|
||||
"webgl": "Block WebGL"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"browserBehavior": "Browser Behavior",
|
||||
"allowAddonsOpenTabs": "Allow browser addons to open new tabs automatically"
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
@@ -875,7 +891,8 @@
|
||||
"loadProxiesFailed": "Failed to load proxies: {{error}}",
|
||||
"setupProxyListenersFailed": "Failed to setup proxy event listeners: {{error}}",
|
||||
"loadVpnConfigsFailed": "Failed to load VPN configs: {{error}}",
|
||||
"setupVpnListenersFailed": "Failed to setup VPN event listeners: {{error}}"
|
||||
"setupVpnListenersFailed": "Failed to setup VPN event listeners: {{error}}",
|
||||
"themeNotFound": "Tokyo Night theme not found"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
@@ -1124,7 +1141,9 @@
|
||||
"syncEnabled": "Sync enabled",
|
||||
"syncDisabled": "Sync disabled",
|
||||
"syncEnableTooltip": "Enable sync",
|
||||
"syncDisableTooltip": "Disable sync"
|
||||
"syncDisableTooltip": "Disable sync",
|
||||
"loadGroupsFailed": "Failed to load extension groups",
|
||||
"assignGroupFailed": "Failed to assign extension group"
|
||||
},
|
||||
"pro": {
|
||||
"badge": "PRO",
|
||||
@@ -1256,12 +1275,11 @@
|
||||
"importedSuccess": "Successfully imported profile \"{{name}}\"",
|
||||
"notInstalled": "{{browser}} is not installed. Please download {{browser}} first from the main window, then try importing again.",
|
||||
"importFailed": "Failed to import profile: {{error}}",
|
||||
"importedAsPrefix": "This profile will be imported as a",
|
||||
"importedAsSuffix": "profile.",
|
||||
"proxyOptional": "Proxy (Optional)",
|
||||
"noProxy": "No proxy",
|
||||
"nextButton": "Next",
|
||||
"importButton": "Import"
|
||||
"importButton": "Import",
|
||||
"importedAs": "This profile will be imported as a {{browser}} profile."
|
||||
},
|
||||
"syncTooltips": {
|
||||
"syncing": "Syncing...",
|
||||
@@ -1503,7 +1521,12 @@
|
||||
"syncTooltipNotSynced": "Not synced",
|
||||
"noTags": "No tags",
|
||||
"syncTooltipCloseToSync": "Close the profile to sync",
|
||||
"syncTooltipDisabledWithLast": "Sync disabled, last sync {{time}}"
|
||||
"syncTooltipDisabledWithLast": "Sync disabled, last sync {{time}}",
|
||||
"addTagsPlaceholder": "Add tags",
|
||||
"tagsHeader": "Tags",
|
||||
"noteHeader": "Note",
|
||||
"vpnsHeading": "VPNs",
|
||||
"createByCountryHeading": "Create by country"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "No release types available.",
|
||||
@@ -1521,7 +1544,14 @@
|
||||
"appUpdate": {
|
||||
"toast": {
|
||||
"updateFailed": "Failed to update Donut Browser",
|
||||
"restartFailed": "Failed to restart"
|
||||
"restartFailed": "Failed to restart",
|
||||
"updateReady": "Update ready, restart to apply",
|
||||
"manualDownloadRequired": "Manual download required",
|
||||
"restartNow": "Restart Now",
|
||||
"viewRelease": "View Release",
|
||||
"later": "Later",
|
||||
"uploading": "Uploading",
|
||||
"downloading": "Downloading"
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1532,7 +1562,10 @@
|
||||
"downloadFailed": "Failed to download {{browser}} {{version}}",
|
||||
"calculating": "calculating...",
|
||||
"extractionFailed": "{{browser}} {{version}}: extraction failed",
|
||||
"extractionFailedDescription": "The corrupt file was deleted. It will be re-downloaded on next attempt."
|
||||
"extractionFailedDescription": "The corrupt file was deleted. It will be re-downloaded on next attempt.",
|
||||
"extracting": "Extracting browser files... Please do not close the app.",
|
||||
"verifying": "Verifying browser files...",
|
||||
"downloadingRolling": "Downloading rolling release build..."
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
|
||||
+61
-28
@@ -60,7 +60,8 @@
|
||||
"optional": "Opcional",
|
||||
"required": "Requerido",
|
||||
"unknownProfile": "Desconocido",
|
||||
"mode": "Modo"
|
||||
"mode": "Modo",
|
||||
"never": "Nunca"
|
||||
},
|
||||
"time": {
|
||||
"days": "días",
|
||||
@@ -72,7 +73,11 @@
|
||||
"aria": {
|
||||
"selectAll": "Seleccionar todo",
|
||||
"selectRow": "Seleccionar fila",
|
||||
"selectProfile": "Seleccionar perfil"
|
||||
"selectProfile": "Seleccionar perfil",
|
||||
"copy": "Copiar al portapapeles",
|
||||
"copied": "Copiado",
|
||||
"showToken": "Mostrar token",
|
||||
"hideToken": "Ocultar token"
|
||||
},
|
||||
"keys": {
|
||||
"escape": "Escape"
|
||||
@@ -87,7 +92,11 @@
|
||||
"title": "Paleta de comandos",
|
||||
"description": "Busca un comando para ejecutar..."
|
||||
},
|
||||
"noResults": "No se encontraron resultados."
|
||||
"noResults": "No se encontraron resultados.",
|
||||
"srOnly": {
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
@@ -196,7 +205,8 @@
|
||||
"group": "Grupo",
|
||||
"proxy": "Proxy / VPN",
|
||||
"lastLaunch": "Último Inicio",
|
||||
"empty": "No se encontraron perfiles."
|
||||
"empty": "No se encontraron perfiles.",
|
||||
"notSelected": "No seleccionado"
|
||||
},
|
||||
"actions": {
|
||||
"launch": "Iniciar",
|
||||
@@ -488,7 +498,8 @@
|
||||
"deleteGroupAndProfiles": "Eliminar Grupo y Perfiles",
|
||||
"loadProfilesFailed": "Error al cargar los perfiles",
|
||||
"unknownGroup": "Grupo desconocido",
|
||||
"profileGroupsAriaLabel": "Grupos de perfiles"
|
||||
"profileGroupsAriaLabel": "Grupos de perfiles",
|
||||
"loading": "Cargando grupos..."
|
||||
},
|
||||
"sync": {
|
||||
"mode": {
|
||||
@@ -631,7 +642,8 @@
|
||||
"mcpAcceptTermsFirst": "(Acepta primero los términos de Wayfern en Configuración)",
|
||||
"mcpStarted": "Servidor MCP iniciado en puerto {{port}}",
|
||||
"mcpStopped": "Servidor MCP detenido",
|
||||
"mcpToggleFailed": "Error al alternar el servidor MCP"
|
||||
"mcpToggleFailed": "Error al alternar el servidor MCP",
|
||||
"openSettings": "Abrir configuración de integraciones"
|
||||
},
|
||||
"import": {
|
||||
"title": "Importar Perfil",
|
||||
@@ -711,6 +723,10 @@
|
||||
"webrtc": "Bloquear WebRTC",
|
||||
"webgl": "Bloquear WebGL"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"browserBehavior": "Comportamiento del navegador",
|
||||
"allowAddonsOpenTabs": "Permitir que los complementos abran nuevas pestañas automáticamente"
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
@@ -875,7 +891,8 @@
|
||||
"loadProxiesFailed": "Error al cargar los proxies: {{error}}",
|
||||
"setupProxyListenersFailed": "Error al configurar los listeners de eventos de proxies: {{error}}",
|
||||
"loadVpnConfigsFailed": "Error al cargar las configuraciones de VPN: {{error}}",
|
||||
"setupVpnListenersFailed": "Error al configurar los listeners de eventos de VPN: {{error}}"
|
||||
"setupVpnListenersFailed": "Error al configurar los listeners de eventos de VPN: {{error}}",
|
||||
"themeNotFound": "Tema Tokyo Night no encontrado"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
@@ -901,15 +918,15 @@
|
||||
"blockWebRTC": "Bloquear WebRTC",
|
||||
"blockWebGL": "Bloquear WebGL",
|
||||
"navigatorProperties": "Propiedades del navegador",
|
||||
"userAgent": "User Agent",
|
||||
"userAgent": "Agente de usuario",
|
||||
"userAgentAndPlatform": "User Agent y plataforma",
|
||||
"platform": "Plataforma",
|
||||
"platformVersion": "Versión de plataforma",
|
||||
"appVersion": "Versión de la aplicación",
|
||||
"osCpu": "OS CPU",
|
||||
"osCpu": "CPU del SO",
|
||||
"hardwareConcurrency": "Concurrencia de hardware",
|
||||
"maxTouchPoints": "Puntos táctiles máximos",
|
||||
"doNotTrack": "Do Not Track",
|
||||
"doNotTrack": "No rastrear",
|
||||
"selectDntPlaceholder": "Seleccionar valor DNT",
|
||||
"dntAllowed": "0 (rastreo permitido)",
|
||||
"dntNotAllowed": "1 (rastreo no permitido)",
|
||||
@@ -931,8 +948,8 @@
|
||||
"outerHeight": "Alto exterior",
|
||||
"innerWidth": "Ancho interior",
|
||||
"innerHeight": "Alto interior",
|
||||
"screenX": "Screen X",
|
||||
"screenY": "Screen Y",
|
||||
"screenX": "Pantalla X",
|
||||
"screenY": "Pantalla Y",
|
||||
"geolocation": "Geolocalización",
|
||||
"timezoneAndGeolocation": "Zona horaria y geolocalización",
|
||||
"timezoneGeolocationDescription": "Estos valores anulan las APIs de zona horaria y geolocalización del navegador.",
|
||||
@@ -946,15 +963,15 @@
|
||||
"region": "Región",
|
||||
"script": "Script",
|
||||
"webglProperties": "Propiedades de WebGL",
|
||||
"webglVendor": "WebGL Vendor",
|
||||
"webglRenderer": "WebGL Renderer",
|
||||
"webglVendor": "Proveedor WebGL",
|
||||
"webglRenderer": "Renderizador WebGL",
|
||||
"webglParameters": "Parámetros de WebGL",
|
||||
"webglParametersJson": "Parámetros de WebGL (JSON)",
|
||||
"webgl2Parameters": "Parámetros de WebGL2",
|
||||
"webglShaderPrecisionFormats": "Formatos de precisión de WebGL Shader",
|
||||
"webgl2ShaderPrecisionFormats": "Formatos de precisión de WebGL2 Shader",
|
||||
"webglShaderPrecisionFormats": "Formatos de precisión de shader WebGL",
|
||||
"webgl2ShaderPrecisionFormats": "Formatos de precisión de shader WebGL2",
|
||||
"canvasFingerprint": "Canvas Fingerprint",
|
||||
"canvasNoiseSeed": "Canvas Noise Seed",
|
||||
"canvasNoiseSeed": "Semilla de ruido de Canvas",
|
||||
"canvasNoiseSeedDescription": "Esta semilla se usa para generar una huella digital de Canvas consistente pero única. Cada perfil debe tener una semilla diferente.",
|
||||
"fonts": "Fuentes",
|
||||
"fontsJson": "Fuentes (JSON array)",
|
||||
@@ -975,8 +992,8 @@
|
||||
"maxChannelCount": "Número máximo de canales",
|
||||
"vendorInfo": "Información del proveedor",
|
||||
"vendor": "Proveedor",
|
||||
"vendorSub": "Vendor Sub",
|
||||
"productSub": "Product Sub",
|
||||
"vendorSub": "Proveedor Sub",
|
||||
"productSub": "Producto Sub",
|
||||
"brand": "Marca",
|
||||
"brandVersion": "Versión de marca",
|
||||
"proFeature": "Esta es una función Pro",
|
||||
@@ -1124,7 +1141,9 @@
|
||||
"syncEnabled": "Sincronización habilitada",
|
||||
"syncDisabled": "Sincronización deshabilitada",
|
||||
"syncEnableTooltip": "Habilitar sincronización",
|
||||
"syncDisableTooltip": "Deshabilitar sincronización"
|
||||
"syncDisableTooltip": "Deshabilitar sincronización",
|
||||
"loadGroupsFailed": "Error al cargar grupos de extensiones",
|
||||
"assignGroupFailed": "Error al asignar grupo de extensiones"
|
||||
},
|
||||
"pro": {
|
||||
"badge": "PRO",
|
||||
@@ -1256,12 +1275,11 @@
|
||||
"importedSuccess": "Perfil \"{{name}}\" importado correctamente",
|
||||
"notInstalled": "{{browser}} no está instalado. Por favor descarga {{browser}} primero desde la ventana principal y luego intenta importar de nuevo.",
|
||||
"importFailed": "Error al importar el perfil: {{error}}",
|
||||
"importedAsPrefix": "Este perfil se importará como un perfil de",
|
||||
"importedAsSuffix": ".",
|
||||
"proxyOptional": "Proxy (Opcional)",
|
||||
"noProxy": "Sin proxy",
|
||||
"nextButton": "Siguiente",
|
||||
"importButton": "Importar"
|
||||
"importButton": "Importar",
|
||||
"importedAs": "Este perfil se importará como un perfil de {{browser}}."
|
||||
},
|
||||
"syncTooltips": {
|
||||
"syncing": "Sincronizando...",
|
||||
@@ -1497,13 +1515,18 @@
|
||||
"syncTooltipSyncing": "Sincronizando...",
|
||||
"syncTooltipSyncedAt": "Sincronizado {{time}}",
|
||||
"syncTooltipSynced": "Sincronizado",
|
||||
"syncTooltipWaiting": "Esperando sincronización",
|
||||
"syncTooltipWaiting": "Esperando para sincronizar",
|
||||
"syncTooltipErrorWith": "Error de sincronización: {{error}}",
|
||||
"syncTooltipError": "Error de sincronización",
|
||||
"syncTooltipNotSynced": "Sin sincronizar",
|
||||
"syncTooltipNotSynced": "No sincronizado",
|
||||
"noTags": "Sin etiquetas",
|
||||
"syncTooltipCloseToSync": "Cierra el perfil para sincronizar",
|
||||
"syncTooltipDisabledWithLast": "Sincronización desactivada, última sincronización {{time}}"
|
||||
"syncTooltipDisabledWithLast": "Sincronización desactivada, última sincronización {{time}}",
|
||||
"addTagsPlaceholder": "Añadir etiquetas",
|
||||
"tagsHeader": "Etiquetas",
|
||||
"noteHeader": "Nota",
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "Crear por país"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "No hay tipos de versión disponibles.",
|
||||
@@ -1521,7 +1544,14 @@
|
||||
"appUpdate": {
|
||||
"toast": {
|
||||
"updateFailed": "Error al actualizar Donut Browser",
|
||||
"restartFailed": "Error al reiniciar"
|
||||
"restartFailed": "Error al reiniciar",
|
||||
"updateReady": "Actualización lista, reinicia para aplicar",
|
||||
"manualDownloadRequired": "Descarga manual requerida",
|
||||
"restartNow": "Reiniciar ahora",
|
||||
"viewRelease": "Ver lanzamiento",
|
||||
"later": "Más tarde",
|
||||
"uploading": "Subiendo",
|
||||
"downloading": "Descargando"
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1532,7 +1562,10 @@
|
||||
"downloadFailed": "Error al descargar {{browser}} {{version}}",
|
||||
"calculating": "calculando...",
|
||||
"extractionFailed": "{{browser}} {{version}}: error de extracción",
|
||||
"extractionFailedDescription": "El archivo dañado fue eliminado. Se volverá a descargar en el próximo intento."
|
||||
"extractionFailedDescription": "El archivo dañado fue eliminado. Se volverá a descargar en el próximo intento.",
|
||||
"extracting": "Extrayendo archivos del navegador... No cierre la aplicación.",
|
||||
"verifying": "Verificando archivos del navegador...",
|
||||
"downloadingRolling": "Descargando compilación rolling release..."
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
|
||||
+60
-27
@@ -60,7 +60,8 @@
|
||||
"optional": "Optionnel",
|
||||
"required": "Requis",
|
||||
"unknownProfile": "Inconnu",
|
||||
"mode": "Mode"
|
||||
"mode": "Mode",
|
||||
"never": "Jamais"
|
||||
},
|
||||
"time": {
|
||||
"days": "jours",
|
||||
@@ -72,7 +73,11 @@
|
||||
"aria": {
|
||||
"selectAll": "Tout sélectionner",
|
||||
"selectRow": "Sélectionner la ligne",
|
||||
"selectProfile": "Sélectionner le profil"
|
||||
"selectProfile": "Sélectionner le profil",
|
||||
"copy": "Copier dans le presse-papiers",
|
||||
"copied": "Copié",
|
||||
"showToken": "Afficher le jeton",
|
||||
"hideToken": "Masquer le jeton"
|
||||
},
|
||||
"keys": {
|
||||
"escape": "Échap"
|
||||
@@ -87,7 +92,11 @@
|
||||
"title": "Palette de commandes",
|
||||
"description": "Rechercher une commande à exécuter..."
|
||||
},
|
||||
"noResults": "Aucun résultat trouvé."
|
||||
"noResults": "Aucun résultat trouvé.",
|
||||
"srOnly": {
|
||||
"copy": "Copier",
|
||||
"copied": "Copié"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
@@ -196,7 +205,8 @@
|
||||
"group": "Groupe",
|
||||
"proxy": "Proxy / VPN",
|
||||
"lastLaunch": "Dernier lancement",
|
||||
"empty": "Aucun profil trouvé."
|
||||
"empty": "Aucun profil trouvé.",
|
||||
"notSelected": "Non sélectionné"
|
||||
},
|
||||
"actions": {
|
||||
"launch": "Lancer",
|
||||
@@ -488,7 +498,8 @@
|
||||
"deleteGroupAndProfiles": "Supprimer le Groupe et les Profils",
|
||||
"loadProfilesFailed": "Échec du chargement des profils",
|
||||
"unknownGroup": "Groupe inconnu",
|
||||
"profileGroupsAriaLabel": "Groupes de profils"
|
||||
"profileGroupsAriaLabel": "Groupes de profils",
|
||||
"loading": "Chargement des groupes..."
|
||||
},
|
||||
"sync": {
|
||||
"mode": {
|
||||
@@ -631,7 +642,8 @@
|
||||
"mcpAcceptTermsFirst": "(Acceptez d'abord les conditions Wayfern dans les Paramètres)",
|
||||
"mcpStarted": "Serveur MCP démarré sur le port {{port}}",
|
||||
"mcpStopped": "Serveur MCP arrêté",
|
||||
"mcpToggleFailed": "Échec du basculement du serveur MCP"
|
||||
"mcpToggleFailed": "Échec du basculement du serveur MCP",
|
||||
"openSettings": "Ouvrir les paramètres d'intégrations"
|
||||
},
|
||||
"import": {
|
||||
"title": "Importer un profil",
|
||||
@@ -711,6 +723,10 @@
|
||||
"webrtc": "Bloquer WebRTC",
|
||||
"webgl": "Bloquer WebGL"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"browserBehavior": "Comportement du navigateur",
|
||||
"allowAddonsOpenTabs": "Autoriser les modules complémentaires à ouvrir automatiquement de nouveaux onglets"
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
@@ -875,7 +891,8 @@
|
||||
"loadProxiesFailed": "Échec du chargement des proxies : {{error}}",
|
||||
"setupProxyListenersFailed": "Échec de la configuration des écouteurs d’événements de proxies : {{error}}",
|
||||
"loadVpnConfigsFailed": "Échec du chargement des configurations VPN : {{error}}",
|
||||
"setupVpnListenersFailed": "Échec de la configuration des écouteurs d’événements VPN : {{error}}"
|
||||
"setupVpnListenersFailed": "Échec de la configuration des écouteurs d’événements VPN : {{error}}",
|
||||
"themeNotFound": "Thème Tokyo Night introuvable"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
@@ -906,10 +923,10 @@
|
||||
"platform": "Plateforme",
|
||||
"platformVersion": "Version de la plateforme",
|
||||
"appVersion": "Version de l'application",
|
||||
"osCpu": "OS CPU",
|
||||
"osCpu": "CPU OS",
|
||||
"hardwareConcurrency": "Concurrence matérielle",
|
||||
"maxTouchPoints": "Points tactiles maximum",
|
||||
"doNotTrack": "Do Not Track",
|
||||
"doNotTrack": "Ne pas suivre",
|
||||
"selectDntPlaceholder": "Sélectionner la valeur DNT",
|
||||
"dntAllowed": "0 (suivi autorisé)",
|
||||
"dntNotAllowed": "1 (suivi non autorisé)",
|
||||
@@ -931,8 +948,8 @@
|
||||
"outerHeight": "Hauteur extérieure",
|
||||
"innerWidth": "Largeur intérieure",
|
||||
"innerHeight": "Hauteur intérieure",
|
||||
"screenX": "Screen X",
|
||||
"screenY": "Screen Y",
|
||||
"screenX": "Écran X",
|
||||
"screenY": "Écran Y",
|
||||
"geolocation": "Géolocalisation",
|
||||
"timezoneAndGeolocation": "Fuseau horaire et géolocalisation",
|
||||
"timezoneGeolocationDescription": "Ces valeurs remplacent les APIs de fuseau horaire et de géolocalisation du navigateur.",
|
||||
@@ -946,15 +963,15 @@
|
||||
"region": "Région",
|
||||
"script": "Script",
|
||||
"webglProperties": "Propriétés WebGL",
|
||||
"webglVendor": "WebGL Vendor",
|
||||
"webglRenderer": "WebGL Renderer",
|
||||
"webglVendor": "Fournisseur WebGL",
|
||||
"webglRenderer": "Moteur de rendu WebGL",
|
||||
"webglParameters": "Paramètres WebGL",
|
||||
"webglParametersJson": "Paramètres WebGL (JSON)",
|
||||
"webgl2Parameters": "Paramètres WebGL2",
|
||||
"webglShaderPrecisionFormats": "Formats de précision WebGL Shader",
|
||||
"webgl2ShaderPrecisionFormats": "Formats de précision WebGL2 Shader",
|
||||
"webglShaderPrecisionFormats": "Formats de précision shader WebGL",
|
||||
"webgl2ShaderPrecisionFormats": "Formats de précision shader WebGL2",
|
||||
"canvasFingerprint": "Canvas Fingerprint",
|
||||
"canvasNoiseSeed": "Canvas Noise Seed",
|
||||
"canvasNoiseSeed": "Graine de bruit Canvas",
|
||||
"canvasNoiseSeedDescription": "Cette graine est utilisée pour générer une empreinte Canvas cohérente mais unique. Chaque profil doit avoir une graine différente.",
|
||||
"fonts": "Polices",
|
||||
"fontsJson": "Polices (JSON array)",
|
||||
@@ -975,8 +992,8 @@
|
||||
"maxChannelCount": "Nombre maximum de canaux",
|
||||
"vendorInfo": "Informations du fournisseur",
|
||||
"vendor": "Fournisseur",
|
||||
"vendorSub": "Vendor Sub",
|
||||
"productSub": "Product Sub",
|
||||
"vendorSub": "Fournisseur Sub",
|
||||
"productSub": "Produit Sub",
|
||||
"brand": "Marque",
|
||||
"brandVersion": "Version de la marque",
|
||||
"proFeature": "Ceci est une fonctionnalité Pro",
|
||||
@@ -1124,7 +1141,9 @@
|
||||
"syncEnabled": "Synchronisation activée",
|
||||
"syncDisabled": "Synchronisation désactivée",
|
||||
"syncEnableTooltip": "Activer la synchronisation",
|
||||
"syncDisableTooltip": "Désactiver la synchronisation"
|
||||
"syncDisableTooltip": "Désactiver la synchronisation",
|
||||
"loadGroupsFailed": "Échec du chargement des groupes d'extensions",
|
||||
"assignGroupFailed": "Échec de l'attribution du groupe d'extensions"
|
||||
},
|
||||
"pro": {
|
||||
"badge": "PRO",
|
||||
@@ -1256,12 +1275,11 @@
|
||||
"importedSuccess": "Profil « {{name}} » importé avec succès",
|
||||
"notInstalled": "{{browser}} n'est pas installé. Veuillez télécharger {{browser}} depuis la fenêtre principale puis réessayer.",
|
||||
"importFailed": "Échec de l'import du profil : {{error}}",
|
||||
"importedAsPrefix": "Ce profil sera importé en tant que profil",
|
||||
"importedAsSuffix": ".",
|
||||
"proxyOptional": "Proxy (optionnel)",
|
||||
"noProxy": "Aucun proxy",
|
||||
"nextButton": "Suivant",
|
||||
"importButton": "Importer"
|
||||
"importButton": "Importer",
|
||||
"importedAs": "Ce profil sera importé en tant que profil {{browser}}."
|
||||
},
|
||||
"syncTooltips": {
|
||||
"syncing": "Synchronisation...",
|
||||
@@ -1497,13 +1515,18 @@
|
||||
"syncTooltipSyncing": "Synchronisation...",
|
||||
"syncTooltipSyncedAt": "Synchronisé {{time}}",
|
||||
"syncTooltipSynced": "Synchronisé",
|
||||
"syncTooltipWaiting": "En attente de sync",
|
||||
"syncTooltipWaiting": "En attente de synchronisation",
|
||||
"syncTooltipErrorWith": "Erreur de sync : {{error}}",
|
||||
"syncTooltipError": "Erreur de sync",
|
||||
"syncTooltipError": "Erreur de synchronisation",
|
||||
"syncTooltipNotSynced": "Non synchronisé",
|
||||
"noTags": "Aucune étiquette",
|
||||
"syncTooltipCloseToSync": "Fermez le profil pour synchroniser",
|
||||
"syncTooltipDisabledWithLast": "Sync désactivée, dernière sync {{time}}"
|
||||
"syncTooltipDisabledWithLast": "Sync désactivée, dernière sync {{time}}",
|
||||
"addTagsPlaceholder": "Ajouter des étiquettes",
|
||||
"tagsHeader": "Étiquettes",
|
||||
"noteHeader": "Note",
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "Créer par pays"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "Aucun type de version disponible.",
|
||||
@@ -1521,7 +1544,14 @@
|
||||
"appUpdate": {
|
||||
"toast": {
|
||||
"updateFailed": "Échec de la mise à jour de Donut Browser",
|
||||
"restartFailed": "Échec du redémarrage"
|
||||
"restartFailed": "Échec du redémarrage",
|
||||
"updateReady": "Mise à jour prête, redémarrer pour appliquer",
|
||||
"manualDownloadRequired": "Téléchargement manuel requis",
|
||||
"restartNow": "Redémarrer maintenant",
|
||||
"viewRelease": "Voir la version",
|
||||
"later": "Plus tard",
|
||||
"uploading": "Envoi",
|
||||
"downloading": "Téléchargement"
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1532,7 +1562,10 @@
|
||||
"downloadFailed": "Échec du téléchargement de {{browser}} {{version}}",
|
||||
"calculating": "calcul en cours...",
|
||||
"extractionFailed": "{{browser}} {{version}} : échec de l’extraction",
|
||||
"extractionFailedDescription": "Le fichier corrompu a été supprimé. Il sera retéléchargé lors de la prochaine tentative."
|
||||
"extractionFailedDescription": "Le fichier corrompu a été supprimé. Il sera retéléchargé lors de la prochaine tentative.",
|
||||
"extracting": "Extraction des fichiers du navigateur... Ne fermez pas l'application.",
|
||||
"verifying": "Vérification des fichiers du navigateur...",
|
||||
"downloadingRolling": "Téléchargement de la version rolling release..."
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
|
||||
+58
-25
@@ -60,7 +60,8 @@
|
||||
"optional": "任意",
|
||||
"required": "必須",
|
||||
"unknownProfile": "不明",
|
||||
"mode": "モード"
|
||||
"mode": "モード",
|
||||
"never": "一度もありません"
|
||||
},
|
||||
"time": {
|
||||
"days": "日",
|
||||
@@ -72,7 +73,11 @@
|
||||
"aria": {
|
||||
"selectAll": "すべて選択",
|
||||
"selectRow": "行を選択",
|
||||
"selectProfile": "プロファイルを選択"
|
||||
"selectProfile": "プロファイルを選択",
|
||||
"copy": "クリップボードにコピー",
|
||||
"copied": "コピーしました",
|
||||
"showToken": "トークンを表示",
|
||||
"hideToken": "トークンを非表示"
|
||||
},
|
||||
"keys": {
|
||||
"escape": "Esc"
|
||||
@@ -87,7 +92,11 @@
|
||||
"title": "コマンドパレット",
|
||||
"description": "実行するコマンドを検索..."
|
||||
},
|
||||
"noResults": "結果が見つかりません。"
|
||||
"noResults": "結果が見つかりません。",
|
||||
"srOnly": {
|
||||
"copy": "コピー",
|
||||
"copied": "コピーしました"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
@@ -196,7 +205,8 @@
|
||||
"group": "グループ",
|
||||
"proxy": "プロキシ / VPN",
|
||||
"lastLaunch": "最終起動",
|
||||
"empty": "プロファイルが見つかりません。"
|
||||
"empty": "プロファイルが見つかりません。",
|
||||
"notSelected": "未選択"
|
||||
},
|
||||
"actions": {
|
||||
"launch": "起動",
|
||||
@@ -488,7 +498,8 @@
|
||||
"deleteGroupAndProfiles": "グループとプロファイルを削除",
|
||||
"loadProfilesFailed": "プロファイルの読み込みに失敗しました",
|
||||
"unknownGroup": "不明なグループ",
|
||||
"profileGroupsAriaLabel": "プロファイルグループ"
|
||||
"profileGroupsAriaLabel": "プロファイルグループ",
|
||||
"loading": "グループを読み込み中..."
|
||||
},
|
||||
"sync": {
|
||||
"mode": {
|
||||
@@ -631,7 +642,8 @@
|
||||
"mcpAcceptTermsFirst": "(設定で先に Wayfern の規約に同意してください)",
|
||||
"mcpStarted": "MCP サーバーをポート {{port}} で起動しました",
|
||||
"mcpStopped": "MCP サーバーを停止しました",
|
||||
"mcpToggleFailed": "MCP サーバーの切り替えに失敗しました"
|
||||
"mcpToggleFailed": "MCP サーバーの切り替えに失敗しました",
|
||||
"openSettings": "統合設定を開く"
|
||||
},
|
||||
"import": {
|
||||
"title": "プロファイルをインポート",
|
||||
@@ -711,6 +723,10 @@
|
||||
"webrtc": "WebRTCをブロック",
|
||||
"webgl": "WebGLをブロック"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"browserBehavior": "ブラウザの動作",
|
||||
"allowAddonsOpenTabs": "ブラウザアドオンが新しいタブを自動的に開くことを許可"
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
@@ -875,7 +891,8 @@
|
||||
"loadProxiesFailed": "プロキシの読み込みに失敗しました: {{error}}",
|
||||
"setupProxyListenersFailed": "プロキシイベントリスナーの設定に失敗しました: {{error}}",
|
||||
"loadVpnConfigsFailed": "VPN設定の読み込みに失敗しました: {{error}}",
|
||||
"setupVpnListenersFailed": "VPNイベントリスナーの設定に失敗しました: {{error}}"
|
||||
"setupVpnListenersFailed": "VPNイベントリスナーの設定に失敗しました: {{error}}",
|
||||
"themeNotFound": "Tokyo Night テーマが見つかりません"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
@@ -901,7 +918,7 @@
|
||||
"blockWebRTC": "WebRTCをブロック",
|
||||
"blockWebGL": "WebGLをブロック",
|
||||
"navigatorProperties": "Navigatorプロパティ",
|
||||
"userAgent": "User Agent",
|
||||
"userAgent": "ユーザーエージェント",
|
||||
"userAgentAndPlatform": "User Agent & Platform",
|
||||
"platform": "Platform",
|
||||
"platformVersion": "Platform Version",
|
||||
@@ -909,7 +926,7 @@
|
||||
"osCpu": "OS CPU",
|
||||
"hardwareConcurrency": "Hardware Concurrency",
|
||||
"maxTouchPoints": "最大タッチポイント数",
|
||||
"doNotTrack": "Do Not Track",
|
||||
"doNotTrack": "追跡しない",
|
||||
"selectDntPlaceholder": "DNT値を選択",
|
||||
"dntAllowed": "0(トラッキング許可)",
|
||||
"dntNotAllowed": "1(トラッキング不許可)",
|
||||
@@ -931,8 +948,8 @@
|
||||
"outerHeight": "外側の高さ",
|
||||
"innerWidth": "内側の幅",
|
||||
"innerHeight": "内側の高さ",
|
||||
"screenX": "Screen X",
|
||||
"screenY": "Screen Y",
|
||||
"screenX": "画面 X",
|
||||
"screenY": "画面 Y",
|
||||
"geolocation": "ジオロケーション",
|
||||
"timezoneAndGeolocation": "タイムゾーンとジオロケーション",
|
||||
"timezoneGeolocationDescription": "これらの値はブラウザのタイムゾーンとジオロケーションAPIを上書きします。",
|
||||
@@ -946,15 +963,15 @@
|
||||
"region": "地域",
|
||||
"script": "スクリプト",
|
||||
"webglProperties": "WebGLプロパティ",
|
||||
"webglVendor": "WebGL Vendor",
|
||||
"webglRenderer": "WebGL Renderer",
|
||||
"webglVendor": "WebGL ベンダー",
|
||||
"webglRenderer": "WebGL レンダラー",
|
||||
"webglParameters": "WebGLパラメータ",
|
||||
"webglParametersJson": "WebGLパラメータ (JSON)",
|
||||
"webgl2Parameters": "WebGL2パラメータ",
|
||||
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
|
||||
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
|
||||
"webglShaderPrecisionFormats": "WebGL シェーダー精度フォーマット",
|
||||
"webgl2ShaderPrecisionFormats": "WebGL2 シェーダー精度フォーマット",
|
||||
"canvasFingerprint": "Canvas Fingerprint",
|
||||
"canvasNoiseSeed": "Canvas Noise Seed",
|
||||
"canvasNoiseSeed": "Canvas ノイズシード",
|
||||
"canvasNoiseSeedDescription": "このシードは一貫性がありながらもユニークなCanvasフィンガープリントを生成するために使用されます。各プロファイルには異なるシードを設定してください。",
|
||||
"fonts": "フォント",
|
||||
"fontsJson": "フォント (JSON配列)",
|
||||
@@ -975,8 +992,8 @@
|
||||
"maxChannelCount": "最大チャンネル数",
|
||||
"vendorInfo": "ベンダー情報",
|
||||
"vendor": "ベンダー",
|
||||
"vendorSub": "Vendor Sub",
|
||||
"productSub": "Product Sub",
|
||||
"vendorSub": "ベンダーサブ",
|
||||
"productSub": "プロダクトサブ",
|
||||
"brand": "ブランド",
|
||||
"brandVersion": "ブランドバージョン",
|
||||
"proFeature": "これはPro機能です",
|
||||
@@ -1124,7 +1141,9 @@
|
||||
"syncEnabled": "同期が有効",
|
||||
"syncDisabled": "同期が無効",
|
||||
"syncEnableTooltip": "同期を有効にする",
|
||||
"syncDisableTooltip": "同期を無効にする"
|
||||
"syncDisableTooltip": "同期を無効にする",
|
||||
"loadGroupsFailed": "拡張機能グループの読み込みに失敗しました",
|
||||
"assignGroupFailed": "拡張機能グループの割り当てに失敗しました"
|
||||
},
|
||||
"pro": {
|
||||
"badge": "PRO",
|
||||
@@ -1256,12 +1275,11 @@
|
||||
"importedSuccess": "プロファイル「{{name}}」をインポートしました",
|
||||
"notInstalled": "{{browser}} はインストールされていません。メインウィンドウから {{browser}} をダウンロードしてからもう一度インポートしてください。",
|
||||
"importFailed": "プロファイルのインポートに失敗しました: {{error}}",
|
||||
"importedAsPrefix": "このプロファイルは次のプロファイルとしてインポートされます:",
|
||||
"importedAsSuffix": "",
|
||||
"proxyOptional": "プロキシ (任意)",
|
||||
"noProxy": "プロキシなし",
|
||||
"nextButton": "次へ",
|
||||
"importButton": "インポート"
|
||||
"importButton": "インポート",
|
||||
"importedAs": "このプロファイルは {{browser}} プロファイルとしてインポートされます。"
|
||||
},
|
||||
"syncTooltips": {
|
||||
"syncing": "同期中...",
|
||||
@@ -1503,7 +1521,12 @@
|
||||
"syncTooltipNotSynced": "未同期",
|
||||
"noTags": "タグなし",
|
||||
"syncTooltipCloseToSync": "プロファイルを閉じて同期",
|
||||
"syncTooltipDisabledWithLast": "同期無効、最終同期 {{time}}"
|
||||
"syncTooltipDisabledWithLast": "同期無効、最終同期 {{time}}",
|
||||
"addTagsPlaceholder": "タグを追加",
|
||||
"tagsHeader": "タグ",
|
||||
"noteHeader": "メモ",
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "国別に作成"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "利用可能なリリースタイプがありません。",
|
||||
@@ -1521,7 +1544,14 @@
|
||||
"appUpdate": {
|
||||
"toast": {
|
||||
"updateFailed": "Donut Browser の更新に失敗しました",
|
||||
"restartFailed": "再起動に失敗しました"
|
||||
"restartFailed": "再起動に失敗しました",
|
||||
"updateReady": "アップデートの準備完了。再起動して適用",
|
||||
"manualDownloadRequired": "手動ダウンロードが必要です",
|
||||
"restartNow": "今すぐ再起動",
|
||||
"viewRelease": "リリースを見る",
|
||||
"later": "後で",
|
||||
"uploading": "アップロード中",
|
||||
"downloading": "ダウンロード中"
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1532,7 +1562,10 @@
|
||||
"downloadFailed": "{{browser}} {{version}} のダウンロードに失敗しました",
|
||||
"calculating": "計算中...",
|
||||
"extractionFailed": "{{browser}} {{version}}: 展開に失敗しました",
|
||||
"extractionFailedDescription": "破損したファイルは削除されました。次回の試行時に再ダウンロードされます。"
|
||||
"extractionFailedDescription": "破損したファイルは削除されました。次回の試行時に再ダウンロードされます。",
|
||||
"extracting": "ブラウザファイルを展開中... アプリを閉じないでください。",
|
||||
"verifying": "ブラウザファイルを検証中...",
|
||||
"downloadingRolling": "ローリングリリースビルドをダウンロード中..."
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
|
||||
+59
-26
@@ -60,7 +60,8 @@
|
||||
"optional": "Opcional",
|
||||
"required": "Obrigatório",
|
||||
"unknownProfile": "Desconhecido",
|
||||
"mode": "Modo"
|
||||
"mode": "Modo",
|
||||
"never": "Nunca"
|
||||
},
|
||||
"time": {
|
||||
"days": "dias",
|
||||
@@ -72,7 +73,11 @@
|
||||
"aria": {
|
||||
"selectAll": "Selecionar tudo",
|
||||
"selectRow": "Selecionar linha",
|
||||
"selectProfile": "Selecionar perfil"
|
||||
"selectProfile": "Selecionar perfil",
|
||||
"copy": "Copiar para a área de transferência",
|
||||
"copied": "Copiado",
|
||||
"showToken": "Mostrar token",
|
||||
"hideToken": "Ocultar token"
|
||||
},
|
||||
"keys": {
|
||||
"escape": "Esc"
|
||||
@@ -87,7 +92,11 @@
|
||||
"title": "Paleta de comandos",
|
||||
"description": "Pesquise um comando para executar..."
|
||||
},
|
||||
"noResults": "Nenhum resultado encontrado."
|
||||
"noResults": "Nenhum resultado encontrado.",
|
||||
"srOnly": {
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
@@ -196,7 +205,8 @@
|
||||
"group": "Grupo",
|
||||
"proxy": "Proxy / VPN",
|
||||
"lastLaunch": "Último Início",
|
||||
"empty": "Nenhum perfil encontrado."
|
||||
"empty": "Nenhum perfil encontrado.",
|
||||
"notSelected": "Não selecionado"
|
||||
},
|
||||
"actions": {
|
||||
"launch": "Iniciar",
|
||||
@@ -488,7 +498,8 @@
|
||||
"deleteGroupAndProfiles": "Excluir Grupo e Perfis",
|
||||
"loadProfilesFailed": "Falha ao carregar os perfis",
|
||||
"unknownGroup": "Grupo desconhecido",
|
||||
"profileGroupsAriaLabel": "Grupos de perfis"
|
||||
"profileGroupsAriaLabel": "Grupos de perfis",
|
||||
"loading": "Carregando grupos..."
|
||||
},
|
||||
"sync": {
|
||||
"mode": {
|
||||
@@ -631,7 +642,8 @@
|
||||
"mcpAcceptTermsFirst": "(Aceite primeiro os termos da Wayfern nas Configurações)",
|
||||
"mcpStarted": "Servidor MCP iniciado na porta {{port}}",
|
||||
"mcpStopped": "Servidor MCP parado",
|
||||
"mcpToggleFailed": "Falha ao alternar o servidor MCP"
|
||||
"mcpToggleFailed": "Falha ao alternar o servidor MCP",
|
||||
"openSettings": "Abrir configurações de integrações"
|
||||
},
|
||||
"import": {
|
||||
"title": "Importar Perfil",
|
||||
@@ -711,6 +723,10 @@
|
||||
"webrtc": "Bloquear WebRTC",
|
||||
"webgl": "Bloquear WebGL"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"browserBehavior": "Comportamento do navegador",
|
||||
"allowAddonsOpenTabs": "Permitir que extensões abram novas abas automaticamente"
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
@@ -875,7 +891,8 @@
|
||||
"loadProxiesFailed": "Falha ao carregar os proxies: {{error}}",
|
||||
"setupProxyListenersFailed": "Falha ao configurar os listeners de eventos de proxies: {{error}}",
|
||||
"loadVpnConfigsFailed": "Falha ao carregar as configurações de VPN: {{error}}",
|
||||
"setupVpnListenersFailed": "Falha ao configurar os listeners de eventos de VPN: {{error}}"
|
||||
"setupVpnListenersFailed": "Falha ao configurar os listeners de eventos de VPN: {{error}}",
|
||||
"themeNotFound": "Tema Tokyo Night não encontrado"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
@@ -901,15 +918,15 @@
|
||||
"blockWebRTC": "Bloquear WebRTC",
|
||||
"blockWebGL": "Bloquear WebGL",
|
||||
"navigatorProperties": "Propriedades do Navigator",
|
||||
"userAgent": "User Agent",
|
||||
"userAgent": "Agente do usuário",
|
||||
"userAgentAndPlatform": "User Agent & Platform",
|
||||
"platform": "Platform",
|
||||
"platformVersion": "Platform Version",
|
||||
"appVersion": "App Version",
|
||||
"osCpu": "OS CPU",
|
||||
"osCpu": "CPU do SO",
|
||||
"hardwareConcurrency": "Hardware Concurrency",
|
||||
"maxTouchPoints": "Pontos de Toque Máximos",
|
||||
"doNotTrack": "Do Not Track",
|
||||
"doNotTrack": "Não rastrear",
|
||||
"selectDntPlaceholder": "Selecionar valor DNT",
|
||||
"dntAllowed": "0 (rastreamento permitido)",
|
||||
"dntNotAllowed": "1 (rastreamento não permitido)",
|
||||
@@ -931,8 +948,8 @@
|
||||
"outerHeight": "Altura Externa",
|
||||
"innerWidth": "Largura Interna",
|
||||
"innerHeight": "Altura Interna",
|
||||
"screenX": "Screen X",
|
||||
"screenY": "Screen Y",
|
||||
"screenX": "Tela X",
|
||||
"screenY": "Tela Y",
|
||||
"geolocation": "Geolocalização",
|
||||
"timezoneAndGeolocation": "Fuso Horário e Geolocalização",
|
||||
"timezoneGeolocationDescription": "Estes valores substituem as APIs de fuso horário e geolocalização do navegador.",
|
||||
@@ -946,15 +963,15 @@
|
||||
"region": "Região",
|
||||
"script": "Script",
|
||||
"webglProperties": "Propriedades WebGL",
|
||||
"webglVendor": "WebGL Vendor",
|
||||
"webglRenderer": "WebGL Renderer",
|
||||
"webglVendor": "Fornecedor WebGL",
|
||||
"webglRenderer": "Renderizador WebGL",
|
||||
"webglParameters": "Parâmetros WebGL",
|
||||
"webglParametersJson": "Parâmetros WebGL (JSON)",
|
||||
"webgl2Parameters": "Parâmetros WebGL2",
|
||||
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
|
||||
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
|
||||
"webglShaderPrecisionFormats": "Formatos de precisão de shader WebGL",
|
||||
"webgl2ShaderPrecisionFormats": "Formatos de precisão de shader WebGL2",
|
||||
"canvasFingerprint": "Canvas Fingerprint",
|
||||
"canvasNoiseSeed": "Canvas Noise Seed",
|
||||
"canvasNoiseSeed": "Semente de ruído Canvas",
|
||||
"canvasNoiseSeedDescription": "Este seed é usado para gerar uma impressão digital Canvas consistente, mas única. Cada perfil deve ter um seed diferente.",
|
||||
"fonts": "Fontes",
|
||||
"fontsJson": "Fontes (JSON array)",
|
||||
@@ -975,8 +992,8 @@
|
||||
"maxChannelCount": "Contagem Máxima de Canais",
|
||||
"vendorInfo": "Informações do Fabricante",
|
||||
"vendor": "Fabricante",
|
||||
"vendorSub": "Vendor Sub",
|
||||
"productSub": "Product Sub",
|
||||
"vendorSub": "Fornecedor Sub",
|
||||
"productSub": "Produto Sub",
|
||||
"brand": "Marca",
|
||||
"brandVersion": "Versão da Marca",
|
||||
"proFeature": "Este é um recurso Pro",
|
||||
@@ -1124,7 +1141,9 @@
|
||||
"syncEnabled": "Sincronização ativada",
|
||||
"syncDisabled": "Sincronização desativada",
|
||||
"syncEnableTooltip": "Ativar sincronização",
|
||||
"syncDisableTooltip": "Desativar sincronização"
|
||||
"syncDisableTooltip": "Desativar sincronização",
|
||||
"loadGroupsFailed": "Falha ao carregar grupos de extensões",
|
||||
"assignGroupFailed": "Falha ao atribuir grupo de extensões"
|
||||
},
|
||||
"pro": {
|
||||
"badge": "PRO",
|
||||
@@ -1256,12 +1275,11 @@
|
||||
"importedSuccess": "Perfil \"{{name}}\" importado com sucesso",
|
||||
"notInstalled": "{{browser}} não está instalado. Baixe {{browser}} primeiro pela janela principal e tente importar novamente.",
|
||||
"importFailed": "Falha ao importar perfil: {{error}}",
|
||||
"importedAsPrefix": "Este perfil será importado como um perfil",
|
||||
"importedAsSuffix": ".",
|
||||
"proxyOptional": "Proxy (Opcional)",
|
||||
"noProxy": "Sem proxy",
|
||||
"nextButton": "Próximo",
|
||||
"importButton": "Importar"
|
||||
"importButton": "Importar",
|
||||
"importedAs": "Este perfil será importado como um perfil {{browser}}."
|
||||
},
|
||||
"syncTooltips": {
|
||||
"syncing": "Sincronizando...",
|
||||
@@ -1503,7 +1521,12 @@
|
||||
"syncTooltipNotSynced": "Não sincronizado",
|
||||
"noTags": "Sem tags",
|
||||
"syncTooltipCloseToSync": "Feche o perfil para sincronizar",
|
||||
"syncTooltipDisabledWithLast": "Sincronização desativada, última sincronização {{time}}"
|
||||
"syncTooltipDisabledWithLast": "Sincronização desativada, última sincronização {{time}}",
|
||||
"addTagsPlaceholder": "Adicionar etiquetas",
|
||||
"tagsHeader": "Etiquetas",
|
||||
"noteHeader": "Nota",
|
||||
"vpnsHeading": "VPNs",
|
||||
"createByCountryHeading": "Criar por país"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "Nenhum tipo de versão disponível.",
|
||||
@@ -1521,7 +1544,14 @@
|
||||
"appUpdate": {
|
||||
"toast": {
|
||||
"updateFailed": "Falha ao atualizar o Donut Browser",
|
||||
"restartFailed": "Falha ao reiniciar"
|
||||
"restartFailed": "Falha ao reiniciar",
|
||||
"updateReady": "Atualização pronta, reinicie para aplicar",
|
||||
"manualDownloadRequired": "Download manual necessário",
|
||||
"restartNow": "Reiniciar agora",
|
||||
"viewRelease": "Ver lançamento",
|
||||
"later": "Mais tarde",
|
||||
"uploading": "Enviando",
|
||||
"downloading": "Baixando"
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1532,7 +1562,10 @@
|
||||
"downloadFailed": "Falha ao baixar {{browser}} {{version}}",
|
||||
"calculating": "calculando...",
|
||||
"extractionFailed": "{{browser}} {{version}}: falha na extração",
|
||||
"extractionFailedDescription": "O arquivo corrompido foi excluído. Será baixado novamente na próxima tentativa."
|
||||
"extractionFailedDescription": "O arquivo corrompido foi excluído. Será baixado novamente na próxima tentativa.",
|
||||
"extracting": "Extraindo arquivos do navegador... Não feche o aplicativo.",
|
||||
"verifying": "Verificando arquivos do navegador...",
|
||||
"downloadingRolling": "Baixando build rolling release..."
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
|
||||
+58
-25
@@ -60,7 +60,8 @@
|
||||
"optional": "Необязательно",
|
||||
"required": "Обязательно",
|
||||
"unknownProfile": "Неизвестный",
|
||||
"mode": "Режим"
|
||||
"mode": "Режим",
|
||||
"never": "Никогда"
|
||||
},
|
||||
"time": {
|
||||
"days": "дней",
|
||||
@@ -72,7 +73,11 @@
|
||||
"aria": {
|
||||
"selectAll": "Выбрать все",
|
||||
"selectRow": "Выбрать строку",
|
||||
"selectProfile": "Выбрать профиль"
|
||||
"selectProfile": "Выбрать профиль",
|
||||
"copy": "Скопировать в буфер обмена",
|
||||
"copied": "Скопировано",
|
||||
"showToken": "Показать токен",
|
||||
"hideToken": "Скрыть токен"
|
||||
},
|
||||
"keys": {
|
||||
"escape": "Esc"
|
||||
@@ -87,7 +92,11 @@
|
||||
"title": "Палитра команд",
|
||||
"description": "Найдите команду для выполнения..."
|
||||
},
|
||||
"noResults": "Результаты не найдены."
|
||||
"noResults": "Результаты не найдены.",
|
||||
"srOnly": {
|
||||
"copy": "Скопировать",
|
||||
"copied": "Скопировано"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
@@ -196,7 +205,8 @@
|
||||
"group": "Группа",
|
||||
"proxy": "Прокси / VPN",
|
||||
"lastLaunch": "Последний запуск",
|
||||
"empty": "Профили не найдены."
|
||||
"empty": "Профили не найдены.",
|
||||
"notSelected": "Не выбрано"
|
||||
},
|
||||
"actions": {
|
||||
"launch": "Запустить",
|
||||
@@ -488,7 +498,8 @@
|
||||
"deleteGroupAndProfiles": "Удалить группу и профили",
|
||||
"loadProfilesFailed": "Не удалось загрузить профили",
|
||||
"unknownGroup": "Неизвестная группа",
|
||||
"profileGroupsAriaLabel": "Группы профилей"
|
||||
"profileGroupsAriaLabel": "Группы профилей",
|
||||
"loading": "Загрузка групп..."
|
||||
},
|
||||
"sync": {
|
||||
"mode": {
|
||||
@@ -631,7 +642,8 @@
|
||||
"mcpAcceptTermsFirst": "(Сначала примите условия Wayfern в Настройках)",
|
||||
"mcpStarted": "MCP сервер запущен на порту {{port}}",
|
||||
"mcpStopped": "MCP сервер остановлен",
|
||||
"mcpToggleFailed": "Не удалось переключить MCP сервер"
|
||||
"mcpToggleFailed": "Не удалось переключить MCP сервер",
|
||||
"openSettings": "Открыть настройки интеграций"
|
||||
},
|
||||
"import": {
|
||||
"title": "Импорт профиля",
|
||||
@@ -711,6 +723,10 @@
|
||||
"webrtc": "Блокировать WebRTC",
|
||||
"webgl": "Блокировать WebGL"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"browserBehavior": "Поведение браузера",
|
||||
"allowAddonsOpenTabs": "Разрешить расширениям браузера автоматически открывать новые вкладки"
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
@@ -875,7 +891,8 @@
|
||||
"loadProxiesFailed": "Не удалось загрузить прокси: {{error}}",
|
||||
"setupProxyListenersFailed": "Не удалось настроить слушатели событий прокси: {{error}}",
|
||||
"loadVpnConfigsFailed": "Не удалось загрузить конфигурации VPN: {{error}}",
|
||||
"setupVpnListenersFailed": "Не удалось настроить слушатели событий VPN: {{error}}"
|
||||
"setupVpnListenersFailed": "Не удалось настроить слушатели событий VPN: {{error}}",
|
||||
"themeNotFound": "Тема Tokyo Night не найдена"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
@@ -906,10 +923,10 @@
|
||||
"platform": "Платформа",
|
||||
"platformVersion": "Версия платформы",
|
||||
"appVersion": "Версия приложения",
|
||||
"osCpu": "OS CPU",
|
||||
"osCpu": "ЦП ОС",
|
||||
"hardwareConcurrency": "Количество потоков процессора",
|
||||
"maxTouchPoints": "Максимальное количество точек касания",
|
||||
"doNotTrack": "Do Not Track",
|
||||
"doNotTrack": "Не отслеживать",
|
||||
"selectDntPlaceholder": "Выберите значение DNT",
|
||||
"dntAllowed": "0 (отслеживание разрешено)",
|
||||
"dntNotAllowed": "1 (отслеживание не разрешено)",
|
||||
@@ -931,8 +948,8 @@
|
||||
"outerHeight": "Внешняя высота",
|
||||
"innerWidth": "Внутренняя ширина",
|
||||
"innerHeight": "Внутренняя высота",
|
||||
"screenX": "Screen X",
|
||||
"screenY": "Screen Y",
|
||||
"screenX": "Экран X",
|
||||
"screenY": "Экран Y",
|
||||
"geolocation": "Геолокация",
|
||||
"timezoneAndGeolocation": "Часовой пояс и геолокация",
|
||||
"timezoneGeolocationDescription": "Эти значения переопределяют API часового пояса и геолокации браузера.",
|
||||
@@ -946,15 +963,15 @@
|
||||
"region": "Регион",
|
||||
"script": "Скрипт",
|
||||
"webglProperties": "Свойства WebGL",
|
||||
"webglVendor": "WebGL Vendor",
|
||||
"webglRenderer": "WebGL Renderer",
|
||||
"webglVendor": "Производитель WebGL",
|
||||
"webglRenderer": "Рендерер WebGL",
|
||||
"webglParameters": "Параметры WebGL",
|
||||
"webglParametersJson": "Параметры WebGL (JSON)",
|
||||
"webgl2Parameters": "Параметры WebGL2",
|
||||
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
|
||||
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
|
||||
"webglShaderPrecisionFormats": "Форматы точности шейдера WebGL",
|
||||
"webgl2ShaderPrecisionFormats": "Форматы точности шейдера WebGL2",
|
||||
"canvasFingerprint": "Отпечаток Canvas",
|
||||
"canvasNoiseSeed": "Canvas Noise Seed",
|
||||
"canvasNoiseSeed": "Сид шума Canvas",
|
||||
"canvasNoiseSeedDescription": "Это зерно используется для генерации постоянного, но уникального отпечатка Canvas. У каждого профиля должно быть своё зерно.",
|
||||
"fonts": "Шрифты",
|
||||
"fontsJson": "Шрифты (JSON-массив)",
|
||||
@@ -975,8 +992,8 @@
|
||||
"maxChannelCount": "Максимальное количество каналов",
|
||||
"vendorInfo": "Информация о производителе",
|
||||
"vendor": "Производитель",
|
||||
"vendorSub": "Vendor Sub",
|
||||
"productSub": "Product Sub",
|
||||
"vendorSub": "Подверсия производителя",
|
||||
"productSub": "Подверсия продукта",
|
||||
"brand": "Бренд",
|
||||
"brandVersion": "Версия бренда",
|
||||
"proFeature": "Это функция Pro",
|
||||
@@ -1124,7 +1141,9 @@
|
||||
"syncEnabled": "Синхронизация включена",
|
||||
"syncDisabled": "Синхронизация отключена",
|
||||
"syncEnableTooltip": "Включить синхронизацию",
|
||||
"syncDisableTooltip": "Отключить синхронизацию"
|
||||
"syncDisableTooltip": "Отключить синхронизацию",
|
||||
"loadGroupsFailed": "Не удалось загрузить группы расширений",
|
||||
"assignGroupFailed": "Не удалось назначить группу расширений"
|
||||
},
|
||||
"pro": {
|
||||
"badge": "PRO",
|
||||
@@ -1256,12 +1275,11 @@
|
||||
"importedSuccess": "Профиль «{{name}}» успешно импортирован",
|
||||
"notInstalled": "{{browser}} не установлен. Сначала загрузите {{browser}} из главного окна, затем попробуйте импортировать снова.",
|
||||
"importFailed": "Не удалось импортировать профиль: {{error}}",
|
||||
"importedAsPrefix": "Этот профиль будет импортирован как профиль",
|
||||
"importedAsSuffix": ".",
|
||||
"proxyOptional": "Прокси (необязательно)",
|
||||
"noProxy": "Без прокси",
|
||||
"nextButton": "Далее",
|
||||
"importButton": "Импорт"
|
||||
"importButton": "Импорт",
|
||||
"importedAs": "Этот профиль будет импортирован как профиль {{browser}}."
|
||||
},
|
||||
"syncTooltips": {
|
||||
"syncing": "Синхронизация...",
|
||||
@@ -1503,7 +1521,12 @@
|
||||
"syncTooltipNotSynced": "Не синхронизировано",
|
||||
"noTags": "Нет тегов",
|
||||
"syncTooltipCloseToSync": "Закройте профиль для синхронизации",
|
||||
"syncTooltipDisabledWithLast": "Синхронизация отключена, последняя синхронизация {{time}}"
|
||||
"syncTooltipDisabledWithLast": "Синхронизация отключена, последняя синхронизация {{time}}",
|
||||
"addTagsPlaceholder": "Добавить теги",
|
||||
"tagsHeader": "Теги",
|
||||
"noteHeader": "Заметка",
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "Создать по стране"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "Нет доступных типов выпусков.",
|
||||
@@ -1521,7 +1544,14 @@
|
||||
"appUpdate": {
|
||||
"toast": {
|
||||
"updateFailed": "Не удалось обновить Donut Browser",
|
||||
"restartFailed": "Не удалось перезапустить"
|
||||
"restartFailed": "Не удалось перезапустить",
|
||||
"updateReady": "Обновление готово, перезапустите для применения",
|
||||
"manualDownloadRequired": "Требуется ручная загрузка",
|
||||
"restartNow": "Перезапустить сейчас",
|
||||
"viewRelease": "Посмотреть релиз",
|
||||
"later": "Позже",
|
||||
"uploading": "Загрузка",
|
||||
"downloading": "Скачивание"
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1532,7 +1562,10 @@
|
||||
"downloadFailed": "Не удалось загрузить {{browser}} {{version}}",
|
||||
"calculating": "вычисление...",
|
||||
"extractionFailed": "{{browser}} {{version}}: ошибка распаковки",
|
||||
"extractionFailedDescription": "Повреждённый файл удалён. Он будет повторно загружен при следующей попытке."
|
||||
"extractionFailedDescription": "Повреждённый файл удалён. Он будет повторно загружен при следующей попытке.",
|
||||
"extracting": "Распаковка файлов браузера... Не закрывайте приложение.",
|
||||
"verifying": "Проверка файлов браузера...",
|
||||
"downloadingRolling": "Загрузка rolling release сборки..."
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
|
||||
+59
-26
@@ -60,7 +60,8 @@
|
||||
"optional": "可选",
|
||||
"required": "必填",
|
||||
"unknownProfile": "未知",
|
||||
"mode": "模式"
|
||||
"mode": "模式",
|
||||
"never": "从不"
|
||||
},
|
||||
"time": {
|
||||
"days": "天",
|
||||
@@ -72,7 +73,11 @@
|
||||
"aria": {
|
||||
"selectAll": "全选",
|
||||
"selectRow": "选择行",
|
||||
"selectProfile": "选择配置文件"
|
||||
"selectProfile": "选择配置文件",
|
||||
"copy": "复制到剪贴板",
|
||||
"copied": "已复制",
|
||||
"showToken": "显示令牌",
|
||||
"hideToken": "隐藏令牌"
|
||||
},
|
||||
"keys": {
|
||||
"escape": "Esc"
|
||||
@@ -87,7 +92,11 @@
|
||||
"title": "命令面板",
|
||||
"description": "搜索要执行的命令..."
|
||||
},
|
||||
"noResults": "未找到结果。"
|
||||
"noResults": "未找到结果。",
|
||||
"srOnly": {
|
||||
"copy": "复制",
|
||||
"copied": "已复制"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
@@ -196,7 +205,8 @@
|
||||
"group": "分组",
|
||||
"proxy": "代理 / VPN",
|
||||
"lastLaunch": "最后启动",
|
||||
"empty": "未找到配置文件。"
|
||||
"empty": "未找到配置文件。",
|
||||
"notSelected": "未选择"
|
||||
},
|
||||
"actions": {
|
||||
"launch": "启动",
|
||||
@@ -488,7 +498,8 @@
|
||||
"deleteGroupAndProfiles": "删除组和配置文件",
|
||||
"loadProfilesFailed": "加载配置文件失败",
|
||||
"unknownGroup": "未知分组",
|
||||
"profileGroupsAriaLabel": "配置文件分组"
|
||||
"profileGroupsAriaLabel": "配置文件分组",
|
||||
"loading": "正在加载组..."
|
||||
},
|
||||
"sync": {
|
||||
"mode": {
|
||||
@@ -631,7 +642,8 @@
|
||||
"mcpAcceptTermsFirst": "(请先在设置中接受 Wayfern 条款)",
|
||||
"mcpStarted": "MCP 服务器已在端口 {{port}} 上启动",
|
||||
"mcpStopped": "MCP 服务器已停止",
|
||||
"mcpToggleFailed": "切换 MCP 服务器失败"
|
||||
"mcpToggleFailed": "切换 MCP 服务器失败",
|
||||
"openSettings": "打开集成设置"
|
||||
},
|
||||
"import": {
|
||||
"title": "导入配置文件",
|
||||
@@ -711,6 +723,10 @@
|
||||
"webrtc": "阻止 WebRTC",
|
||||
"webgl": "阻止 WebGL"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"browserBehavior": "浏览器行为",
|
||||
"allowAddonsOpenTabs": "允许浏览器附加组件自动打开新标签页"
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
@@ -875,7 +891,8 @@
|
||||
"loadProxiesFailed": "加载代理失败: {{error}}",
|
||||
"setupProxyListenersFailed": "设置代理事件监听器失败: {{error}}",
|
||||
"loadVpnConfigsFailed": "加载 VPN 配置失败: {{error}}",
|
||||
"setupVpnListenersFailed": "设置 VPN 事件监听器失败: {{error}}"
|
||||
"setupVpnListenersFailed": "设置 VPN 事件监听器失败: {{error}}",
|
||||
"themeNotFound": "未找到 Tokyo Night 主题"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
@@ -901,15 +918,15 @@
|
||||
"blockWebRTC": "阻止 WebRTC",
|
||||
"blockWebGL": "阻止 WebGL",
|
||||
"navigatorProperties": "Navigator 属性",
|
||||
"userAgent": "User Agent",
|
||||
"userAgent": "用户代理",
|
||||
"userAgentAndPlatform": "User Agent 和平台",
|
||||
"platform": "平台",
|
||||
"platformVersion": "平台版本",
|
||||
"appVersion": "应用版本",
|
||||
"osCpu": "OS CPU",
|
||||
"osCpu": "操作系统 CPU",
|
||||
"hardwareConcurrency": "硬件并发数",
|
||||
"maxTouchPoints": "最大触摸点数",
|
||||
"doNotTrack": "Do Not Track",
|
||||
"doNotTrack": "请勿跟踪",
|
||||
"selectDntPlaceholder": "选择 DNT 值",
|
||||
"dntAllowed": "0(允许跟踪)",
|
||||
"dntNotAllowed": "1(不允许跟踪)",
|
||||
@@ -931,8 +948,8 @@
|
||||
"outerHeight": "外部高度",
|
||||
"innerWidth": "内部宽度",
|
||||
"innerHeight": "内部高度",
|
||||
"screenX": "Screen X",
|
||||
"screenY": "Screen Y",
|
||||
"screenX": "屏幕 X",
|
||||
"screenY": "屏幕 Y",
|
||||
"geolocation": "地理位置",
|
||||
"timezoneAndGeolocation": "时区和地理位置",
|
||||
"timezoneGeolocationDescription": "这些值会覆盖浏览器的时区和地理位置 API。",
|
||||
@@ -946,15 +963,15 @@
|
||||
"region": "地区",
|
||||
"script": "脚本",
|
||||
"webglProperties": "WebGL 属性",
|
||||
"webglVendor": "WebGL Vendor",
|
||||
"webglRenderer": "WebGL Renderer",
|
||||
"webglVendor": "WebGL 供应商",
|
||||
"webglRenderer": "WebGL 渲染器",
|
||||
"webglParameters": "WebGL 参数",
|
||||
"webglParametersJson": "WebGL 参数 (JSON)",
|
||||
"webgl2Parameters": "WebGL2 参数",
|
||||
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
|
||||
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
|
||||
"webglShaderPrecisionFormats": "WebGL 着色器精度格式",
|
||||
"webgl2ShaderPrecisionFormats": "WebGL2 着色器精度格式",
|
||||
"canvasFingerprint": "Canvas 指纹",
|
||||
"canvasNoiseSeed": "Canvas Noise Seed",
|
||||
"canvasNoiseSeed": "Canvas 噪声种子",
|
||||
"canvasNoiseSeedDescription": "此种子用于生成一致但唯一的 Canvas 指纹。每个配置文件应使用不同的种子。",
|
||||
"fonts": "字体",
|
||||
"fontsJson": "字体 (JSON 数组)",
|
||||
@@ -975,8 +992,8 @@
|
||||
"maxChannelCount": "最大通道数",
|
||||
"vendorInfo": "供应商信息",
|
||||
"vendor": "供应商",
|
||||
"vendorSub": "Vendor Sub",
|
||||
"productSub": "Product Sub",
|
||||
"vendorSub": "供应商子版本",
|
||||
"productSub": "产品子版本",
|
||||
"brand": "品牌",
|
||||
"brandVersion": "品牌版本",
|
||||
"proFeature": "这是 Pro 功能",
|
||||
@@ -1124,7 +1141,9 @@
|
||||
"syncEnabled": "同步已启用",
|
||||
"syncDisabled": "同步已禁用",
|
||||
"syncEnableTooltip": "启用同步",
|
||||
"syncDisableTooltip": "禁用同步"
|
||||
"syncDisableTooltip": "禁用同步",
|
||||
"loadGroupsFailed": "加载扩展组失败",
|
||||
"assignGroupFailed": "分配扩展组失败"
|
||||
},
|
||||
"pro": {
|
||||
"badge": "PRO",
|
||||
@@ -1256,12 +1275,11 @@
|
||||
"importedSuccess": "已成功导入配置文件「{{name}}」",
|
||||
"notInstalled": "{{browser}} 未安装。请先从主窗口下载 {{browser}},然后再尝试导入。",
|
||||
"importFailed": "导入配置文件失败: {{error}}",
|
||||
"importedAsPrefix": "此配置文件将作为以下配置文件导入:",
|
||||
"importedAsSuffix": "",
|
||||
"proxyOptional": "代理 (可选)",
|
||||
"noProxy": "无代理",
|
||||
"nextButton": "下一步",
|
||||
"importButton": "导入"
|
||||
"importButton": "导入",
|
||||
"importedAs": "此配置文件将作为 {{browser}} 配置文件导入。"
|
||||
},
|
||||
"syncTooltips": {
|
||||
"syncing": "同步中...",
|
||||
@@ -1503,7 +1521,12 @@
|
||||
"syncTooltipNotSynced": "未同步",
|
||||
"noTags": "无标签",
|
||||
"syncTooltipCloseToSync": "关闭配置文件以进行同步",
|
||||
"syncTooltipDisabledWithLast": "同步已禁用,上次同步 {{time}}"
|
||||
"syncTooltipDisabledWithLast": "同步已禁用,上次同步 {{time}}",
|
||||
"addTagsPlaceholder": "添加标签",
|
||||
"tagsHeader": "标签",
|
||||
"noteHeader": "备注",
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "按国家创建"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "没有可用的发布类型。",
|
||||
@@ -1521,7 +1544,14 @@
|
||||
"appUpdate": {
|
||||
"toast": {
|
||||
"updateFailed": "更新 Donut Browser 失败",
|
||||
"restartFailed": "重启失败"
|
||||
"restartFailed": "重启失败",
|
||||
"updateReady": "更新就绪,请重启以应用",
|
||||
"manualDownloadRequired": "需要手动下载",
|
||||
"restartNow": "立即重启",
|
||||
"viewRelease": "查看版本",
|
||||
"later": "稍后",
|
||||
"uploading": "上传中",
|
||||
"downloading": "下载中"
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1532,7 +1562,10 @@
|
||||
"downloadFailed": "下载 {{browser}} {{version}} 失败",
|
||||
"calculating": "计算中...",
|
||||
"extractionFailed": "{{browser}} {{version}}: 解压失败",
|
||||
"extractionFailedDescription": "损坏的文件已删除。下次尝试时将重新下载。"
|
||||
"extractionFailedDescription": "损坏的文件已删除。下次尝试时将重新下载。",
|
||||
"extracting": "正在提取浏览器文件...请不要关闭应用。",
|
||||
"verifying": "正在验证浏览器文件...",
|
||||
"downloadingRolling": "正在下载滚动发布版本..."
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
|
||||
Reference in New Issue
Block a user