chore: copy

This commit is contained in:
zhom
2026-04-30 00:10:19 +04:00
parent 571bfcb213
commit 57167b979f
26 changed files with 1048 additions and 830 deletions
+2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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}
+7 -5
View File
@@ -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>
+1 -1
View File
@@ -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>
)}
+6 -4
View File
@@ -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 {
+208 -193
View File
@@ -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
+1 -1
View File
@@ -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>
);
+1 -1
View File
@@ -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">
+3 -3
View File
@@ -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>
+34 -13
View File
@@ -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">
+11 -7
View File
@@ -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]);
+14 -31
View File
@@ -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>
+312 -322
View File
@@ -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>
+29 -69
View File
@@ -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>
+5 -1
View File
@@ -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" />
+6 -2
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 lextraction",
"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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": {