mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-28 22:59:02 +03:00
feat: add i18n (translation/localization) (#16877)
* Translation module init * Add more i18n keys * fix: fix string wrong * refactor: use namespace translation file * chore: add more translation key * fix: fix some page name error * refactor: change Trans tag for t function * chore: fix some key not work * chore: fix SearchFilterDialog i18n key error Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: fix en i18n file filter missing some keys * chore: add some i18n keys * chore: add more i18n keys again * feat: add search page i18n * feat: add explore model i18n keys * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more live i18n keys * feat: add more search setting i18n keys * fix: remove some comment * fix: fix some setting page url error * Update web/src/views/settings/SearchSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * fix: add system missing keys * fix: update password update i18n keys * chore: remove outdate translation.json file * fix: fix exploreSettings error * chore: add object setting i18n keys * Update web/src/views/recording/RecordingView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/public/locales/en/components/filter.json Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/overlay/ExportDialog.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more i18n keys * fix: fix motionDetectionTuner html node * feat: add more page i18n keys * fix: cameraStream i18n keys error * feat: add Player i18n keys * feat: add more toast i18n keys * feat: change explore setting name * feat: add more document title i18n keys * feat: add more search i18n keys * fix: fix accessDenied i18n keys error * chore: add objectType i18n * chore: add inputWithTags i18n * chore: add SearchFilterDialog i18n * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: add some missing i18n keys * chore: remove most import { t } from "i18next"; --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
co-authored by
Josh Hawkins
parent
db541abed4
commit
d34533981f
@@ -20,16 +20,19 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { DialogClose } from "../ui/dialog";
|
||||
import { LuLogOut, LuSquarePen } from "react-icons/lu";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import SetPasswordDialog from "../overlay/SetPasswordDialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type AccountSettingsProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: profile } = useSWR("profile");
|
||||
const { data: config } = useSWR("config");
|
||||
const logoutUrl = config?.proxy?.logout_url || `${baseUrl}api/logout`;
|
||||
@@ -48,7 +51,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setPasswordDialogOpen(false);
|
||||
toast.success("Password updated successfully.", {
|
||||
toast.success(t("users.toast.success.updatePassword"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
@@ -58,9 +61,14 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Error setting password: ${errorMessage}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("users.toast.error.setPasswordFailed", {
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -83,7 +91,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>Account</p>
|
||||
<p>{t("menu.user.account", { ns: "common" })}</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
@@ -95,8 +103,13 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
>
|
||||
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
|
||||
<DropdownMenuLabel>
|
||||
Current User: {profile?.username || "anonymous"}{" "}
|
||||
{profile?.role && `(${profile.role})`}
|
||||
{t("menu.user.current", {
|
||||
ns: "common",
|
||||
user:
|
||||
profile?.username || t("menu.user.anonymous", { ns: "common" }),
|
||||
})}{" "}
|
||||
{t("role." + profile?.role) &&
|
||||
`(${t("role." + profile?.role, { ns: "common" })})`}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} />
|
||||
{profile?.username && profile.username !== "anonymous" && (
|
||||
@@ -104,22 +117,22 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Set Password"
|
||||
aria-label={t("menu.user.setPassword", { ns: "common" })}
|
||||
onClick={() => setPasswordDialogOpen(true)}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
<span>Set Password</span>
|
||||
<span>{t("menu.user.setPassword", { ns: "common" })}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Log out"
|
||||
aria-label={t("menu.user.logout", { ns: "common" })}
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
<span>Logout</span>
|
||||
<span>{t("menu.user.logout", { ns: "common" })}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
LuActivity,
|
||||
LuGithub,
|
||||
LuLanguages,
|
||||
LuLifeBuoy,
|
||||
LuList,
|
||||
LuLogOut,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
LuSettings,
|
||||
LuSun,
|
||||
LuSunMoon,
|
||||
LuEarth,
|
||||
} from "react-icons/lu";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -52,21 +54,28 @@ import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import useSWR from "swr";
|
||||
import RestartDialog from "../overlay/dialog/RestartDialog";
|
||||
|
||||
import { useLanguage } from "@/context/language-provider";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import SetPasswordDialog from "../overlay/SetPasswordDialog";
|
||||
import { toast } from "sonner";
|
||||
import axios from "axios";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type GeneralSettingsProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const { data: profile } = useSWR("profile");
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const logoutUrl = config?.proxy?.logout_url || "/api/logout";
|
||||
|
||||
// settings
|
||||
|
||||
const { language, setLanguage, systemLanguage } = useLanguage();
|
||||
const { theme, colorScheme, setTheme, setColorScheme } = useTheme();
|
||||
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||
@@ -90,9 +99,12 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setPasswordDialogOpen(false);
|
||||
toast.success("Password updated successfully.", {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.success(
|
||||
t("users.toast.success.updatePassword", { ns: "views/settings" }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -100,9 +112,15 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Error setting password: ${errorMessage}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("users.toast.error.setPasswordFailed", {
|
||||
ns: "views/settings",
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -126,7 +144,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>Settings</p>
|
||||
<p>{t("menu.settings")}</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
@@ -150,8 +168,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{isMobile && (
|
||||
<div className="mb-2">
|
||||
<DropdownMenuLabel>
|
||||
Current User: {profile?.username || "anonymous"}{" "}
|
||||
{profile?.role && `(${profile.role})`}
|
||||
{t("menu.user.current", {
|
||||
user: profile?.username || t("menu.user.anonymous"),
|
||||
})}{" "}
|
||||
{t("role." + profile?.role) &&
|
||||
`(${t("role." + profile?.role)})`}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator
|
||||
className={isDesktop ? "mt-3" : "mt-1"}
|
||||
@@ -163,11 +184,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Set Password"
|
||||
aria-label={t("menu.user.setPassword")}
|
||||
onClick={() => setPasswordDialogOpen(true)}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
<span>Set Password</span>
|
||||
<span>{t("menu.user.setPassword")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
@@ -176,18 +197,18 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Log out"
|
||||
aria-label={t("menu.user.logout", { ns: "common" })}
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
<span>Logout</span>
|
||||
<span>{t("menu.user.logout", { ns: "common" })}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
</div>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<DropdownMenuLabel>System</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t("menu.system")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
|
||||
<Link to="/system#general">
|
||||
@@ -197,10 +218,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="System metrics"
|
||||
aria-label={t("menu.systemMetrics")}
|
||||
>
|
||||
<LuActivity className="mr-2 size-4" />
|
||||
<span>System metrics</span>
|
||||
<span>{t("menu.systemMetrics")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link to="/logs">
|
||||
@@ -210,10 +231,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="System logs"
|
||||
aria-label={t("menu.systemLogs")}
|
||||
>
|
||||
<LuList className="mr-2 size-4" />
|
||||
<span>System logs</span>
|
||||
<span>{t("menu.systemLogs")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
@@ -222,7 +243,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
<DropdownMenuLabel
|
||||
className={isDesktop && isAdmin ? "mt-3" : "mt-1"}
|
||||
>
|
||||
Configuration
|
||||
{t("menu.configuration")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
@@ -233,10 +254,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Settings"
|
||||
aria-label={t("menu.settings")}
|
||||
>
|
||||
<LuSettings className="mr-2 size-4" />
|
||||
<span>Settings</span>
|
||||
<span>{t("menu.settings")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
@@ -248,10 +269,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Configuration editor"
|
||||
aria-label={t("menu.configurationEditor")}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
<span>Configuration editor</span>
|
||||
<span>{t("menu.configurationEditor")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</>
|
||||
@@ -271,7 +292,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
|
||||
Appearance
|
||||
{t("menu.appearance")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<SubItem>
|
||||
@@ -280,8 +301,8 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
>
|
||||
<LuSunMoon className="mr-2 size-4" />
|
||||
<span>Dark Mode</span>
|
||||
<LuLanguages className="mr-2 size-4" />
|
||||
<span>{t("menu.languages")}</span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
@@ -296,16 +317,16 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Light mode"
|
||||
onClick={() => setTheme("light")}
|
||||
aria-label={t("menu.language.en")}
|
||||
onClick={() => setLanguage("en")}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
{language.trim() === "en" ? (
|
||||
<>
|
||||
<LuSun className="mr-2 size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
Light
|
||||
<LuLanguages className="mr-2 size-4" />
|
||||
{t("menu.language.en")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">Light</span>
|
||||
<span className="ml-6 mr-2">{t("menu.language.en")}</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
@@ -314,16 +335,18 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Dark mode"
|
||||
onClick={() => setTheme("dark")}
|
||||
aria-label={t("menu.language.zhCN")}
|
||||
onClick={() => setLanguage("zh-CN")}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
{language === "zh-CN" ? (
|
||||
<>
|
||||
<LuMoon className="mr-2 size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
Dark
|
||||
<LuLanguages className="mr-2 size-4" />
|
||||
{t("menu.language.zhCN")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">Dark</span>
|
||||
<span className="ml-6 mr-2">
|
||||
{t("menu.language.zhCN")}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
@@ -332,16 +355,16 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Use the system settings for light or dark mode"
|
||||
onClick={() => setTheme("system")}
|
||||
aria-label={t("menu.language.withSystem.label")}
|
||||
onClick={() => setLanguage(systemLanguage)}
|
||||
>
|
||||
{theme === "system" ? (
|
||||
{language === systemLanguage ? (
|
||||
<>
|
||||
<CgDarkMode className="mr-2 size-4 scale-100 transition-all" />
|
||||
System
|
||||
<LuEarth className="mr-2 size-4 scale-100 transition-all" />
|
||||
{t("menu.withSystem")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">System</span>
|
||||
<span className="ml-6 mr-2">{t("menu.withSystem")}</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</SubItemContent>
|
||||
@@ -354,7 +377,84 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
}
|
||||
>
|
||||
<LuSunMoon className="mr-2 size-4" />
|
||||
<span>Theme</span>
|
||||
<span>{t("menu.darkMode.label")}</span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
className={
|
||||
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
|
||||
}
|
||||
>
|
||||
<span tabIndex={0} className="sr-only" />
|
||||
<MenuItem
|
||||
className={
|
||||
isDesktop
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label={t("menu.darkMode.light")}
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<>
|
||||
<LuSun className="mr-2 size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
{t("menu.darkMode.light")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
{t("menu.darkMode.light")}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={
|
||||
isDesktop
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label={t("menu.darkMode.dark")}
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<>
|
||||
<LuMoon className="mr-2 size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
{t("menu.darkMode.dark")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
{t("menu.darkMode.dark")}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={
|
||||
isDesktop
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label={t("menu.darkMode.withSystem.label")}
|
||||
onClick={() => setTheme("system")}
|
||||
>
|
||||
{theme === "system" ? (
|
||||
<>
|
||||
<CgDarkMode className="mr-2 size-4 scale-100 transition-all" />
|
||||
{t("menu.withSystem")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">{t("menu.withSystem")}</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</SubItemContent>
|
||||
</Portal>
|
||||
</SubItem>
|
||||
<SubItem>
|
||||
<SubItemTrigger
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
>
|
||||
<LuSunMoon className="mr-2 size-4" />
|
||||
<span>{t("menu.theme.label")}</span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
@@ -377,11 +477,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{scheme === colorScheme ? (
|
||||
<>
|
||||
<IoColorPalette className="mr-2 size-4 rotate-0 scale-100 transition-all" />
|
||||
{friendlyColorSchemeName(scheme)}
|
||||
{t(friendlyColorSchemeName(scheme))}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
{friendlyColorSchemeName(scheme)}
|
||||
{t(friendlyColorSchemeName(scheme))}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
@@ -390,7 +490,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
</Portal>
|
||||
</SubItem>
|
||||
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
|
||||
Help
|
||||
{t("menu.help")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<a href="https://docs.frigate.video" target="_blank">
|
||||
@@ -398,10 +498,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Frigate documentation"
|
||||
aria-label={t("menu.documentation.label")}
|
||||
>
|
||||
<LuLifeBuoy className="mr-2 size-4" />
|
||||
<span>Documentation</span>
|
||||
<span>{t("menu.documentation")}</span>
|
||||
</MenuItem>
|
||||
</a>
|
||||
<a
|
||||
@@ -429,11 +529,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Restart Frigate"
|
||||
aria-label={t("menu.restart")}
|
||||
onClick={() => setRestartDialogOpen(true)}
|
||||
>
|
||||
<LuRotateCw className="mr-2 size-4" />
|
||||
<span>Restart Frigate</span>
|
||||
<span>{t("menu.restart")}</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
useNotifications,
|
||||
useNotificationSuspend,
|
||||
} from "@/api/ws";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type LiveContextMenuProps = {
|
||||
className?: string;
|
||||
@@ -85,6 +86,7 @@ export default function LiveContextMenu({
|
||||
config,
|
||||
children,
|
||||
}: LiveContextMenuProps) {
|
||||
const { t } = useTranslation("views/live");
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
// camera enabled
|
||||
@@ -209,7 +211,7 @@ export default function LiveContextMenu({
|
||||
// notifications
|
||||
|
||||
const notificationsEnabledInConfig =
|
||||
config?.cameras[camera].notifications.enabled_in_config;
|
||||
config?.cameras[camera]?.notifications?.enabled_in_config;
|
||||
|
||||
const { payload: notificationState, send: sendNotification } =
|
||||
useNotifications(camera);
|
||||
@@ -234,14 +236,19 @@ export default function LiveContextMenu({
|
||||
};
|
||||
|
||||
const formatSuspendedUntil = (timestamp: string) => {
|
||||
if (timestamp === "0") return "Frigate restarts.";
|
||||
// Some languages require a change in word order
|
||||
if (timestamp === "0") return t("time.untilForRestart", { ns: "common" });
|
||||
|
||||
return formatUnixTimestampToDateTime(Number.parseInt(timestamp), {
|
||||
const time = formatUnixTimestampToDateTime(Number.parseInt(timestamp), {
|
||||
time_style: "medium",
|
||||
date_style: "medium",
|
||||
timezone: config?.ui.timezone,
|
||||
strftime_fmt: `%b %d, ${config?.ui.time_format == "24hour" ? "%H:%M" : "%I:%M %p"}`,
|
||||
strftime_fmt:
|
||||
config?.ui.time_format == "24hour"
|
||||
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
|
||||
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
|
||||
});
|
||||
return t("time.untilForTime", { ns: "common", time });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -256,7 +263,7 @@ export default function LiveContextMenu({
|
||||
{preferredLiveMode == "jsmpeg" && isRestreamed && (
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<IoIosWarning className="mr-1 size-4 text-danger" />
|
||||
<p className="mr-2 text-xs">Low-bandwidth mode</p>
|
||||
<p className="mr-2 text-xs">{t("lowBandwidthMode")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -265,7 +272,7 @@ export default function LiveContextMenu({
|
||||
<ContextMenuSeparator className="mb-1" />
|
||||
<div className="p-2 text-sm">
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<p>Audio</p>
|
||||
<p>{t("audio")}</p>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<VolumeIcon
|
||||
className="size-5"
|
||||
@@ -292,7 +299,7 @@ export default function LiveContextMenu({
|
||||
onClick={() => sendEnabled(isEnabled ? "OFF" : "ON")}
|
||||
>
|
||||
<div className="text-primary">
|
||||
{isEnabled ? "Disable" : "Enable"} Camera
|
||||
{isEnabled ? t("camera.disable") : t("camera.enable")}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
@@ -302,7 +309,7 @@ export default function LiveContextMenu({
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={isEnabled ? muteAll : undefined}
|
||||
>
|
||||
<div className="text-primary">Mute All Cameras</div>
|
||||
<div className="text-primary">{t("muteCameras.enable")}</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem disabled={!isEnabled}>
|
||||
@@ -310,7 +317,7 @@ export default function LiveContextMenu({
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={isEnabled ? unmuteAll : undefined}
|
||||
>
|
||||
<div className="text-primary">Unmute All Cameras</div>
|
||||
<div className="text-primary">{t("muteCameras.disable")}</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
@@ -320,7 +327,9 @@ export default function LiveContextMenu({
|
||||
onClick={isEnabled ? toggleStats : undefined}
|
||||
>
|
||||
<div className="text-primary">
|
||||
{statsState ? "Hide" : "Show"} Stream Stats
|
||||
{statsState
|
||||
? t("streamStats.disable")
|
||||
: t("streamStats.enable")}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
@@ -333,7 +342,9 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="text-primary">Debug View</div>
|
||||
<div className="text-primary">
|
||||
{t("streaming.debugView", { ns: "components/dialog" })}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
{cameraGroup && cameraGroup !== "default" && (
|
||||
@@ -344,7 +355,7 @@ export default function LiveContextMenu({
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={isEnabled ? () => setShowSettings(true) : undefined}
|
||||
>
|
||||
<div className="text-primary">Streaming Settings</div>
|
||||
<div className="text-primary">{t("streamingSettings")}</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
@@ -357,7 +368,9 @@ export default function LiveContextMenu({
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={isEnabled ? resetPreferredLiveMode : undefined}
|
||||
>
|
||||
<div className="text-primary">Reset</div>
|
||||
<div className="text-primary">
|
||||
{t("button.reset", { ns: "common" })}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
@@ -368,7 +381,7 @@ export default function LiveContextMenu({
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger disabled={!isEnabled}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Notifications</span>
|
||||
<span>{t("notifications")}</span>
|
||||
</div>
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
@@ -379,25 +392,29 @@ export default function LiveContextMenu({
|
||||
{isSuspended ? (
|
||||
<>
|
||||
<IoIosNotificationsOff className="size-5 text-muted-foreground" />
|
||||
<span>Suspended</span>
|
||||
<span>
|
||||
{t("button.suspended", { ns: "common" })}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoIosNotifications className="size-5 text-muted-foreground" />
|
||||
<span>Enabled</span>
|
||||
<span>
|
||||
{t("button.enabled", { ns: "common" })}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoIosNotificationsOff className="size-5 text-danger" />
|
||||
<span>Disabled</span>
|
||||
<span>{t("button.disabled", { ns: "common" })}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isSuspended && (
|
||||
<span className="text-xs text-primary-variant">
|
||||
Until {formatSuspendedUntil(notificationSuspendUntil)}
|
||||
{formatSuspendedUntil(notificationSuspendUntil)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -418,9 +435,11 @@ export default function LiveContextMenu({
|
||||
>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{notificationState === "ON" ? (
|
||||
<span>Unsuspend</span>
|
||||
<span>
|
||||
{t("button.unsuspended", { ns: "common" })}
|
||||
</span>
|
||||
) : (
|
||||
<span>Enable</span>
|
||||
<span>{t("button.enable", { ns: "common" })}</span>
|
||||
)}
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
@@ -431,7 +450,7 @@ export default function LiveContextMenu({
|
||||
<ContextMenuSeparator />
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="mb-2 text-sm font-medium text-muted-foreground">
|
||||
Suspend for:
|
||||
{t("suspend.forTime")}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<ContextMenuItem
|
||||
@@ -440,7 +459,7 @@ export default function LiveContextMenu({
|
||||
isEnabled ? () => handleSuspend("5") : undefined
|
||||
}
|
||||
>
|
||||
5 minutes
|
||||
{t("time.5minutes", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@@ -450,7 +469,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
10 minutes
|
||||
{t("time.10minutes", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@@ -460,7 +479,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
30 minutes
|
||||
{t("time.30minutes", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@@ -470,7 +489,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
1 hour
|
||||
{t("time.1hour", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@@ -480,7 +499,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
12 hours
|
||||
{t("time.12hours", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@@ -490,7 +509,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
24 hours
|
||||
{t("time.24hours", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@@ -500,7 +519,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Until restart
|
||||
{t("time.untilRestart", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type SearchResultActionsProps = {
|
||||
searchResult: SearchResult;
|
||||
findSimilar: () => void;
|
||||
@@ -59,6 +61,8 @@ export default function SearchResultActions({
|
||||
isContextMenu = false,
|
||||
children,
|
||||
}: SearchResultActionsProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -68,7 +72,7 @@ export default function SearchResultActions({
|
||||
.delete(`events/${searchResult.id}`)
|
||||
.then((resp) => {
|
||||
if (resp.status == 200) {
|
||||
toast.success("Tracked object deleted successfully.", {
|
||||
toast.success(t("searchResult.deleteTrackedObject.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
refreshResults();
|
||||
@@ -79,9 +83,12 @@ export default function SearchResultActions({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to delete tracked object: ${errorMessage}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("searchResult.deleteTrackedObject.toast.error", { errorMessage }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -90,45 +97,45 @@ export default function SearchResultActions({
|
||||
const menuItems = (
|
||||
<>
|
||||
{searchResult.has_clip && (
|
||||
<MenuItem aria-label="Download video">
|
||||
<MenuItem aria-label={t("itemMenu.downloadVideo.aria")}>
|
||||
<a
|
||||
className="flex items-center"
|
||||
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
|
||||
download={`${searchResult.camera}_${searchResult.label}.mp4`}
|
||||
>
|
||||
<LuDownload className="mr-2 size-4" />
|
||||
<span>Download video</span>
|
||||
<span>{t("itemMenu.downloadVideo")}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
)}
|
||||
{searchResult.has_snapshot && (
|
||||
<MenuItem aria-label="Download snapshot">
|
||||
<MenuItem aria-label={t("itemMenu.downloadSnapshot.aria")}>
|
||||
<a
|
||||
className="flex items-center"
|
||||
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
|
||||
download={`${searchResult.camera}_${searchResult.label}.jpg`}
|
||||
>
|
||||
<LuCamera className="mr-2 size-4" />
|
||||
<span>Download snapshot</span>
|
||||
<span>{t("itemMenu.downloadSnapshot.label")}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
)}
|
||||
{searchResult.data.type == "object" && (
|
||||
<MenuItem
|
||||
aria-label="Show the object lifecycle"
|
||||
aria-label={t("itemMenu.viewObjectLifecycle.aria")}
|
||||
onClick={showObjectLifecycle}
|
||||
>
|
||||
<FaArrowsRotate className="mr-2 size-4" />
|
||||
<span>View object lifecycle</span>
|
||||
<span>{t("itemMenu.viewObjectLifecycle.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{config?.semantic_search?.enabled && isContextMenu && (
|
||||
<MenuItem
|
||||
aria-label="Find similar tracked objects"
|
||||
aria-label={t("itemMenu.findSimilar.aria")}
|
||||
onClick={findSimilar}
|
||||
>
|
||||
<MdImageSearch className="mr-2 size-4" />
|
||||
<span>Find similar</span>
|
||||
<span>{t("itemMenu.findSimilar.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{isMobileOnly &&
|
||||
@@ -137,17 +144,20 @@ export default function SearchResultActions({
|
||||
searchResult.end_time &&
|
||||
searchResult.data.type == "object" &&
|
||||
!searchResult.plus_id && (
|
||||
<MenuItem aria-label="Submit to Frigate Plus" onClick={showSnapshot}>
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.submitToPlus.aria")}
|
||||
onClick={showSnapshot}
|
||||
>
|
||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||
<span>Submit to Frigate+</span>
|
||||
<span>{t("itemMenu.submitToPlus")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
aria-label="Delete this tracked object"
|
||||
aria-label={t("itemMenu.deleteTrackedObject.label")}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<LuTrash2 className="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
<span>{t("button.delete", { ns: "common" })}</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
@@ -160,24 +170,20 @@ export default function SearchResultActions({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("dialog.confirmDelete")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
Deleting this tracked object removes the snapshot, any saved
|
||||
embeddings, and any associated object lifecycle entries. Recorded
|
||||
footage of this tracked object in History view will <em>NOT</em> be
|
||||
deleted.
|
||||
<br />
|
||||
<br />
|
||||
Are you sure you want to proceed?
|
||||
<Trans ns="views/explore">dialog.confirmDelete.desc</Trans>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
{t("button.delete", { ns: "common" })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -198,7 +204,9 @@ export default function SearchResultActions({
|
||||
onClick={findSimilar}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Find similar</TooltipContent>
|
||||
<TooltipContent>
|
||||
{t("itemMenu.findSimilar.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -215,7 +223,9 @@ export default function SearchResultActions({
|
||||
onClick={showSnapshot}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Submit to Frigate+</TooltipContent>
|
||||
<TooltipContent>
|
||||
{t("itemMenu.submitToPlus.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user