mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +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(
|
||||
default_factory=AudioConfig,
|
||||
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(
|
||||
default_factory=CameraAudioTranscriptionConfig,
|
||||
@ -96,7 +96,7 @@ class CameraConfig(FrigateBaseModel):
|
||||
face_recognition: CameraFaceRecognitionConfig = Field(
|
||||
default_factory=CameraFaceRecognitionConfig,
|
||||
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(
|
||||
title="FFmpeg",
|
||||
@ -115,7 +115,7 @@ class CameraConfig(FrigateBaseModel):
|
||||
motion: MotionConfig = Field(
|
||||
None,
|
||||
title="Motion detection",
|
||||
description="Default motion detection settings; can be overridden per-camera.",
|
||||
description="Default motion detection settings for this camera.",
|
||||
)
|
||||
objects: ObjectConfig = Field(
|
||||
default_factory=ObjectConfig,
|
||||
@ -125,12 +125,12 @@ class CameraConfig(FrigateBaseModel):
|
||||
record: RecordConfig = Field(
|
||||
default_factory=RecordConfig,
|
||||
title="Recording",
|
||||
description="Recording and retention settings; can be overridden per-camera.",
|
||||
description="Recording and retention settings for this camera.",
|
||||
)
|
||||
review: ReviewConfig = Field(
|
||||
default_factory=ReviewConfig,
|
||||
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(
|
||||
default_factory=CameraSemanticSearchConfig,
|
||||
@ -140,7 +140,7 @@ class CameraConfig(FrigateBaseModel):
|
||||
snapshots: SnapshotsConfig = Field(
|
||||
default_factory=SnapshotsConfig,
|
||||
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(
|
||||
default_factory=TimestampStyleConfig,
|
||||
@ -162,7 +162,7 @@ class CameraConfig(FrigateBaseModel):
|
||||
notifications: NotificationConfig = Field(
|
||||
default_factory=NotificationConfig,
|
||||
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(
|
||||
default_factory=OnvifConfig,
|
||||
|
||||
@ -49,7 +49,7 @@ class FfmpegConfig(FrigateBaseModel):
|
||||
path: str = Field(
|
||||
default="default",
|
||||
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(
|
||||
default=FFMPEG_GLOBAL_ARGS_DEFAULT,
|
||||
|
||||
@ -11,7 +11,7 @@ class MotionConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
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(
|
||||
default=30,
|
||||
|
||||
@ -11,7 +11,7 @@ class NotificationConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
title="Enable notifications",
|
||||
description="Enable or disable notifications globally.",
|
||||
description="Enable or disable notifications; can be overridden per-camera.",
|
||||
)
|
||||
email: Optional[str] = Field(
|
||||
default=None,
|
||||
|
||||
@ -132,7 +132,7 @@ class ObjectConfig(FrigateBaseModel):
|
||||
track: list[str] = Field(
|
||||
default=DEFAULT_TRACKED_OBJECTS,
|
||||
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(
|
||||
default_factory=dict,
|
||||
|
||||
@ -98,7 +98,7 @@ class RecordConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
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(
|
||||
default=60,
|
||||
|
||||
@ -30,7 +30,7 @@ class SnapshotsConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
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(
|
||||
default=True,
|
||||
|
||||
@ -46,7 +46,7 @@ class AudioTranscriptionConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
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(
|
||||
default="en",
|
||||
@ -240,7 +240,7 @@ class FaceRecognitionConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
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(
|
||||
default="small",
|
||||
@ -322,12 +322,12 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
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(
|
||||
default="small",
|
||||
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(
|
||||
default=0.7,
|
||||
|
||||
@ -15,10 +15,10 @@
|
||||
},
|
||||
"audio": {
|
||||
"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": {
|
||||
"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": {
|
||||
"label": "End timeout",
|
||||
@ -138,7 +138,7 @@
|
||||
},
|
||||
"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": {
|
||||
"label": "Enable 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.",
|
||||
"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": {
|
||||
"label": "FFmpeg global args",
|
||||
@ -254,10 +254,10 @@
|
||||
},
|
||||
"motion": {
|
||||
"label": "Motion detection",
|
||||
"description": "Default motion detection settings; can be overridden per-camera.",
|
||||
"description": "Default motion detection settings for this camera.",
|
||||
"enabled": {
|
||||
"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": {
|
||||
"label": "Motion threshold",
|
||||
@ -308,7 +308,7 @@
|
||||
"description": "Object tracking defaults including which labels to track and per-object filters.",
|
||||
"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": {
|
||||
"label": "Object filters",
|
||||
@ -400,10 +400,10 @@
|
||||
},
|
||||
"record": {
|
||||
"label": "Recording",
|
||||
"description": "Recording and retention settings; can be overridden per-camera.",
|
||||
"description": "Recording and retention settings for this camera.",
|
||||
"enabled": {
|
||||
"label": "Enable recording",
|
||||
"description": "Enable or disable recording globally; individual cameras can override this."
|
||||
"description": "Enable or disable recording for this camera."
|
||||
},
|
||||
"expire_interval": {
|
||||
"label": "Record cleanup interval",
|
||||
@ -496,7 +496,7 @@
|
||||
},
|
||||
"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": {
|
||||
"label": "Alerts config",
|
||||
"description": "Settings for which tracked objects generate alerts and how alerts are retained.",
|
||||
@ -620,10 +620,10 @@
|
||||
},
|
||||
"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": {
|
||||
"label": "Snapshots enabled",
|
||||
"description": "Enable or disable saving snapshots globally."
|
||||
"description": "Enable or disable saving snapshots for this camera."
|
||||
},
|
||||
"clean_copy": {
|
||||
"label": "Save clean copy",
|
||||
@ -744,10 +744,10 @@
|
||||
},
|
||||
"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": {
|
||||
"label": "Enable notifications",
|
||||
"description": "Enable or disable notifications globally."
|
||||
"description": "Enable or disable notifications for this camera."
|
||||
},
|
||||
"email": {
|
||||
"label": "Notification email",
|
||||
|
||||
@ -130,7 +130,12 @@ export function createConfigSection({
|
||||
defaultCollapsed = false,
|
||||
showTitle,
|
||||
}: BaseSectionProps) {
|
||||
const { t } = useTranslation([i18nNamespace, "views/settings", "common"]);
|
||||
const { t, i18n } = useTranslation([
|
||||
i18nNamespace,
|
||||
"config/cameras",
|
||||
"views/settings",
|
||||
"common",
|
||||
]);
|
||||
const [isOpen, setIsOpen] = useState(!defaultCollapsed);
|
||||
const [pendingData, setPendingData] = useState<ConfigSectionData | null>(
|
||||
null,
|
||||
@ -455,13 +460,31 @@ export function createConfigSection({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get section title from config namespace
|
||||
const title = t("label", {
|
||||
ns: i18nNamespace,
|
||||
defaultValue:
|
||||
sectionPath.charAt(0).toUpperCase() +
|
||||
sectionPath.slice(1).replace(/_/g, " "),
|
||||
});
|
||||
// Get section title from config namespace. For camera-level sections we
|
||||
// prefer the `config/cameras` namespace where keys are nested under the
|
||||
// section name (e.g., `audio.label`). Fall back to provided i18nNamespace.
|
||||
const defaultTitle =
|
||||
sectionPath.charAt(0).toUpperCase() +
|
||||
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 = (
|
||||
<div className="space-y-6">
|
||||
@ -491,6 +514,9 @@ export function createConfigSection({
|
||||
? config?.cameras?.[cameraName]
|
||||
: undefined,
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
@ -597,21 +623,30 @@ export function createConfigSection({
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{shouldShowTitle && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Heading as="h4">{title}</Heading>
|
||||
{showOverrideIndicator && level === "camera" && isOverridden && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t("overridden", {
|
||||
ns: "common",
|
||||
defaultValue: "Overridden",
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{t("modified", { ns: "common", defaultValue: "Modified" })}
|
||||
</Badge>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<Heading as="h4">{title}</Heading>
|
||||
{showOverrideIndicator &&
|
||||
level === "camera" &&
|
||||
isOverridden && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t("overridden", {
|
||||
ns: "common",
|
||||
defaultValue: "Overridden",
|
||||
})}
|
||||
</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>
|
||||
{level === "camera" && isOverridden && (
|
||||
|
||||
@ -1,16 +1,40 @@
|
||||
// Description Field Template
|
||||
import type { DescriptionFieldProps } from "@rjsf/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfigFormContext } from "@/types/configForm";
|
||||
|
||||
export function DescriptionFieldTemplate(props: DescriptionFieldProps) {
|
||||
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 (
|
||||
<p id={id} className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
{resolvedDescription}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@ -81,7 +81,13 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
// Get i18n namespace from form context (passed through registry)
|
||||
const formContext = registry?.formContext as ConfigFormContext | 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([
|
||||
effectiveNamespace || i18nNamespace || "common",
|
||||
i18nNamespace || "common",
|
||||
"views/settings",
|
||||
]);
|
||||
@ -126,10 +132,21 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
|
||||
// Try to get translated label, falling back to schema title, then RJSF 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`;
|
||||
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) {
|
||||
finalLabel = schemaTitle;
|
||||
} else if (translatedFilterObjectLabel) {
|
||||
@ -145,11 +162,25 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
let fieldLabel = schemaTitle;
|
||||
if (!fieldLabel) {
|
||||
const fieldTranslationKey = `${fieldName}.label`;
|
||||
const prefixedFieldTranslationKey =
|
||||
sectionI18nPrefix &&
|
||||
!fieldTranslationKey.startsWith(`${sectionI18nPrefix}.`)
|
||||
? `${sectionI18nPrefix}.${fieldTranslationKey}`
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
i18nNamespace &&
|
||||
i18n.exists(fieldTranslationKey, { ns: i18nNamespace })
|
||||
prefixedFieldTranslationKey &&
|
||||
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 {
|
||||
fieldLabel = humanizeKey(fieldName);
|
||||
}
|
||||
@ -177,11 +208,25 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
let fieldLabel = schemaTitle;
|
||||
if (!fieldLabel) {
|
||||
const fieldTranslationKey = `${fieldName}.label`;
|
||||
const prefixedFieldTranslationKey =
|
||||
sectionI18nPrefix &&
|
||||
!fieldTranslationKey.startsWith(`${sectionI18nPrefix}.`)
|
||||
? `${sectionI18nPrefix}.${fieldTranslationKey}`
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
i18nNamespace &&
|
||||
i18n.exists(fieldTranslationKey, { ns: i18nNamespace })
|
||||
prefixedFieldTranslationKey &&
|
||||
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 {
|
||||
fieldLabel = humanizeKey(fieldName);
|
||||
}
|
||||
@ -198,10 +243,19 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
|
||||
// Try to get translated description, falling back to schema description
|
||||
let finalDescription = description || "";
|
||||
if (i18nNamespace && translationPath) {
|
||||
if (effectiveNamespace && translationPath) {
|
||||
const prefixedDescriptionKey =
|
||||
sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`)
|
||||
? `${sectionI18nPrefix}.${translationPath}.description`
|
||||
: undefined;
|
||||
const descriptionKey = `${translationPath}.description`;
|
||||
if (i18n.exists(descriptionKey, { ns: i18nNamespace })) {
|
||||
finalDescription = t(descriptionKey, { ns: i18nNamespace });
|
||||
if (
|
||||
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) {
|
||||
finalDescription = schemaDescription;
|
||||
}
|
||||
|
||||
@ -68,7 +68,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
|
||||
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([
|
||||
effectiveNamespace || formContext?.i18nNamespace || "common",
|
||||
formContext?.i18nNamespace || "common",
|
||||
"config/groups",
|
||||
"common",
|
||||
@ -127,12 +134,18 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
}
|
||||
|
||||
// Try i18n translation, fall back to schema or original values
|
||||
const i18nNs = formContext?.i18nNamespace;
|
||||
const i18nNs = effectiveNamespace;
|
||||
|
||||
let inferredLabel: string | undefined;
|
||||
if (i18nNs && translationPath) {
|
||||
const prefixedLabelKey =
|
||||
sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`)
|
||||
? `${sectionI18nPrefix}.${translationPath}.label`
|
||||
: undefined;
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@ -146,8 +159,17 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
|
||||
let inferredDescription: string | undefined;
|
||||
if (i18nNs && translationPath) {
|
||||
const prefixedDescriptionKey =
|
||||
sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`)
|
||||
? `${sectionI18nPrefix}.${translationPath}.description`
|
||||
: undefined;
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,5 +20,6 @@ export type ConfigFormContext = {
|
||||
fullCameraConfig?: CameraConfig;
|
||||
fullConfig?: FrigateConfig;
|
||||
i18nNamespace?: string;
|
||||
sectionI18nPrefix?: string;
|
||||
t?: (key: string, options?: Record<string, unknown>) => string;
|
||||
};
|
||||
|
||||
@ -53,6 +53,7 @@ i18n
|
||||
"views/exports",
|
||||
"views/explore",
|
||||
// Config section translations
|
||||
"config/cameras",
|
||||
"config/detect",
|
||||
"config/record",
|
||||
"config/snapshots",
|
||||
|
||||
@ -187,6 +187,7 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
||||
"config/objects",
|
||||
"config/review",
|
||||
"config/audio",
|
||||
"config/cameras",
|
||||
"config/audio_transcription",
|
||||
"config/birdseye",
|
||||
"config/camera_mqtt",
|
||||
@ -322,11 +323,15 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
||||
<ul className="space-y-1">
|
||||
{sections.map((section) => {
|
||||
const isOverridden = overriddenSections.includes(section.key);
|
||||
const sectionLabel = t("label", {
|
||||
ns: section.i18nNamespace,
|
||||
defaultValue:
|
||||
section.key.charAt(0).toUpperCase() +
|
||||
section.key.slice(1).replace(/_/g, " "),
|
||||
const defaultSectionLabel =
|
||||
section.key.charAt(0).toUpperCase() +
|
||||
section.key.slice(1).replace(/_/g, " ");
|
||||
const sectionLabel = t(`${section.key}.label`, {
|
||||
ns: "config/cameras",
|
||||
defaultValue: t("label", {
|
||||
ns: section.i18nNamespace,
|
||||
defaultValue: defaultSectionLabel,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user