mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-29 03:20:21 +03:00
add section description from schema and clarify global vs camera level descriptions
This commit is contained in:
parent
8b7156438e
commit
8f681d5689
@ -76,7 +76,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
audio: AudioConfig = Field(
|
audio: AudioConfig = Field(
|
||||||
default_factory=AudioConfig,
|
default_factory=AudioConfig,
|
||||||
title="Audio events",
|
title="Audio events",
|
||||||
description="Settings for audio-based event detection; can be overridden per-camera.",
|
description="Settings for audio-based event detection for this camera.",
|
||||||
)
|
)
|
||||||
audio_transcription: CameraAudioTranscriptionConfig = Field(
|
audio_transcription: CameraAudioTranscriptionConfig = Field(
|
||||||
default_factory=CameraAudioTranscriptionConfig,
|
default_factory=CameraAudioTranscriptionConfig,
|
||||||
@ -96,7 +96,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
face_recognition: CameraFaceRecognitionConfig = Field(
|
face_recognition: CameraFaceRecognitionConfig = Field(
|
||||||
default_factory=CameraFaceRecognitionConfig,
|
default_factory=CameraFaceRecognitionConfig,
|
||||||
title="Face recognition",
|
title="Face recognition",
|
||||||
description="Settings for face detection and recognition; can be overridden per-camera.",
|
description="Settings for face detection and recognition for this camera.",
|
||||||
)
|
)
|
||||||
ffmpeg: CameraFfmpegConfig = Field(
|
ffmpeg: CameraFfmpegConfig = Field(
|
||||||
title="FFmpeg",
|
title="FFmpeg",
|
||||||
@ -115,7 +115,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
motion: MotionConfig = Field(
|
motion: MotionConfig = Field(
|
||||||
None,
|
None,
|
||||||
title="Motion detection",
|
title="Motion detection",
|
||||||
description="Default motion detection settings; can be overridden per-camera.",
|
description="Default motion detection settings for this camera.",
|
||||||
)
|
)
|
||||||
objects: ObjectConfig = Field(
|
objects: ObjectConfig = Field(
|
||||||
default_factory=ObjectConfig,
|
default_factory=ObjectConfig,
|
||||||
@ -125,12 +125,12 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
record: RecordConfig = Field(
|
record: RecordConfig = Field(
|
||||||
default_factory=RecordConfig,
|
default_factory=RecordConfig,
|
||||||
title="Recording",
|
title="Recording",
|
||||||
description="Recording and retention settings; can be overridden per-camera.",
|
description="Recording and retention settings for this camera.",
|
||||||
)
|
)
|
||||||
review: ReviewConfig = Field(
|
review: ReviewConfig = Field(
|
||||||
default_factory=ReviewConfig,
|
default_factory=ReviewConfig,
|
||||||
title="Review",
|
title="Review",
|
||||||
description="Settings that control alerts, detections, and GenAI review summaries used by the UI and storage; can be overridden per-camera.",
|
description="Settings that control alerts, detections, and GenAI review summaries used by the UI and storage for this camera.",
|
||||||
)
|
)
|
||||||
semantic_search: CameraSemanticSearchConfig = Field(
|
semantic_search: CameraSemanticSearchConfig = Field(
|
||||||
default_factory=CameraSemanticSearchConfig,
|
default_factory=CameraSemanticSearchConfig,
|
||||||
@ -140,7 +140,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
snapshots: SnapshotsConfig = Field(
|
snapshots: SnapshotsConfig = Field(
|
||||||
default_factory=SnapshotsConfig,
|
default_factory=SnapshotsConfig,
|
||||||
title="Snapshots",
|
title="Snapshots",
|
||||||
description="Settings for saved JPEG snapshots of tracked objects; can be overridden per-camera.",
|
description="Settings for saved JPEG snapshots of tracked objects for this camera.",
|
||||||
)
|
)
|
||||||
timestamp_style: TimestampStyleConfig = Field(
|
timestamp_style: TimestampStyleConfig = Field(
|
||||||
default_factory=TimestampStyleConfig,
|
default_factory=TimestampStyleConfig,
|
||||||
@ -162,7 +162,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
notifications: NotificationConfig = Field(
|
notifications: NotificationConfig = Field(
|
||||||
default_factory=NotificationConfig,
|
default_factory=NotificationConfig,
|
||||||
title="Notifications",
|
title="Notifications",
|
||||||
description="Settings to enable and control notifications; can be overridden per-camera.",
|
description="Settings to enable and control notifications for this camera.",
|
||||||
)
|
)
|
||||||
onvif: OnvifConfig = Field(
|
onvif: OnvifConfig = Field(
|
||||||
default_factory=OnvifConfig,
|
default_factory=OnvifConfig,
|
||||||
|
|||||||
@ -49,7 +49,7 @@ class FfmpegConfig(FrigateBaseModel):
|
|||||||
path: str = Field(
|
path: str = Field(
|
||||||
default="default",
|
default="default",
|
||||||
title="FFmpeg path",
|
title="FFmpeg path",
|
||||||
description='Path to the FFmpeg binary to use globally or a version alias ("5.0" or "7.0").',
|
description='Path to the FFmpeg binary to use or a version alias ("5.0" or "7.0").',
|
||||||
)
|
)
|
||||||
global_args: Union[str, list[str]] = Field(
|
global_args: Union[str, list[str]] = Field(
|
||||||
default=FFMPEG_GLOBAL_ARGS_DEFAULT,
|
default=FFMPEG_GLOBAL_ARGS_DEFAULT,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class MotionConfig(FrigateBaseModel):
|
|||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
title="Enable motion detection",
|
title="Enable motion detection",
|
||||||
description="Enable or disable motion detection globally; per-camera settings can override this.",
|
description="Enable or disable motion detection; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
threshold: int = Field(
|
threshold: int = Field(
|
||||||
default=30,
|
default=30,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class NotificationConfig(FrigateBaseModel):
|
|||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Enable notifications",
|
title="Enable notifications",
|
||||||
description="Enable or disable notifications globally.",
|
description="Enable or disable notifications; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
email: Optional[str] = Field(
|
email: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
|||||||
@ -132,7 +132,7 @@ class ObjectConfig(FrigateBaseModel):
|
|||||||
track: list[str] = Field(
|
track: list[str] = Field(
|
||||||
default=DEFAULT_TRACKED_OBJECTS,
|
default=DEFAULT_TRACKED_OBJECTS,
|
||||||
title="Objects to track",
|
title="Objects to track",
|
||||||
description="List of object labels to track globally; camera configs can override this.",
|
description="List of object labels to track; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
filters: dict[str, FilterConfig] = Field(
|
filters: dict[str, FilterConfig] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
|
|||||||
@ -98,7 +98,7 @@ class RecordConfig(FrigateBaseModel):
|
|||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Enable recording",
|
title="Enable recording",
|
||||||
description="Enable or disable recording globally; individual cameras can override this.",
|
description="Enable or disable recording; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
expire_interval: int = Field(
|
expire_interval: int = Field(
|
||||||
default=60,
|
default=60,
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class SnapshotsConfig(FrigateBaseModel):
|
|||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Snapshots enabled",
|
title="Snapshots enabled",
|
||||||
description="Enable or disable saving snapshots globally.",
|
description="Enable or disable saving snapshots; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
clean_copy: bool = Field(
|
clean_copy: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class AudioTranscriptionConfig(FrigateBaseModel):
|
|||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Enable audio transcription",
|
title="Enable audio transcription",
|
||||||
description="Enable or disable automatic audio transcription globally.",
|
description="Enable or disable automatic audio transcription; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
language: str = Field(
|
language: str = Field(
|
||||||
default="en",
|
default="en",
|
||||||
@ -240,7 +240,7 @@ class FaceRecognitionConfig(FrigateBaseModel):
|
|||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Enable face recognition",
|
title="Enable face recognition",
|
||||||
description="Enable or disable face recognition globally.",
|
description="Enable or disable face recognition; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
model_size: str = Field(
|
model_size: str = Field(
|
||||||
default="small",
|
default="small",
|
||||||
@ -322,12 +322,12 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
|
|||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Enable LPR",
|
title="Enable LPR",
|
||||||
description="Enable or disable LPR globally; camera-level settings can override.",
|
description="Enable or disable license plate recognition; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
model_size: str = Field(
|
model_size: str = Field(
|
||||||
default="small",
|
default="small",
|
||||||
title="Model size",
|
title="Model size",
|
||||||
description="Model size used for text detection/recognition; small runs on CPU, large on GPU.",
|
description="Model size used for text detection/recognition. Most users should use 'small'.",
|
||||||
)
|
)
|
||||||
detection_threshold: float = Field(
|
detection_threshold: float = Field(
|
||||||
default=0.7,
|
default=0.7,
|
||||||
|
|||||||
@ -15,10 +15,10 @@
|
|||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"label": "Audio events",
|
"label": "Audio events",
|
||||||
"description": "Settings for audio-based event detection; can be overridden per-camera.",
|
"description": "Settings for audio-based event detection for this camera.",
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable audio detection",
|
"label": "Enable audio detection",
|
||||||
"description": "Enable or disable audio event detection; can be overridden per-camera."
|
"description": "Enable or disable audio event detection for this camera."
|
||||||
},
|
},
|
||||||
"max_not_heard": {
|
"max_not_heard": {
|
||||||
"label": "End timeout",
|
"label": "End timeout",
|
||||||
@ -138,7 +138,7 @@
|
|||||||
},
|
},
|
||||||
"face_recognition": {
|
"face_recognition": {
|
||||||
"label": "Face recognition",
|
"label": "Face recognition",
|
||||||
"description": "Settings for face detection and recognition; can be overridden per-camera.",
|
"description": "Settings for face detection and recognition for this camera.",
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable face recognition",
|
"label": "Enable face recognition",
|
||||||
"description": "Enable or disable face recognition."
|
"description": "Enable or disable face recognition."
|
||||||
@ -153,7 +153,7 @@
|
|||||||
"description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.",
|
"description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.",
|
||||||
"path": {
|
"path": {
|
||||||
"label": "FFmpeg path",
|
"label": "FFmpeg path",
|
||||||
"description": "Path to the FFmpeg binary to use globally or a version alias (\"5.0\" or \"7.0\")."
|
"description": "Path to the FFmpeg binary to use for this camera or a version alias (\"5.0\" or \"7.0\")."
|
||||||
},
|
},
|
||||||
"global_args": {
|
"global_args": {
|
||||||
"label": "FFmpeg global args",
|
"label": "FFmpeg global args",
|
||||||
@ -254,10 +254,10 @@
|
|||||||
},
|
},
|
||||||
"motion": {
|
"motion": {
|
||||||
"label": "Motion detection",
|
"label": "Motion detection",
|
||||||
"description": "Default motion detection settings; can be overridden per-camera.",
|
"description": "Default motion detection settings for this camera.",
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable motion detection",
|
"label": "Enable motion detection",
|
||||||
"description": "Enable or disable motion detection globally; per-camera settings can override this."
|
"description": "Enable or disable motion detection for this camera."
|
||||||
},
|
},
|
||||||
"threshold": {
|
"threshold": {
|
||||||
"label": "Motion threshold",
|
"label": "Motion threshold",
|
||||||
@ -308,7 +308,7 @@
|
|||||||
"description": "Object tracking defaults including which labels to track and per-object filters.",
|
"description": "Object tracking defaults including which labels to track and per-object filters.",
|
||||||
"track": {
|
"track": {
|
||||||
"label": "Objects to track",
|
"label": "Objects to track",
|
||||||
"description": "List of object labels to track globally; camera configs can override this."
|
"description": "List of object labels to track for this camera."
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"label": "Object filters",
|
"label": "Object filters",
|
||||||
@ -400,10 +400,10 @@
|
|||||||
},
|
},
|
||||||
"record": {
|
"record": {
|
||||||
"label": "Recording",
|
"label": "Recording",
|
||||||
"description": "Recording and retention settings; can be overridden per-camera.",
|
"description": "Recording and retention settings for this camera.",
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable recording",
|
"label": "Enable recording",
|
||||||
"description": "Enable or disable recording globally; individual cameras can override this."
|
"description": "Enable or disable recording for this camera."
|
||||||
},
|
},
|
||||||
"expire_interval": {
|
"expire_interval": {
|
||||||
"label": "Record cleanup interval",
|
"label": "Record cleanup interval",
|
||||||
@ -496,7 +496,7 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"label": "Review",
|
"label": "Review",
|
||||||
"description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage; can be overridden per-camera.",
|
"description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage for this camera.",
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"label": "Alerts config",
|
"label": "Alerts config",
|
||||||
"description": "Settings for which tracked objects generate alerts and how alerts are retained.",
|
"description": "Settings for which tracked objects generate alerts and how alerts are retained.",
|
||||||
@ -620,10 +620,10 @@
|
|||||||
},
|
},
|
||||||
"snapshots": {
|
"snapshots": {
|
||||||
"label": "Snapshots",
|
"label": "Snapshots",
|
||||||
"description": "Settings for saved JPEG snapshots of tracked objects; can be overridden per-camera.",
|
"description": "Settings for saved JPEG snapshots of tracked objects for this camera.",
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Snapshots enabled",
|
"label": "Snapshots enabled",
|
||||||
"description": "Enable or disable saving snapshots globally."
|
"description": "Enable or disable saving snapshots for this camera."
|
||||||
},
|
},
|
||||||
"clean_copy": {
|
"clean_copy": {
|
||||||
"label": "Save clean copy",
|
"label": "Save clean copy",
|
||||||
@ -744,10 +744,10 @@
|
|||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"label": "Notifications",
|
"label": "Notifications",
|
||||||
"description": "Settings to enable and control notifications; can be overridden per-camera.",
|
"description": "Settings to enable and control notifications for this camera.",
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable notifications",
|
"label": "Enable notifications",
|
||||||
"description": "Enable or disable notifications globally."
|
"description": "Enable or disable notifications for this camera."
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"label": "Notification email",
|
"label": "Notification email",
|
||||||
|
|||||||
@ -130,7 +130,12 @@ export function createConfigSection({
|
|||||||
defaultCollapsed = false,
|
defaultCollapsed = false,
|
||||||
showTitle,
|
showTitle,
|
||||||
}: BaseSectionProps) {
|
}: BaseSectionProps) {
|
||||||
const { t } = useTranslation([i18nNamespace, "views/settings", "common"]);
|
const { t, i18n } = useTranslation([
|
||||||
|
i18nNamespace,
|
||||||
|
"config/cameras",
|
||||||
|
"views/settings",
|
||||||
|
"common",
|
||||||
|
]);
|
||||||
const [isOpen, setIsOpen] = useState(!defaultCollapsed);
|
const [isOpen, setIsOpen] = useState(!defaultCollapsed);
|
||||||
const [pendingData, setPendingData] = useState<ConfigSectionData | null>(
|
const [pendingData, setPendingData] = useState<ConfigSectionData | null>(
|
||||||
null,
|
null,
|
||||||
@ -455,13 +460,31 @@ export function createConfigSection({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get section title from config namespace
|
// Get section title from config namespace. For camera-level sections we
|
||||||
const title = t("label", {
|
// prefer the `config/cameras` namespace where keys are nested under the
|
||||||
ns: i18nNamespace,
|
// section name (e.g., `audio.label`). Fall back to provided i18nNamespace.
|
||||||
defaultValue:
|
const defaultTitle =
|
||||||
sectionPath.charAt(0).toUpperCase() +
|
sectionPath.charAt(0).toUpperCase() +
|
||||||
sectionPath.slice(1).replace(/_/g, " "),
|
sectionPath.slice(1).replace(/_/g, " ");
|
||||||
});
|
const title =
|
||||||
|
level === "camera"
|
||||||
|
? t(`${sectionPath}.label`, {
|
||||||
|
ns: "config/cameras",
|
||||||
|
defaultValue: defaultTitle,
|
||||||
|
})
|
||||||
|
: t("label", {
|
||||||
|
ns: i18nNamespace,
|
||||||
|
defaultValue: defaultTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sectionDescription =
|
||||||
|
level === "camera"
|
||||||
|
? i18n.exists(`${sectionPath}.description`, { ns: "config/cameras" })
|
||||||
|
? t(`${sectionPath}.description`, { ns: "config/cameras" })
|
||||||
|
: undefined
|
||||||
|
: i18n.exists("description", { ns: i18nNamespace })
|
||||||
|
? t("description", { ns: i18nNamespace })
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const sectionContent = (
|
const sectionContent = (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -491,6 +514,9 @@ export function createConfigSection({
|
|||||||
? config?.cameras?.[cameraName]
|
? config?.cameras?.[cameraName]
|
||||||
: undefined,
|
: undefined,
|
||||||
fullConfig: config,
|
fullConfig: config,
|
||||||
|
// When rendering camera-level sections, provide the section path so
|
||||||
|
// field templates can look up keys under the `config/cameras` namespace
|
||||||
|
sectionI18nPrefix: level === "camera" ? sectionPath : undefined,
|
||||||
t,
|
t,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -597,21 +623,30 @@ export function createConfigSection({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{shouldShowTitle && (
|
{shouldShowTitle && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-col gap-1">
|
||||||
<Heading as="h4">{title}</Heading>
|
<div className="flex items-center gap-3">
|
||||||
{showOverrideIndicator && level === "camera" && isOverridden && (
|
<Heading as="h4">{title}</Heading>
|
||||||
<Badge variant="secondary" className="text-xs">
|
{showOverrideIndicator &&
|
||||||
{t("overridden", {
|
level === "camera" &&
|
||||||
ns: "common",
|
isOverridden && (
|
||||||
defaultValue: "Overridden",
|
<Badge variant="secondary" className="text-xs">
|
||||||
})}
|
{t("overridden", {
|
||||||
</Badge>
|
ns: "common",
|
||||||
)}
|
defaultValue: "Overridden",
|
||||||
{hasChanges && (
|
})}
|
||||||
<Badge variant="outline" className="text-xs">
|
</Badge>
|
||||||
{t("modified", { ns: "common", defaultValue: "Modified" })}
|
)}
|
||||||
</Badge>
|
{hasChanges && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{t("modified", { ns: "common", defaultValue: "Modified" })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{sectionDescription && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{sectionDescription}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{level === "camera" && isOverridden && (
|
{level === "camera" && isOverridden && (
|
||||||
|
|||||||
@ -1,16 +1,40 @@
|
|||||||
// Description Field Template
|
// Description Field Template
|
||||||
import type { DescriptionFieldProps } from "@rjsf/utils";
|
import type { DescriptionFieldProps } from "@rjsf/utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ConfigFormContext } from "@/types/configForm";
|
||||||
|
|
||||||
export function DescriptionFieldTemplate(props: DescriptionFieldProps) {
|
export function DescriptionFieldTemplate(props: DescriptionFieldProps) {
|
||||||
const { description, id } = props;
|
const { description, id } = props;
|
||||||
|
const formContext = (
|
||||||
|
props as { registry?: { formContext?: ConfigFormContext } }
|
||||||
|
).registry?.formContext;
|
||||||
|
|
||||||
if (!description) {
|
const isCameraLevel = formContext?.level === "camera";
|
||||||
|
const sectionI18nPrefix = formContext?.sectionI18nPrefix;
|
||||||
|
const i18nNamespace = formContext?.i18nNamespace;
|
||||||
|
const effectiveNamespace = isCameraLevel ? "config/cameras" : i18nNamespace;
|
||||||
|
|
||||||
|
const { t, i18n } = useTranslation([
|
||||||
|
effectiveNamespace || i18nNamespace || "common",
|
||||||
|
i18nNamespace || "common",
|
||||||
|
]);
|
||||||
|
|
||||||
|
let resolvedDescription = description;
|
||||||
|
|
||||||
|
if (isCameraLevel && sectionI18nPrefix && effectiveNamespace) {
|
||||||
|
const descriptionKey = `${sectionI18nPrefix}.description`;
|
||||||
|
if (i18n.exists(descriptionKey, { ns: effectiveNamespace })) {
|
||||||
|
resolvedDescription = t(descriptionKey, { ns: effectiveNamespace });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedDescription) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p id={id} className="text-sm text-muted-foreground">
|
<p id={id} className="text-sm text-muted-foreground">
|
||||||
{description}
|
{resolvedDescription}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,7 +81,13 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
// Get i18n namespace from form context (passed through registry)
|
// Get i18n namespace from form context (passed through registry)
|
||||||
const formContext = registry?.formContext as ConfigFormContext | undefined;
|
const formContext = registry?.formContext as ConfigFormContext | undefined;
|
||||||
const i18nNamespace = formContext?.i18nNamespace as string | undefined;
|
const i18nNamespace = formContext?.i18nNamespace as string | undefined;
|
||||||
|
const sectionI18nPrefix = formContext?.sectionI18nPrefix as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
const isCameraLevel = formContext?.level === "camera";
|
||||||
|
const effectiveNamespace = isCameraLevel ? "config/cameras" : i18nNamespace;
|
||||||
const { t, i18n } = useTranslation([
|
const { t, i18n } = useTranslation([
|
||||||
|
effectiveNamespace || i18nNamespace || "common",
|
||||||
i18nNamespace || "common",
|
i18nNamespace || "common",
|
||||||
"views/settings",
|
"views/settings",
|
||||||
]);
|
]);
|
||||||
@ -126,10 +132,21 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
|
|
||||||
// Try to get translated label, falling back to schema title, then RJSF label
|
// Try to get translated label, falling back to schema title, then RJSF label
|
||||||
let finalLabel = label;
|
let finalLabel = label;
|
||||||
if (i18nNamespace && translationPath) {
|
if (effectiveNamespace && translationPath) {
|
||||||
|
// Prefer camera-scoped translations when a section prefix is provided
|
||||||
|
const prefixedTranslationKey =
|
||||||
|
sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`)
|
||||||
|
? `${sectionI18nPrefix}.${translationPath}.label`
|
||||||
|
: undefined;
|
||||||
const translationKey = `${translationPath}.label`;
|
const translationKey = `${translationPath}.label`;
|
||||||
if (i18n.exists(translationKey, { ns: i18nNamespace })) {
|
|
||||||
finalLabel = t(translationKey, { ns: i18nNamespace });
|
if (
|
||||||
|
prefixedTranslationKey &&
|
||||||
|
i18n.exists(prefixedTranslationKey, { ns: effectiveNamespace })
|
||||||
|
) {
|
||||||
|
finalLabel = t(prefixedTranslationKey, { ns: effectiveNamespace });
|
||||||
|
} else if (i18n.exists(translationKey, { ns: effectiveNamespace })) {
|
||||||
|
finalLabel = t(translationKey, { ns: effectiveNamespace });
|
||||||
} else if (schemaTitle) {
|
} else if (schemaTitle) {
|
||||||
finalLabel = schemaTitle;
|
finalLabel = schemaTitle;
|
||||||
} else if (translatedFilterObjectLabel) {
|
} else if (translatedFilterObjectLabel) {
|
||||||
@ -145,11 +162,25 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
let fieldLabel = schemaTitle;
|
let fieldLabel = schemaTitle;
|
||||||
if (!fieldLabel) {
|
if (!fieldLabel) {
|
||||||
const fieldTranslationKey = `${fieldName}.label`;
|
const fieldTranslationKey = `${fieldName}.label`;
|
||||||
|
const prefixedFieldTranslationKey =
|
||||||
|
sectionI18nPrefix &&
|
||||||
|
!fieldTranslationKey.startsWith(`${sectionI18nPrefix}.`)
|
||||||
|
? `${sectionI18nPrefix}.${fieldTranslationKey}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
i18nNamespace &&
|
prefixedFieldTranslationKey &&
|
||||||
i18n.exists(fieldTranslationKey, { ns: i18nNamespace })
|
effectiveNamespace &&
|
||||||
|
i18n.exists(prefixedFieldTranslationKey, { ns: effectiveNamespace })
|
||||||
) {
|
) {
|
||||||
fieldLabel = t(fieldTranslationKey, { ns: i18nNamespace });
|
fieldLabel = t(prefixedFieldTranslationKey, {
|
||||||
|
ns: effectiveNamespace,
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
effectiveNamespace &&
|
||||||
|
i18n.exists(fieldTranslationKey, { ns: effectiveNamespace })
|
||||||
|
) {
|
||||||
|
fieldLabel = t(fieldTranslationKey, { ns: effectiveNamespace });
|
||||||
} else {
|
} else {
|
||||||
fieldLabel = humanizeKey(fieldName);
|
fieldLabel = humanizeKey(fieldName);
|
||||||
}
|
}
|
||||||
@ -177,11 +208,25 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
let fieldLabel = schemaTitle;
|
let fieldLabel = schemaTitle;
|
||||||
if (!fieldLabel) {
|
if (!fieldLabel) {
|
||||||
const fieldTranslationKey = `${fieldName}.label`;
|
const fieldTranslationKey = `${fieldName}.label`;
|
||||||
|
const prefixedFieldTranslationKey =
|
||||||
|
sectionI18nPrefix &&
|
||||||
|
!fieldTranslationKey.startsWith(`${sectionI18nPrefix}.`)
|
||||||
|
? `${sectionI18nPrefix}.${fieldTranslationKey}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
i18nNamespace &&
|
prefixedFieldTranslationKey &&
|
||||||
i18n.exists(fieldTranslationKey, { ns: i18nNamespace })
|
effectiveNamespace &&
|
||||||
|
i18n.exists(prefixedFieldTranslationKey, { ns: effectiveNamespace })
|
||||||
) {
|
) {
|
||||||
fieldLabel = t(fieldTranslationKey, { ns: i18nNamespace });
|
fieldLabel = t(prefixedFieldTranslationKey, {
|
||||||
|
ns: effectiveNamespace,
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
effectiveNamespace &&
|
||||||
|
i18n.exists(fieldTranslationKey, { ns: effectiveNamespace })
|
||||||
|
) {
|
||||||
|
fieldLabel = t(fieldTranslationKey, { ns: effectiveNamespace });
|
||||||
} else {
|
} else {
|
||||||
fieldLabel = humanizeKey(fieldName);
|
fieldLabel = humanizeKey(fieldName);
|
||||||
}
|
}
|
||||||
@ -198,10 +243,19 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
|
|
||||||
// Try to get translated description, falling back to schema description
|
// Try to get translated description, falling back to schema description
|
||||||
let finalDescription = description || "";
|
let finalDescription = description || "";
|
||||||
if (i18nNamespace && translationPath) {
|
if (effectiveNamespace && translationPath) {
|
||||||
|
const prefixedDescriptionKey =
|
||||||
|
sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`)
|
||||||
|
? `${sectionI18nPrefix}.${translationPath}.description`
|
||||||
|
: undefined;
|
||||||
const descriptionKey = `${translationPath}.description`;
|
const descriptionKey = `${translationPath}.description`;
|
||||||
if (i18n.exists(descriptionKey, { ns: i18nNamespace })) {
|
if (
|
||||||
finalDescription = t(descriptionKey, { ns: i18nNamespace });
|
prefixedDescriptionKey &&
|
||||||
|
i18n.exists(prefixedDescriptionKey, { ns: effectiveNamespace })
|
||||||
|
) {
|
||||||
|
finalDescription = t(prefixedDescriptionKey, { ns: effectiveNamespace });
|
||||||
|
} else if (i18n.exists(descriptionKey, { ns: effectiveNamespace })) {
|
||||||
|
finalDescription = t(descriptionKey, { ns: effectiveNamespace });
|
||||||
} else if (schemaDescription) {
|
} else if (schemaDescription) {
|
||||||
finalDescription = schemaDescription;
|
finalDescription = schemaDescription;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
|
const isCameraLevel = formContext?.level === "camera";
|
||||||
|
const effectiveNamespace = isCameraLevel
|
||||||
|
? "config/cameras"
|
||||||
|
: formContext?.i18nNamespace;
|
||||||
|
const sectionI18nPrefix = formContext?.sectionI18nPrefix;
|
||||||
|
|
||||||
const { t, i18n } = useTranslation([
|
const { t, i18n } = useTranslation([
|
||||||
|
effectiveNamespace || formContext?.i18nNamespace || "common",
|
||||||
formContext?.i18nNamespace || "common",
|
formContext?.i18nNamespace || "common",
|
||||||
"config/groups",
|
"config/groups",
|
||||||
"common",
|
"common",
|
||||||
@ -127,12 +134,18 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try i18n translation, fall back to schema or original values
|
// Try i18n translation, fall back to schema or original values
|
||||||
const i18nNs = formContext?.i18nNamespace;
|
const i18nNs = effectiveNamespace;
|
||||||
|
|
||||||
let inferredLabel: string | undefined;
|
let inferredLabel: string | undefined;
|
||||||
if (i18nNs && translationPath) {
|
if (i18nNs && translationPath) {
|
||||||
|
const prefixedLabelKey =
|
||||||
|
sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`)
|
||||||
|
? `${sectionI18nPrefix}.${translationPath}.label`
|
||||||
|
: undefined;
|
||||||
const labelKey = `${translationPath}.label`;
|
const labelKey = `${translationPath}.label`;
|
||||||
if (i18n.exists(labelKey, { ns: i18nNs })) {
|
if (prefixedLabelKey && i18n.exists(prefixedLabelKey, { ns: i18nNs })) {
|
||||||
|
inferredLabel = t(prefixedLabelKey, { ns: i18nNs });
|
||||||
|
} else if (i18n.exists(labelKey, { ns: i18nNs })) {
|
||||||
inferredLabel = t(labelKey, { ns: i18nNs });
|
inferredLabel = t(labelKey, { ns: i18nNs });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,8 +159,17 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
|
|
||||||
let inferredDescription: string | undefined;
|
let inferredDescription: string | undefined;
|
||||||
if (i18nNs && translationPath) {
|
if (i18nNs && translationPath) {
|
||||||
|
const prefixedDescriptionKey =
|
||||||
|
sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`)
|
||||||
|
? `${sectionI18nPrefix}.${translationPath}.description`
|
||||||
|
: undefined;
|
||||||
const descriptionKey = `${translationPath}.description`;
|
const descriptionKey = `${translationPath}.description`;
|
||||||
if (i18n.exists(descriptionKey, { ns: i18nNs })) {
|
if (
|
||||||
|
prefixedDescriptionKey &&
|
||||||
|
i18n.exists(prefixedDescriptionKey, { ns: i18nNs })
|
||||||
|
) {
|
||||||
|
inferredDescription = t(prefixedDescriptionKey, { ns: i18nNs });
|
||||||
|
} else if (i18n.exists(descriptionKey, { ns: i18nNs })) {
|
||||||
inferredDescription = t(descriptionKey, { ns: i18nNs });
|
inferredDescription = t(descriptionKey, { ns: i18nNs });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,5 +20,6 @@ export type ConfigFormContext = {
|
|||||||
fullCameraConfig?: CameraConfig;
|
fullCameraConfig?: CameraConfig;
|
||||||
fullConfig?: FrigateConfig;
|
fullConfig?: FrigateConfig;
|
||||||
i18nNamespace?: string;
|
i18nNamespace?: string;
|
||||||
|
sectionI18nPrefix?: string;
|
||||||
t?: (key: string, options?: Record<string, unknown>) => string;
|
t?: (key: string, options?: Record<string, unknown>) => string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,6 +53,7 @@ i18n
|
|||||||
"views/exports",
|
"views/exports",
|
||||||
"views/explore",
|
"views/explore",
|
||||||
// Config section translations
|
// Config section translations
|
||||||
|
"config/cameras",
|
||||||
"config/detect",
|
"config/detect",
|
||||||
"config/record",
|
"config/record",
|
||||||
"config/snapshots",
|
"config/snapshots",
|
||||||
|
|||||||
@ -187,6 +187,7 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
"config/objects",
|
"config/objects",
|
||||||
"config/review",
|
"config/review",
|
||||||
"config/audio",
|
"config/audio",
|
||||||
|
"config/cameras",
|
||||||
"config/audio_transcription",
|
"config/audio_transcription",
|
||||||
"config/birdseye",
|
"config/birdseye",
|
||||||
"config/camera_mqtt",
|
"config/camera_mqtt",
|
||||||
@ -322,11 +323,15 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{sections.map((section) => {
|
{sections.map((section) => {
|
||||||
const isOverridden = overriddenSections.includes(section.key);
|
const isOverridden = overriddenSections.includes(section.key);
|
||||||
const sectionLabel = t("label", {
|
const defaultSectionLabel =
|
||||||
ns: section.i18nNamespace,
|
section.key.charAt(0).toUpperCase() +
|
||||||
defaultValue:
|
section.key.slice(1).replace(/_/g, " ");
|
||||||
section.key.charAt(0).toUpperCase() +
|
const sectionLabel = t(`${section.key}.label`, {
|
||||||
section.key.slice(1).replace(/_/g, " "),
|
ns: "config/cameras",
|
||||||
|
defaultValue: t("label", {
|
||||||
|
ns: section.i18nNamespace,
|
||||||
|
defaultValue: defaultSectionLabel,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user