chore: add more i18n keys again

This commit is contained in:
ZhaiSoul 2025-03-11 13:07:54 +08:00
parent b35fbef325
commit a52592bcac
30 changed files with 330 additions and 143 deletions

View File

@ -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": {

View File

@ -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"
}
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"label": "Filter", "filter": "Filter",
"labels": { "labels": {
"all": "All Labels", "all": "All Labels",
"all.short": "Labels", "all.short": "Labels",

View File

@ -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: "
} }
} }

View 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"
}
}
}

View File

@ -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}}",

View File

@ -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"
}, },

View File

@ -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": "立即删除"
}
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"label": "过滤器", "filter": "过滤器",
"labels": { "labels": {
"all": "所有标签", "all": "所有标签",
"all.short": "标签", "all.short": "标签",

View File

@ -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": "暂停时长:"
} }
} }

View File

@ -0,0 +1,11 @@
{
"export": "导出",
"calendar": "日历",
"filter": "筛选",
"toast": {
"error": {
"noValidTimeSelected": "未选择有效的时间范围",
"endTimeMustAfterStartTime": "结束时间必须晚于开始时间"
}
}
}

View File

@ -258,7 +258,8 @@
"toast": { "toast": {
"success": { "success": {
"createUser": "用户 {{user}} 创建成功", "createUser": "用户 {{user}} 创建成功",
"deleteUser": "用户 {{user}} 删除成功" "deleteUser": "用户 {{user}} 删除成功",
"updatePassword": "已成功修改密码"
}, },
"error": { "error": {
"setPasswordFailed": "保存密码出现错误:{{errorMessage}}", "setPasswordFailed": "保存密码出现错误:{{errorMessage}}",

View File

@ -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]) => (

View File

@ -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", position: "top-center",
}, });
);
} }
}) })
.catch((error) => { .catch((error) => {
if (error.response?.data?.message) { const errorMessage =
toast.error( error.response?.data?.message || error.message || "Unknown error";
`Failed to start export: ${error.response.data.message}`, toast.error(t("export.toast.error.failed", { error: errorMessage }), {
{ position: "top-center" },
);
} else {
toast.error(`Failed to start export: ${error.message}`, {
position: "top-center", 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>

View File

@ -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" },
); );

View File

@ -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>

View File

@ -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]}

View File

@ -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(
t("users.toast.success.updatePassword", { ns: "views/settings" }),
{
position: "top-center", 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(
t("users.toast.error.setPasswordFailed", {
ns: "views/settings",
errorMessage,
}),
{
position: "top-center", 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

View File

@ -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(
t("users.toast.success.updatePassword", { ns: "views/settings" }),
{
position: "top-center", 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(
t("users.toast.error.setPasswordFailed", {
ns: "views/settings",
errorMessage,
}),
{
position: "top-center", 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

View File

@ -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>

View File

@ -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(
t("export.toast.error.failed", {
ns: "components/dialog",
errorMessage,
}),
{
position: "top-center", position: "top-center",
},
);
}); });
}); }, [camera, name, range, setRange, setName, setMode, t]);
}, [camera, name, range, setRange, setName, setMode]);
// 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

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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>

View File

@ -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,
}), }),

View File

@ -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>

View File

@ -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(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center", 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" },
); );

View File

@ -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>
)} )}

View File

@ -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(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center", 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",