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:
GuoQing Liu
2025-03-16 10:36:20 -05:00
committed by GitHub
co-authored by Josh Hawkins
parent db541abed4
commit d34533981f
150 changed files with 6809 additions and 1926 deletions
+24 -11
View File
@@ -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>
+152 -52
View File
@@ -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>
</>
)}
+47 -28
View File
@@ -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>
+38 -28
View File
@@ -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>
)}