chore: add some i18n keys

This commit is contained in:
ZhaiSoul 2025-03-11 11:10:53 +08:00
parent 2dc6283727
commit b35fbef325
7 changed files with 162 additions and 42 deletions

View File

@ -8,6 +8,24 @@
"button": "Force Reload Now" "button": "Force Reload Now"
} }
}, },
"explore": {
"plus": {
"submitToPlus": {
"label": "Submit To Frigate+",
"desc": "Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model."
},
"review": {
"true_one": "This is a {{label}}",
"true_other": "This is an {{label}}",
"false_one": "This is not a {{label}}",
"false_other": "This is not an {{label}}",
"state.submitted": "Submitted"
}
},
"video": {
"viewInHistory": "View in History"
}
},
"export": { "export": {
"time": { "time": {
"fromTimeline": "Select from Timeline", "fromTimeline": "Select from Timeline",

View File

@ -7,6 +7,19 @@
"object_lifecycle": "object lifecycle" "object_lifecycle": "object lifecycle"
}, },
"details": { "details": {
"item": {
"title": "Review Item Details",
"desc": "Review item details",
"button": {
"share": "Share this review item",
"viewInExplore": "View in Explore"
},
"tips": {
"mismatch_one": "{{count}} unavailable object was detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.",
"mismatch_other": "{{count}} unavailable objects were detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.",
"hasMissingObjects": "Adjust your configuration if you want Frigate to save tracked objects for the following labels: <em>{{objects}}</em>"
}
},
"label": "Label", "label": "Label",
"editSubLable": "Edit sub label", "editSubLable": "Edit sub label",
"editSubLable.desc": "Enter a new sub label for this {{label}}", "editSubLable.desc": "Enter a new sub label for this {{label}}",
@ -14,7 +27,9 @@
"topScore": "Top Score", "topScore": "Top Score",
"topScore.info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail.", "topScore.info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail.",
"estimatedSpeed": "Estimated Speed", "estimatedSpeed": "Estimated Speed",
"objects": "Objects",
"camera": "Camera", "camera": "Camera",
"zones": "Zones",
"timestamp": "Timestamp", "timestamp": "Timestamp",
"button": { "button": {
"findSimilar": "Find Similar" "findSimilar": "Find Similar"
@ -52,6 +67,10 @@
"submitToPlus": { "submitToPlus": {
"label": "Submit to Frigate+", "label": "Submit to Frigate+",
"aria": "Submit to Frigate Plus" "aria": "Submit to Frigate Plus"
},
"viewInHistory": {
"label": "View in History",
"aria": "View in History"
} }
}, },
"dialog": { "dialog": {

View File

@ -8,6 +8,24 @@
"button": "强制刷新" "button": "强制刷新"
} }
}, },
"explore": {
"plus": {
"submitToPlus": {
"label": "提交至 Frigate+",
"desc": "您希望避开的地点中的物体不应被视为误报。若将其作为误报提交可能会导致AI模型容易混淆相关物体的识别。"
},
"review": {
"true_one": "这是 {{label}}",
"true_other": "这是 {{label}}",
"false_one": "这不是 {{label}}",
"false_other": "这不是 {{label}}",
"state.submitted": "已提交"
}
},
"video": {
"viewInHistory": "在历史中查看"
}
},
"export": { "export": {
"time": { "time": {
"fromTimeline": "从时间线选择", "fromTimeline": "从时间线选择",

View File

@ -0,0 +1,3 @@
{
}

View File

@ -7,6 +7,19 @@
"object_lifecycle": "对象生命周期" "object_lifecycle": "对象生命周期"
}, },
"details": { "details": {
"item": {
"title": "回放项目详情",
"desc": "回放项目详情",
"button": {
"share": "分享该回放",
"viewInExplore": "在探测中查看"
},
"tips": {
"mismatch_one": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。",
"mismatch_other": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。",
"hasMissingObjects": "如果希望 Frigate 保存以下标签的跟踪对象,请调整您的配置:<em>{{objects}}</em>"
}
},
"label": "标签", "label": "标签",
"editSubLable": "编辑子标签", "editSubLable": "编辑子标签",
"editSubLable.desc": "为 {{label}} 输入新的子标签", "editSubLable.desc": "为 {{label}} 输入新的子标签",
@ -14,7 +27,9 @@
"topScore": "最高得分", "topScore": "最高得分",
"topScore.info": "最高分是跟踪对象的最高中位数得分,因此可能与搜索结果缩略图上显示的得分不同。", "topScore.info": "最高分是跟踪对象的最高中位数得分,因此可能与搜索结果缩略图上显示的得分不同。",
"estimatedSpeed": "预计速度", "estimatedSpeed": "预计速度",
"objects": "对象",
"camera": "摄像头", "camera": "摄像头",
"zones": "区域",
"timestamp": "时间", "timestamp": "时间",
"button": { "button": {
"findSimilar": "查找相似项" "findSimilar": "查找相似项"
@ -52,6 +67,10 @@
"submitToPlus": { "submitToPlus": {
"label": "提交至 Frigate+", "label": "提交至 Frigate+",
"aria": "提交至 Frigate Plus" "aria": "提交至 Frigate Plus"
},
"viewInHistory": {
"label": "在历史记录中查看",
"aria": "在历史记录中查看"
} }
}, },
"dialog": { "dialog": {

View File

@ -42,7 +42,7 @@ import { DownloadVideoButton } from "@/components/button/DownloadVideoButton";
import { TooltipPortal } from "@radix-ui/react-tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip";
import { LuSearch } from "react-icons/lu"; import { LuSearch } from "react-icons/lu";
import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { t } from "i18next"; import { Trans, useTranslation } from "react-i18next";
type ReviewDetailDialogProps = { type ReviewDetailDialogProps = {
review?: ReviewSegment; review?: ReviewSegment;
@ -52,6 +52,7 @@ export default function ReviewDetailDialog({
review, review,
setReview, setReview,
}: ReviewDetailDialogProps) { }: ReviewDetailDialogProps) {
const { t } = useTranslation(["views/explore"]);
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
@ -96,8 +97,8 @@ export default function ReviewDetailDialog({
const formattedDate = useFormattedTimestamp( const formattedDate = useFormattedTimestamp(
review?.start_time ?? 0, review?.start_time ?? 0,
config?.ui.time_format == "24hour" config?.ui.time_format == "24hour"
? t("time.formattedTimestampWithYear.24hour") ? t("time.formattedTimestampWithYear.24hour", { ns: "common" })
: t("time.formattedTimestampWithYear"), : t("time.formattedTimestampWithYear", { ns: "common" }),
config?.ui.timezone, config?.ui.timezone,
); );
@ -178,8 +179,10 @@ export default function ReviewDetailDialog({
<span tabIndex={0} className="sr-only" /> <span tabIndex={0} className="sr-only" />
{pane == "overview" && ( {pane == "overview" && (
<Header className="justify-center"> <Header className="justify-center">
<Title>Review Item Details</Title> <Title>{t("details.item.title")}</Title>
<Description className="sr-only">Review item details</Description> <Description className="sr-only">
{t("details.item.desc")}
</Description>
<div <div
className={cn( className={cn(
"absolute flex gap-2 lg:flex-col", "absolute flex gap-2 lg:flex-col",
@ -200,7 +203,9 @@ export default function ReviewDetailDialog({
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>Share this review item</TooltipContent> <TooltipContent>
{t("details.item.button.share")}
</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
@ -212,7 +217,9 @@ export default function ReviewDetailDialog({
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>Download</TooltipContent> <TooltipContent>
{t("button.download", { ns: "common" })}
</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
</div> </div>
@ -223,19 +230,25 @@ export default function ReviewDetailDialog({
<div className="flex w-full flex-row"> <div className="flex w-full flex-row">
<div className="flex w-full flex-col gap-3"> <div className="flex w-full flex-col gap-3">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Camera</div> <div className="text-sm text-primary/40">
{t("details.camera")}
</div>
<div className="text-sm capitalize"> <div className="text-sm capitalize">
{review.camera.replaceAll("_", " ")} {review.camera.replaceAll("_", " ")}
</div> </div>
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Timestamp</div> <div className="text-sm text-primary/40">
{t("details.timestamp")}
</div>
<div className="text-sm">{formattedDate}</div> <div className="text-sm">{formattedDate}</div>
</div> </div>
</div> </div>
<div className="flex w-full flex-col items-center gap-2"> <div className="flex w-full flex-col items-center gap-2">
<div className="flex w-full flex-col gap-1.5 lg:pr-8"> <div className="flex w-full flex-col gap-1.5 lg:pr-8">
<div className="text-sm text-primary/40">Objects</div> <div className="text-sm text-primary/40">
{t("details.objects")}
</div>
<div className="scrollbar-container flex max-h-32 flex-col items-start gap-2 overflow-y-auto text-sm capitalize"> <div className="scrollbar-container flex max-h-32 flex-col items-start gap-2 overflow-y-auto text-sm capitalize">
{events?.map((event) => { {events?.map((event) => {
return ( return (
@ -261,7 +274,9 @@ export default function ReviewDetailDialog({
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>View in Explore</TooltipContent> <TooltipContent>
{t("details.item.button.viewInExplore")}
</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
</div> </div>
@ -271,7 +286,9 @@ export default function ReviewDetailDialog({
</div> </div>
{review.data.zones.length > 0 && ( {review.data.zones.length > 0 && (
<div className="scrollbar-container flex max-h-32 w-full flex-col gap-1.5"> <div className="scrollbar-container flex max-h-32 w-full flex-col gap-1.5">
<div className="text-sm text-primary/40">Zones</div> <div className="text-sm text-primary/40">
{t("details.zones")}
</div>
<div className="flex flex-col items-start gap-2 text-sm capitalize"> <div className="flex flex-col items-start gap-2 text-sm capitalize">
{review.data.zones.map((zone) => { {review.data.zones.map((zone) => {
return ( return (
@ -295,18 +312,23 @@ export default function ReviewDetailDialog({
(events?.length ?? 0) - (events?.length ?? 0) -
(review?.data.detections.length ?? 0), (review?.data.detections.length ?? 0),
); );
const objectLabel =
detectedCount === 1 ? "object was" : "objects were";
return `${detectedCount} unavailable ${objectLabel} detected and included in this review item.`; return t("details.item.tips.mismatch", {
})()}{" "} count: detectedCount,
Those objects either did not qualify as an alert or detection });
or have already been cleaned up/deleted. })()}
{missingObjects.length > 0 && ( {missingObjects.length > 0 && (
<div className="mt-2"> <div className="mt-2">
Adjust your configuration if you want Frigate to save <Trans
tracked objects for the following labels:{" "} ns="views/explore"
{missingObjects.join(", ")} values={{
objects: missingObjects
.map((x) => t(x, { ns: "objects" }))
.join(", "),
}}
>
details.item.tips.hasMissingObjects
</Trans>
</div> </div>
)} )}
</div> </div>
@ -349,6 +371,8 @@ function EventItem({
setSelectedEvent, setSelectedEvent,
setUpload, setUpload,
}: EventItemProps) { }: EventItemProps) {
const { t } = useTranslation(["views/explore"]);
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
@ -418,7 +442,9 @@ function EventItem({
</Chip> </Chip>
</a> </a>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Download</TooltipContent> <TooltipContent>
{t("button.download", { ns: "common" })}
</TooltipContent>
</Tooltip> </Tooltip>
{event.has_snapshot && {event.has_snapshot &&
@ -436,7 +462,9 @@ function EventItem({
<FrigatePlusIcon className="size-4 text-white" /> <FrigatePlusIcon className="size-4 text-white" />
</Chip> </Chip>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Submit to Frigate+</TooltipContent> <TooltipContent>
{t("itemMenu.submitToPlus.label")}
</TooltipContent>
</Tooltip> </Tooltip>
)} )}
@ -453,7 +481,9 @@ function EventItem({
<FaArrowsRotate className="size-4 text-white" /> <FaArrowsRotate className="size-4 text-white" />
</Chip> </Chip>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>View Object Lifecycle</TooltipContent> <TooltipContent>
{t("itemMenu.viewObjectLifecycle.label")}
</TooltipContent>
</Tooltip> </Tooltip>
)} )}
@ -471,7 +501,9 @@ function EventItem({
<FaImages className="size-4 text-white" /> <FaImages className="size-4 text-white" />
</Chip> </Chip>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Find Similar</TooltipContent> <TooltipContent>
{t("itemMenu.findSimilar.label")}
</TooltipContent>
</Tooltip> </Tooltip>
)} )}
</div> </div>

View File

@ -578,8 +578,8 @@ function ObjectDetailsTab({
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
{averageEstimatedSpeed}{" "} {averageEstimatedSpeed}{" "}
{config?.ui.unit_system == "imperial" {config?.ui.unit_system == "imperial"
? t("unit.speed.mph") ? t("unit.speed.mph", { ns: "common" })
: t("unit.speed.kph")}{" "} : t("unit.speed.kph", { ns: "common" })}{" "}
{velocityAngle != undefined && ( {velocityAngle != undefined && (
<span className="text-primary/40"> <span className="text-primary/40">
<FaArrowRight <FaArrowRight
@ -624,7 +624,7 @@ function ObjectDetailsTab({
/> />
{config?.semantic_search.enabled && search.data.type == "object" && ( {config?.semantic_search.enabled && search.data.type == "object" && (
<Button <Button
aria-label="Find similar tracked objects" aria-label={t("itemMenu.findSimilar.aria")}
onClick={() => { onClick={() => {
setSearch(undefined); setSearch(undefined);
@ -633,7 +633,7 @@ function ObjectDetailsTab({
} }
}} }}
> >
Find Similar {t("itemMenu.findSimilar.label")}
</Button> </Button>
)} )}
</div> </div>
@ -855,12 +855,10 @@ export function ObjectSnapshotTab({
"text-lg font-semibold leading-none tracking-tight" "text-lg font-semibold leading-none tracking-tight"
} }
> >
Submit To Frigate+ {t("explore.submitToPlus.label")}
</div> </div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
Objects in locations you want to avoid are not false {t("explore.submitToPlus.desc")}
positives. Submitting them as false positives will
confuse the model.
</div> </div>
</div> </div>
@ -875,9 +873,13 @@ export function ObjectSnapshotTab({
onSubmitToPlus(false); onSubmitToPlus(false);
}} }}
> >
This is{" "} {/^[aeiou]/i.test(search?.label || "")
{/^[aeiou]/i.test(search?.label || "") ? "an" : "a"}{" "} ? t("explore.plus.review.true_other", {
{search?.label} label: search?.label,
})
: t("explore.plus.review.true_one", {
label: search?.label,
})}
</Button> </Button>
<Button <Button
className="text-white" className="text-white"
@ -888,9 +890,13 @@ export function ObjectSnapshotTab({
onSubmitToPlus(true); onSubmitToPlus(true);
}} }}
> >
This is not{" "} {/^[aeiou]/i.test(search?.label || "")
{/^[aeiou]/i.test(search?.label || "") ? "an" : "a"}{" "} ? t("explore.plus.review.false_other", {
{search?.label} label: search?.label,
})
: t("explore.plus.review.false_one", {
label: search?.label,
})}
</Button> </Button>
</> </>
)} )}
@ -898,7 +904,7 @@ export function ObjectSnapshotTab({
{state == "submitted" && ( {state == "submitted" && (
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
<FaCheckCircle className="text-success" /> <FaCheckCircle className="text-success" />
Submitted {t("explore.plus.review.state.submitted")}
</div> </div>
)} )}
</div> </div>
@ -917,6 +923,7 @@ type VideoTabProps = {
}; };
export function VideoTab({ search }: VideoTabProps) { export function VideoTab({ search }: VideoTabProps) {
const { t } = useTranslation(["views/explore"]);
const navigate = useNavigate(); const navigate = useNavigate();
const { data: reviewItem } = useSWR<ReviewSegment>([ const { data: reviewItem } = useSWR<ReviewSegment>([
`review/event/${search.id}`, `review/event/${search.id}`,
@ -951,7 +958,9 @@ export function VideoTab({ search }: VideoTabProps) {
</Chip> </Chip>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>View in History</TooltipContent> <TooltipContent>
{t("itemMenu.viewInHistory.label")}
</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
@ -966,7 +975,9 @@ export function VideoTab({ search }: VideoTabProps) {
</a> </a>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>Download</TooltipContent> <TooltipContent>
{t("button.download", { ns: "common" })}
</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
</div> </div>