feat: extension export via api

This commit is contained in:
zhom
2026-08-16 19:50:12 +04:00
parent 2be0d4df0b
commit d7f002d8ac
26 changed files with 4489 additions and 443 deletions
+420 -167
View File
@@ -11,6 +11,7 @@ import {
} from "@tanstack/react-table";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { open } from "@tauri-apps/plugin-dialog";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaChrome } from "react-icons/fa";
@@ -19,6 +20,8 @@ import {
LuChevronDown,
LuChevronUp,
LuExternalLink,
LuFolderOpen,
LuLink,
LuPencil,
LuPuzzle,
LuRefreshCw,
@@ -82,6 +85,18 @@ import { RippleButton } from "./ui/ripple";
type SyncStatus = "disabled" | "syncing" | "synced" | "error" | "waiting";
/** A payload staged in the UI, before it is handed to the backend. */
type PendingSource =
| { kind: "archive"; fileName: string; data: number[] }
| { kind: "folder"; path: string };
const ARCHIVE_EXTENSIONS = [".crx", ".zip"];
function pathBaseName(path: string): string {
const segments = path.split(/[/\\]/).filter(Boolean);
return segments[segments.length - 1] ?? path;
}
function getSyncStatusDot(
item: { sync_enabled?: boolean; last_sync?: number },
liveStatus: SyncStatus | undefined,
@@ -148,14 +163,13 @@ export function ExtensionManagementDialog({
const [extensionGroups, setExtensionGroups] = useState<ExtensionGroup[]>([]);
const [isLoading, setIsLoading] = useState(false);
// Extension upload state
// Extension import state
const [isUploading, setIsUploading] = useState(false);
const [extensionName, setExtensionName] = useState("");
const [showUploadForm, setShowUploadForm] = useState(false);
const [pendingFile, setPendingFile] = useState<{
name: string;
data: number[];
} | null>(null);
const [pendingSource, setPendingSource] = useState<PendingSource | null>(
null,
);
const [linkFolder, setLinkFolder] = useState(false);
// Group state
const [showCreateGroup, setShowCreateGroup] = useState(false);
@@ -192,10 +206,9 @@ export function ExtensionManagementDialog({
null,
);
const [editExtensionName, setEditExtensionName] = useState("");
const [pendingUpdateFile, setPendingUpdateFile] = useState<{
name: string;
data: number[];
} | null>(null);
const [pendingUpdateSource, setPendingUpdateSource] =
useState<PendingSource | null>(null);
const [editLinkFolder, setEditLinkFolder] = useState(false);
// Extension icons
const [extensionIcons, setExtensionIcons] = useState<Record<string, string>>(
@@ -295,6 +308,30 @@ export function ExtensionManagementDialog({
};
}, []);
/** Structured backend codes win; anything else falls back to a local message
* so the user never sees a raw Rust string. */
const showActionError = useCallback(
(err: unknown, fallback: string) => {
showErrorToast(
parseBackendError(err) ? translateBackendError(t, err) : fallback,
);
},
[t],
);
const resetImportForm = useCallback(() => {
setPendingSource(null);
setExtensionName("");
setLinkFolder(false);
}, []);
const closeEditExtension = useCallback(() => {
setEditingExtension(null);
setEditExtensionName("");
setPendingUpdateSource(null);
setEditLinkFolder(false);
}, []);
const handleToggleExtSync = useCallback(
async (ext: Extension) => {
setIsTogglingExtSync((prev) => ({ ...prev, [ext.id]: true }));
@@ -310,16 +347,12 @@ export function ExtensionManagementDialog({
);
void loadData();
} catch (err) {
showErrorToast(
parseBackendError(err)
? translateBackendError(t, err)
: t("proxies.management.updateSyncFailed"),
);
showActionError(err, t("proxies.management.updateSyncFailed"));
} finally {
setIsTogglingExtSync((prev) => ({ ...prev, [ext.id]: false }));
}
},
[loadData, t],
[loadData, showActionError, t],
);
const handleToggleGroupSync = useCallback(
@@ -337,119 +370,178 @@ export function ExtensionManagementDialog({
);
void loadData();
} catch (err) {
showErrorToast(
parseBackendError(err)
? translateBackendError(t, err)
: t("proxies.management.updateSyncFailed"),
);
showActionError(err, t("proxies.management.updateSyncFailed"));
} finally {
setIsTogglingGroupSync((prev) => ({ ...prev, [group.id]: false }));
}
},
[loadData, t],
[loadData, showActionError, t],
);
const handleUpdateExtension = useCallback(async () => {
if (!editingExtension || !editExtensionName.trim()) return;
try {
await invoke("update_extension", {
extensionId: editingExtension.id,
name: editExtensionName.trim(),
fileName: pendingUpdateFile?.name ?? null,
fileData: pendingUpdateFile?.data ?? null,
});
if (pendingUpdateSource?.kind === "folder") {
await invoke("update_extension_from_path", {
extensionId: editingExtension.id,
name: editExtensionName.trim(),
path: pendingUpdateSource.path,
link: editLinkFolder,
});
} else {
await invoke("update_extension", {
extensionId: editingExtension.id,
name: editExtensionName.trim(),
fileName: pendingUpdateSource?.fileName ?? null,
fileData: pendingUpdateSource?.data ?? null,
});
}
showSuccessToast(t("extensions.updateSuccess"));
setEditingExtension(null);
setEditExtensionName("");
setPendingUpdateFile(null);
closeEditExtension();
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.updateFailed"));
}
}, [editingExtension, editExtensionName, pendingUpdateFile, loadData, t]);
}, [
editingExtension,
editExtensionName,
pendingUpdateSource,
editLinkFolder,
closeEditExtension,
loadData,
showActionError,
t,
]);
const handleEditFileSelect = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const validExtensions = [".xpi", ".crx", ".zip"];
const isValid = validExtensions.some((ext) =>
/** Reads a picked archive into memory, shared by the import and the replace
* flows. Resolves to null when the file is rejected or unreadable. */
const readArchiveFile = useCallback(
(file: File): Promise<PendingSource | null> => {
const isValid = ARCHIVE_EXTENSIONS.some((ext) =>
file.name.toLowerCase().endsWith(ext),
);
if (!isValid) {
showErrorToast(t("extensions.invalidFileType"));
return;
return Promise.resolve(null);
}
const reader = new FileReader();
reader.onload = (event) => {
const arrayBuffer = event.target?.result as ArrayBuffer;
const data = Array.from(new Uint8Array(arrayBuffer));
setPendingUpdateFile({ name: file.name, data });
};
reader.readAsArrayBuffer(file);
e.target.value = "";
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => {
const arrayBuffer = event.target?.result as ArrayBuffer;
resolve({
kind: "archive",
fileName: file.name,
data: Array.from(new Uint8Array(arrayBuffer)),
});
};
reader.onerror = () => {
showErrorToast(t("extensions.readError"));
resolve(null);
};
reader.readAsArrayBuffer(file);
});
},
[t],
);
const handleEditFileSelect = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
void readArchiveFile(file).then((source) => {
if (!source) return;
setPendingUpdateSource(source);
setEditLinkFolder(false);
});
},
[readArchiveFile],
);
const handleFileSelect = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
const validExtensions = [".xpi", ".crx", ".zip"];
const isValid = validExtensions.some((ext) =>
file.name.toLowerCase().endsWith(ext),
);
if (!isValid) {
showErrorToast(t("extensions.invalidFileType"));
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const arrayBuffer = event.target?.result as ArrayBuffer;
const data = Array.from(new Uint8Array(arrayBuffer));
const baseName = file.name
.replace(/\.(xpi|crx|zip)$/i, "")
.replace(/[-_]/g, " ");
setExtensionName(baseName);
setPendingFile({ name: file.name, data });
setShowUploadForm(true);
};
reader.onerror = () => {
showErrorToast(t("extensions.readError"));
};
reader.readAsArrayBuffer(file);
// Reset input
e.target.value = "";
void readArchiveFile(file).then((source) => {
if (!source) return;
setExtensionName(
file.name.replace(/\.(crx|zip)$/i, "").replace(/[-_]/g, " "),
);
setLinkFolder(false);
setPendingSource(source);
});
},
[t],
[readArchiveFile],
);
/** Native directory picker, the "Load unpacked" entry point. */
const pickExtensionFolder = useCallback(async (): Promise<string | null> => {
try {
const selected = await open({
directory: true,
multiple: false,
title: t("extensions.selectFolderTitle"),
});
return typeof selected === "string" ? selected : null;
} catch (err) {
console.error("Failed to open folder dialog:", err);
showErrorToast(t("importProfile.folderDialogFailed"));
return null;
}
}, [t]);
const handleLoadUnpacked = useCallback(async () => {
const folder = await pickExtensionFolder();
if (!folder) return;
setExtensionName(pathBaseName(folder).replace(/[-_]/g, " "));
setLinkFolder(false);
setPendingSource({ kind: "folder", path: folder });
}, [pickExtensionFolder]);
const handleEditFolderSelect = useCallback(async () => {
const folder = await pickExtensionFolder();
if (!folder) return;
setPendingUpdateSource({ kind: "folder", path: folder });
}, [pickExtensionFolder]);
const handleUpload = useCallback(async () => {
if (!pendingFile || !extensionName.trim()) return;
if (!pendingSource || !extensionName.trim()) return;
setIsUploading(true);
try {
await invoke("add_extension", {
name: extensionName.trim(),
fileName: pendingFile.name,
fileData: pendingFile.data,
});
if (pendingSource.kind === "folder") {
await invoke("add_unpacked_extension", {
name: extensionName.trim(),
path: pendingSource.path,
link: linkFolder,
});
} else {
await invoke("add_extension", {
name: extensionName.trim(),
fileName: pendingSource.fileName,
fileData: pendingSource.data,
});
}
showSuccessToast(t("extensions.uploadSuccess"));
setShowUploadForm(false);
setPendingFile(null);
setExtensionName("");
resetImportForm();
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.uploadFailed"));
} finally {
setIsUploading(false);
}
}, [pendingFile, extensionName, loadData, t]);
}, [
pendingSource,
extensionName,
linkFolder,
resetImportForm,
loadData,
showActionError,
t,
]);
const handleDeleteExtension = useCallback(async () => {
if (!extensionToDelete) return;
@@ -460,11 +552,11 @@ export function ExtensionManagementDialog({
setExtensionToDelete(null);
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.deleteFailed"));
} finally {
setIsDeleting(false);
}
}, [extensionToDelete, loadData, t]);
}, [extensionToDelete, loadData, showActionError, t]);
const handleCreateGroup = useCallback(async () => {
if (!newGroupName.trim()) return;
@@ -475,9 +567,9 @@ export function ExtensionManagementDialog({
setNewGroupName("");
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.groupCreateFailed"));
}
}, [newGroupName, loadData, t]);
}, [newGroupName, loadData, showActionError, t]);
const handleSaveGroupEdits = useCallback(async () => {
if (!editingGroup || !editGroupName.trim()) return;
@@ -518,9 +610,16 @@ export function ExtensionManagementDialog({
setEditGroupExtensionIds([]);
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.groupUpdateFailed"));
}
}, [editingGroup, editGroupName, editGroupExtensionIds, loadData, t]);
}, [
editingGroup,
editGroupName,
editGroupExtensionIds,
loadData,
showActionError,
t,
]);
const handleDeleteGroup = useCallback(async () => {
if (!groupToDelete) return;
@@ -531,11 +630,11 @@ export function ExtensionManagementDialog({
setGroupToDelete(null);
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.groupDeleteFailed"));
} finally {
setIsDeleting(false);
}
}, [groupToDelete, loadData, t]);
}, [groupToDelete, loadData, showActionError, t]);
const selectedExtensions = useMemo(
() => extensions.filter((ext) => extRowSelection[ext.id]),
@@ -561,11 +660,11 @@ export function ExtensionManagementDialog({
setExtRowSelection({});
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.deleteFailed"));
} finally {
setIsDeleting(false);
}
}, [selectedExtensions, loadData, t]);
}, [selectedExtensions, loadData, showActionError, t]);
const handleBulkDeleteGroups = useCallback(async () => {
if (selectedGroups.length === 0) return;
@@ -581,18 +680,27 @@ export function ExtensionManagementDialog({
setGroupRowSelection({});
void loadData();
} catch (err) {
showErrorToast(err instanceof Error ? err.message : String(err));
showActionError(err, t("extensions.groupDeleteFailed"));
} finally {
setIsDeleting(false);
}
}, [selectedGroups, loadData, t]);
}, [selectedGroups, loadData, showActionError, t]);
const handleBulkToggleExtSync = useCallback(async () => {
if (selectedExtensions.length === 0) return;
const allOn = selectedExtensions.every((e) => e.sync_enabled);
const targetEnabled = !allOn;
// A linked extension has no payload to upload, so enabling sync on one is
// refused by the backend. Skip them instead of failing the whole batch.
const targets = targetEnabled
? selectedExtensions.filter((ext) => !ext.linked_path)
: selectedExtensions;
if (targets.length === 0) {
showErrorToast(t("extensions.linkedNoSync"));
return;
}
const results = await Promise.allSettled(
selectedExtensions.map((ext) =>
targets.map((ext) =>
invoke("set_extension_sync_enabled", {
extensionId: ext.id,
enabled: targetEnabled,
@@ -603,10 +711,9 @@ export function ExtensionManagementDialog({
| PromiseRejectedResult
| undefined;
if (firstRejection) {
showErrorToast(
parseBackendError(firstRejection.reason)
? translateBackendError(t, firstRejection.reason)
: t("proxies.management.updateSyncFailed"),
showActionError(
firstRejection.reason,
t("proxies.management.updateSyncFailed"),
);
} else {
showSuccessToast(
@@ -616,7 +723,7 @@ export function ExtensionManagementDialog({
);
}
void loadData();
}, [selectedExtensions, loadData, t]);
}, [selectedExtensions, loadData, showActionError, t]);
const handleBulkToggleGroupSync = useCallback(async () => {
if (selectedGroups.length === 0) return;
@@ -634,10 +741,9 @@ export function ExtensionManagementDialog({
| PromiseRejectedResult
| undefined;
if (firstRejection) {
showErrorToast(
parseBackendError(firstRejection.reason)
? translateBackendError(t, firstRejection.reason)
: t("proxies.management.updateSyncFailed"),
showActionError(
firstRejection.reason,
t("proxies.management.updateSyncFailed"),
);
} else {
showSuccessToast(
@@ -647,7 +753,7 @@ export function ExtensionManagementDialog({
);
}
void loadData();
}, [selectedGroups, loadData, t]);
}, [selectedGroups, loadData, showActionError, t]);
const renderCompatIcons = useCallback(
(compat: string[]) => {
@@ -691,6 +797,42 @@ export function ExtensionManagementDialog({
[extensionIcons],
);
/** What the extension actually is: a stored archive, a folder packed into
* the store, or a folder loaded in place from the user's disk. */
const renderSource = useCallback(
(ext: Extension) => {
if (ext.linked_path) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground">
<LuLink className="size-3 shrink-0" />
<span className="truncate">
{t("extensions.source.linked")}
</span>
</span>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs break-all">
{t("extensions.source.linkedTooltip", {
path: ext.linked_path,
})}
</p>
</TooltipContent>
</Tooltip>
);
}
return (
<span className="block min-w-0 truncate text-xs text-muted-foreground">
{ext.source_kind === "unpacked"
? t("extensions.source.unpacked")
: t("extensions.source.archive")}
</span>
);
},
[t],
);
const MAX_VISIBLE_ICONS = 3;
const extensionColumns = useMemo<ColumnDef<Extension>[]>(
@@ -762,6 +904,13 @@ export function ExtensionManagementDialog({
cell: ({ row }) =>
renderCompatIcons(row.original.browser_compatibility),
},
{
id: "source",
size: 128,
enableSorting: false,
header: () => null,
cell: ({ row }) => renderSource(row.original),
},
{
id: "sync",
size: 88,
@@ -770,6 +919,7 @@ export function ExtensionManagementDialog({
cell: ({ row }) => {
const ext = row.original;
const syncDot = getSyncStatusDot(ext, extSyncStatus[ext.id], t);
const isLinked = Boolean(ext.linked_path);
return (
<div className="flex shrink-0 items-center gap-2">
<Tooltip>
@@ -790,15 +940,17 @@ export function ExtensionManagementDialog({
<AnimatedSwitch
checked={ext.sync_enabled}
onCheckedChange={() => void handleToggleExtSync(ext)}
disabled={isTogglingExtSync[ext.id]}
disabled={isLinked || isTogglingExtSync[ext.id]}
/>
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{ext.sync_enabled
? t("syncTooltips.disable")
: t("syncTooltips.enable")}
{isLinked
? t("extensions.linkedNoSync")
: ext.sync_enabled
? t("syncTooltips.disable")
: t("syncTooltips.enable")}
</p>
</TooltipContent>
</Tooltip>
@@ -824,7 +976,8 @@ export function ExtensionManagementDialog({
onClick={() => {
setEditingExtension(ext);
setEditExtensionName(ext.name);
setPendingUpdateFile(null);
setPendingUpdateSource(null);
setEditLinkFolder(Boolean(ext.linked_path));
}}
>
<LuPencil className="size-3.5" />
@@ -859,6 +1012,7 @@ export function ExtensionManagementDialog({
handleToggleExtSync,
renderExtensionIcon,
renderCompatIcons,
renderSource,
],
);
@@ -1160,25 +1314,48 @@ export function ExtensionManagementDialog({
</AnimatedTabsList>
<div className="flex items-center gap-2">
{activeTab === "extensions" && (
<Tooltip>
<TooltipTrigger asChild>
<RippleButton
size="sm"
variant="outline"
disabled={limitedMode}
onClick={() =>
document.getElementById("ext-file-input")?.click()
}
aria-label={t("extensions.upload")}
>
<LuUpload className="size-4" />
<span className="hidden @2xl:inline">
{t("extensions.upload")}
</span>
</RippleButton>
</TooltipTrigger>
<TooltipContent>{t("extensions.upload")}</TooltipContent>
</Tooltip>
<>
<Tooltip>
<TooltipTrigger asChild>
<RippleButton
size="sm"
variant="outline"
disabled={limitedMode}
onClick={() =>
document.getElementById("ext-file-input")?.click()
}
aria-label={t("extensions.upload")}
>
<LuUpload className="size-4" />
<span className="hidden @2xl:inline">
{t("extensions.upload")}
</span>
</RippleButton>
</TooltipTrigger>
<TooltipContent>
{t("extensions.upload")}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<RippleButton
size="sm"
variant="outline"
disabled={limitedMode}
onClick={() => void handleLoadUnpacked()}
aria-label={t("extensions.loadUnpacked")}
>
<LuFolderOpen className="size-4" />
<span className="hidden @2xl:inline">
{t("extensions.loadUnpacked")}
</span>
</RippleButton>
</TooltipTrigger>
<TooltipContent>
{t("extensions.loadUnpackedTooltip")}
</TooltipContent>
</Tooltip>
</>
)}
{activeTab === "groups" && (
<Tooltip>
@@ -1216,21 +1393,51 @@ export function ExtensionManagementDialog({
<Input
id="ext-file-input"
type="file"
accept=".xpi,.crx,.zip"
accept=".crx,.zip"
className="hidden"
onChange={handleFileSelect}
disabled={limitedMode}
/>
{/* Upload form */}
{showUploadForm && pendingFile && (
{/* Import form */}
{pendingSource && (
<div className="space-y-3 rounded-md border p-3">
<div className="text-sm text-muted-foreground">
{t("extensions.selectedFile")}:{" "}
<span className="font-medium text-foreground">
{pendingFile.name}
{pendingSource.kind === "folder"
? t("extensions.selectedFolder")
: t("extensions.selectedFile")}
:{" "}
<span className="font-medium break-all text-foreground">
{pendingSource.kind === "folder"
? pendingSource.path
: pendingSource.fileName}
</span>
</div>
{pendingSource.kind === "folder" && (
<div className="flex items-start gap-2">
<Checkbox
id="ext-link-folder"
checked={linkFolder}
onCheckedChange={(value) => {
setLinkFolder(value === true);
}}
className="mt-0.5"
/>
<div className="space-y-0.5">
<Label
htmlFor="ext-link-folder"
className="text-sm font-normal"
>
{t("extensions.linkFolder")}
</Label>
<p className="text-xs text-muted-foreground">
{linkFolder
? t("extensions.linkFolderOn")
: t("extensions.linkFolderOff")}
</p>
</div>
</div>
)}
<div className="flex gap-2">
<Input
value={extensionName}
@@ -1252,11 +1459,7 @@ export function ExtensionManagementDialog({
<Button
size="sm"
variant="outline"
onClick={() => {
setShowUploadForm(false);
setPendingFile(null);
setExtensionName("");
}}
onClick={resetImportForm}
>
{t("common.buttons.cancel")}
</Button>
@@ -1611,11 +1814,7 @@ export function ExtensionManagementDialog({
<Dialog
open={editingExtension !== null}
onOpenChange={(open) => {
if (!open) {
setEditingExtension(null);
setEditExtensionName("");
setPendingUpdateFile(null);
}
if (!open) closeEditExtension();
}}
>
<DialogContent className="flex max-h-[90vh] max-w-lg flex-col">
@@ -1684,9 +1883,35 @@ export function ExtensionManagementDialog({
)}
</div>
<span className="text-muted-foreground">
{t("common.labels.type")}
{t("extensions.source.label")}
</span>
<span>.{editingExtension.file_type}</span>
<span>
{editingExtension.linked_path
? t("extensions.source.linked")
: editingExtension.source_kind === "unpacked"
? t("extensions.source.unpacked")
: t("extensions.source.archive")}
</span>
{editingExtension.linked_path ? (
<>
<span className="text-muted-foreground">
{t("extensions.source.folderLabel")}
</span>
<span className="break-all">
{editingExtension.linked_path}
</span>
<p className="col-span-2 text-xs text-muted-foreground">
{t("extensions.linkFolderOn")}
</p>
</>
) : (
<>
<span className="text-muted-foreground">
{t("common.labels.type")}
</span>
<span>.{editingExtension.file_type}</span>
</>
)}
{editingExtension.homepage_url && (
<>
<span className="text-muted-foreground">
@@ -1716,10 +1941,10 @@ export function ExtensionManagementDialog({
</div>
</div>
{/* Re-upload */}
{/* Replace the payload with another archive or folder */}
<div className="space-y-2">
<Label>{t("extensions.reupload")}</Label>
<div className="flex items-center gap-2">
<Label>{t("extensions.replaceSource")}</Label>
<div className="flex flex-wrap items-center gap-2">
<RippleButton
size="sm"
variant="outline"
@@ -1733,30 +1958,58 @@ export function ExtensionManagementDialog({
<input
id="ext-edit-file-input"
type="file"
accept=".xpi,.crx,.zip"
accept=".crx,.zip"
className="hidden"
onChange={handleEditFileSelect}
/>
{pendingUpdateFile && (
<RippleButton
size="sm"
variant="outline"
onClick={() => void handleEditFolderSelect()}
>
<LuFolderOpen className="mr-1 size-3" />
{t("extensions.selectFolder")}
</RippleButton>
{pendingUpdateSource && (
<span className="max-w-[200px] truncate text-xs text-muted-foreground">
{pendingUpdateFile.name}
{pendingUpdateSource.kind === "folder"
? pendingUpdateSource.path
: pendingUpdateSource.fileName}
</span>
)}
</div>
{pendingUpdateSource?.kind === "folder" && (
<div className="flex items-start gap-2 pt-1">
<Checkbox
id="ext-edit-link-folder"
checked={editLinkFolder}
onCheckedChange={(value) => {
setEditLinkFolder(value === true);
}}
className="mt-0.5"
/>
<div className="space-y-0.5">
<Label
htmlFor="ext-edit-link-folder"
className="text-sm font-normal"
>
{t("extensions.linkFolder")}
</Label>
<p className="text-xs text-muted-foreground">
{editLinkFolder
? t("extensions.linkFolderOn")
: t("extensions.linkFolderOff")}
</p>
</div>
</div>
)}
</div>
</div>
)}
</ScrollArea>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setEditingExtension(null);
setEditExtensionName("");
setPendingUpdateFile(null);
}}
>
<Button variant="outline" onClick={closeEditExtension}>
{t("common.buttons.cancel")}
</Button>
<RippleButton
+35 -3
View File
@@ -1271,7 +1271,7 @@
"deleteConfirmDescription": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleteGroupConfirmTitle": "Delete Extension Group",
"deleteGroupConfirmDescription": "Are you sure you want to delete the group \"{{name}}\"? This action cannot be undone.",
"invalidFileType": "Invalid file type. Please upload a .crx, .xpi, or .zip file.",
"invalidFileType": "Invalid file type. Please choose a .crx or .zip file.",
"readError": "Failed to read the extension file.",
"assignTitle": "Assign Extension Group",
"assignDescription": "Assign {{count}} selected profile(s) to an extension group.",
@@ -1279,7 +1279,6 @@
"assignSuccess": "Extension group assigned successfully",
"editExtension": "Edit extension",
"updateSuccess": "Extension updated successfully",
"reupload": "Re-upload",
"version": "Version",
"author": "Author",
"homepage": "Homepage",
@@ -1287,10 +1286,26 @@
"editGroupDescription": "Update the group name and manage which extensions are included.",
"groupExtensions": "Extensions in this group",
"noExtensionsInGroup": "No extensions added yet",
"editExtensionDescription": "Update extension name, view metadata, or re-upload the extension file.",
"editExtensionDescription": "Update the extension name, view its metadata, or replace it with another archive or folder.",
"metadata": "Metadata",
"noMetadata": "No metadata available from manifest.",
"selectFile": "Choose File",
"loadUnpacked": "Load unpacked",
"loadUnpackedTooltip": "Load an extension from a folder containing manifest.json",
"selectFolderTitle": "Select Extension Folder",
"selectedFolder": "Selected folder",
"selectFolder": "Choose Folder",
"linkFolder": "Load in place from this folder",
"linkFolderOff": "The folder is copied into Donut. The extension is portable and syncs to your other devices.",
"linkFolderOn": "Donut loads the extension straight from this folder on every launch. Your edits apply on the next browser start, but the extension stays on this machine and never syncs.",
"replaceSource": "Replace source",
"linkedNoSync": "Linked extensions stay on this machine and can't sync.",
"uploadFailed": "Failed to add extension",
"updateFailed": "Failed to update extension",
"deleteFailed": "Failed to delete extension",
"groupCreateFailed": "Failed to create extension group",
"groupUpdateFailed": "Failed to update extension group",
"groupDeleteFailed": "Failed to delete extension group",
"syncEnabled": "Sync enabled",
"syncDisabled": "Sync disabled",
"syncEnableTooltip": "Enable sync",
@@ -1303,6 +1318,14 @@
"groupsTitle": "Delete extension groups",
"groupsDescription": "Delete {{count}} extension groups? {{names}}",
"confirmButton": "Delete"
},
"source": {
"label": "Source",
"archive": "Archive",
"unpacked": "Unpacked folder",
"linked": "Linked folder",
"folderLabel": "Folder",
"linkedTooltip": "Loaded in place from {{path}}"
}
},
"pro": {
@@ -1842,6 +1865,15 @@
"vpnNotFound": "VPN not found",
"extensionNotFound": "Extension not found",
"extensionGroupNotFound": "Extension group not found",
"extensionUnsupportedFileType": "That file type isn't supported. An extension has to be a .crx or .zip archive, or a folder.",
"extensionDirNotFound": "That folder no longer exists.",
"extensionNotADirectory": "That path is not a folder.",
"extensionManifestMissing": "There is no manifest.json in that folder. Choose the folder that holds the extension's manifest.json.",
"extensionManifestInvalid": "The manifest.json in that folder could not be read.",
"extensionDirTooLarge": "That folder is too large to copy into Donut (the limit is 256 MB and 20,000 files). Link it in place instead.",
"extensionPathHasComma": "That folder's path contains a comma, which Chromium cannot load. Rename or move the folder.",
"extensionLinkRequiresDirectory": "Only a folder can be loaded in place. Turn linking off to add an archive.",
"extensionLinkedCannotSync": "This extension is loaded from a folder on this machine, so there is nothing to sync.",
"cannotModifyCloudManagedProxy": "Cannot modify sync for a cloud-managed proxy",
"syncLockedByProfile": "Sync cannot be disabled while this is used by synced profiles",
"syncNotConfigured": "Sync is not configured. Sign in or configure a self-hosted server first.",
+35 -3
View File
@@ -1274,7 +1274,7 @@
"deleteConfirmDescription": "¿Estás seguro de que deseas eliminar \"{{name}}\"? Esta acción no se puede deshacer.",
"deleteGroupConfirmTitle": "Eliminar Grupo de Extensiones",
"deleteGroupConfirmDescription": "¿Estás seguro de que deseas eliminar el grupo \"{{name}}\"? Esta acción no se puede deshacer.",
"invalidFileType": "Tipo de archivo no válido. Suba un archivo .crx, .xpi o .zip.",
"invalidFileType": "Tipo de archivo no válido. Elige un archivo .crx o .zip.",
"readError": "No se pudo leer el archivo de extensión.",
"assignTitle": "Asignar Grupo de Extensiones",
"assignDescription": "Asignar {{count}} perfil(es) seleccionado(s) a un grupo de extensiones.",
@@ -1282,7 +1282,6 @@
"assignSuccess": "Grupo de extensiones asignado exitosamente",
"editExtension": "Editar extensión",
"updateSuccess": "Extensión actualizada exitosamente",
"reupload": "Re-subir",
"version": "Versión",
"author": "Autor",
"homepage": "Página de inicio",
@@ -1290,10 +1289,26 @@
"editGroupDescription": "Actualiza el nombre del grupo y gestiona qué extensiones están incluidas.",
"groupExtensions": "Extensiones en este grupo",
"noExtensionsInGroup": "Aún no se han añadido extensiones",
"editExtensionDescription": "Actualizar el nombre de la extensión, ver metadatos o volver a cargar el archivo de extensión.",
"editExtensionDescription": "Actualiza el nombre de la extensión, consulta sus metadatos o reemplázala por otro archivo comprimido o carpeta.",
"metadata": "Metadatos",
"noMetadata": "No hay metadatos disponibles del manifiesto.",
"selectFile": "Elegir archivo",
"loadUnpacked": "Cargar sin empaquetar",
"loadUnpackedTooltip": "Carga una extensión desde una carpeta que contenga manifest.json",
"selectFolderTitle": "Seleccionar carpeta de la extensión",
"selectedFolder": "Carpeta seleccionada",
"selectFolder": "Elegir carpeta",
"linkFolder": "Cargar directamente desde esta carpeta",
"linkFolderOff": "La carpeta se copia en Donut. La extensión es portátil y se sincroniza con tus otros dispositivos.",
"linkFolderOn": "Donut carga la extensión directamente desde esta carpeta en cada inicio. Tus cambios se aplican al abrir el navegador de nuevo, pero la extensión permanece en este equipo y nunca se sincroniza.",
"replaceSource": "Reemplazar origen",
"linkedNoSync": "Las extensiones enlazadas permanecen en este equipo y no se pueden sincronizar.",
"uploadFailed": "No se pudo añadir la extensión",
"updateFailed": "No se pudo actualizar la extensión",
"deleteFailed": "No se pudo eliminar la extensión",
"groupCreateFailed": "No se pudo crear el grupo de extensiones",
"groupUpdateFailed": "No se pudo actualizar el grupo de extensiones",
"groupDeleteFailed": "No se pudo eliminar el grupo de extensiones",
"syncEnabled": "Sincronización habilitada",
"syncDisabled": "Sincronización deshabilitada",
"syncEnableTooltip": "Habilitar sincronización",
@@ -1306,6 +1321,14 @@
"groupsTitle": "Eliminar grupos de extensiones",
"groupsDescription": "¿Eliminar {{count}} grupos de extensiones? {{names}}",
"confirmButton": "Eliminar"
},
"source": {
"label": "Origen",
"archive": "Archivo comprimido",
"unpacked": "Carpeta sin empaquetar",
"linked": "Carpeta enlazada",
"folderLabel": "Carpeta",
"linkedTooltip": "Se carga directamente desde {{path}}"
}
},
"pro": {
@@ -1849,6 +1872,15 @@
"vpnNotFound": "VPN no encontrada",
"extensionNotFound": "Extensión no encontrada",
"extensionGroupNotFound": "Grupo de extensiones no encontrado",
"extensionUnsupportedFileType": "Ese tipo de archivo no es compatible. Una extensión debe ser un archivo .crx o .zip, o una carpeta.",
"extensionDirNotFound": "Esa carpeta ya no existe.",
"extensionNotADirectory": "Esa ruta no es una carpeta.",
"extensionManifestMissing": "No hay ningún manifest.json en esa carpeta. Elige la carpeta que contiene el manifest.json de la extensión.",
"extensionManifestInvalid": "No se pudo leer el manifest.json de esa carpeta.",
"extensionDirTooLarge": "Esa carpeta es demasiado grande para copiarla en Donut (el límite es 256 MB y 20 000 archivos). Enlázala en su ubicación.",
"extensionPathHasComma": "La ruta de esa carpeta contiene una coma, que Chromium no puede cargar. Cambia el nombre de la carpeta o muévela.",
"extensionLinkRequiresDirectory": "Solo se puede cargar en su ubicación una carpeta. Desactiva el enlace para añadir un archivo comprimido.",
"extensionLinkedCannotSync": "Esta extensión se carga desde una carpeta de este equipo, así que no hay nada que sincronizar.",
"cannotModifyCloudManagedProxy": "No se puede modificar la sincronización de un proxy gestionado en la nube",
"syncLockedByProfile": "No se puede desactivar la sincronización mientras se usa en perfiles sincronizados",
"syncNotConfigured": "La sincronización no está configurada. Inicia sesión o configura un servidor propio.",
+35 -3
View File
@@ -1274,7 +1274,7 @@
"deleteConfirmDescription": "Êtes-vous sûr de vouloir supprimer \"{{name}}\" ? Cette action est irréversible.",
"deleteGroupConfirmTitle": "Supprimer le Groupe d'Extensions",
"deleteGroupConfirmDescription": "Êtes-vous sûr de vouloir supprimer le groupe \"{{name}}\" ? Cette action est irréversible.",
"invalidFileType": "Type de fichier non valide. Veuillez télécharger un fichier .crx, .xpi ou .zip.",
"invalidFileType": "Type de fichier non valide. Choisissez un fichier .crx ou .zip.",
"readError": "Impossible de lire le fichier d'extension.",
"assignTitle": "Assigner un Groupe d'Extensions",
"assignDescription": "Assigner {{count}} profil(s) sélectionné(s) à un groupe d'extensions.",
@@ -1282,7 +1282,6 @@
"assignSuccess": "Groupe d'extensions assigné avec succès",
"editExtension": "Modifier l'extension",
"updateSuccess": "Extension mise à jour avec succès",
"reupload": "Re-télécharger",
"version": "Version",
"author": "Auteur",
"homepage": "Page d'accueil",
@@ -1290,10 +1289,26 @@
"editGroupDescription": "Mettez à jour le nom du groupe et gérez les extensions incluses.",
"groupExtensions": "Extensions dans ce groupe",
"noExtensionsInGroup": "Aucune extension ajoutée",
"editExtensionDescription": "Modifier le nom de l'extension, voir les métadonnées ou re-télécharger le fichier d'extension.",
"editExtensionDescription": "Modifiez le nom de l'extension, consultez ses métadonnées ou remplacez-la par une autre archive ou un autre dossier.",
"metadata": "Métadonnées",
"noMetadata": "Aucune métadonnée disponible depuis le manifeste.",
"selectFile": "Choisir un fichier",
"loadUnpacked": "Charger non empaquetée",
"loadUnpackedTooltip": "Chargez une extension depuis un dossier contenant manifest.json",
"selectFolderTitle": "Sélectionner le dossier de l'extension",
"selectedFolder": "Dossier sélectionné",
"selectFolder": "Choisir un dossier",
"linkFolder": "Charger directement depuis ce dossier",
"linkFolderOff": "Le dossier est copié dans Donut. L'extension est portable et se synchronise avec vos autres appareils.",
"linkFolderOn": "Donut charge l'extension directement depuis ce dossier à chaque lancement. Vos modifications s'appliquent au prochain démarrage du navigateur, mais l'extension reste sur cet ordinateur et ne se synchronise jamais.",
"replaceSource": "Remplacer la source",
"linkedNoSync": "Les extensions liées restent sur cet ordinateur et ne peuvent pas être synchronisées.",
"uploadFailed": "Échec de l'ajout de l'extension",
"updateFailed": "Échec de la mise à jour de l'extension",
"deleteFailed": "Échec de la suppression de l'extension",
"groupCreateFailed": "Échec de la création du groupe d'extensions",
"groupUpdateFailed": "Échec de la mise à jour du groupe d'extensions",
"groupDeleteFailed": "Échec de la suppression du groupe d'extensions",
"syncEnabled": "Synchronisation activée",
"syncDisabled": "Synchronisation désactivée",
"syncEnableTooltip": "Activer la synchronisation",
@@ -1306,6 +1321,14 @@
"groupsTitle": "Supprimer les groupes d'extensions",
"groupsDescription": "Supprimer {{count}} groupes d'extensions ? {{names}}",
"confirmButton": "Supprimer"
},
"source": {
"label": "Source",
"archive": "Archive",
"unpacked": "Dossier non empaqueté",
"linked": "Dossier lié",
"folderLabel": "Dossier",
"linkedTooltip": "Chargée directement depuis {{path}}"
}
},
"pro": {
@@ -1849,6 +1872,15 @@
"vpnNotFound": "VPN introuvable",
"extensionNotFound": "Extension introuvable",
"extensionGroupNotFound": "Groupe d'extensions introuvable",
"extensionUnsupportedFileType": "Ce type de fichier n'est pas pris en charge. Une extension doit être une archive .crx ou .zip, ou un dossier.",
"extensionDirNotFound": "Ce dossier n'existe plus.",
"extensionNotADirectory": "Ce chemin n'est pas un dossier.",
"extensionManifestMissing": "Il n'y a pas de manifest.json dans ce dossier. Choisissez le dossier qui contient le manifest.json de l'extension.",
"extensionManifestInvalid": "Le fichier manifest.json de ce dossier n'a pas pu être lu.",
"extensionDirTooLarge": "Ce dossier est trop volumineux pour être copié dans Donut (la limite est de 256 Mo et 20 000 fichiers). Liez-le sur place à la place.",
"extensionPathHasComma": "Le chemin de ce dossier contient une virgule, que Chromium ne peut pas charger. Renommez ou déplacez le dossier.",
"extensionLinkRequiresDirectory": "Seul un dossier peut être chargé sur place. Désactivez la liaison pour ajouter une archive.",
"extensionLinkedCannotSync": "Cette extension est chargée depuis un dossier de cet ordinateur : il n'y a rien à synchroniser.",
"cannotModifyCloudManagedProxy": "Impossible de modifier la synchronisation d'un proxy géré dans le cloud",
"syncLockedByProfile": "La synchronisation ne peut pas être désactivée tant qu'elle est utilisée par des profils synchronisés",
"syncNotConfigured": "La synchronisation n'est pas configurée. Connectez-vous ou configurez un serveur auto-hébergé.",
+35 -3
View File
@@ -1271,7 +1271,7 @@
"deleteConfirmDescription": "「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。",
"deleteGroupConfirmTitle": "拡張機能グループを削除",
"deleteGroupConfirmDescription": "グループ「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。",
"invalidFileType": "無効なファイルタイプです。.crx、.xpi、または .zip ファイルをアップロードしてください。",
"invalidFileType": "ファイル形式が正しくありません。.crx または .zip ファイルを選択してください。",
"readError": "拡張機能ファイルの読み取りに失敗しました。",
"assignTitle": "拡張機能グループの割り当て",
"assignDescription": "選択した{{count}}件のプロファイルを拡張機能グループに割り当てます。",
@@ -1279,7 +1279,6 @@
"assignSuccess": "拡張機能グループが正常に割り当てられました",
"editExtension": "拡張機能を編集",
"updateSuccess": "拡張機能が正常に更新されました",
"reupload": "再アップロード",
"version": "バージョン",
"author": "作者",
"homepage": "ホームページ",
@@ -1287,10 +1286,26 @@
"editGroupDescription": "グループ名を更新し、含まれる拡張機能を管理します。",
"groupExtensions": "このグループの拡張機能",
"noExtensionsInGroup": "拡張機能がまだ追加されていません",
"editExtensionDescription": "拡張機能の名前を更新、メタデータを表示、またはファイルを再アップロードします。",
"editExtensionDescription": "拡張機能の名前を変更したり、メタデータを確認したり、別のアーカイブやフォルダに置き換えたりできます。",
"metadata": "メタデータ",
"noMetadata": "マニフェストからのメタデータはありません。",
"selectFile": "ファイルを選択",
"loadUnpacked": "フォルダから読み込む",
"loadUnpackedTooltip": "manifest.json を含むフォルダから拡張機能を読み込みます",
"selectFolderTitle": "拡張機能のフォルダを選択",
"selectedFolder": "選択したフォルダ",
"selectFolder": "フォルダを選択",
"linkFolder": "このフォルダから直接読み込む",
"linkFolderOff": "フォルダは Donut にコピーされます。拡張機能は持ち運べるようになり、他のデバイスにも同期されます。",
"linkFolderOn": "Donut は起動のたびにこのフォルダから直接拡張機能を読み込みます。編集内容は次回のブラウザ起動時に反映されますが、拡張機能はこの端末にのみ残り、同期されません。",
"replaceSource": "ソースを置き換える",
"linkedNoSync": "リンクした拡張機能はこの端末にのみ残るため、同期できません。",
"uploadFailed": "拡張機能を追加できませんでした",
"updateFailed": "拡張機能を更新できませんでした",
"deleteFailed": "拡張機能を削除できませんでした",
"groupCreateFailed": "拡張機能グループを作成できませんでした",
"groupUpdateFailed": "拡張機能グループを更新できませんでした",
"groupDeleteFailed": "拡張機能グループを削除できませんでした",
"syncEnabled": "同期が有効",
"syncDisabled": "同期が無効",
"syncEnableTooltip": "同期を有効にする",
@@ -1303,6 +1318,14 @@
"groupsTitle": "拡張機能グループを削除",
"groupsDescription": "{{count}}件の拡張機能グループを削除しますか? {{names}}",
"confirmButton": "削除"
},
"source": {
"label": "ソース",
"archive": "アーカイブ",
"unpacked": "展開済みフォルダ",
"linked": "リンクされたフォルダ",
"folderLabel": "フォルダ",
"linkedTooltip": "{{path}} から直接読み込みます"
}
},
"pro": {
@@ -1842,6 +1865,15 @@
"vpnNotFound": "VPNが見つかりません",
"extensionNotFound": "拡張機能が見つかりません",
"extensionGroupNotFound": "拡張機能グループが見つかりません",
"extensionUnsupportedFileType": "この形式には対応していません。拡張機能は .crx または .zip アーカイブ、あるいはフォルダである必要があります。",
"extensionDirNotFound": "そのフォルダは存在しません。",
"extensionNotADirectory": "そのパスはフォルダではありません。",
"extensionManifestMissing": "そのフォルダに manifest.json がありません。拡張機能の manifest.json があるフォルダを選択してください。",
"extensionManifestInvalid": "そのフォルダの manifest.json を読み取れませんでした。",
"extensionDirTooLarge": "そのフォルダは大きすぎて Donut にコピーできません(上限は 256 MB・20,000 ファイル)。代わりにリンクして読み込んでください。",
"extensionPathHasComma": "そのフォルダのパスにカンマが含まれており、Chromium が読み込めません。フォルダの名前を変更するか、移動してください。",
"extensionLinkRequiresDirectory": "その場で読み込めるのはフォルダのみです。アーカイブを追加するにはリンクをオフにしてください。",
"extensionLinkedCannotSync": "この拡張機能はこの端末のフォルダから読み込まれているため、同期する対象がありません。",
"cannotModifyCloudManagedProxy": "クラウド管理のプロキシの同期は変更できません",
"syncLockedByProfile": "同期済みプロファイルで使用中のため、同期を無効にできません",
"syncNotConfigured": "同期が設定されていません。サインインするか、セルフホストサーバーを設定してください。",
+35 -3
View File
@@ -1271,7 +1271,7 @@
"deleteConfirmDescription": "\"{{name}}\"을(를) 정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
"deleteGroupConfirmTitle": "확장 프로그램 그룹 삭제",
"deleteGroupConfirmDescription": "그룹 \"{{name}}\"을(를) 정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
"invalidFileType": "잘못된 파일 형입니다. .crx, .xpi 또는 .zip 파일을 업로드하세요.",
"invalidFileType": "지원하지 않는 파일 형입니다. .crx 또는 .zip 파일을 선택하세요.",
"readError": "확장 프로그램 파일 읽기 실패.",
"assignTitle": "확장 프로그램 그룹 할당",
"assignDescription": "선택한 {{count}}개 프로필을 확장 프로그램 그룹에 할당합니다.",
@@ -1279,7 +1279,6 @@
"assignSuccess": "확장 프로그램 그룹이 할당되었습니다",
"editExtension": "확장 프로그램 편집",
"updateSuccess": "확장 프로그램이 업데이트되었습니다",
"reupload": "다시 업로드",
"version": "버전",
"author": "작성자",
"homepage": "홈페이지",
@@ -1287,10 +1286,26 @@
"editGroupDescription": "그룹 이름을 업데이트하고 포함된 확장 프로그램을 관리합니다.",
"groupExtensions": "이 그룹의 확장 프로그램",
"noExtensionsInGroup": "아직 추가된 확장 프로그램이 없습니다",
"editExtensionDescription": "확장 프로그램 이름을 업데이트하거나, 메타데이터를 보거나, 확장 프로그램 파일을 다시 업로드합니다.",
"editExtensionDescription": "확장 프로그램 이름을 변경하고, 메타데이터를 확인하고, 다른 압축 파일이나 폴더로 교체할 수 있습니다.",
"metadata": "메타데이터",
"noMetadata": "manifest에서 사용할 수 있는 메타데이터가 없습니다.",
"selectFile": "파일 선택",
"loadUnpacked": "폴더에서 불러오기",
"loadUnpackedTooltip": "manifest.json이 있는 폴더에서 확장 프로그램을 불러옵니다",
"selectFolderTitle": "확장 프로그램 폴더 선택",
"selectedFolder": "선택한 폴더",
"selectFolder": "폴더 선택",
"linkFolder": "이 폴더에서 바로 불러오기",
"linkFolderOff": "폴더가 Donut으로 복사됩니다. 확장 프로그램을 옮길 수 있고 다른 기기와 동기화됩니다.",
"linkFolderOn": "Donut이 실행할 때마다 이 폴더에서 바로 확장 프로그램을 불러옵니다. 수정한 내용은 브라우저를 다시 시작할 때 적용되지만, 확장 프로그램은 이 컴퓨터에만 남고 동기화되지 않습니다.",
"replaceSource": "소스 교체",
"linkedNoSync": "연결된 확장 프로그램은 이 컴퓨터에만 있어 동기화할 수 없습니다.",
"uploadFailed": "확장 프로그램을 추가하지 못했습니다",
"updateFailed": "확장 프로그램을 업데이트하지 못했습니다",
"deleteFailed": "확장 프로그램을 삭제하지 못했습니다",
"groupCreateFailed": "확장 프로그램 그룹을 만들지 못했습니다",
"groupUpdateFailed": "확장 프로그램 그룹을 업데이트하지 못했습니다",
"groupDeleteFailed": "확장 프로그램 그룹을 삭제하지 못했습니다",
"syncEnabled": "동기화 사용됨",
"syncDisabled": "동기화 사용 안 함",
"syncEnableTooltip": "동기화 사용",
@@ -1303,6 +1318,14 @@
"groupsTitle": "확장 프로그램 그룹 삭제",
"groupsDescription": "{{count}}개의 확장 프로그램 그룹을 삭제하시겠습니까? {{names}}",
"confirmButton": "삭제"
},
"source": {
"label": "소스",
"archive": "압축 파일",
"unpacked": "압축 해제된 폴더",
"linked": "연결된 폴더",
"folderLabel": "폴더",
"linkedTooltip": "{{path}}에서 바로 불러옵니다"
}
},
"pro": {
@@ -1842,6 +1865,15 @@
"vpnNotFound": "VPN을 찾을 수 없습니다",
"extensionNotFound": "확장 프로그램을 찾을 수 없습니다",
"extensionGroupNotFound": "확장 프로그램 그룹을 찾을 수 없습니다",
"extensionUnsupportedFileType": "지원하지 않는 파일 형식입니다. 확장 프로그램은 .crx 또는 .zip 압축 파일이거나 폴더여야 합니다.",
"extensionDirNotFound": "해당 폴더가 더 이상 존재하지 않습니다.",
"extensionNotADirectory": "해당 경로는 폴더가 아닙니다.",
"extensionManifestMissing": "해당 폴더에 manifest.json이 없습니다. 확장 프로그램의 manifest.json이 있는 폴더를 선택하세요.",
"extensionManifestInvalid": "해당 폴더의 manifest.json을 읽을 수 없습니다.",
"extensionDirTooLarge": "폴더가 너무 커서 Donut으로 복사할 수 없습니다(최대 256MB, 20,000개 파일). 대신 폴더를 연결해 사용하세요.",
"extensionPathHasComma": "폴더 경로에 쉼표가 있어 Chromium이 불러올 수 없습니다. 폴더 이름을 바꾸거나 옮기세요.",
"extensionLinkRequiresDirectory": "폴더만 그 자리에서 불러올 수 있습니다. 압축 파일을 추가하려면 연결을 끄세요.",
"extensionLinkedCannotSync": "이 확장 프로그램은 이 컴퓨터의 폴더에서 불러오므로 동기화할 항목이 없습니다.",
"cannotModifyCloudManagedProxy": "클라우드 관리 프록시의 동기화는 수정할 수 없습니다",
"syncLockedByProfile": "동기화된 프로필에서 사용 중인 동안에는 동기화를 비활성화할 수 없습니다",
"syncNotConfigured": "동기화가 구성되지 않았습니다. 먼저 로그인하거나 자체 호스팅 서버를 구성하세요.",
+35 -3
View File
@@ -1274,7 +1274,7 @@
"deleteConfirmDescription": "Tem certeza de que deseja excluir \"{{name}}\"? Esta ação não pode ser desfeita.",
"deleteGroupConfirmTitle": "Excluir Grupo de Extensões",
"deleteGroupConfirmDescription": "Tem certeza de que deseja excluir o grupo \"{{name}}\"? Esta ação não pode ser desfeita.",
"invalidFileType": "Tipo de arquivo inválido. Envie um arquivo .crx, .xpi ou .zip.",
"invalidFileType": "Tipo de arquivo inválido. Escolha um arquivo .crx ou .zip.",
"readError": "Falha ao ler o arquivo de extensão.",
"assignTitle": "Atribuir Grupo de Extensões",
"assignDescription": "Atribuir {{count}} perfil(is) selecionado(s) a um grupo de extensões.",
@@ -1282,7 +1282,6 @@
"assignSuccess": "Grupo de extensões atribuído com sucesso",
"editExtension": "Editar extensão",
"updateSuccess": "Extensão atualizada com sucesso",
"reupload": "Re-enviar",
"version": "Versão",
"author": "Autor",
"homepage": "Página inicial",
@@ -1290,10 +1289,26 @@
"editGroupDescription": "Atualize o nome do grupo e gerencie quais extensões estão incluídas.",
"groupExtensions": "Extensões neste grupo",
"noExtensionsInGroup": "Nenhuma extensão adicionada ainda",
"editExtensionDescription": "Atualizar o nome da extensão, ver metadados ou reenviar o arquivo da extensão.",
"editExtensionDescription": "Atualize o nome da extensão, veja seus metadados ou substitua-a por outro arquivo compactado ou pasta.",
"metadata": "Metadados",
"noMetadata": "Nenhum metadado disponível do manifesto.",
"selectFile": "Escolher arquivo",
"loadUnpacked": "Carregar descompactada",
"loadUnpackedTooltip": "Carregue uma extensão a partir de uma pasta com manifest.json",
"selectFolderTitle": "Selecionar pasta da extensão",
"selectedFolder": "Pasta selecionada",
"selectFolder": "Escolher pasta",
"linkFolder": "Carregar direto desta pasta",
"linkFolderOff": "A pasta é copiada para o Donut. A extensão fica portátil e sincroniza com seus outros dispositivos.",
"linkFolderOn": "O Donut carrega a extensão direto desta pasta a cada inicialização. Suas edições valem na próxima abertura do navegador, mas a extensão fica só neste computador e nunca sincroniza.",
"replaceSource": "Substituir origem",
"linkedNoSync": "Extensões vinculadas ficam só neste computador e não sincronizam.",
"uploadFailed": "Falha ao adicionar a extensão",
"updateFailed": "Falha ao atualizar a extensão",
"deleteFailed": "Falha ao excluir a extensão",
"groupCreateFailed": "Falha ao criar o grupo de extensões",
"groupUpdateFailed": "Falha ao atualizar o grupo de extensões",
"groupDeleteFailed": "Falha ao excluir o grupo de extensões",
"syncEnabled": "Sincronização ativada",
"syncDisabled": "Sincronização desativada",
"syncEnableTooltip": "Ativar sincronização",
@@ -1306,6 +1321,14 @@
"groupsTitle": "Excluir grupos de extensões",
"groupsDescription": "Excluir {{count}} grupos de extensões? {{names}}",
"confirmButton": "Excluir"
},
"source": {
"label": "Origem",
"archive": "Arquivo compactado",
"unpacked": "Pasta descompactada",
"linked": "Pasta vinculada",
"folderLabel": "Pasta",
"linkedTooltip": "Carregada direto de {{path}}"
}
},
"pro": {
@@ -1849,6 +1872,15 @@
"vpnNotFound": "VPN não encontrada",
"extensionNotFound": "Extensão não encontrada",
"extensionGroupNotFound": "Grupo de extensões não encontrado",
"extensionUnsupportedFileType": "Esse tipo de arquivo não é compatível. Uma extensão precisa ser um arquivo .crx ou .zip, ou uma pasta.",
"extensionDirNotFound": "Essa pasta não existe mais.",
"extensionNotADirectory": "Esse caminho não é uma pasta.",
"extensionManifestMissing": "Não há manifest.json nessa pasta. Escolha a pasta que contém o manifest.json da extensão.",
"extensionManifestInvalid": "Não foi possível ler o manifest.json dessa pasta.",
"extensionDirTooLarge": "Essa pasta é grande demais para copiar para o Donut (o limite é 256 MB e 20.000 arquivos). Vincule-a no lugar.",
"extensionPathHasComma": "O caminho dessa pasta tem uma vírgula, que o Chromium não consegue carregar. Renomeie ou mova a pasta.",
"extensionLinkRequiresDirectory": "Só uma pasta pode ser carregada no lugar. Desative o vínculo para adicionar um arquivo compactado.",
"extensionLinkedCannotSync": "Esta extensão é carregada de uma pasta deste computador, então não há nada para sincronizar.",
"cannotModifyCloudManagedProxy": "Não é possível modificar a sincronização de um proxy gerenciado na nuvem",
"syncLockedByProfile": "A sincronização não pode ser desativada enquanto estiver em uso por perfis sincronizados",
"syncNotConfigured": "A sincronização não está configurada. Faça login ou configure um servidor auto-hospedado.",
+35 -3
View File
@@ -1277,7 +1277,7 @@
"deleteConfirmDescription": "Вы уверены, что хотите удалить «{{name}}»? Это действие нельзя отменить.",
"deleteGroupConfirmTitle": "Удалить группу расширений",
"deleteGroupConfirmDescription": "Вы уверены, что хотите удалить группу «{{name}}»? Это действие нельзя отменить.",
"invalidFileType": "Недопустимый тип файла. Загрузите файл .crx, .xpi или .zip.",
"invalidFileType": "Неподдерживаемый тип файла. Выберите файл .crx или .zip.",
"readError": "Не удалось прочитать файл расширения.",
"assignTitle": "Назначить группу расширений",
"assignDescription": "Назначить {{count}} выбранных профилей в группу расширений.",
@@ -1285,7 +1285,6 @@
"assignSuccess": "Группа расширений успешно назначена",
"editExtension": "Редактировать расширение",
"updateSuccess": "Расширение успешно обновлено",
"reupload": "Загрузить заново",
"version": "Версия",
"author": "Автор",
"homepage": "Домашняя страница",
@@ -1293,10 +1292,26 @@
"editGroupDescription": "Обновите название группы и управляйте включёнными расширениями.",
"groupExtensions": "Расширения в этой группе",
"noExtensionsInGroup": "Расширения ещё не добавлены",
"editExtensionDescription": "Обновите имя расширения, просмотрите метаданные или загрузите файл расширения повторно.",
"editExtensionDescription": "Измените имя расширения, посмотрите его метаданные или замените его другим архивом либо папкой.",
"metadata": "Метаданные",
"noMetadata": "Метаданные из манифеста недоступны.",
"selectFile": "Выбрать файл",
"loadUnpacked": "Загрузить из папки",
"loadUnpackedTooltip": "Загрузить расширение из папки с файлом manifest.json",
"selectFolderTitle": "Выберите папку расширения",
"selectedFolder": "Выбранная папка",
"selectFolder": "Выбрать папку",
"linkFolder": "Загружать прямо из этой папки",
"linkFolderOff": "Папка копируется в Donut. Расширение можно переносить, и оно синхронизируется с другими устройствами.",
"linkFolderOn": "Donut загружает расширение прямо из этой папки при каждом запуске. Изменения применяются при следующем старте браузера, но расширение остаётся только на этом компьютере и не синхронизируется.",
"replaceSource": "Заменить источник",
"linkedNoSync": "Связанные расширения остаются только на этом компьютере и не синхронизируются.",
"uploadFailed": "Не удалось добавить расширение",
"updateFailed": "Не удалось обновить расширение",
"deleteFailed": "Не удалось удалить расширение",
"groupCreateFailed": "Не удалось создать группу расширений",
"groupUpdateFailed": "Не удалось обновить группу расширений",
"groupDeleteFailed": "Не удалось удалить группу расширений",
"syncEnabled": "Синхронизация включена",
"syncDisabled": "Синхронизация отключена",
"syncEnableTooltip": "Включить синхронизацию",
@@ -1309,6 +1324,14 @@
"groupsTitle": "Удалить группы расширений",
"groupsDescription": "Удалить {{count}} групп расширений? {{names}}",
"confirmButton": "Удалить"
},
"source": {
"label": "Источник",
"archive": "Архив",
"unpacked": "Распакованная папка",
"linked": "Связанная папка",
"folderLabel": "Папка",
"linkedTooltip": "Загружается прямо из {{path}}"
}
},
"pro": {
@@ -1856,6 +1879,15 @@
"vpnNotFound": "VPN не найден",
"extensionNotFound": "Расширение не найдено",
"extensionGroupNotFound": "Группа расширений не найдена",
"extensionUnsupportedFileType": "Этот тип файла не поддерживается. Расширение должно быть архивом .crx или .zip либо папкой.",
"extensionDirNotFound": "Эта папка больше не существует.",
"extensionNotADirectory": "Указанный путь не является папкой.",
"extensionManifestMissing": "В этой папке нет файла manifest.json. Выберите папку, в которой лежит manifest.json расширения.",
"extensionManifestInvalid": "Не удалось прочитать manifest.json в этой папке.",
"extensionDirTooLarge": "Папка слишком большая, чтобы скопировать её в Donut (не более 256 МБ и 20 000 файлов). Вместо этого свяжите её.",
"extensionPathHasComma": "В пути к папке есть запятая, которую Chromium не может обработать. Переименуйте или переместите папку.",
"extensionLinkRequiresDirectory": "Загружать на месте можно только папку. Отключите связывание, чтобы добавить архив.",
"extensionLinkedCannotSync": "Это расширение загружается из папки на этом компьютере, поэтому синхронизировать нечего.",
"cannotModifyCloudManagedProxy": "Невозможно изменить синхронизацию для облачного прокси",
"syncLockedByProfile": "Невозможно отключить синхронизацию, пока используется синхронизированными профилями",
"syncNotConfigured": "Синхронизация не настроена. Войдите или настройте собственный сервер.",
+35 -3
View File
@@ -1271,7 +1271,7 @@
"deleteConfirmDescription": "\"{{name}}\" uzantısını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"deleteGroupConfirmTitle": "Uzantı Grubunu Sil",
"deleteGroupConfirmDescription": "\"{{name}}\" grubunu silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"invalidFileType": "Geçersiz dosya türü. Lütfen bir .crx, .xpi veya .zip dosyası yükleyin.",
"invalidFileType": "Geçersiz dosya türü. Lütfen bir .crx veya .zip dosyası seçin.",
"readError": "Uzantı dosyası okunamadı.",
"assignTitle": "Uzantı Grubu Ata",
"assignDescription": "Seçili {{count}} profili bir uzantı grubuna atayın.",
@@ -1279,7 +1279,6 @@
"assignSuccess": "Uzantı grubu başarıyla atandı",
"editExtension": "Uzantıyı düzenle",
"updateSuccess": "Uzantı başarıyla güncellendi",
"reupload": "Yeniden yükle",
"version": "Sürüm",
"author": "Yazar",
"homepage": "Ana sayfa",
@@ -1287,10 +1286,26 @@
"editGroupDescription": "Grup adını güncelleyin ve gruba dahil uzantıları yönetin.",
"groupExtensions": "Bu gruptaki uzantılar",
"noExtensionsInGroup": "Henüz uzantı eklenmedi",
"editExtensionDescription": "Uzantı adını güncelleyin, üst verileri görüntüleyin veya uzantı dosyasını yeniden yükleyin.",
"editExtensionDescription": "Uzantının adını güncelleyin, meta verilerini görüntüleyin veya başka bir arşiv ya da klasörle değiştirin.",
"metadata": "Üst Veriler",
"noMetadata": "Manifest'te üst veri yok.",
"selectFile": "Dosya Seç",
"loadUnpacked": "Klasörden yükle",
"loadUnpackedTooltip": "manifest.json içeren bir klasörden uzantı yükleyin",
"selectFolderTitle": "Uzantı klasörünü seçin",
"selectedFolder": "Seçilen klasör",
"selectFolder": "Klasör seç",
"linkFolder": "Doğrudan bu klasörden yükle",
"linkFolderOff": "Klasör Donut'a kopyalanır. Uzantı taşınabilir olur ve diğer cihazlarınızla eşitlenir.",
"linkFolderOn": "Donut her başlatmada uzantıyı doğrudan bu klasörden yükler. Değişiklikleriniz tarayıcının bir sonraki açılışında geçerli olur, ancak uzantı yalnızca bu bilgisayarda kalır ve hiçbir zaman eşitlenmez.",
"replaceSource": "Kaynağı değiştir",
"linkedNoSync": "Bağlı uzantılar yalnızca bu bilgisayarda kalır ve eşitlenemez.",
"uploadFailed": "Uzantı eklenemedi",
"updateFailed": "Uzantı güncellenemedi",
"deleteFailed": "Uzantı silinemedi",
"groupCreateFailed": "Uzantı grubu oluşturulamadı",
"groupUpdateFailed": "Uzantı grubu güncellenemedi",
"groupDeleteFailed": "Uzantı grubu silinemedi",
"syncEnabled": "Eşitleme etkinleştirildi",
"syncDisabled": "Eşitleme devre dışı bırakıldı",
"syncEnableTooltip": "Eşitlemeyi etkinleştir",
@@ -1303,6 +1318,14 @@
"groupsTitle": "Uzantı gruplarını sil",
"groupsDescription": "{{count}} uzantı grubu silinsin mi? {{names}}",
"confirmButton": "Sil"
},
"source": {
"label": "Kaynak",
"archive": "Arşiv",
"unpacked": "Paketlenmemiş klasör",
"linked": "Bağlı klasör",
"folderLabel": "Klasör",
"linkedTooltip": "Doğrudan {{path}} konumundan yükleniyor"
}
},
"pro": {
@@ -1842,6 +1865,15 @@
"vpnNotFound": "VPN bulunamadı",
"extensionNotFound": "Uzantı bulunamadı",
"extensionGroupNotFound": "Uzantı grubu bulunamadı",
"extensionUnsupportedFileType": "Bu dosya türü desteklenmiyor. Bir uzantı .crx ya da .zip arşivi veya bir klasör olmalıdır.",
"extensionDirNotFound": "Bu klasör artık mevcut değil.",
"extensionNotADirectory": "Bu yol bir klasör değil.",
"extensionManifestMissing": "Bu klasörde manifest.json yok. Uzantının manifest.json dosyasını içeren klasörü seçin.",
"extensionManifestInvalid": "Bu klasördeki manifest.json okunamadı.",
"extensionDirTooLarge": "Bu klasör Donut'a kopyalanamayacak kadar büyük (sınır: 256 MB ve 20.000 dosya). Bunun yerine klasörü bağlayın.",
"extensionPathHasComma": "Bu klasörün yolunda virgül var ve Chromium bunu yükleyemez. Klasörü yeniden adlandırın veya taşıyın.",
"extensionLinkRequiresDirectory": "Yerinde yalnızca bir klasör yüklenebilir. Arşiv eklemek için bağlamayı kapatın.",
"extensionLinkedCannotSync": "Bu uzantı, bu bilgisayardaki bir klasörden yükleniyor; eşitlenecek bir şey yok.",
"cannotModifyCloudManagedProxy": "Bulut tarafından yönetilen bir proxy'nin eşitlemesi değiştirilemez",
"syncLockedByProfile": "Eşitlenen profiller tarafından kullanılırken eşitleme devre dışı bırakılamaz",
"syncNotConfigured": "Eşitleme yapılandırılmadı. Önce oturum açın veya kendi sunucunuzu yapılandırın.",
+35 -3
View File
@@ -1271,7 +1271,7 @@
"deleteConfirmDescription": "Bạn có chắc muốn xóa \"{{name}}\"? Hành động này không thể hoàn tác.",
"deleteGroupConfirmTitle": "Xóa nhóm tiện ích",
"deleteGroupConfirmDescription": "Bạn có chắc muốn xóa nhóm \"{{name}}\"? Hành động này không thể hoàn tác.",
"invalidFileType": "Loại tệp không hợp lệ. Vui lòng tải lên tệp .crx, .xpi hoặc .zip.",
"invalidFileType": "Loại tệp không hợp lệ. Vui lòng chọn tệp .crx hoặc .zip.",
"readError": "Đọc tệp tiện ích thất bại.",
"assignTitle": "Gán nhóm tiện ích",
"assignDescription": "Gán {{count}} profile đã chọn vào nhóm tiện ích.",
@@ -1279,7 +1279,6 @@
"assignSuccess": "Gán nhóm tiện ích thành công",
"editExtension": "Chỉnh sửa tiện ích",
"updateSuccess": "Cập nhật tiện ích thành công",
"reupload": "Tải lên lại",
"version": "Phiên bản",
"author": "Tác giả",
"homepage": "Trang chủ",
@@ -1287,10 +1286,26 @@
"editGroupDescription": "Cập nhật tên nhóm và quản lý tiện ích trong nhóm.",
"groupExtensions": "Tiện ích trong nhóm này",
"noExtensionsInGroup": "Chưa thêm tiện ích nào",
"editExtensionDescription": "Cập nhật tên tiện ích, xem metadata hoặc tải lên lại tệp tiện ích.",
"editExtensionDescription": "Cập nhật tên tiện ích, xem siêu dữ liệu hoặc thay bằng tệp nén hay thư mục khác.",
"metadata": "Metadata",
"noMetadata": "Không có metadata từ manifest.",
"selectFile": "Chọn tệp",
"loadUnpacked": "Tải từ thư mục",
"loadUnpackedTooltip": "Tải tiện ích từ thư mục có chứa manifest.json",
"selectFolderTitle": "Chọn thư mục tiện ích",
"selectedFolder": "Thư mục đã chọn",
"selectFolder": "Chọn thư mục",
"linkFolder": "Tải trực tiếp từ thư mục này",
"linkFolderOff": "Thư mục được sao chép vào Donut. Tiện ích có thể mang đi và đồng bộ sang các thiết bị khác của bạn.",
"linkFolderOn": "Donut tải tiện ích trực tiếp từ thư mục này mỗi lần khởi chạy. Các thay đổi của bạn có hiệu lực ở lần mở trình duyệt tiếp theo, nhưng tiện ích chỉ nằm trên máy này và không bao giờ đồng bộ.",
"replaceSource": "Thay nguồn",
"linkedNoSync": "Tiện ích được liên kết chỉ nằm trên máy này nên không thể đồng bộ.",
"uploadFailed": "Không thêm được tiện ích",
"updateFailed": "Không cập nhật được tiện ích",
"deleteFailed": "Không xóa được tiện ích",
"groupCreateFailed": "Không tạo được nhóm tiện ích",
"groupUpdateFailed": "Không cập nhật được nhóm tiện ích",
"groupDeleteFailed": "Không xóa được nhóm tiện ích",
"syncEnabled": "Đã bật đồng bộ",
"syncDisabled": "Đã tắt đồng bộ",
"syncEnableTooltip": "Bật đồng bộ",
@@ -1303,6 +1318,14 @@
"groupsTitle": "Xóa nhóm tiện ích",
"groupsDescription": "Xóa {{count}} nhóm tiện ích? {{names}}",
"confirmButton": "Xóa"
},
"source": {
"label": "Nguồn",
"archive": "Tệp nén",
"unpacked": "Thư mục đã giải nén",
"linked": "Thư mục liên kết",
"folderLabel": "Thư mục",
"linkedTooltip": "Tải trực tiếp từ {{path}}"
}
},
"pro": {
@@ -1842,6 +1865,15 @@
"vpnNotFound": "Không tìm thấy VPN",
"extensionNotFound": "Không tìm thấy tiện ích",
"extensionGroupNotFound": "Không tìm thấy nhóm tiện ích",
"extensionUnsupportedFileType": "Loại tệp này không được hỗ trợ. Tiện ích phải là tệp nén .crx hoặc .zip, hoặc một thư mục.",
"extensionDirNotFound": "Thư mục đó không còn tồn tại.",
"extensionNotADirectory": "Đường dẫn đó không phải là thư mục.",
"extensionManifestMissing": "Thư mục đó không có manifest.json. Hãy chọn thư mục chứa manifest.json của tiện ích.",
"extensionManifestInvalid": "Không đọc được manifest.json trong thư mục đó.",
"extensionDirTooLarge": "Thư mục đó quá lớn để sao chép vào Donut (giới hạn là 256 MB và 20.000 tệp). Hãy liên kết thư mục thay vì sao chép.",
"extensionPathHasComma": "Đường dẫn thư mục có dấu phẩy nên Chromium không tải được. Hãy đổi tên hoặc di chuyển thư mục.",
"extensionLinkRequiresDirectory": "Chỉ có thể tải tại chỗ một thư mục. Hãy tắt liên kết để thêm tệp nén.",
"extensionLinkedCannotSync": "Tiện ích này được tải từ một thư mục trên máy này nên không có gì để đồng bộ.",
"cannotModifyCloudManagedProxy": "Không thể chỉnh sửa đồng bộ cho proxy được quản lý bởi đám mây",
"syncLockedByProfile": "Không thể tắt đồng bộ khi đang được sử dụng bởi profile đã đồng bộ",
"syncNotConfigured": "Chưa cấu hình đồng bộ. Đăng nhập hoặc cấu hình máy chủ tự lưu trữ trước.",
+35 -3
View File
@@ -1271,7 +1271,7 @@
"deleteConfirmDescription": "确定要删除「{{name}}」吗?此操作无法撤消。",
"deleteGroupConfirmTitle": "删除扩展程序组",
"deleteGroupConfirmDescription": "确定要删除分组「{{name}}」吗?此操作无法撤消。",
"invalidFileType": "无效的文件类型。请上传 .crx、.xpi 或 .zip 文件。",
"invalidFileType": "文件类型无效。请选择 .crx 或 .zip 文件。",
"readError": "读取扩展程序文件失败。",
"assignTitle": "分配扩展程序组",
"assignDescription": "将 {{count}} 个选定的配置文件分配到扩展程序组。",
@@ -1279,7 +1279,6 @@
"assignSuccess": "扩展程序组分配成功",
"editExtension": "编辑扩展",
"updateSuccess": "扩展更新成功",
"reupload": "重新上传",
"version": "版本",
"author": "作者",
"homepage": "主页",
@@ -1287,10 +1286,26 @@
"editGroupDescription": "更新分组名称并管理包含的扩展。",
"groupExtensions": "此分组中的扩展",
"noExtensionsInGroup": "尚未添加扩展",
"editExtensionDescription": "更新扩展名称、查看元数据或重新上传扩展文件。",
"editExtensionDescription": "修改扩展名称、查看元数据,或用其他压缩包或文件夹替换它。",
"metadata": "元数据",
"noMetadata": "清单中没有可用的元数据。",
"selectFile": "选择文件",
"loadUnpacked": "加载文件夹",
"loadUnpackedTooltip": "从包含 manifest.json 的文件夹加载扩展",
"selectFolderTitle": "选择扩展文件夹",
"selectedFolder": "已选文件夹",
"selectFolder": "选择文件夹",
"linkFolder": "直接从该文件夹加载",
"linkFolderOff": "文件夹会复制到 Donut,扩展可随身携带并同步到你的其他设备。",
"linkFolderOn": "Donut 每次启动都直接从该文件夹加载扩展。你的修改会在下次启动浏览器时生效,但扩展只保留在本机,不会同步。",
"replaceSource": "替换来源",
"linkedNoSync": "已链接的扩展只保留在本机,无法同步。",
"uploadFailed": "添加扩展失败",
"updateFailed": "更新扩展失败",
"deleteFailed": "删除扩展失败",
"groupCreateFailed": "创建扩展组失败",
"groupUpdateFailed": "更新扩展组失败",
"groupDeleteFailed": "删除扩展组失败",
"syncEnabled": "同步已启用",
"syncDisabled": "同步已禁用",
"syncEnableTooltip": "启用同步",
@@ -1303,6 +1318,14 @@
"groupsTitle": "删除扩展组",
"groupsDescription": "删除 {{count}} 个扩展组?{{names}}",
"confirmButton": "删除"
},
"source": {
"label": "来源",
"archive": "压缩包",
"unpacked": "解压文件夹",
"linked": "链接文件夹",
"folderLabel": "文件夹",
"linkedTooltip": "直接从 {{path}} 加载"
}
},
"pro": {
@@ -1842,6 +1865,15 @@
"vpnNotFound": "未找到 VPN",
"extensionNotFound": "未找到扩展",
"extensionGroupNotFound": "未找到扩展分组",
"extensionUnsupportedFileType": "不支持该文件类型。扩展必须是 .crx 或 .zip 压缩包,或者一个文件夹。",
"extensionDirNotFound": "该文件夹已不存在。",
"extensionNotADirectory": "该路径不是文件夹。",
"extensionManifestMissing": "该文件夹中没有 manifest.json。请选择包含扩展 manifest.json 的文件夹。",
"extensionManifestInvalid": "无法读取该文件夹中的 manifest.json。",
"extensionDirTooLarge": "该文件夹过大,无法复制到 Donut(上限为 256 MB、20,000 个文件)。请改用链接方式加载。",
"extensionPathHasComma": "该文件夹路径中包含逗号,Chromium 无法加载。请重命名或移动该文件夹。",
"extensionLinkRequiresDirectory": "只有文件夹才能就地加载。要添加压缩包,请关闭链接选项。",
"extensionLinkedCannotSync": "该扩展是从本机文件夹加载的,没有需要同步的内容。",
"cannotModifyCloudManagedProxy": "无法修改云管理代理的同步",
"syncLockedByProfile": "在被已同步的配置文件使用时无法禁用同步",
"syncNotConfigured": "同步未配置。请先登录或配置自托管服务器。",
+27
View File
@@ -28,6 +28,15 @@ export type BackendErrorCode =
| "VPN_NOT_FOUND"
| "EXTENSION_NOT_FOUND"
| "EXTENSION_GROUP_NOT_FOUND"
| "EXTENSION_UNSUPPORTED_FILE_TYPE"
| "EXTENSION_DIR_NOT_FOUND"
| "EXTENSION_NOT_A_DIRECTORY"
| "EXTENSION_MANIFEST_MISSING"
| "EXTENSION_MANIFEST_INVALID"
| "EXTENSION_DIR_TOO_LARGE"
| "EXTENSION_PATH_HAS_COMMA"
| "EXTENSION_LINK_REQUIRES_DIRECTORY"
| "EXTENSION_LINKED_CANNOT_SYNC"
| "CANNOT_MODIFY_CLOUD_MANAGED_PROXY"
| "SYNC_LOCKED_BY_PROFILE"
| "SYNC_NOT_CONFIGURED"
@@ -219,6 +228,24 @@ export function translateBackendError(t: TFunction, err: unknown): string {
return t("backendErrors.extensionNotFound");
case "EXTENSION_GROUP_NOT_FOUND":
return t("backendErrors.extensionGroupNotFound");
case "EXTENSION_UNSUPPORTED_FILE_TYPE":
return t("backendErrors.extensionUnsupportedFileType");
case "EXTENSION_DIR_NOT_FOUND":
return t("backendErrors.extensionDirNotFound");
case "EXTENSION_NOT_A_DIRECTORY":
return t("backendErrors.extensionNotADirectory");
case "EXTENSION_MANIFEST_MISSING":
return t("backendErrors.extensionManifestMissing");
case "EXTENSION_MANIFEST_INVALID":
return t("backendErrors.extensionManifestInvalid");
case "EXTENSION_DIR_TOO_LARGE":
return t("backendErrors.extensionDirTooLarge");
case "EXTENSION_PATH_HAS_COMMA":
return t("backendErrors.extensionPathHasComma");
case "EXTENSION_LINK_REQUIRES_DIRECTORY":
return t("backendErrors.extensionLinkRequiresDirectory");
case "EXTENSION_LINKED_CANNOT_SYNC":
return t("backendErrors.extensionLinkedCannotSync");
case "CANNOT_MODIFY_CLOUD_MANAGED_PROXY":
return t("backendErrors.cannotModifyCloudManagedProxy");
case "SYNC_LOCKED_BY_PROFILE":
+5
View File
@@ -62,6 +62,11 @@ export interface Extension {
description?: string;
author?: string;
homepage_url?: string;
/** How the payload was imported: a `.crx`/`.zip` archive, or a folder. */
source_kind: "archive" | "unpacked";
/** Absolute folder the extension is loaded from in place. Set means nothing
* was copied into Donut, so the extension is machine-local and never syncs. */
linked_path?: string;
}
export interface ExtensionGroup {