mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-05 21:17:43 +03:00
chore: add more i18n keys again
This commit is contained in:
parent
b35fbef325
commit
a52592bcac
@ -1,5 +1,8 @@
|
||||
{
|
||||
"time": {
|
||||
"untilForTime": "Until {{time}}",
|
||||
"untilForRestart": "Until Frigate restarts.",
|
||||
"untilRestart": "Until restart",
|
||||
"ago": "{{timeAgo}} ago",
|
||||
"justNow": "Just now",
|
||||
"today": "Today",
|
||||
@ -11,6 +14,12 @@
|
||||
"lastWeek": "Last Week",
|
||||
"thisMonth": "This Month",
|
||||
"lastMonth": "Last Month",
|
||||
"5minutes": "5 minutes",
|
||||
"10minutes": "10 minutes",
|
||||
"30minutes": "30 minutes",
|
||||
"1hour": "1 hour",
|
||||
"12hours": "12 hours",
|
||||
"24hours": "24 hours",
|
||||
"pm": "pm",
|
||||
"am": "am",
|
||||
"yr": "{{time}}yr",
|
||||
@ -43,6 +52,9 @@
|
||||
"apply": "Apply",
|
||||
"reset": "Reset",
|
||||
"enabled": "Enabled",
|
||||
"enable": "Enable",
|
||||
"disabled": "Disabled",
|
||||
"disable": "Disable",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"cancel": "Cancel",
|
||||
@ -61,7 +73,9 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"download": "Download",
|
||||
"info": "Info"
|
||||
"info": "Info",
|
||||
"suspended": "Suspended",
|
||||
"unsuspended": "Unsuspend"
|
||||
},
|
||||
"menu": {
|
||||
"system": "System",
|
||||
@ -103,9 +117,11 @@
|
||||
"uiPlayground": "UI Playground",
|
||||
"faceLibrary": "Face Library",
|
||||
"user": {
|
||||
"account": "Account",
|
||||
"current": "Current User: {{user}}",
|
||||
"anonymous": "anonymous",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"setPassword": "Set Password"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
|
||||
@ -72,5 +72,16 @@
|
||||
"overwrite": "{{searchName}} already exists. Saving will overwrite the existing value.",
|
||||
"success": "Search ({{searchName}}) has been saved."
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
"confirmDelete": {
|
||||
"title": "Confirm Delete",
|
||||
"desc": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future."
|
||||
},
|
||||
"button": {
|
||||
"export": "Export",
|
||||
"markAsReviewed": "Mark as reviewed",
|
||||
"deleteNow": "Delete Now"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"label": "Filter",
|
||||
"filter": "Filter",
|
||||
"labels": {
|
||||
"all": "All Labels",
|
||||
"all.short": "Labels",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"documentTitle": "Live - Frigate",
|
||||
"documentTitle.withCamera": "{{camera}} - Live - Frigate",
|
||||
"lowBandwidthMode": "Low-bandwidth Mode",
|
||||
"twoWayTalk": {
|
||||
"enable": "Enable Two Way Talk",
|
||||
"disable": "Disable Two Way Talk"
|
||||
@ -42,6 +43,10 @@
|
||||
"enable": "Enable Camera",
|
||||
"disable": "Disable Camera"
|
||||
},
|
||||
"muteCameras": {
|
||||
"enable": "Mute All Cameras",
|
||||
"disable": "Unmute All Cameras"
|
||||
},
|
||||
"detect": {
|
||||
"enable": "Enable Detect",
|
||||
"disable": "Disable Detect"
|
||||
@ -62,6 +67,10 @@
|
||||
"enable": "Enable Autotracking",
|
||||
"disable": "Disable Autotracking"
|
||||
},
|
||||
"streamStats": {
|
||||
"enable": "Show Stream Stats",
|
||||
"disable": "Hide Stream Stats"
|
||||
},
|
||||
"manualRecording": {
|
||||
"start": "Start on-demand recording",
|
||||
"started": "Started manual on-demand recording.",
|
||||
@ -70,5 +79,11 @@
|
||||
"end": "End on-demand recording",
|
||||
"ended": "Ended manual on-demand recording.",
|
||||
"failedToEnd": "Failed to end manual on-demand recording."
|
||||
},
|
||||
"streamingSettings": "Streaming Settings",
|
||||
"notifications": "Notifications",
|
||||
"audio": "Audio",
|
||||
"suspend:": {
|
||||
"forTime": "Suspend for: "
|
||||
}
|
||||
}
|
||||
|
||||
11
web/public/locales/en/views/recording.json
Normal file
11
web/public/locales/en/views/recording.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"export": "Export",
|
||||
"calendar": "Calendar",
|
||||
"filter": "Filter",
|
||||
"toast": {
|
||||
"error": {
|
||||
"noValidTimeSelected": "No valid time range selected",
|
||||
"endTimeMustAfterStartTime": "End time must be after start time"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -258,7 +258,8 @@
|
||||
"toast": {
|
||||
"success": {
|
||||
"createUser": "User {{user}} created successfully",
|
||||
"deleteUser": "User {{user}} deleted successfully"
|
||||
"deleteUser": "User {{user}} deleted successfully",
|
||||
"updatePassword": "Password updated successfully."
|
||||
},
|
||||
"error": {
|
||||
"setPasswordFailed": "Failed to save password: {{errorMessage}}",
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
{
|
||||
"time": {
|
||||
"untilForTime": "直到 {{time}}",
|
||||
"untilForRestart": "直到 Frigate 重启。",
|
||||
"untilRestart": "直到重启",
|
||||
"ago": "{{timeAgo}} 前",
|
||||
"justNow": "刚才",
|
||||
"today": "今天",
|
||||
@ -11,6 +14,12 @@
|
||||
"lastWeek": "上个周",
|
||||
"thisMonth": "本月",
|
||||
"lastMonth": "上个月",
|
||||
"5minutes": "5 分钟",
|
||||
"10minutes": "10 分钟",
|
||||
"30minutes": "30 分钟",
|
||||
"1hour": "1 小时",
|
||||
"12hours": "12 小时",
|
||||
"24hours": "24 小时",
|
||||
"pm": "上午",
|
||||
"am": "下午",
|
||||
"yr": "{{time}}年",
|
||||
@ -43,6 +52,9 @@
|
||||
"apply": "应用",
|
||||
"reset": "重置",
|
||||
"enabled": "启用",
|
||||
"enable": "启用",
|
||||
"disabled": "禁用",
|
||||
"disable": "禁用",
|
||||
"save": "保存",
|
||||
"saving": "保存中……",
|
||||
"cancel": "取消",
|
||||
@ -61,7 +73,9 @@
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"download": "下载",
|
||||
"info": "信息"
|
||||
"info": "信息",
|
||||
"suspended": "已暂停",
|
||||
"unsuspended": "取消暂停"
|
||||
},
|
||||
"menu": {
|
||||
"system": "系统",
|
||||
@ -102,9 +116,11 @@
|
||||
"uiPlayground": "UI Playground",
|
||||
"faceLibrary": "人脸管理",
|
||||
"user": {
|
||||
"account": "账号",
|
||||
"current": "当前用户:{{user}}",
|
||||
"anonymous": "匿名",
|
||||
"logout": "登出"
|
||||
"logout": "登出",
|
||||
"setPassword": "设置密码"
|
||||
},
|
||||
"restart": "重启 Frigate"
|
||||
},
|
||||
|
||||
@ -72,5 +72,16 @@
|
||||
"overwrite": "{{searchName}} 已存在。保存将覆盖现有值。",
|
||||
"success": "搜索 ({{searchName}}) 已保存。"
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
"confirmDelete": {
|
||||
"title": "确认删除",
|
||||
"desc": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。"
|
||||
},
|
||||
"button": {
|
||||
"export": "导出",
|
||||
"markAsReviewed": "标记为已审核",
|
||||
"deleteNow": "立即删除"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"label": "过滤器",
|
||||
"filter": "过滤器",
|
||||
"labels": {
|
||||
"all": "所有标签",
|
||||
"all.short": "标签",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"documentTitle": "实时监控 - Frigate",
|
||||
"documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate",
|
||||
"lowBandwidthMode": "低带宽模式",
|
||||
"twoWayTalk": {
|
||||
"enable": "开启双向对话",
|
||||
"disable": "关闭双向对话"
|
||||
@ -42,6 +43,10 @@
|
||||
"enable": "开启摄像头",
|
||||
"disable": "关闭摄像头"
|
||||
},
|
||||
"muteCameras": {
|
||||
"enable": "屏蔽所有摄像头",
|
||||
"disable": "取消屏蔽所有摄像头"
|
||||
},
|
||||
"detect": {
|
||||
"enable": "启用检测",
|
||||
"disable": "关闭检测"
|
||||
@ -62,6 +67,10 @@
|
||||
"enable": "启用自动追踪",
|
||||
"disable": "关闭自动追踪"
|
||||
},
|
||||
"streamStats": {
|
||||
"enable": "显示视频流统计信息",
|
||||
"disable": "隐藏视频流统计信息"
|
||||
},
|
||||
"manualRecording": {
|
||||
"start": "开始手动按需录制",
|
||||
"started": "已启用手动按需录制",
|
||||
@ -70,5 +79,11 @@
|
||||
"end": "停止手动按需录制",
|
||||
"ended": "已完成手动按需录制",
|
||||
"failedToEnd": "停止手动录制失败"
|
||||
},
|
||||
"streamingSettings": "视频流设置",
|
||||
"notifications": "通知",
|
||||
"audio": "音频",
|
||||
"suspend": {
|
||||
"forTime": "暂停时长:"
|
||||
}
|
||||
}
|
||||
|
||||
11
web/public/locales/zh-CN/views/recording.json
Normal file
11
web/public/locales/zh-CN/views/recording.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"export": "导出",
|
||||
"calendar": "日历",
|
||||
"filter": "筛选",
|
||||
"toast": {
|
||||
"error": {
|
||||
"noValidTimeSelected": "未选择有效的时间范围",
|
||||
"endTimeMustAfterStartTime": "结束时间必须晚于开始时间"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -258,7 +258,8 @@
|
||||
"toast": {
|
||||
"success": {
|
||||
"createUser": "用户 {{user}} 创建成功",
|
||||
"deleteUser": "用户 {{user}} 删除成功"
|
||||
"deleteUser": "用户 {{user}} 删除成功",
|
||||
"updatePassword": "已成功修改密码"
|
||||
},
|
||||
"error": {
|
||||
"setPasswordFailed": "保存密码出现错误:{{errorMessage}}",
|
||||
@ -317,7 +318,7 @@
|
||||
"title": "更改用户权限组",
|
||||
"desc": "更新 <span className=\"font-medium\">{{username}}</span> 的权限",
|
||||
"roleInfo": "<p>请选择此用户的适当角色:</p><ul className=\"mt-2 space-y-1 pl-5\"><li> • <span className=\"font-medium\">管理员 (Admin):</span> 拥有所有功能的完整访问权限。</li><li> • <span className=\"font-medium\">查看者 (Viewer):</span> 仅限访问实时监控、回放、探测和导出功能。</li></ul>"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
|
||||
@ -4,8 +4,8 @@ import {
|
||||
StatusMessage,
|
||||
} from "@/context/statusbar-provider";
|
||||
import useStats, { useAutoFrigateStats } from "@/hooks/use-stats";
|
||||
import { t } from "i18next";
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { FaCheck } from "react-icons/fa";
|
||||
import { IoIosWarning } from "react-icons/io";
|
||||
@ -13,6 +13,8 @@ import { MdCircle } from "react-icons/md";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function Statusbar() {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
|
||||
const { messages, addMessage, clearMessages } = useContext(
|
||||
StatusBarMessagesContext,
|
||||
)!;
|
||||
@ -131,7 +133,7 @@ export default function Statusbar() {
|
||||
{Object.entries(messages).length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FaCheck className="size-3 text-green-500" />
|
||||
{t("stats.healthy", { ns: "views/system" })}
|
||||
{t("stats.healthy")}
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(messages).map(([key, messageArray]) => (
|
||||
|
||||
@ -35,7 +35,7 @@ import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { buttonVariants } from "../ui/button";
|
||||
import { t } from "i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type ReviewCardProps = {
|
||||
event: ReviewSegment;
|
||||
@ -47,6 +47,7 @@ export default function ReviewCard({
|
||||
currentTime,
|
||||
onClick,
|
||||
}: ReviewCardProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||
const formattedDate = useFormattedTimestamp(
|
||||
@ -83,28 +84,20 @@ export default function ReviewCard({
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
t("export.toast.success", { ns: "components/dialog" }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response?.data?.message) {
|
||||
toast.error(
|
||||
`Failed to start export: ${error.response.data.message}`,
|
||||
{ position: "top-center" },
|
||||
);
|
||||
} else {
|
||||
toast.error(`Failed to start export: ${error.message}`, {
|
||||
toast.success(t("export.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message || error.message || "Unknown error";
|
||||
toast.error(t("export.toast.error.failed", { error: errorMessage }), {
|
||||
position: "top-center",
|
||||
});
|
||||
});
|
||||
setOptionsOpen(false);
|
||||
}, [event]);
|
||||
}, [event, t]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
await axios.post(`reviews/delete`, { ids: [event.id] });
|
||||
@ -219,24 +212,24 @@ export default function ReviewCard({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
{t("recording.confirmDelete.title")}
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete all recorded video associated with
|
||||
this review item?
|
||||
<br />
|
||||
<br />
|
||||
Hold the <em>Shift</em> key to bypass this dialog in the future.
|
||||
<Trans ns="components/dialog">
|
||||
recording.confirmDelete.title
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
|
||||
Cancel
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={onDelete}
|
||||
>
|
||||
Delete
|
||||
{t("button.delete", { ns: "common" })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -250,7 +243,9 @@ export default function ReviewCard({
|
||||
onClick={onExport}
|
||||
>
|
||||
<FaCompactDisc className="text-secondary-foreground" />
|
||||
<div className="text-primary">Export</div>
|
||||
<div className="text-primary">
|
||||
{t("recording.button.export")}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
{!event.has_been_reviewed && (
|
||||
@ -260,7 +255,9 @@ export default function ReviewCard({
|
||||
onClick={onMarkAsReviewed}
|
||||
>
|
||||
<FaCircleCheck className="text-secondary-foreground" />
|
||||
<div className="text-primary">Mark as reviewed</div>
|
||||
<div className="text-primary">
|
||||
{t("recording.button.markAsReviewed")}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
@ -271,7 +268,9 @@ export default function ReviewCard({
|
||||
>
|
||||
<HiTrash className="text-secondary-foreground" />
|
||||
<div className="text-primary">
|
||||
{bypassDialogRef.current ? "Delete Now" : "Delete"}
|
||||
{bypassDialogRef.current
|
||||
? t("recording.button.deleteNow")
|
||||
: t("button.delete", { ns: "common" })}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
|
||||
@ -743,7 +743,10 @@ export function CameraGroupEdit({
|
||||
setAllGroupsStreamingSettings(updatedSettings);
|
||||
} else {
|
||||
toast.error(
|
||||
t("toast.save.error", { errorMessage: res.statusText }),
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
@ -758,6 +761,7 @@ export function CameraGroupEdit({
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage,
|
||||
ns: "common",
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
|
||||
@ -23,8 +23,7 @@ import { FilterList, GeneralFilter } from "@/types/filter";
|
||||
import CalendarFilterButton from "./CalendarFilterButton";
|
||||
import { CamerasFilterButton } from "./CamerasFilterButton";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const REVIEW_FILTERS = [
|
||||
"cameras",
|
||||
@ -265,6 +264,7 @@ function ShowReviewFilter({
|
||||
showReviewed,
|
||||
setShowReviewed,
|
||||
}: ShowReviewedFilterProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [showReviewedSwitch, setShowReviewedSwitch] = useOptimisticState(
|
||||
showReviewed,
|
||||
setShowReviewed,
|
||||
@ -280,7 +280,7 @@ function ShowReviewFilter({
|
||||
}
|
||||
/>
|
||||
<Label className="ml-2 cursor-pointer text-primary" htmlFor="reviewed">
|
||||
{t("review.showReviewed", { ns: "components/filter" })}
|
||||
{t("review.showReviewed")}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@ -322,6 +322,7 @@ function GeneralFilterButton({
|
||||
selectedZones,
|
||||
onUpdateFilter,
|
||||
}: GeneralFilterButtonProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState<GeneralFilter>({
|
||||
labels: selectedLabels,
|
||||
@ -366,7 +367,7 @@ function GeneralFilterButton({
|
||||
: "text-primary"
|
||||
}`}
|
||||
>
|
||||
{t("label", { ns: "components/filter" })}
|
||||
{t("filter")}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -441,6 +442,7 @@ export function GeneralFilterContent({
|
||||
onReset,
|
||||
onClose,
|
||||
}: GeneralFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<>
|
||||
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||
@ -474,7 +476,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
{t("labels.all", { ns: "components/filter" })}
|
||||
{t("labels.all")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -521,7 +523,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allZones"
|
||||
>
|
||||
{t("zones.all", { ns: "components/filter" })}
|
||||
{t("zones.all")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -595,6 +597,7 @@ function ShowMotionOnlyButton({
|
||||
motionOnly,
|
||||
setMotionOnly,
|
||||
}: ShowMotionOnlyButtonProps) {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
const [motionOnlyButton, setMotionOnlyButton] = useOptimisticState(
|
||||
motionOnly,
|
||||
setMotionOnly,
|
||||
@ -613,7 +616,7 @@ function ShowMotionOnlyButton({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="collapse-motion"
|
||||
>
|
||||
{t("motion.only", { ns: "views/events" })}
|
||||
{t("motion.only")}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useTheme } from "@/context/theme-provider";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdCircle } from "react-icons/md";
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -24,6 +24,7 @@ export function CameraLineGraph({
|
||||
updateTimes,
|
||||
data,
|
||||
}: CameraLineGraphProps) {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
@ -128,7 +129,7 @@ export function CameraLineGraph({
|
||||
style={{ color: GRAPH_COLORS[labelIdx] }}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("cameras.label." + label, { ns: "views/settings" })}
|
||||
{t("cameras.label." + label)}
|
||||
</div>
|
||||
<div className="text-xs text-primary">
|
||||
{lastValues[labelIdx]}
|
||||
|
||||
@ -50,9 +50,12 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
.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) => {
|
||||
@ -60,9 +63,15 @@ 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", {
|
||||
ns: "views/settings",
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -85,7 +94,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>Account</p>
|
||||
<p>{t("menu.user.account")}</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
@ -100,7 +109,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
{t("menu.user.current", {
|
||||
user: profile?.username || t("menu.user.anonymous"),
|
||||
})}{" "}
|
||||
{profile?.role && `(${profile.role})`}
|
||||
{t("role." + profile?.role) && `(${t("role." + profile.role)})`}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} />
|
||||
{profile?.username && profile.username !== "anonymous" && (
|
||||
@ -112,7 +121,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
onClick={() => setPasswordDialogOpen(true)}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
<span>Set Password</span>
|
||||
<span>{t("menu.user.setPassword")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
|
||||
@ -97,9 +97,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) => {
|
||||
@ -107,9 +110,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",
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -157,8 +166,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"}
|
||||
@ -174,7 +186,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
onClick={() => setPasswordDialogOpen(true)}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
<span>Set Password</span>
|
||||
<span>{t("menu.user.setPassword")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
|
||||
@ -44,8 +44,7 @@ import {
|
||||
useNotifications,
|
||||
useNotificationSuspend,
|
||||
} from "@/api/ws";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type LiveContextMenuProps = {
|
||||
className?: string;
|
||||
@ -87,6 +86,7 @@ export default function LiveContextMenu({
|
||||
config,
|
||||
children,
|
||||
}: LiveContextMenuProps) {
|
||||
const { t } = useTranslation("views/live");
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
// camera enabled
|
||||
@ -236,17 +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:
|
||||
config?.ui.time_format == "24hour"
|
||||
? t("time.formattedTimestampExcludeSeconds.24hour")
|
||||
: t("time.formattedTimestampExcludeSeconds"),
|
||||
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
|
||||
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
|
||||
});
|
||||
return t("time.untilForTime", { ns: "common", time });
|
||||
};
|
||||
|
||||
return (
|
||||
@ -261,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>
|
||||
@ -270,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"
|
||||
@ -297,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>
|
||||
@ -307,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}>
|
||||
@ -315,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 />
|
||||
@ -325,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>
|
||||
@ -338,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" && (
|
||||
@ -349,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>
|
||||
</>
|
||||
@ -362,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">{t("button")}</div>
|
||||
<div className="text-primary">
|
||||
{t("button.reset", { ns: "common" })}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
@ -373,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>
|
||||
@ -384,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>
|
||||
@ -423,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>
|
||||
@ -436,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
|
||||
@ -445,7 +459,7 @@ export default function LiveContextMenu({
|
||||
isEnabled ? () => handleSuspend("5") : undefined
|
||||
}
|
||||
>
|
||||
5 minutes
|
||||
{t("time.5minutes", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@ -455,7 +469,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
10 minutes
|
||||
{t("time.10minutes", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@ -465,7 +479,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
30 minutes
|
||||
{t("time.30minutes", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@ -475,7 +489,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
1 hour
|
||||
{t("time.1hour", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@ -485,7 +499,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
12 hours
|
||||
{t("time.12hours", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@ -495,7 +509,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
24 hours
|
||||
{t("time.24hours", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={!isEnabled}
|
||||
@ -505,7 +519,7 @@ export default function LiveContextMenu({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Until restart
|
||||
{t("time.untilRestart", { ns: "common" })}
|
||||
</ContextMenuItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -20,7 +20,7 @@ import axios from "axios";
|
||||
import SaveExportOverlay from "./SaveExportOverlay";
|
||||
import { isIOS, isMobile } from "react-device-detect";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
||||
|
||||
@ -70,6 +70,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
setMode,
|
||||
setShowExportPreview,
|
||||
}: MobileReviewSettingsDrawerProps) {
|
||||
const { t } = useTranslation(["views/recording"]);
|
||||
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
|
||||
|
||||
// exports
|
||||
@ -77,12 +78,14 @@ export default function MobileReviewSettingsDrawer({
|
||||
const [name, setName] = useState("");
|
||||
const onStartExport = useCallback(() => {
|
||||
if (!range) {
|
||||
toast.error("No valid time range selected", { position: "top-center" });
|
||||
toast.error(t("toast.error.noValidTimeSelected"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (range.before < range.after) {
|
||||
toast.error("End time must be after start time", {
|
||||
toast.error(t("toast.error.endTimeMustAfterStartTime"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
@ -114,11 +117,17 @@ export default function MobileReviewSettingsDrawer({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to start export: ${errorMessage}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("export.toast.error.failed", {
|
||||
ns: "components/dialog",
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
});
|
||||
}, [camera, name, range, setRange, setName, setMode]);
|
||||
}, [camera, name, range, setRange, setName, setMode, t]);
|
||||
|
||||
// filters
|
||||
|
||||
@ -147,7 +156,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
}}
|
||||
>
|
||||
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
|
||||
Export
|
||||
{t("export")}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("calendar") && (
|
||||
@ -160,7 +169,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
<FaCalendarAlt
|
||||
className={`${filter?.after ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
/>
|
||||
Calendar
|
||||
{t("calendar")}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("filter") && (
|
||||
@ -173,7 +182,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
<FaFilter
|
||||
className={`${filter?.labels || filter?.zones ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
/>
|
||||
Filter
|
||||
{t("filter")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -210,10 +219,10 @@ export default function MobileReviewSettingsDrawer({
|
||||
className="absolute left-0 text-selected"
|
||||
onClick={() => setDrawerMode("select")}
|
||||
>
|
||||
Back
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
|
||||
Calendar
|
||||
{t("calendar")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
@ -260,7 +269,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
className="absolute left-0 text-selected"
|
||||
onClick={() => setDrawerMode("select")}
|
||||
>
|
||||
Back
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
|
||||
Filter
|
||||
|
||||
@ -211,6 +211,7 @@ export default function ObjectMaskEditPane({
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
@ -226,6 +227,7 @@ export default function ObjectMaskEditPane({
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
|
||||
@ -330,7 +330,7 @@ export default function ZoneEditPane({
|
||||
// Wait for the config to be updated
|
||||
mutatedConfig = await updateConfig();
|
||||
} catch (error) {
|
||||
toast.error(t("toast.save.error.noMessage"), {
|
||||
toast.error(t("toast.save.error.noMessage", { ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
@ -422,7 +422,10 @@ export default function ZoneEditPane({
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(
|
||||
t("toast.save.error", { errorMessage: res.statusText }),
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
@ -437,6 +440,7 @@ export default function ZoneEditPane({
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
|
||||
@ -22,11 +22,13 @@ import {
|
||||
import EventView from "@/views/events/EventView";
|
||||
import { RecordingView } from "@/views/recording/RecordingView";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function Events() {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
@ -78,11 +80,11 @@ export default function Events() {
|
||||
|
||||
useEffect(() => {
|
||||
if (recording) {
|
||||
document.title = t("recordings.documentTitle", { ns: "views/events" });
|
||||
document.title = t("recordings.documentTitle");
|
||||
} else {
|
||||
document.title = t("documentTitle", { ns: "views/events" });
|
||||
document.title = t("documentTitle");
|
||||
}
|
||||
}, [recording, severity]);
|
||||
}, [recording, severity, t]);
|
||||
|
||||
// review filter
|
||||
|
||||
|
||||
@ -37,13 +37,13 @@ import AuthenticationView from "@/views/settings/AuthenticationView";
|
||||
import NotificationView from "@/views/settings/NotificationsSettingsView";
|
||||
import SearchSettingsView from "@/views/settings/SearchSettingsView";
|
||||
import UiSettingsView from "@/views/settings/UiSettingsView";
|
||||
import { t } from "i18next";
|
||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useInitialCameraState } from "@/api/ws";
|
||||
import { isInIframe } from "@/utils/isIFrame";
|
||||
import { isPWA } from "@/utils/isPWA";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const allSettingsViews = [
|
||||
"uiSettings",
|
||||
@ -58,6 +58,7 @@ const allSettingsViews = [
|
||||
type SettingsType = (typeof allSettingsViews)[number];
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const [page, setPage] = useState<SettingsType>("uiSettings");
|
||||
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
||||
const tabsRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -164,7 +165,7 @@ export default function Settings() {
|
||||
useSearchEffect("page", (page: string) => {
|
||||
if (allSettingsViews.includes(page as SettingsType)) {
|
||||
// Restrict viewer to UI settings
|
||||
if (!isAdmin && !["UI settings", "debug"].includes(page)) {
|
||||
if (!isAdmin && !["uiSettings", "debug"].includes(page)) {
|
||||
setPage("uiSettings");
|
||||
} else {
|
||||
setPage(page as SettingsType);
|
||||
@ -200,7 +201,7 @@ export default function Settings() {
|
||||
onValueChange={(value: SettingsType) => {
|
||||
if (value) {
|
||||
// Restrict viewer navigation
|
||||
if (!isAdmin && !["UI settings", "debug"].includes(value)) {
|
||||
if (!isAdmin && !["uiSettings", "debug"].includes(value)) {
|
||||
setPageToggle("uiSettings");
|
||||
} else {
|
||||
setPageToggle(value);
|
||||
@ -216,9 +217,7 @@ export default function Settings() {
|
||||
data-nav-item={item}
|
||||
aria-label={`Select ${item}`}
|
||||
>
|
||||
<div className="capitalize">
|
||||
{t("menu." + item, { ns: "views/settings" })}
|
||||
</div>
|
||||
<div className="capitalize">{t("menu." + item)}</div>
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
|
||||
@ -214,7 +214,7 @@ export default function EventView({
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("export.toast.error", {
|
||||
t("export.toast.error.failed", {
|
||||
ns: "components/dialog",
|
||||
message: errorMessage,
|
||||
}),
|
||||
|
||||
@ -27,8 +27,7 @@ import { LuExternalLink } from "react-icons/lu";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { MdCircle } from "react-icons/md";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useAlertsState, useDetectionsState, useEnabledState } from "@/api/ws";
|
||||
@ -47,6 +46,8 @@ export default function CameraSettingsView({
|
||||
selectedCamera,
|
||||
setUnsavedChanges,
|
||||
}: CameraSettingsViewProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -81,7 +82,7 @@ export default function CameraSettingsView({
|
||||
.map((label) => t(label, { ns: "objects" }))
|
||||
.join(", ")
|
||||
: "";
|
||||
}, [cameraConfig]);
|
||||
}, [cameraConfig, t]);
|
||||
|
||||
const detectionsLabels = useMemo(() => {
|
||||
return cameraConfig?.review.detections.labels
|
||||
@ -89,7 +90,7 @@ export default function CameraSettingsView({
|
||||
.map((label) => t(label, { ns: "objects" }))
|
||||
.join(", ")
|
||||
: "";
|
||||
}, [cameraConfig]);
|
||||
}, [cameraConfig, t]);
|
||||
|
||||
// form
|
||||
|
||||
@ -165,7 +166,10 @@ export default function CameraSettingsView({
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(
|
||||
t("toast.save.error", { errorMessage: res.statusText }),
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
@ -180,6 +184,7 @@ export default function CameraSettingsView({
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
@ -190,7 +195,7 @@ export default function CameraSettingsView({
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[updateConfig, setIsLoading, selectedCamera, cameraConfig],
|
||||
[updateConfig, setIsLoading, selectedCamera, cameraConfig, t],
|
||||
);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
@ -461,7 +466,6 @@ export default function CameraSettingsView({
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
ns: "views/settings",
|
||||
},
|
||||
)
|
||||
: t("camera.reviewClassification.objectAlertsTips", {
|
||||
@ -469,7 +473,6 @@ export default function CameraSettingsView({
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
ns: "views/settings",
|
||||
})}
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
@ -125,15 +125,22 @@ export default function MotionTunerView({
|
||||
setChangedValue(false);
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: error.response.data.message,
|
||||
ns: "common",
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
|
||||
@ -641,17 +641,19 @@ export function CameraNotificationSwitch({
|
||||
};
|
||||
|
||||
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(parseInt(timestamp), {
|
||||
const time = formatUnixTimestampToDateTime(parseInt(timestamp), {
|
||||
time_style: "medium",
|
||||
date_style: "medium",
|
||||
timezone: config?.ui.timezone,
|
||||
strftime_fmt:
|
||||
config?.ui.time_format == "24hour"
|
||||
? t("time.formattedTimestampExcludeSeconds.24hour")
|
||||
: t("time.formattedTimestampExcludeSeconds"),
|
||||
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
|
||||
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
|
||||
});
|
||||
return t("time.untilForTime", { ns: "common", time });
|
||||
};
|
||||
|
||||
return (
|
||||
@ -677,7 +679,7 @@ export function CameraNotificationSwitch({
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row items-center gap-2 text-sm text-danger">
|
||||
Notifications suspended until{" "}
|
||||
Notifications suspended{" "}
|
||||
{formatSuspendedUntil(notificationSuspendUntil)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -100,9 +100,15 @@ export default function ExploreSettingsView({
|
||||
setChangedValue(false);
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -113,6 +119,7 @@ export default function ExploreSettingsView({
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user