mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-18 00:47:19 +02:00
feat: extension export via api
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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é.",
|
||||
|
||||
@@ -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": "同期が設定されていません。サインインするか、セルフホストサーバーを設定してください。",
|
||||
|
||||
@@ -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": "동기화가 구성되지 않았습니다. 먼저 로그인하거나 자체 호스팅 서버를 구성하세요.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "Синхронизация не настроена. Войдите или настройте собственный сервер.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "同步未配置。请先登录或配置自托管服务器。",
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user