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.allCameras": "All Cameras",
"live.cameras": "Cameras",
"live.cameras.count_one": "{{count}} Camera",
"live.cameras.count_other": "{{count}} Cameras",
"review": "Review",
"explore": "Explore",
"export": "Export",
@ -156,5 +158,16 @@
"next": "Next",
"next.label": "Go to next page",
"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",
"copyConfig": "Copy Config",
"saveAndRestart": "Save & Restart",

View File

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

View File

@ -1,4 +1,5 @@
{
"documentTitle": "Face Library - Frigate",
"uploadFaceImage": {
"title": "Upload Face Image",
"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": {
"uiSettings": "UI Settings",
"exploreSettings": "Explore Settings",
"classificationSettings": "Classification Settings",
"cameraSettings": "Camera Settings",
"masksAndZones": "Masks / Zones",
"motionTuner": "Motion Tuner",
@ -9,6 +19,16 @@
"users": "Users",
"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": {
"title": "General Settings",
"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.",
"documentation": "Read the Documentation"
},
"globalSettings": {
"title": "Global Settings",
"desc": "Temporarily suspend notifications for specific cameras on all registered devices."
},
"email": "Email",
"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.",
@ -386,6 +409,19 @@
"deviceSpecific": "Device Specific Settings",
"registerDevice": "Register 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": {
"success": {
"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",
"metrics": "System metrics",
"logs": {

View File

@ -130,6 +130,8 @@
"live": "实时监控",
"live.allCameras": "所有摄像头",
"live.cameras": "摄像头",
"live.cameras.count_one": "{{count}} 个摄像头",
"live.cameras.count_other": "{{count}} 个摄像头",
"review": "回放",
"explore": "探测",
"export": "导出",
@ -156,5 +158,16 @@
"admin": "管理员",
"viewer": "查看者",
"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": "配置编辑器",
"copyConfig": "复制配置",
"saveAndRestart": "保存并重启",

View File

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

View File

@ -1,4 +1,5 @@
{
"documentTitle": "人脸库 - Frigate",
"uploadFaceImage": {
"title": "上传人脸图片",
"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": {
"uiSettings": "界面设置",
"exploreSettings": "搜索设置",
"classificationSettings": "分类设置",
"cameraSettings": "摄像头设置",
"masksAndZones": "遮罩/ 区域",
"motionTuner": "运动调整器",
@ -373,6 +389,10 @@
"desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。",
"documentation": "阅读文档(英文)"
},
"globalSettings": {
"title": "全局设置",
"desc": "临时暂停所有已注册设备上特定摄像头的通知。"
},
"notificationUnavailable": {
"title": "通知功能不可用",
"desc": "网页推送通知需要安全连接(<code>https://...</code>)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。",
@ -387,6 +407,19 @@
"deviceSpecific": "设备专用设置",
"registerDevice": "注册该设备",
"unregisterDevice": "取消注册该设备",
"sendTestNotification": "发送测试通知",
"active": "通知已启用",
"suspended": "通知已暂停 {{time}}",
"suspendTime": {
"5minutes": "暂停 5 分钟",
"10minutes": "暂停 10 分钟",
"30minutes": "暂停 30 分钟",
"1hour": "暂停 1 小时",
"12hours": "暂停 12 小时",
"24hours": "暂停 24 小时",
"untilRestart": "暂停直到重启"
},
"cancelSuspension": "取消暂停",
"toast": {
"success": {
"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": "系统",
"metrics": "系统指标",
"logs": {

View File

@ -288,24 +288,22 @@ export default function ReviewCard({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
<AlertDialogTitle>
{t("recording.confirmDelete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
this review item?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
<Trans ns="components/dialog">recording.confirmDelete.desc</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
Cancel
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={onDelete}
>
Delete
{t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -318,7 +316,7 @@ export default function ReviewCard({
onClick={onExport}
>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
<div className="text-primary">{t("recording.button.export")}</div>
</div>
{!event.has_been_reviewed && (
<div
@ -326,7 +324,9 @@ export default function ReviewCard({
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
<div className="text-primary">
{t("recording.button.markAsReviewed")}
</div>
</div>
)}
<div
@ -335,7 +335,9 @@ export default function ReviewCard({
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">
{bypassDialogRef.current ? "Delete Now" : "Delete"}
{bypassDialogRef.current
? t("recording.button.deleteNow")
: t("button.delete", { ns: "common" })}
</div>
</div>
</DrawerContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -185,8 +185,8 @@ export default function Settings() {
});
useEffect(() => {
document.title = "Settings - Frigate";
}, []);
document.title = t("documentTitle.default");
}, [t]);
return (
<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"}`}
value={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>
</ToggleGroupItem>
@ -284,17 +287,19 @@ export default function Settings() {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>You have unsaved changes.</AlertDialogTitle>
<AlertDialogTitle>
{t("dialog.unsavedChanges.title")}
</AlertDialogTitle>
<AlertDialogDescription>
Do you want to save your changes before continuing?
{t("dialog.unsavedChanges.desc")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => handleDialog(false)}>
Cancel
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDialog(true)}>
Save
{t("button.save", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -319,6 +324,8 @@ function CameraSelectButton({
cameraEnabledStates,
currentPage,
}: CameraSelectButtonProps) {
const { t } = useTranslation(["views/settings"]);
const [open, setOpen] = useState(false);
if (!allCameras.length) {
@ -334,7 +341,7 @@ function CameraSelectButton({
<FaVideo className="text-background dark:text-primary" />
<div className="hidden text-background dark:text-primary md:block">
{selectedCamera == undefined
? "No Camera"
? t("cameraSetting.noCamera")
: selectedCamera.replaceAll("_", " ")}
</div>
</Button>
@ -344,7 +351,7 @@ function CameraSelectButton({
{isMobile && (
<>
<DropdownMenuLabel className="flex justify-center">
Camera
{t("cameraSetting.camera")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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