feat: add more document title i18n keys

This commit is contained in:
ZhaiSoul 2025-03-15 14:23:03 +08:00
parent 18ff446465
commit 29087cfcbc
29 changed files with 226 additions and 78 deletions

View File

@ -123,6 +123,8 @@
"live": "Live", "live": "Live",
"live.allCameras": "All Cameras", "live.allCameras": "All Cameras",
"live.cameras": "Cameras", "live.cameras": "Cameras",
"live.cameras.count_one": "{{count}} Camera",
"live.cameras.count_other": "{{count}} Cameras",
"review": "Review", "review": "Review",
"explore": "Explore", "explore": "Explore",
"export": "Export", "export": "Export",
@ -156,5 +158,16 @@
"next": "Next", "next": "Next",
"next.label": "Go to next page", "next.label": "Go to next page",
"more": "More pages" "more": "More pages"
} },
"accessDefined": {
"documentTitle": "Access Defined - Frigate",
"title": "Access Defined",
"desc": "You don't have permission to view this page."
},
"notFound": {
"documentTitle": "Not Found - Frigate",
"title": "404",
"desc": "Page not found"
},
"selectItem": "Select {{item}}"
} }

View File

@ -1,4 +1,5 @@
{ {
"documentTitle": "Config Editor - Frigate",
"configEditor": "Config Editor", "configEditor": "Config Editor",
"copyConfig": "Copy Config", "copyConfig": "Copy Config",
"saveAndRestart": "Save & Restart", "saveAndRestart": "Save & Restart",

View File

@ -1,4 +1,5 @@
{ {
"documentTitle": "Explore - Frigate",
"generativeAI": "Generative AI", "generativeAI": "Generative AI",
"exploreIsUnavailable": { "exploreIsUnavailable": {
"title": "Explore is Unavailable", "title": "Explore is Unavailable",

View File

@ -1,4 +1,5 @@
{ {
"documentTitle": "Face Library - Frigate",
"uploadFaceImage": { "uploadFaceImage": {
"title": "Upload Face Image", "title": "Upload Face Image",
"desc": "Upload an image to scan for faces and include for {{pageToggle}}" "desc": "Upload an image to scan for faces and include for {{pageToggle}}"

View File

@ -1,7 +1,17 @@
{ {
"documentTitle": {
"default": "Settings - Frigate",
"authentication": "Authentication Settings - Frigate",
"camera": "Camera Settings - Frigate",
"classification": "Classification Settings - Frigate",
"masksAndZones": "Mask and Zone Editor - Frigate",
"motionTuner": "Motion Tuner - Frigate",
"object": "Object Settings - Frigate",
"general": "General Settings - Frigate"
},
"menu": { "menu": {
"uiSettings": "UI Settings", "uiSettings": "UI Settings",
"exploreSettings": "Explore Settings", "classificationSettings": "Classification Settings",
"cameraSettings": "Camera Settings", "cameraSettings": "Camera Settings",
"masksAndZones": "Masks / Zones", "masksAndZones": "Masks / Zones",
"motionTuner": "Motion Tuner", "motionTuner": "Motion Tuner",
@ -9,6 +19,16 @@
"users": "Users", "users": "Users",
"notifications": "Notifications" "notifications": "Notifications"
}, },
"dialog": {
"unsavedChanges": {
"title": "You have unsaved changes.",
"desc": "Do you want to save your changes before continuing?"
}
},
"cameraSetting": {
"camera": "Camera",
"noCamera": "No Camera"
},
"general": { "general": {
"title": "General Settings", "title": "General Settings",
"liveDashboard": { "liveDashboard": {
@ -376,7 +396,10 @@
"desc": "Web push notifications require a secure context (<code>https://...</code>). This is a browser limitation. Access Frigate securely to use notifications.", "desc": "Web push notifications require a secure context (<code>https://...</code>). This is a browser limitation. Access Frigate securely to use notifications.",
"documentation": "Read the Documentation" "documentation": "Read the Documentation"
}, },
"globalSettings": {
"title": "Global Settings",
"desc": "Temporarily suspend notifications for specific cameras on all registered devices."
},
"email": "Email", "email": "Email",
"email.placeholder": "e.g. example@email.com", "email.placeholder": "e.g. example@email.com",
"email.desc": "A valid email is required and will be used to notify you if there are any issues with the push service.", "email.desc": "A valid email is required and will be used to notify you if there are any issues with the push service.",
@ -386,6 +409,19 @@
"deviceSpecific": "Device Specific Settings", "deviceSpecific": "Device Specific Settings",
"registerDevice": "Register This Device", "registerDevice": "Register This Device",
"unregisterDevice": "Unregister This Device", "unregisterDevice": "Unregister This Device",
"sendTestNotification": "Send a test notification",
"active": "Notifications Active",
"suspended": "Notifications suspended {{time}}",
"suspendTime": {
"5minutes": "Suspend for 5 minutes",
"10minutes": "Suspend for 10 minutes",
"30minutes": "Suspend for 30 minutes",
"1hour": "Suspend for 1 hour",
"12hours": "Suspend for 12 hours",
"24hours": "Suspend for 24 hours",
"untilRestart": "Suspend until restart"
},
"cancelSuspension": "Cancel Suspension",
"toast": { "toast": {
"success": { "success": {
"registered": "Successfully registered for notifications. Restarting Frigate is required before any notifications (including a test notification) can be sent.", "registered": "Successfully registered for notifications. Restarting Frigate is required before any notifications (including a test notification) can be sent.",

View File

@ -1,4 +1,15 @@
{ {
"documentTitle": {
"cameras": "Cameras Stats - Frigate",
"storage": "Storage Stats - Frigate",
"general": "General Stats - Frigate",
"features": "Features Stats- Frigate",
"logs": {
"frigate": "Frigate Logs - Frigate",
"go2rtc": "Go2RTC Logs - Frigate",
"nginx": "Nginx Logs - Frigate"
}
},
"title": "System", "title": "System",
"metrics": "System metrics", "metrics": "System metrics",
"logs": { "logs": {

View File

@ -130,6 +130,8 @@
"live": "实时监控", "live": "实时监控",
"live.allCameras": "所有摄像头", "live.allCameras": "所有摄像头",
"live.cameras": "摄像头", "live.cameras": "摄像头",
"live.cameras.count_one": "{{count}} 个摄像头",
"live.cameras.count_other": "{{count}} 个摄像头",
"review": "回放", "review": "回放",
"explore": "探测", "explore": "探测",
"export": "导出", "export": "导出",
@ -156,5 +158,16 @@
"admin": "管理员", "admin": "管理员",
"viewer": "查看者", "viewer": "查看者",
"desc": "管理员可以完全访问 Frigate UI 的所有功能。查看者则仅限于在 UI 中查看摄像头、审核项和历史录像。" "desc": "管理员可以完全访问 Frigate UI 的所有功能。查看者则仅限于在 UI 中查看摄像头、审核项和历史录像。"
} },
"accessDefined": {
"documentTitle": "没有权限 - Frigate",
"title": "没有权限",
"desc": "您没有权限查看此页面。"
},
"notFound": {
"documentTitle": "没有找到页面 - Frigate",
"title": "404",
"desc": "页面未找到"
},
"selectItem": "选择 {{item}}"
} }

View File

@ -1,4 +1,5 @@
{ {
"documentTitle": "配置编辑器 - Frigate",
"configEditor": "配置编辑器", "configEditor": "配置编辑器",
"copyConfig": "复制配置", "copyConfig": "复制配置",
"saveAndRestart": "保存并重启", "saveAndRestart": "保存并重启",

View File

@ -1,4 +1,5 @@
{ {
"documentTitle": "探索 - Frigate",
"generativeAI": "生成式 AI", "generativeAI": "生成式 AI",
"exploreIsUnavailable": { "exploreIsUnavailable": {
"title": "探索功能不可用", "title": "探索功能不可用",

View File

@ -1,4 +1,5 @@
{ {
"documentTitle": "人脸库 - Frigate",
"uploadFaceImage": { "uploadFaceImage": {
"title": "上传人脸图片", "title": "上传人脸图片",
"desc": "上传图片以扫描人脸并包含在{{pageToggle}}中" "desc": "上传图片以扫描人脸并包含在{{pageToggle}}中"

View File

@ -1,7 +1,23 @@
{ {
"documentTitle": {
"default": "设置 - Frigate",
"authentication": "身份验证设置 - Frigate",
"camera": "摄像头设置 - Frigate",
"classification": "分类设置 - Frigate",
"masksAndZones": "遮罩和区域编辑器 - Frigate",
"motionTuner": "运动调整器 - Frigate",
"object": "对象设置 - Frigate",
"general": "常规设置 - Frigate"
},
"dialog": {
"unsavedChanges": {
"title": "你有未保存的更改。",
"desc": "是否要在继续之前保存更改?"
}
},
"menu": { "menu": {
"uiSettings": "界面设置", "uiSettings": "界面设置",
"exploreSettings": "搜索设置", "classificationSettings": "分类设置",
"cameraSettings": "摄像头设置", "cameraSettings": "摄像头设置",
"masksAndZones": "遮罩/ 区域", "masksAndZones": "遮罩/ 区域",
"motionTuner": "运动调整器", "motionTuner": "运动调整器",
@ -373,6 +389,10 @@
"desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。", "desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。",
"documentation": "阅读文档(英文)" "documentation": "阅读文档(英文)"
}, },
"globalSettings": {
"title": "全局设置",
"desc": "临时暂停所有已注册设备上特定摄像头的通知。"
},
"notificationUnavailable": { "notificationUnavailable": {
"title": "通知功能不可用", "title": "通知功能不可用",
"desc": "网页推送通知需要安全连接(<code>https://...</code>)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。", "desc": "网页推送通知需要安全连接(<code>https://...</code>)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。",
@ -387,6 +407,19 @@
"deviceSpecific": "设备专用设置", "deviceSpecific": "设备专用设置",
"registerDevice": "注册该设备", "registerDevice": "注册该设备",
"unregisterDevice": "取消注册该设备", "unregisterDevice": "取消注册该设备",
"sendTestNotification": "发送测试通知",
"active": "通知已启用",
"suspended": "通知已暂停 {{time}}",
"suspendTime": {
"5minutes": "暂停 5 分钟",
"10minutes": "暂停 10 分钟",
"30minutes": "暂停 30 分钟",
"1hour": "暂停 1 小时",
"12hours": "暂停 12 小时",
"24hours": "暂停 24 小时",
"untilRestart": "暂停直到重启"
},
"cancelSuspension": "取消暂停",
"toast": { "toast": {
"success": { "success": {
"registered": "已成功注册通知。需要重启 Frigate 才能发送任何通知(包括测试通知)。", "registered": "已成功注册通知。需要重启 Frigate 才能发送任何通知(包括测试通知)。",

View File

@ -1,4 +1,15 @@
{ {
"documentTitle": {
"cameras": "摄像头统计 - Frigate",
"storage": "存储统计 - Frigate",
"general": "常规统计 - Frigate",
"features": "功能统计 - Frigate",
"logs": {
"frigate": "Frigate 日志 - Frigate",
"go2rtc": "Go2RTC 日志 - Frigate",
"nginx": "Nginx 日志 - Frigate"
}
},
"title": "系统", "title": "系统",
"metrics": "系统指标", "metrics": "系统指标",
"logs": { "logs": {

View File

@ -288,24 +288,22 @@ 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">recording.confirmDelete.desc</Trans>
this review item?
<br />
<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>
@ -318,7 +316,7 @@ 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>
{!event.has_been_reviewed && ( {!event.has_been_reviewed && (
<div <div
@ -326,7 +324,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>
)} )}
<div <div
@ -335,7 +335,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>
</DrawerContent> </DrawerContent>

View File

@ -44,8 +44,12 @@ export function CamerasFilterButton({
if (!selectedCameras || selectedCameras.length == 0) { if (!selectedCameras || selectedCameras.length == 0) {
return t("menu.live.allCameras", { ns: "common" }); return t("menu.live.allCameras", { ns: "common" });
} }
return t("menu.live.cameras.count", {
return `${selectedCameras.includes("birdseye") ? selectedCameras.length - 1 : selectedCameras.length} Camera${selectedCameras.length !== 1 ? "s" : ""}`; count: selectedCameras.includes("birdseye")
? selectedCameras.length - 1
: selectedCameras.length,
ns: "common",
});
}, [selectedCameras, t]); }, [selectedCameras, t]);
// ui // ui

View File

@ -1,21 +1,20 @@
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import { t } from "i18next";
import { useEffect } from "react"; import { useEffect } from "react";
import { FaExclamationTriangle } from "react-icons/fa"; import { FaExclamationTriangle } from "react-icons/fa";
export default function AccessDenied() { export default function AccessDenied() {
useEffect(() => { useEffect(() => {
document.title = "Access Denied - Frigate"; document.title = t("accessDefined.documentTitle");
}, []); }, []);
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center text-center"> <div className="flex min-h-screen flex-col items-center justify-center text-center">
<FaExclamationTriangle className="mb-4 size-8" /> <FaExclamationTriangle className="mb-4 size-8" />
<Heading as="h2" className="mb-2"> <Heading as="h2" className="mb-2">
Access Denied {t("accessDefined.title")}
</Heading> </Heading>
<p className="text-primary-variant"> <p className="text-primary-variant">{t("accessDefined.desc")}</p>
You don't have permission to view this page.
</p>
</div> </div>
); );
} }

View File

@ -29,8 +29,8 @@ function ConfigEditor() {
const apiHost = useApiHost(); const apiHost = useApiHost();
useEffect(() => { useEffect(() => {
document.title = "Config Editor - Frigate"; document.title = t("documentTitle");
}, []); }, [t]);
const { data: config } = useSWR<string>("config/raw"); const { data: config } = useSWR<string>("config/raw");

View File

@ -38,8 +38,8 @@ export default function FaceLibrary() {
// title // title
useEffect(() => { useEffect(() => {
document.title = "Face Library - Frigate"; document.title = t("documentTitle");
}, []); }, [t]);
const [page, setPage] = useState<string>(); const [page, setPage] = useState<string>();
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);

View File

@ -49,8 +49,8 @@ function Logs() {
const lastFetchedIndexRef = useRef(-1); const lastFetchedIndexRef = useRef(-1);
useEffect(() => { useEffect(() => {
document.title = `${logService[0].toUpperCase()}${logService.substring(1)} Logs - Frigate`; document.title = t("documentTitle.logs." + logService);
}, [logService]); }, [logService, t]);
useEffect(() => { useEffect(() => {
if (tabsRef.current) { if (tabsRef.current) {

View File

@ -1,15 +1,16 @@
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import { t } from "i18next";
import { useEffect } from "react"; import { useEffect } from "react";
function NoMatch() { function NoMatch() {
useEffect(() => { useEffect(() => {
document.title = "Not Found - Frigate"; document.title = t("notFound.documentTitle");
}, []); }, []);
return ( return (
<> <>
<Heading as="h2">404</Heading> <Heading as="h2">{t("notFound.title")}</Heading>
<p>Page not found</p> <p>{t("notFound.desc")}</p>
</> </>
); );
} }

View File

@ -185,8 +185,8 @@ export default function Settings() {
}); });
useEffect(() => { useEffect(() => {
document.title = "Settings - Frigate"; document.title = t("documentTitle.default");
}, []); }, [t]);
return ( return (
<div className="flex size-full flex-col p-2"> <div className="flex size-full flex-col p-2">
@ -215,7 +215,10 @@ export default function Settings() {
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "uiSettings" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`} className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "uiSettings" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
value={item} value={item}
data-nav-item={item} data-nav-item={item}
aria-label={`Select ${item}`} aria-label={t("selectItem", {
item: t("menu." + item),
ns: "common",
})}
> >
<div className="capitalize">{t("menu." + item)}</div> <div className="capitalize">{t("menu." + item)}</div>
</ToggleGroupItem> </ToggleGroupItem>
@ -284,17 +287,19 @@ export default function Settings() {
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>You have unsaved changes.</AlertDialogTitle> <AlertDialogTitle>
{t("dialog.unsavedChanges.title")}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Do you want to save your changes before continuing? {t("dialog.unsavedChanges.desc")}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel onClick={() => handleDialog(false)}> <AlertDialogCancel onClick={() => handleDialog(false)}>
Cancel {t("button.cancel", { ns: "common" })}
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction onClick={() => handleDialog(true)}> <AlertDialogAction onClick={() => handleDialog(true)}>
Save {t("button.save", { ns: "common" })}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -319,6 +324,8 @@ function CameraSelectButton({
cameraEnabledStates, cameraEnabledStates,
currentPage, currentPage,
}: CameraSelectButtonProps) { }: CameraSelectButtonProps) {
const { t } = useTranslation(["views/settings"]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
if (!allCameras.length) { if (!allCameras.length) {
@ -334,7 +341,7 @@ function CameraSelectButton({
<FaVideo className="text-background dark:text-primary" /> <FaVideo className="text-background dark:text-primary" />
<div className="hidden text-background dark:text-primary md:block"> <div className="hidden text-background dark:text-primary md:block">
{selectedCamera == undefined {selectedCamera == undefined
? "No Camera" ? t("cameraSetting.noCamera")
: selectedCamera.replaceAll("_", " ")} : selectedCamera.replaceAll("_", " ")}
</div> </div>
</Button> </Button>
@ -344,7 +351,7 @@ function CameraSelectButton({
{isMobile && ( {isMobile && (
<> <>
<DropdownMenuLabel className="flex justify-center"> <DropdownMenuLabel className="flex justify-center">
Camera {t("cameraSetting.camera")}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</> </>

View File

@ -12,7 +12,6 @@ import Logo from "@/components/Logo";
import useOptimisticState from "@/hooks/use-optimistic-state"; import useOptimisticState from "@/hooks/use-optimistic-state";
import CameraMetrics from "@/views/system/CameraMetrics"; import CameraMetrics from "@/views/system/CameraMetrics";
import { useHashState } from "@/hooks/use-overlay-state"; import { useHashState } from "@/hooks/use-overlay-state";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import FeatureMetrics from "@/views/system/FeatureMetrics"; import FeatureMetrics from "@/views/system/FeatureMetrics";
@ -54,9 +53,9 @@ function System() {
useEffect(() => { useEffect(() => {
if (pageToggle) { if (pageToggle) {
document.title = `${capitalizeFirstLetter(pageToggle)} Stats - Frigate`; document.title = t("documentTitle." + pageToggle);
} }
}, [pageToggle]); }, [pageToggle, t]);
// stats collection // stats collection

View File

@ -36,11 +36,12 @@ export default function ExploreView({
setSimilaritySearch, setSimilaritySearch,
onSelectSearch, onSelectSearch,
}: ExploreViewProps) { }: ExploreViewProps) {
const { t } = useTranslation(["views/explore"]);
// title // title
useEffect(() => { useEffect(() => {
document.title = "Explore - Frigate"; document.title = t("documentTitle");
}, []); }, [t]);
// data // data

View File

@ -49,8 +49,8 @@ export default function AuthenticationView() {
>(); >();
useEffect(() => { useEffect(() => {
document.title = "Authentication Settings - Frigate"; document.title = t("documentTitle.authentication");
}, []); }, [t]);
const onSavePassword = useCallback( const onSavePassword = useCallback(
(user: string, password: string) => { (user: string, password: string) => {

View File

@ -193,8 +193,8 @@ export default function ClassificationSettingsView({
}, [changedValue]); }, [changedValue]);
useEffect(() => { useEffect(() => {
document.title = "Classification Settings - Frigate"; document.title = t("documentTitle.classification");
}, []); }, [t]);
if (!config) { if (!config) {
return <ActivityIndicator />; return <ActivityIndicator />;

View File

@ -185,8 +185,8 @@ export default function MasksAndZonesView({
setActivePolygonIndex(undefined); setActivePolygonIndex(undefined);
setHoveredPolygonIndex(null); setHoveredPolygonIndex(null);
setUnsavedChanges(false); setUnsavedChanges(false);
document.title = "Mask and Zone Editor - Frigate"; document.title = t("documentTitle.masksAndZones");
}, [allPolygons, setUnsavedChanges]); }, [allPolygons, setUnsavedChanges, t]);
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
setAllPolygons([...(editingPolygons ?? [])]); setAllPolygons([...(editingPolygons ?? [])]);
@ -425,8 +425,8 @@ export default function MasksAndZonesView({
}); });
useEffect(() => { useEffect(() => {
document.title = "Mask and Zone Editor - Frigate"; document.title = t("documentTitle.masksAndZones");
}, []); }, [t]);
if (!cameraConfig && !selectedCamera) { if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />; return <ActivityIndicator />;

View File

@ -179,8 +179,8 @@ export default function MotionTunerView({
}, [changedValue, selectedCamera]); }, [changedValue, selectedCamera]);
useEffect(() => { useEffect(() => {
document.title = "Motion Tuner - Frigate"; document.title = t("documentTitle.motionTuner");
}, []); }, [t]);
if (!cameraConfig && !selectedCamera) { if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />; return <ActivityIndicator />;

View File

@ -560,10 +560,10 @@ export default function NotificationView({
</Button> </Button>
{registration != null && registration.active && ( {registration != null && registration.active && (
<Button <Button
aria-label="Send a test notification" aria-label={t("notification.sendTestNotification")}
onClick={() => sendTestNotification("notification_test")} onClick={() => sendTestNotification("notification_test")}
> >
Send a test notification {t("notification.sendTestNotification")}
</Button> </Button>
)} )}
</div> </div>
@ -573,14 +573,11 @@ export default function NotificationView({
<div className="space-y-3"> <div className="space-y-3">
<Separator className="my-2 flex bg-secondary" /> <Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2"> <Heading as="h4" className="my-2">
Global Settings {t("notification.globalSettings.title")}
</Heading> </Heading>
<div className="max-w-xl"> <div className="max-w-xl">
<div className="mb-5 mt-2 flex flex-col gap-2 text-sm text-primary-variant"> <div className="mb-5 mt-2 flex flex-col gap-2 text-sm text-primary-variant">
<p> <p>{t("notification.globalSettings.desc")}</p>
Temporarily suspend notifications for specific cameras
on all registered devices.
</p>
</div> </div>
</div> </div>
@ -680,12 +677,13 @@ export function CameraNotificationSwitch({
{!isSuspended ? ( {!isSuspended ? (
<div className="flex flex-row items-center gap-2 text-sm text-success"> <div className="flex flex-row items-center gap-2 text-sm text-success">
Notifications Active {t("notification.active")}
</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{" "} {t("notification.suspended", {
{formatSuspendedUntil(notificationSuspendUntil)} time: formatSuspendedUntil(notificationSuspendUntil),
})}
</div> </div>
)} )}
</div> </div>
@ -698,13 +696,27 @@ export function CameraNotificationSwitch({
<SelectValue placeholder="Suspend" /> <SelectValue placeholder="Suspend" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="5">Suspend for 5 minutes</SelectItem> <SelectItem value="5">
<SelectItem value="10">Suspend for 10 minutes</SelectItem> {t("notification.suspendTime.5minutes")}
<SelectItem value="30">Suspend for 30 minutes</SelectItem> </SelectItem>
<SelectItem value="60">Suspend for 1 hour</SelectItem> <SelectItem value="10">
<SelectItem value="840">Suspend for 12 hours</SelectItem> {t("notification.suspendTime.10minutes")}
<SelectItem value="1440">Suspend for 24 hours</SelectItem> </SelectItem>
<SelectItem value="off">Suspend until restart</SelectItem> <SelectItem value="30">
{t("notification.suspendTime.30minutes")}
</SelectItem>
<SelectItem value="60">
{t("notification.suspendTime.1hour")}
</SelectItem>
<SelectItem value="840">
{t("notification.suspendTime.12hour")}
</SelectItem>
<SelectItem value="1440">
{t("notification.suspendTime.24hour")}
</SelectItem>
<SelectItem value="off">
{t("notification.suspendTime.untilRestart")}
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
) : ( ) : (
@ -713,7 +725,7 @@ export function CameraNotificationSwitch({
size="sm" size="sm"
onClick={handleCancelSuspension} onClick={handleCancelSuspension}
> >
Cancel Suspension {t("notification.cancelSuspension")}
</Button> </Button>
)} )}
</div> </div>

View File

@ -136,8 +136,8 @@ export default function ObjectSettingsView({
}, [options, optionsLoaded]); }, [options, optionsLoaded]);
useEffect(() => { useEffect(() => {
document.title = "Object Settings - Frigate"; document.title = t("documentTitle.object");
}, []); }, [t]);
if (!cameraConfig) { if (!cameraConfig) {
return <ActivityIndicator />; return <ActivityIndicator />;

View File

@ -86,8 +86,8 @@ export default function UiSettingsView() {
}, [config, t]); }, [config, t]);
useEffect(() => { useEffect(() => {
document.title = "General Settings - Frigate"; document.title = t("documentTitle.general");
}, []); }, [t]);
// settings // settings