From 250e206eef049d73376b18396d812fb6513bd25e Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:00:28 +0400 Subject: [PATCH] refactor: extension cleanup --- next-env.d.ts | 2 +- src/app/page.tsx | 38 ++ .../extension-group-assignment-dialog.tsx | 188 +++++++ src/components/home-header.tsx | 7 + src/components/profile-data-table.tsx | 50 +- src/components/profile-info-dialog.tsx | 515 +++++++++--------- src/i18n/locales/en.json | 9 +- src/i18n/locales/es.json | 9 +- src/i18n/locales/fr.json | 9 +- src/i18n/locales/ja.json | 9 +- src/i18n/locales/pt.json | 9 +- src/i18n/locales/ru.json | 9 +- src/i18n/locales/zh.json | 9 +- 13 files changed, 584 insertions(+), 279 deletions(-) create mode 100644 src/components/extension-group-assignment-dialog.tsx diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..b87975d 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./dist/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/page.tsx b/src/app/page.tsx index fc8bb7b..3e1c5ad 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,6 +11,7 @@ import { CookieCopyDialog } from "@/components/cookie-copy-dialog"; import { CookieManagementDialog } from "@/components/cookie-management-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; +import { ExtensionGroupAssignmentDialog } from "@/components/extension-group-assignment-dialog"; import { ExtensionManagementDialog } from "@/components/extension-management-dialog"; import { GroupAssignmentDialog } from "@/components/group-assignment-dialog"; import { GroupBadges } from "@/components/group-badges"; @@ -146,6 +147,14 @@ export default function Home() { useState(false); const [groupAssignmentDialogOpen, setGroupAssignmentDialogOpen] = useState(false); + const [ + extensionGroupAssignmentDialogOpen, + setExtensionGroupAssignmentDialogOpen, + ] = useState(false); + const [ + selectedProfilesForExtensionGroup, + setSelectedProfilesForExtensionGroup, + ] = useState([]); const [proxyAssignmentDialogOpen, setProxyAssignmentDialogOpen] = useState(false); const [cookieCopyDialogOpen, setCookieCopyDialogOpen] = useState(false); @@ -701,6 +710,22 @@ export default function Home() { setSelectedProfiles([]); }, [selectedProfiles, handleAssignProfilesToGroup]); + const handleAssignExtensionGroup = useCallback((profileIds: string[]) => { + setSelectedProfilesForExtensionGroup(profileIds); + setExtensionGroupAssignmentDialogOpen(true); + }, []); + + const handleBulkExtensionGroupAssignment = useCallback(() => { + if (selectedProfiles.length === 0) return; + handleAssignExtensionGroup(selectedProfiles); + setSelectedProfiles([]); + }, [selectedProfiles, handleAssignExtensionGroup]); + + const handleExtensionGroupAssignmentComplete = useCallback(() => { + setExtensionGroupAssignmentDialogOpen(false); + setSelectedProfilesForExtensionGroup([]); + }, []); + const handleAssignProfilesToProxy = useCallback((profileIds: string[]) => { setSelectedProfilesForProxy(profileIds); setProxyAssignmentDialogOpen(true); @@ -1042,6 +1067,7 @@ export default function Home() { onExtensionManagementDialogOpen={setExtensionManagementDialogOpen} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} + crossOsUnlocked={crossOsUnlocked} /> @@ -1072,6 +1098,8 @@ export default function Home() { onBulkGroupAssignment={handleBulkGroupAssignment} onBulkProxyAssignment={handleBulkProxyAssignment} onBulkCopyCookies={handleBulkCopyCookies} + onBulkExtensionGroupAssignment={handleBulkExtensionGroupAssignment} + onAssignExtensionGroup={handleAssignExtensionGroup} onOpenProfileSyncDialog={handleOpenProfileSyncDialog} onToggleProfileSync={handleToggleProfileSync} crossOsUnlocked={crossOsUnlocked} @@ -1192,6 +1220,16 @@ export default function Home() { profiles={profiles} /> + { + setExtensionGroupAssignmentDialogOpen(false); + }} + selectedProfiles={selectedProfilesForExtensionGroup} + onAssignmentComplete={handleExtensionGroupAssignmentComplete} + profiles={profiles} + /> + { diff --git a/src/components/extension-group-assignment-dialog.tsx b/src/components/extension-group-assignment-dialog.tsx new file mode 100644 index 0000000..7eebf9b --- /dev/null +++ b/src/components/extension-group-assignment-dialog.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { LoadingButton } from "@/components/loading-button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { BrowserProfile, ExtensionGroup } from "@/types"; +import { RippleButton } from "./ui/ripple"; + +interface ExtensionGroupAssignmentDialogProps { + isOpen: boolean; + onClose: () => void; + selectedProfiles: string[]; + onAssignmentComplete: () => void; + profiles?: BrowserProfile[]; +} + +export function ExtensionGroupAssignmentDialog({ + isOpen, + onClose, + selectedProfiles, + onAssignmentComplete, + profiles = [], +}: ExtensionGroupAssignmentDialogProps) { + const { t } = useTranslation(); + const [groups, setGroups] = useState([]); + const [selectedGroupId, setSelectedGroupId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isAssigning, setIsAssigning] = useState(false); + const [error, setError] = useState(null); + + const loadGroups = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const groupList = await invoke("list_extension_groups"); + setGroups(groupList); + } catch (err) { + console.error("Failed to load extension groups:", err); + setError( + err instanceof Error ? err.message : "Failed to load extension groups", + ); + } finally { + setIsLoading(false); + } + }, []); + + const handleAssign = useCallback(async () => { + setIsAssigning(true); + setError(null); + try { + for (const profileId of selectedProfiles) { + await invoke("assign_extension_group_to_profile", { + profileId, + extensionGroupId: selectedGroupId, + }); + } + + toast.success(t("extensions.assignSuccess")); + onAssignmentComplete(); + onClose(); + } catch (err) { + console.error("Failed to assign extension group:", err); + const errorMessage = + err instanceof Error ? err.message : "Failed to assign extension group"; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsAssigning(false); + } + }, [selectedProfiles, selectedGroupId, onAssignmentComplete, onClose, t]); + + useEffect(() => { + if (isOpen) { + void loadGroups(); + setSelectedGroupId(null); + setError(null); + } + }, [isOpen, loadGroups]); + + return ( + + + + {t("extensions.assignTitle")} + + {t("extensions.assignDescription", { + count: selectedProfiles.length, + })} + + + + + + {t("extensions.assignTitle")}: + + + {selectedProfiles.map((profileId) => { + const profile = profiles.find( + (p: BrowserProfile) => p.id === profileId, + ); + const displayName = profile ? profile.name : profileId; + return ( + + • {displayName} + + ); + })} + + + + + + + {t("extensions.extensionGroup")}: + + {isLoading ? ( + + {t("common.buttons.loading")} + + ) : ( + { + setSelectedGroupId(value === "none" ? null : value); + }} + > + + + + + + {t("extensions.noGroup")} + + {groups.map((group) => ( + + {group.name} + + ))} + + + )} + + + {error && ( + + {error} + + )} + + + + + {t("common.buttons.cancel")} + + void handleAssign()} + disabled={isLoading} + > + {t("common.buttons.apply")} + + + + + ); +} diff --git a/src/components/home-header.tsx b/src/components/home-header.tsx index 4beb49f..e87c48d 100644 --- a/src/components/home-header.tsx +++ b/src/components/home-header.tsx @@ -10,6 +10,7 @@ import { LuUsers, LuX, } from "react-icons/lu"; +import { cn } from "@/lib/utils"; import { Logo } from "./icons/logo"; import { Button } from "./ui/button"; import { CardTitle } from "./ui/card"; @@ -20,6 +21,7 @@ import { DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { Input } from "./ui/input"; +import { ProBadge } from "./ui/pro-badge"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; type Props = { @@ -33,6 +35,7 @@ type Props = { onExtensionManagementDialogOpen: (open: boolean) => void; searchQuery: string; onSearchQueryChange: (query: string) => void; + crossOsUnlocked?: boolean; }; const HomeHeader = ({ @@ -46,6 +49,7 @@ const HomeHeader = ({ onExtensionManagementDialogOpen, searchQuery, onSearchQueryChange, + crossOsUnlocked = false, }: Props) => { const { t } = useTranslation(); const handleLogoClick = () => { @@ -134,12 +138,15 @@ const HomeHeader = ({ {t("header.menu.groups")} { onExtensionManagementDialogOpen(true); }} > {t("header.menu.extensions")} + {!crossOsUnlocked && } { diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index c331993..784c516 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -23,11 +23,15 @@ import { LuCookie, LuInfo, LuLock, + LuPuzzle, LuTrash2, LuUsers, } from "react-icons/lu"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; -import { ProfileInfoDialog } from "@/components/profile-info-dialog"; +import { + ProfileBypassRulesDialog, + ProfileInfoDialog, +} from "@/components/profile-info-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -791,6 +795,8 @@ interface ProfilesDataTableProps { onBulkGroupAssignment?: () => void; onBulkProxyAssignment?: () => void; onBulkCopyCookies?: () => void; + onBulkExtensionGroupAssignment?: () => void; + onAssignExtensionGroup?: (profileIds: string[]) => void; onOpenProfileSyncDialog?: (profile: BrowserProfile) => void; onToggleProfileSync?: (profile: BrowserProfile) => void; crossOsUnlocked?: boolean; @@ -816,6 +822,8 @@ export function ProfilesDataTable({ onBulkGroupAssignment, onBulkProxyAssignment, onBulkCopyCookies, + onBulkExtensionGroupAssignment, + onAssignExtensionGroup, onOpenProfileSyncDialog, onToggleProfileSync, crossOsUnlocked = false, @@ -886,6 +894,8 @@ export function ProfilesDataTable({ const [isDeleting, setIsDeleting] = React.useState(false); const [profileForInfoDialog, setProfileForInfoDialog] = React.useState(null); + const [bypassRulesProfile, setBypassRulesProfile] = + React.useState(null); const [launchingProfiles, setLaunchingProfiles] = React.useState>( new Set(), ); @@ -2505,6 +2515,8 @@ export function ProfilesDataTable({ onConfigureCamoufox={onConfigureCamoufox} onCopyCookiesToProfile={onCopyCookiesToProfile} onOpenCookieManagement={onOpenCookieManagement} + onAssignExtensionGroup={onAssignExtensionGroup} + onOpenBypassRules={(profile) => setBypassRulesProfile(profile)} onCloneProfile={onCloneProfile} onDeleteProfile={(profile) => { setProfileForInfoDialog(null); @@ -2538,6 +2550,27 @@ export function ProfilesDataTable({ )} + {onBulkExtensionGroupAssignment && ( + + + + {!crossOsUnlocked && ( + + PRO + + )} + + + )} {onBulkCopyCookies && ( - + + + {!crossOsUnlocked && ( + + PRO + + )} + )} {onBulkDelete && ( @@ -2568,6 +2608,12 @@ export function ProfilesDataTable({ profileName={trafficDialogProfile.name} /> )} + setBypassRulesProfile(null)} + profileId={bypassRulesProfile?.id ?? null} + initialRules={bypassRulesProfile?.proxy_bypass_rules ?? []} + /> > ); } diff --git a/src/components/profile-info-dialog.tsx b/src/components/profile-info-dialog.tsx index c7f03a2..1559bd4 100644 --- a/src/components/profile-info-dialog.tsx +++ b/src/components/profile-info-dialog.tsx @@ -14,6 +14,7 @@ import { LuGlobe, LuGroup, LuPlus, + LuPuzzle, LuRefreshCw, LuSettings, LuShieldCheck, @@ -60,6 +61,8 @@ interface ProfileInfoDialogProps { onConfigureCamoufox?: (profile: BrowserProfile) => void; onCopyCookiesToProfile?: (profile: BrowserProfile) => void; onOpenCookieManagement?: (profile: BrowserProfile) => void; + onAssignExtensionGroup?: (profileIds: string[]) => void; + onOpenBypassRules?: (profile: BrowserProfile) => void; onCloneProfile?: (profile: BrowserProfile) => void; onDeleteProfile?: (profile: BrowserProfile) => void; crossOsUnlocked?: boolean; @@ -103,6 +106,8 @@ export function ProfileInfoDialog({ onConfigureCamoufox, onCopyCookiesToProfile, onOpenCookieManagement, + onAssignExtensionGroup, + onOpenBypassRules, onCloneProfile, onDeleteProfile, crossOsUnlocked = false, @@ -117,10 +122,6 @@ export function ProfileInfoDialog({ const [extensionGroupName, setExtensionGroupName] = React.useState< string | null >(null); - const [bypassRules, setBypassRules] = React.useState([]); - const [newRule, setNewRule] = React.useState(""); - const [bypassRulesDialogOpen, setBypassRulesDialogOpen] = - React.useState(false); React.useEffect(() => { if (!isOpen || !profile?.group_id) { @@ -159,12 +160,8 @@ export function ProfileInfoDialog({ React.useEffect(() => { if (!isOpen) { setCopied(false); - setNewRule(""); } - if (isOpen && profile) { - setBypassRules(profile.proxy_bypass_rules ?? []); - } - }, [isOpen, profile]); + }, [isOpen]); if (!profile) return null; @@ -206,31 +203,6 @@ export function ProfileInfoDialog({ action(); }; - const updateBypassRules = async (rules: string[]) => { - if (!profile) return; - try { - await invoke("update_profile_proxy_bypass_rules", { - profileId: profile.id, - rules, - }); - setBypassRules(rules); - } catch { - // ignore - } - }; - - const handleAddRule = () => { - const trimmed = newRule.trim(); - if (!trimmed || bypassRules.includes(trimmed)) return; - const updated = [...bypassRules, trimmed]; - setNewRule(""); - void updateBypassRules(updated); - }; - - const handleRemoveRule = (rule: string) => { - void updateBypassRules(bypassRules.filter((r) => r !== rule)); - }; - const releaseLabel = profile.release_type.charAt(0).toUpperCase() + profile.release_type.slice(1); @@ -305,10 +277,18 @@ export function ProfileInfoDialog({ disabled: isDisabled, hidden: profile.ephemeral === true, }, + { + icon: , + label: t("profileInfo.actions.assignExtensionGroup"), + onClick: () => handleAction(() => onAssignExtensionGroup?.([profile.id])), + disabled: isDisabled || !crossOsUnlocked, + proBadge: !crossOsUnlocked, + hidden: profile.ephemeral === true, + }, { icon: , label: t("profileInfo.network.bypassRulesTitle"), - onClick: () => setBypassRulesDialogOpen(true), + onClick: () => handleAction(() => onOpenBypassRules?.(profile)), }, { icon: , @@ -322,247 +302,254 @@ export function ProfileInfoDialog({ const visibleActions = actions.filter((a) => !a.hidden); return ( - <> - !open && onClose()}> - - - {t("profileInfo.title")} - - - - - {t("profileInfo.tabs.info")} - - - {t("profileInfo.tabs.settings")} - - - - - - {/* Hero */} - - - - - - - {profile.name} - - - - {getBrowserDisplayName(profile.browser)}{" "} - {profile.version} + !open && onClose()}> + + + {t("profileInfo.title")} + + + + + {t("profileInfo.tabs.info")} + + + {t("profileInfo.tabs.settings")} + + + + + + {/* Hero */} + + + + + + + {profile.name} + + + + {getBrowserDisplayName(profile.browser)}{" "} + {profile.version} + + + {releaseLabel} + + {isRunning && ( + + {t("common.status.running")} + )} + {profile.ephemeral && ( - {releaseLabel} + {t("profiles.ephemeralBadge")} - {isRunning && ( - - {t("common.status.running")} - - )} - {profile.ephemeral && ( - - {t("profiles.ephemeralBadge")} - - )} - {showCrossOs && profile.host_os && ( - - - {getOSDisplayName(profile.host_os)} - - )} - - - - - {/* Profile ID */} - - - ID - - - {profile.id} - - void handleCopyId()} - className="text-muted-foreground hover:text-foreground transition-colors shrink-0" - > - {copied ? ( - - ) : ( - )} - - - - {/* Network & Organization */} - - - - - - - - {/* Sync */} - - - - {t("profileInfo.fields.syncStatus")} - - {syncLabel} - - - {syncMode === "Disabled" - ? t("sync.mode.disabled") - : syncStatus?.status === "syncing" - ? t("common.status.syncing") - : t("common.status.synced")} - - - - {/* Tags */} - {hasTags && ( - - - {t("profileInfo.fields.tags")} - - - {profile.tags?.map((tag) => ( - - {tag} - - ))} - - - )} - - {/* Note */} - {hasNote && ( - - - {t("profileInfo.fields.note")} - - - {profile.note} - - - )} - - {/* Team */} - {profile.created_by_email && ( - - - {t("sync.team.title")} - - - {t("sync.team.createdBy", { - email: profile.created_by_email, - })} - - - )} - - - - - - - {visibleActions.map((action) => ( - + + {getOSDisplayName(profile.host_os)} + )} - > - {action.icon} - - {action.label} - {action.proBadge && } - - - - ))} + + - - - - - - {t("common.buttons.close")} - - - - - setBypassRulesDialogOpen(false)} - bypassRules={bypassRules} - newRule={newRule} - onNewRuleChange={setNewRule} - onAddRule={handleAddRule} - onRemoveRule={handleRemoveRule} - /> - > + + {/* Profile ID */} + + + ID + + + {profile.id} + + void handleCopyId()} + className="text-muted-foreground hover:text-foreground transition-colors shrink-0" + > + {copied ? ( + + ) : ( + + )} + + + + {/* Network & Organization */} + + + + + + + + {/* Sync */} + + + + {t("profileInfo.fields.syncStatus")} + + {syncLabel} + + + {syncMode === "Disabled" + ? t("sync.mode.disabled") + : syncStatus?.status === "syncing" + ? t("common.status.syncing") + : t("common.status.synced")} + + + + {/* Tags */} + {hasTags && ( + + + {t("profileInfo.fields.tags")} + + + {profile.tags?.map((tag) => ( + + {tag} + + ))} + + + )} + + {/* Note */} + {hasNote && ( + + + {t("profileInfo.fields.note")} + + + {profile.note} + + + )} + + {/* Team */} + {profile.created_by_email && ( + + + {t("sync.team.title")} + + + {t("sync.team.createdBy", { + email: profile.created_by_email, + })} + + + )} + + + + + + + {visibleActions.map((action) => ( + + {action.icon} + + {action.label} + {action.proBadge && } + + + + ))} + + + + + + ); } interface ProfileBypassRulesDialogProps { isOpen: boolean; onClose: () => void; - bypassRules: string[]; - newRule: string; - onNewRuleChange: (value: string) => void; - onAddRule: () => void; - onRemoveRule: (rule: string) => void; + profileId: string | null; + initialRules?: string[]; } -function ProfileBypassRulesDialog({ +export function ProfileBypassRulesDialog({ isOpen, onClose, - bypassRules, - newRule, - onNewRuleChange, - onAddRule, - onRemoveRule, + profileId, + initialRules, }: ProfileBypassRulesDialogProps) { const { t } = useTranslation(); + const [bypassRules, setBypassRules] = React.useState([]); + const [newRule, setNewRule] = React.useState(""); + + React.useEffect(() => { + if (isOpen) { + setBypassRules(initialRules ?? []); + setNewRule(""); + } + }, [isOpen, initialRules]); + + const updateBypassRules = async (rules: string[]) => { + if (!profileId) return; + try { + await invoke("update_profile_proxy_bypass_rules", { + profileId, + rules, + }); + setBypassRules(rules); + } catch { + // ignore + } + }; + + const handleAddRule = () => { + const trimmed = newRule.trim(); + if (!trimmed || bypassRules.includes(trimmed)) return; + const updated = [...bypassRules, trimmed]; + setNewRule(""); + void updateBypassRules(updated); + }; + + const handleRemoveRule = (rule: string) => { + void updateBypassRules(bypassRules.filter((r) => r !== rule)); + }; return ( !open && onClose()}> @@ -578,14 +565,18 @@ function ProfileBypassRulesDialog({ onNewRuleChange(e.target.value)} + onChange={(e) => setNewRule(e.target.value)} onKeyDown={(e) => { - if (e.key === "Enter") onAddRule(); + if (e.key === "Enter") handleAddRule(); }} placeholder={t("profileInfo.network.rulePlaceholder")} className="flex-1 text-sm" /> - + {t("profileInfo.network.addRule")} @@ -604,7 +595,7 @@ function ProfileBypassRulesDialog({ {rule} onRemoveRule(rule)} + onClick={() => handleRemoveRule(rule)} className="text-muted-foreground hover:text-destructive transition-colors shrink-0" > diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4c205c1..c66887a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -730,7 +730,8 @@ "ruleTypes": "Supports hostnames, IP addresses, and regex patterns." }, "actions": { - "manageCookies": "Manage Cookies" + "manageCookies": "Manage Cookies", + "assignExtensionGroup": "Assign Extension Group" }, "clone": { "title": "Clone Profile", @@ -774,7 +775,11 @@ "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.", - "readError": "Failed to read the extension file." + "readError": "Failed to read the extension file.", + "assignTitle": "Assign Extension Group", + "assignDescription": "Assign {{count}} selected profile(s) to an extension group.", + "noGroup": "None (No Extension Group)", + "assignSuccess": "Extension group assigned successfully" }, "pro": { "badge": "PRO", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index aaa551f..ced0191 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -730,7 +730,8 @@ "ruleTypes": "Soporta nombres de host, direcciones IP y patrones regex." }, "actions": { - "manageCookies": "Administrar Cookies" + "manageCookies": "Administrar Cookies", + "assignExtensionGroup": "Asignar Grupo de Extensiones" }, "clone": { "title": "Clonar Perfil", @@ -774,7 +775,11 @@ "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.", - "readError": "No se pudo leer el archivo de extensión." + "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.", + "noGroup": "Ninguno (Sin Grupo de Extensiones)", + "assignSuccess": "Grupo de extensiones asignado exitosamente" }, "pro": { "badge": "PRO", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 5b79b1b..2396f38 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -730,7 +730,8 @@ "ruleTypes": "Prend en charge les noms d'hôte, les adresses IP et les expressions régulières." }, "actions": { - "manageCookies": "Gérer les Cookies" + "manageCookies": "Gérer les Cookies", + "assignExtensionGroup": "Assigner un Groupe d'Extensions" }, "clone": { "title": "Cloner le Profil", @@ -774,7 +775,11 @@ "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.", - "readError": "Impossible de lire le fichier d'extension." + "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.", + "noGroup": "Aucun (Pas de Groupe d'Extensions)", + "assignSuccess": "Groupe d'extensions assigné avec succès" }, "pro": { "badge": "PRO", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index e8cff3c..eae6c82 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -730,7 +730,8 @@ "ruleTypes": "ホスト名、IPアドレス、正規表現パターンをサポートしています。" }, "actions": { - "manageCookies": "Cookieを管理" + "manageCookies": "Cookieを管理", + "assignExtensionGroup": "拡張機能グループを割り当て" }, "clone": { "title": "プロフィールを複製", @@ -774,7 +775,11 @@ "deleteGroupConfirmTitle": "拡張機能グループを削除", "deleteGroupConfirmDescription": "グループ「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。", "invalidFileType": "無効なファイルタイプです。.crx、.xpi、または .zip ファイルをアップロードしてください。", - "readError": "拡張機能ファイルの読み取りに失敗しました。" + "readError": "拡張機能ファイルの読み取りに失敗しました。", + "assignTitle": "拡張機能グループの割り当て", + "assignDescription": "選択した{{count}}件のプロファイルを拡張機能グループに割り当てます。", + "noGroup": "なし(拡張機能グループなし)", + "assignSuccess": "拡張機能グループが正常に割り当てられました" }, "pro": { "badge": "PRO", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 45a8676..bfc7b85 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -730,7 +730,8 @@ "ruleTypes": "Suporta nomes de host, endereços IP e padrões regex." }, "actions": { - "manageCookies": "Gerenciar Cookies" + "manageCookies": "Gerenciar Cookies", + "assignExtensionGroup": "Atribuir Grupo de Extensões" }, "clone": { "title": "Clonar Perfil", @@ -774,7 +775,11 @@ "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.", - "readError": "Falha ao ler o arquivo de extensão." + "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.", + "noGroup": "Nenhum (Sem Grupo de Extensões)", + "assignSuccess": "Grupo de extensões atribuído com sucesso" }, "pro": { "badge": "PRO", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index d5d0cf7..733fa08 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -730,7 +730,8 @@ "ruleTypes": "Поддерживает имена хостов, IP-адреса и шаблоны регулярных выражений." }, "actions": { - "manageCookies": "Управление Cookie" + "manageCookies": "Управление Cookie", + "assignExtensionGroup": "Назначить группу расширений" }, "clone": { "title": "Клонировать профиль", @@ -774,7 +775,11 @@ "deleteGroupConfirmTitle": "Удалить группу расширений", "deleteGroupConfirmDescription": "Вы уверены, что хотите удалить группу «{{name}}»? Это действие нельзя отменить.", "invalidFileType": "Недопустимый тип файла. Загрузите файл .crx, .xpi или .zip.", - "readError": "Не удалось прочитать файл расширения." + "readError": "Не удалось прочитать файл расширения.", + "assignTitle": "Назначить группу расширений", + "assignDescription": "Назначить {{count}} выбранных профилей в группу расширений.", + "noGroup": "Нет (Без группы расширений)", + "assignSuccess": "Группа расширений успешно назначена" }, "pro": { "badge": "PRO", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index bdf51bb..47e2f9e 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -730,7 +730,8 @@ "ruleTypes": "支持主机名、IP地址和正则表达式模式。" }, "actions": { - "manageCookies": "管理 Cookie" + "manageCookies": "管理 Cookie", + "assignExtensionGroup": "分配扩展程序组" }, "clone": { "title": "克隆配置文件", @@ -774,7 +775,11 @@ "deleteGroupConfirmTitle": "删除扩展程序组", "deleteGroupConfirmDescription": "确定要删除分组「{{name}}」吗?此操作无法撤消。", "invalidFileType": "无效的文件类型。请上传 .crx、.xpi 或 .zip 文件。", - "readError": "读取扩展程序文件失败。" + "readError": "读取扩展程序文件失败。", + "assignTitle": "分配扩展程序组", + "assignDescription": "将 {{count}} 个选定的配置文件分配到扩展程序组。", + "noGroup": "无(不使用扩展程序组)", + "assignSuccess": "扩展程序组分配成功" }, "pro": { "badge": "PRO",
- {t("profileInfo.fields.syncStatus")} -
{syncLabel}
- {profile.note} -
- {t("sync.team.title")} -
- {t("sync.team.createdBy", { - email: profile.created_by_email, - })} -
+ {t("profileInfo.fields.syncStatus")} +
+ {profile.note} +
+ {t("sync.team.title")} +
+ {t("sync.team.createdBy", { + email: profile.created_by_email, + })} +