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