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
+11 -3
View File
@@ -50,6 +50,7 @@ import { Toaster } from "@/components/ui/sonner";
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import LiveContextMenu from "@/components/menu/LiveContextMenu";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
import { useTranslation } from "react-i18next";
type DraggableGridLayoutProps = {
cameras: CameraConfig[];
@@ -79,6 +80,7 @@ export default function DraggableGridLayout({
fullscreen,
toggleFullscreen,
}: DraggableGridLayoutProps) {
const { t } = useTranslation(["views/live"]);
const { data: config } = useSWR<FrigateConfig>("config");
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
@@ -658,7 +660,9 @@ export default function DraggableGridLayout({
</div>
</TooltipTrigger>
<TooltipContent>
{isEditMode ? "Exit Editing" : "Edit Layout"}
{isEditMode
? t("editLayout.exitEdit")
: t("editLayout.label")}
</TooltipContent>
</Tooltip>
{!isEditMode && (
@@ -676,7 +680,9 @@ export default function DraggableGridLayout({
</div>
</TooltipTrigger>
<TooltipContent>
{isEditMode ? "Exit Editing" : "Edit Camera Group"}
{isEditMode
? t("editLayout.exitEdit")
: t("editLayout.group.label")}
</TooltipContent>
</Tooltip>
)}
@@ -694,7 +700,9 @@ export default function DraggableGridLayout({
</div>
</TooltipTrigger>
<TooltipContent>
{fullscreen ? "Exit Fullscreen" : "Fullscreen"}
{fullscreen
? t("button.exitFullscreen", { ns: "common" })
: t("button.fullscreen", { ns: "common" })}
</TooltipContent>
</Tooltip>
</>
+18 -4
View File
@@ -14,6 +14,7 @@ import {
isSafari,
useMobileOrientation,
} from "react-device-detect";
import { useTranslation } from "react-i18next";
import { FaCompress, FaExpand } from "react-icons/fa";
import { IoMdArrowBack } from "react-icons/io";
import { LuPictureInPicture } from "react-icons/lu";
@@ -32,6 +33,7 @@ export default function LiveBirdseyeView({
fullscreen,
toggleFullscreen,
}: LiveBirdseyeViewProps) {
const { t } = useTranslation(["views/live"]);
const { data: config } = useSWR<FrigateConfig>("config");
const navigate = useNavigate();
const { isPortrait } = useMobileOrientation();
@@ -144,12 +146,16 @@ export default function LiveBirdseyeView({
{!fullscreen ? (
<Button
className={`flex items-center gap-2 rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
aria-label="Go Back"
aria-label={t("label.back", { ns: "common" })}
size={isMobile ? "icon" : "sm"}
onClick={() => navigate(-1)}
>
<IoMdArrowBack className="size-5" />
{isDesktop && <div className="text-primary">Back</div>}
{isDesktop && (
<div className="text-primary">
{t("button.back", { ns: "common" })}
</div>
)}
</Button>
) : (
<div />
@@ -164,7 +170,11 @@ export default function LiveBirdseyeView({
variant={fullscreen ? "overlay" : "primary"}
Icon={fullscreen ? FaCompress : FaExpand}
isActive={fullscreen}
title={fullscreen ? "Close" : "Fullscreen"}
title={
fullscreen
? t("button.close", { ns: "common" })
: t("button.fullscreen", { ns: "common" })
}
onClick={toggleFullscreen}
/>
)}
@@ -174,7 +184,11 @@ export default function LiveBirdseyeView({
variant={fullscreen ? "overlay" : "primary"}
Icon={LuPictureInPicture}
isActive={pip}
title={pip ? "Close" : "Picture in Picture"}
title={
pip
? t("button.close", { ns: "common" })
: t("button.pictureInPicture", { ns: "common" })
}
onClick={() => {
if (!pip) {
setPip(true);
+193 -117
View File
@@ -103,6 +103,7 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import useSWR from "swr";
import { cn } from "@/lib/utils";
import { useSessionPersistence } from "@/hooks/use-session-persistence";
import {
Select,
SelectContent,
@@ -117,6 +118,7 @@ import axios from "axios";
import { toast } from "sonner";
import { Toaster } from "@/components/ui/sonner";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { Trans, useTranslation } from "react-i18next";
type LiveCameraViewProps = {
config?: FrigateConfig;
@@ -132,6 +134,7 @@ export default function LiveCameraView({
fullscreen,
toggleFullscreen,
}: LiveCameraViewProps) {
const { t } = useTranslation(["views/live"]);
const navigate = useNavigate();
const { isPortrait } = useMobileOrientation();
const mainRef = useRef<HTMLDivElement | null>(null);
@@ -427,16 +430,20 @@ export default function LiveCameraView({
>
<Button
className={`flex items-center gap-2.5 rounded-lg`}
aria-label="Go back"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={() => navigate(-1)}
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">Back</div>}
{isDesktop && (
<div className="text-primary">
{t("button.back", { ns: "common" })}
</div>
)}
</Button>
<Button
className="flex items-center gap-2.5 rounded-lg"
aria-label="Show historical footage"
aria-label={t("history.label")}
size="sm"
onClick={() => {
navigate("review", {
@@ -452,7 +459,11 @@ export default function LiveCameraView({
}}
>
<LuHistory className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">History</div>}
{isDesktop && (
<div className="text-primary">
{t("button.history", { ns: "common" })}
</div>
)}
</Button>
</div>
) : (
@@ -465,13 +476,15 @@ export default function LiveCameraView({
{fullscreen && (
<Button
className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary"
aria-label="Go back"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={() => navigate(-1)}
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && (
<div className="text-secondary-foreground">Back</div>
<div className="text-secondary-foreground">
{t("button.back", { ns: "common" })}
</div>
)}
</Button>
)}
@@ -481,7 +494,11 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"}
Icon={fullscreen ? FaCompress : FaExpand}
isActive={fullscreen}
title={fullscreen ? "Close" : "Fullscreen"}
title={
fullscreen
? t("button.close", { ns: "common" })
: t("button.fullscreen", { ns: "common" })
}
onClick={toggleFullscreen}
/>
)}
@@ -491,7 +508,11 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"}
Icon={LuPictureInPicture}
isActive={pip}
title={pip ? "Close" : "Picture in Picture"}
title={
pip
? t("button.close", { ns: "common" })
: t("button.pictureInPicture", { ns: "common" })
}
onClick={() => {
if (!pip) {
setPip(true);
@@ -667,6 +688,7 @@ function PtzControlPanel({
clickOverlay: boolean;
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { t } = useTranslation(["views/live"]);
const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`);
const { send: sendPtz } = usePtzCommand(camera);
@@ -752,7 +774,7 @@ function PtzControlPanel({
{ptz?.features?.includes("pt") && (
<>
<TooltipButton
label="Move camera left"
label={t("ptz.move.left.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_LEFT");
@@ -767,7 +789,7 @@ function PtzControlPanel({
<FaAngleLeft />
</TooltipButton>
<TooltipButton
label="Move camera up"
label={t("ptz.move.up.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_UP");
@@ -782,7 +804,7 @@ function PtzControlPanel({
<FaAngleUp />
</TooltipButton>
<TooltipButton
label="Move camera down"
label={t("ptz.move.down.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_DOWN");
@@ -797,7 +819,7 @@ function PtzControlPanel({
<FaAngleDown />
</TooltipButton>
<TooltipButton
label="Move camera right"
label={t("ptz.move.right.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_RIGHT");
@@ -816,7 +838,7 @@ function PtzControlPanel({
{ptz?.features?.includes("zoom") && (
<>
<TooltipButton
label="Zoom in"
label={t("ptz.zoom.in.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("ZOOM_IN");
@@ -831,7 +853,7 @@ function PtzControlPanel({
<MdZoomIn />
</TooltipButton>
<TooltipButton
label="Zoom out"
label={t("ptz.zoom.out.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("ZOOM_OUT");
@@ -854,14 +876,19 @@ function PtzControlPanel({
<TooltipTrigger asChild>
<Button
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
aria-label="Click in the frame to center the camera"
aria-label={t("ptz.move.clickMove.label")}
onClick={() => setClickOverlay(!clickOverlay)}
>
<TbViewfinder />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{clickOverlay ? "Disable" : "Enable"} click to move</p>
<p>
{clickOverlay
? t("ptz.move.clickMove.disable")
: t("ptz.move.clickMove.enable")}{" "}
click to move
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -872,7 +899,7 @@ function PtzControlPanel({
<TooltipTrigger asChild>
<DropdownMenu modal={!isDesktop}>
<DropdownMenuTrigger asChild>
<Button aria-label="PTZ camera presets">
<Button aria-label={t("ptz.presets")}>
<BsThreeDotsVertical />
</Button>
</DropdownMenuTrigger>
@@ -894,7 +921,7 @@ function PtzControlPanel({
</DropdownMenu>
</TooltipTrigger>
<TooltipContent>
<p>PTZ camera presets</p>
<p>{t("ptz.presets")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -904,6 +931,7 @@ function PtzControlPanel({
}
function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
const { t } = useTranslation(["views/live"]);
const rankMap = { all: 0, motion: 1, active_objects: 2 };
const getValidMode = (retain?: { mode?: string }): keyof typeof rankMap => {
const mode = retain?.mode;
@@ -918,13 +946,25 @@ function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
? recordRetainMode
: alertsRetainMode;
const source = effectiveRetainMode === recordRetainMode ? "camera" : "alerts";
const source =
effectiveRetainMode === recordRetainMode
? t("camera", { ns: "views/events" })
: t("alerts", { ns: "views/events" });
return effectiveRetainMode !== "all" ? (
<div>
Your {source} recording retention configuration is set to{" "}
<code>mode: {effectiveRetainMode}</code>, so this on-demand recording will
only keep segments with {effectiveRetainMode.replaceAll("_", " ")}.
<Trans
ns="views/live"
values={{
source,
effectiveRetainMode,
effectiveRetainModeName: t(
"effectiveRetainMode.modes." + effectiveRetainMode,
),
}}
>
effectiveRetainMode.notAllTips
</Trans>
</div>
) : null;
}
@@ -967,6 +1007,8 @@ function FrigateCameraFeatures({
supports2WayTalk,
cameraEnabled,
}: FrigateCameraFeaturesProps) {
const { t } = useTranslation(["views/live"]);
const { payload: detectState, send: sendDetect } = useDetectState(
camera.name,
);
@@ -1010,14 +1052,9 @@ function FrigateCameraFeatures({
setIsRecording(true);
const toastId = toast.success(
<div className="flex flex-col space-y-3">
<div className="font-semibold">
Started manual on-demand recording.
</div>
<div className="font-semibold">{t("manualRecording.started")}</div>
{!camera.record.enabled || camera.record.alerts.retain.days == 0 ? (
<div>
Since recording is disabled or restricted in the config for this
camera, only a snapshot will be saved.
</div>
<div>{t("manualRecording.recordDisabledTips")}</div>
) : (
<OnDemandRetentionMessage camera={camera} />
)}
@@ -1030,11 +1067,11 @@ function FrigateCameraFeatures({
setActiveToastId(toastId);
}
} catch (error) {
toast.error("Failed to start manual on-demand recording.", {
toast.error(t("manualRecording.failedToStart"), {
position: "top-center",
});
}
}, [camera]);
}, [camera, t]);
const endEvent = useCallback(() => {
if (activeToastId) {
@@ -1047,16 +1084,16 @@ function FrigateCameraFeatures({
});
recordingEventIdRef.current = null;
setIsRecording(false);
toast.success("Ended manual on-demand recording.", {
toast.success(t("manualRecording.ended"), {
position: "top-center",
});
}
} catch (error) {
toast.error("Failed to end manual on-demand recording.", {
toast.error(t("manualRecording.failedToEnd"), {
position: "top-center",
});
}
}, [activeToastId]);
}, [activeToastId, t]);
const handleEventButtonClick = useCallback(() => {
if (isRecording) {
@@ -1092,7 +1129,9 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={enabledState == "ON" ? LuPower : LuPowerOff}
isActive={enabledState == "ON"}
title={`${enabledState == "ON" ? "Disable" : "Enable"} Camera`}
title={
enabledState == "ON" ? t("camera.disable") : t("camera.enable")
}
onClick={() => sendEnabled(enabledState == "ON" ? "OFF" : "ON")}
disabled={false}
/>
@@ -1101,7 +1140,9 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={detectState == "ON" ? MdPersonSearch : MdPersonOff}
isActive={detectState == "ON"}
title={`${detectState == "ON" ? "Disable" : "Enable"} Detect`}
title={
detectState == "ON" ? t("detect.disable") : t("detect.enable")
}
onClick={() => sendDetect(detectState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
@@ -1110,7 +1151,11 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={recordState == "ON" ? LuVideo : LuVideoOff}
isActive={recordState == "ON"}
title={`${recordState == "ON" ? "Disable" : "Enable"} Recording`}
title={
recordState == "ON"
? t("recording.disable")
: t("recording.enable")
}
onClick={() => sendRecord(recordState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
@@ -1119,7 +1164,11 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={snapshotState == "ON" ? MdPhotoCamera : MdNoPhotography}
isActive={snapshotState == "ON"}
title={`${snapshotState == "ON" ? "Disable" : "Enable"} Snapshots`}
title={
snapshotState == "ON"
? t("snapshots.disable")
: t("snapshots.enable")
}
onClick={() => sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
@@ -1129,7 +1178,11 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={audioState == "ON" ? LuEar : LuEarOff}
isActive={audioState == "ON"}
title={`${audioState == "ON" ? "Disable" : "Enable"} Audio Detect`}
title={
audioState == "ON"
? t("audioDetect.disable")
: t("audioDetect.enable")
}
onClick={() => sendAudio(audioState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
@@ -1142,7 +1195,11 @@ function FrigateCameraFeatures({
autotrackingState == "ON" ? TbViewfinder : TbViewfinderOff
}
isActive={autotrackingState == "ON"}
title={`${autotrackingState == "ON" ? "Disable" : "Enable"} Autotracking`}
title={
autotrackingState == "ON"
? t("autotracking.disable")
: t("autotracking.enable")
}
onClick={() =>
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
}
@@ -1159,7 +1216,7 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={isRecording ? TbRecordMail : TbRecordMailOff}
isActive={isRecording}
title={`${isRecording ? "Stop" : "Start"} on-demand recording`}
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
onClick={handleEventButtonClick}
disabled={!cameraEnabled}
/>
@@ -1180,20 +1237,29 @@ function FrigateCameraFeatures({
<div className="flex flex-col gap-5 p-4">
{!isRestreamed && (
<div className="flex flex-col gap-2">
<Label>Stream</Label>
<Label>
{t("streaming.label", { ns: "components/dialog" })}
</Label>
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" />
<div>Restreaming is not enabled for this camera.</div>
<div>
{t("streaming.restreaming.NotEnabled", {
ns: "components/dialog",
})}
</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
Set up go2rtc for additional live view options and audio
for this camera.
{t("streaming.restreaming.desc", {
ns: "components/dialog",
})}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -1201,7 +1267,9 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
{t("streaming.restreaming.readTheDocumentation", {
ns: "components/dialog",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -1213,7 +1281,9 @@ function FrigateCameraFeatures({
{isRestreamed &&
Object.values(camera.live.streams).length > 0 && (
<div className="flex flex-col gap-1">
<Label htmlFor="streaming-method">Stream</Label>
<Label htmlFor="streaming-method">
{t("stream.title")}
</Label>
<Select
value={streamName}
onValueChange={(value) => {
@@ -1248,22 +1318,23 @@ function FrigateCameraFeatures({
{supportsAudioOutput ? (
<>
<LuCheck className="size-4 text-success" />
<div>Audio is available for this stream</div>
<div>{t("stream.audio.available")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>Audio is unavailable for this stream</div>
<div>{t("stream.audio.unavailable")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
Audio must be output from your camera and
configured in go2rtc for this stream.
{t("stream.audio.tips")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -1271,7 +1342,7 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
{t("stream.audio.tips.documentation")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -1288,26 +1359,23 @@ function FrigateCameraFeatures({
{supports2WayTalk ? (
<>
<LuCheck className="size-4 text-success" />
<div>
Two-way talk is available for this stream
</div>
<div>{t("stream.twoWayTalk.available")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>
Two-way talk is unavailable for this stream
</div>
<div>{t("stream.twoWayTalk.available")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
Your device must suppport the feature and
WebRTC must be configured for two-way talk.
{t("stream.twoWayTalk.tips")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live/#webrtc-extra-configuration"
@@ -1315,7 +1383,9 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
{t(
"stream.twoWayTalk.tips.documentation",
)}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -1332,20 +1402,19 @@ function FrigateCameraFeatures({
<IoIosWarning className="mr-1 size-8 text-danger" />
<p className="text-sm">
Live view is in low-bandwidth mode due to buffering
or stream errors.
{t("stream.lowBandwidth.tips")}
</p>
</div>
<Button
className={`flex items-center gap-2.5 rounded-lg`}
aria-label="Reset the stream"
aria-label={t("stream.lowBandwidth.resetStream")}
variant="outline"
size="sm"
onClick={() => setLowBandwidth(false)}
>
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
<div className="text-primary-variant">
Reset stream
{t("stream.lowBandwidth.resetStream")}
</div>
</Button>
</div>
@@ -1359,7 +1428,7 @@ function FrigateCameraFeatures({
className="mx-0 cursor-pointer text-primary"
htmlFor="backgroundplay"
>
Play in background
{t("stream.playInBackground.label")}
</Label>
<Switch
className="ml-1"
@@ -1371,8 +1440,7 @@ function FrigateCameraFeatures({
/>
</div>
<p className="text-sm text-muted-foreground">
Enable this option to continue streaming when the player is
hidden.
{t("stream.playInBackground.tips")}
</p>
</div>
)}
@@ -1382,7 +1450,9 @@ function FrigateCameraFeatures({
className="mx-0 cursor-pointer text-primary"
htmlFor="showstats"
>
Show stream stats
{t("streaming.showStats.label", {
ns: "components/dialog",
})}
</Label>
<Switch
className="ml-1"
@@ -1392,13 +1462,12 @@ function FrigateCameraFeatures({
/>
</div>
<p className="text-sm text-muted-foreground">
Enable this option to show stream statistics as an overlay on
the camera feed.
{t("streaming.showStats.desc", { ns: "components/dialog" })}
</p>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between text-sm font-medium leading-none">
Debug View
{t("streaming.debugView", { ns: "components/dialog" })}
<LuExternalLink
onClick={() =>
navigate(`/settings?page=debug&camera=${camera.name}`)
@@ -1427,7 +1496,7 @@ function FrigateCameraFeatures({
variant="primary"
Icon={FaCog}
isActive={false}
title={`${camera} Settings`}
title={t("cameraSettings.title", { camera })}
/>
</DrawerTrigger>
<DrawerContent className="rounded-2xl px-2 py-4">
@@ -1435,14 +1504,14 @@ function FrigateCameraFeatures({
{isAdmin && (
<>
<FilterSwitch
label="Camera Enabled"
label={t("cameraSettings.cameraEnabled")}
isChecked={enabledState == "ON"}
onCheckedChange={() =>
sendEnabled(enabledState == "ON" ? "OFF" : "ON")
}
/>
<FilterSwitch
label="Object Detection"
label={t("cameraSettings.objectDetection")}
isChecked={detectState == "ON"}
onCheckedChange={() =>
sendDetect(detectState == "ON" ? "OFF" : "ON")
@@ -1450,7 +1519,7 @@ function FrigateCameraFeatures({
/>
{recordingEnabled && (
<FilterSwitch
label="Recording"
label={t("cameraSettings.recording")}
isChecked={recordState == "ON"}
onCheckedChange={() =>
sendRecord(recordState == "ON" ? "OFF" : "ON")
@@ -1458,7 +1527,7 @@ function FrigateCameraFeatures({
/>
)}
<FilterSwitch
label="Snapshots"
label={t("cameraSettings.snapshots")}
isChecked={snapshotState == "ON"}
onCheckedChange={() =>
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
@@ -1466,7 +1535,7 @@ function FrigateCameraFeatures({
/>
{audioDetectEnabled && (
<FilterSwitch
label="Audio Detection"
label={t("cameraSettings.audioDetection")}
isChecked={audioState == "ON"}
onCheckedChange={() =>
sendAudio(audioState == "ON" ? "OFF" : "ON")
@@ -1475,7 +1544,7 @@ function FrigateCameraFeatures({
)}
{autotrackingEnabled && (
<FilterSwitch
label="Autotracking"
label={t("cameraSettings.autotracking")}
isChecked={autotrackingState == "ON"}
onCheckedChange={() =>
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
@@ -1489,20 +1558,27 @@ function FrigateCameraFeatures({
<div className="mt-3 flex flex-col gap-5">
{!isRestreamed && (
<div className="flex flex-col gap-2 p-2">
<Label>Stream</Label>
<Label>{t("streaming", { ns: "components/dialog" })}</Label>
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" />
<div>Restreaming is not enabled for this camera.</div>
<div>
{t("streaming.restreaming.disabled", {
ns: "components/dialog",
})}
</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
Set up go2rtc for additional live view options and audio for
this camera.
{t("streaming.restreaming.desc", {
ns: "components/dialog",
})}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -1510,7 +1586,9 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
{t("streaming.restreaming.readTheDocumentation", {
ns: "components/dialog",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -1521,7 +1599,7 @@ function FrigateCameraFeatures({
)}
{isRestreamed && Object.values(camera.live.streams).length > 0 && (
<div className="mt-1 p-2">
<div className="mb-1 text-sm">Stream</div>
<div className="mb-1 text-sm">{t("stream.title")}</div>
<Select
value={streamName}
onValueChange={(value) => {
@@ -1555,22 +1633,23 @@ function FrigateCameraFeatures({
{supportsAudioOutput ? (
<>
<LuCheck className="size-4 text-success" />
<div>Audio is available for this stream</div>
<div>{t("stream.audio.available")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>Audio is unavailable for this stream</div>
<div>{t("stream.audio.unavailable")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-52 text-xs">
Audio must be output from your camera and configured
in go2rtc for this stream.
{t("stream.audio.tips")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -1578,7 +1657,7 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
{t("stream.audio.tips.documentation")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -1595,22 +1674,23 @@ function FrigateCameraFeatures({
{supports2WayTalk ? (
<>
<LuCheck className="size-4 text-success" />
<div>Two-way talk is available for this stream</div>
<div>{t("stream.twoWayTalk.available")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>Two-way talk is unavailable for this stream</div>
<div>{t("stream.twoWayTalk.unavailable")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-52 text-xs">
Your device must suppport the feature and WebRTC
must be configured for two-way talk.
{t("stream.twoWayTalk.tips")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live/#webrtc-extra-configuration"
@@ -1618,7 +1698,7 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
{t("stream.twoWayTalk.tips.documentation")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -1633,10 +1713,7 @@ function FrigateCameraFeatures({
<div className="flex flex-row items-center gap-2">
<IoIosWarning className="mr-1 size-8 text-danger" />
<p className="text-sm">
Live view is in low-bandwidth mode due to buffering or
stream errors.
</p>
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p>
</div>
<Button
className={`flex items-center gap-2.5 rounded-lg`}
@@ -1646,7 +1723,9 @@ function FrigateCameraFeatures({
onClick={() => setLowBandwidth(false)}
>
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
<div className="text-primary-variant">Reset stream</div>
<div className="text-primary-variant">
{t("stream.lowBandwidth.resetStream")}
</div>
</Button>
</div>
)}
@@ -1654,7 +1733,7 @@ function FrigateCameraFeatures({
)}
<div className="flex flex-col gap-1 px-2">
<div className="mb-1 text-sm font-medium leading-none">
On-Demand Recording
{t("manualRecording.title")}
</div>
<Button
onClick={handleEventButtonClick}
@@ -1663,46 +1742,43 @@ function FrigateCameraFeatures({
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
)}
>
{isRecording ? "End" : "Start"} on-demand recording
{t("manualRecording." + isRecording ? "end" : "start")}
</Button>
<p className="text-sm text-muted-foreground">
Start a manual event based on this camera's recording retention
settings.
{t("manualRecording.tips")}
</p>
</div>
{isRestreamed && (
<>
<div className="flex flex-col gap-2">
<FilterSwitch
label="Play in Background"
label={t("manualRecording.playInBackground.label")}
isChecked={playInBackground}
onCheckedChange={(checked) => {
setPlayInBackground(checked);
}}
/>
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
Enable this option to continue streaming when the player is
hidden.
{t("manualRecording.playInBackground.desc")}
</p>
</div>
<div className="flex flex-col gap-2">
<FilterSwitch
label="Show Stats"
label={t("manualRecording.showStats.label")}
isChecked={showStats}
onCheckedChange={(checked) => {
setShowStats(checked);
}}
/>
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
Enable this option to show stream statistics as an overlay on
the camera feed.
{t("manualRecording.showStats.desc")}
</p>
</div>
</>
)}
<div className="mb-3 flex flex-col gap-1 px-2">
<div className="flex items-center justify-between text-sm font-medium leading-none">
Debug View
{t("manualRecording.debugView")}
<LuExternalLink
onClick={() =>
navigate(`/settings?page=debug&camera=${camera.name}`)
+9 -2
View File
@@ -43,6 +43,7 @@ import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import { useResizeObserver } from "@/hooks/resize-observer";
import LiveContextMenu from "@/components/menu/LiveContextMenu";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
import { useTranslation } from "react-i18next";
type LiveDashboardViewProps = {
cameras: CameraConfig[];
@@ -60,6 +61,8 @@ export default function LiveDashboardView({
fullscreen,
toggleFullscreen,
}: LiveDashboardViewProps) {
const { t } = useTranslation(["views/live"]);
const { data: config } = useSWR<FrigateConfig>("config");
// layout
@@ -471,7 +474,9 @@ export default function LiveDashboardView({
}
const streamName =
currentGroupStreamingSettings?.[camera.name]?.streamName ||
Object.values(camera.live.streams)?.[0];
camera?.live?.streams
? Object?.values(camera?.live?.streams)?.[0]
: "";
const autoLive =
currentGroupStreamingSettings?.[camera.name]?.streamType !==
"no-streaming";
@@ -560,7 +565,9 @@ export default function LiveDashboardView({
</div>
</TooltipTrigger>
<TooltipContent>
{fullscreen ? "Exit Fullscreen" : "Fullscreen"}
{fullscreen
? t("button.exitFullscreen", { ns: "common" })
: t("button.fullscreen", { ns: "common" })}
</TooltipContent>
</Tooltip>
</div>