diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py index d3f66daf4..94f348da9 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -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, diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 9a9ad1cfb..8e374576b 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -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, diff --git a/frigate/config/camera/motion.py b/frigate/config/camera/motion.py index 4237bf95b..5a6ffe1ee 100644 --- a/frigate/config/camera/motion.py +++ b/frigate/config/camera/motion.py @@ -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, diff --git a/frigate/config/camera/notification.py b/frigate/config/camera/notification.py index a11a1de2d..3a5403e47 100644 --- a/frigate/config/camera/notification.py +++ b/frigate/config/camera/notification.py @@ -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, diff --git a/frigate/config/camera/objects.py b/frigate/config/camera/objects.py index 2da687714..fe10f5fda 100644 --- a/frigate/config/camera/objects.py +++ b/frigate/config/camera/objects.py @@ -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, diff --git a/frigate/config/camera/record.py b/frigate/config/camera/record.py index 0acafef80..386cf0f46 100644 --- a/frigate/config/camera/record.py +++ b/frigate/config/camera/record.py @@ -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, diff --git a/frigate/config/camera/snapshots.py b/frigate/config/camera/snapshots.py index d74df80ac..c4633962b 100644 --- a/frigate/config/camera/snapshots.py +++ b/frigate/config/camera/snapshots.py @@ -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, diff --git a/frigate/config/classification.py b/frigate/config/classification.py index ba9c2d72f..977b649a8 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -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, diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index 6fc485f81..3fadcac23 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -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", diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index e96e8ef9b..8a8bce596 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -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( 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 = (
@@ -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 (
{shouldShowTitle && ( -
-
- {title} - {showOverrideIndicator && level === "camera" && isOverridden && ( - - {t("overridden", { - ns: "common", - defaultValue: "Overridden", - })} - - )} - {hasChanges && ( - - {t("modified", { ns: "common", defaultValue: "Modified" })} - +
+
+
+ {title} + {showOverrideIndicator && + level === "camera" && + isOverridden && ( + + {t("overridden", { + ns: "common", + defaultValue: "Overridden", + })} + + )} + {hasChanges && ( + + {t("modified", { ns: "common", defaultValue: "Modified" })} + + )} +
+ {sectionDescription && ( +

+ {sectionDescription} +

)}
{level === "camera" && isOverridden && ( diff --git a/web/src/components/config-form/theme/templates/DescriptionFieldTemplate.tsx b/web/src/components/config-form/theme/templates/DescriptionFieldTemplate.tsx index 4d1b6f7eb..11885b16d 100644 --- a/web/src/components/config-form/theme/templates/DescriptionFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/DescriptionFieldTemplate.tsx @@ -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 (

- {description} + {resolvedDescription}

); } diff --git a/web/src/components/config-form/theme/templates/FieldTemplate.tsx b/web/src/components/config-form/theme/templates/FieldTemplate.tsx index 79091cda5..b2cc1ecc6 100644 --- a/web/src/components/config-form/theme/templates/FieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/FieldTemplate.tsx @@ -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; } diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index 0503978db..1c9d207ea 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -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 }); } } diff --git a/web/src/types/configForm.ts b/web/src/types/configForm.ts index 3a83da47d..60a98d174 100644 --- a/web/src/types/configForm.ts +++ b/web/src/types/configForm.ts @@ -20,5 +20,6 @@ export type ConfigFormContext = { fullCameraConfig?: CameraConfig; fullConfig?: FrigateConfig; i18nNamespace?: string; + sectionI18nPrefix?: string; t?: (key: string, options?: Record) => string; }; diff --git a/web/src/utils/i18n.ts b/web/src/utils/i18n.ts index c31d3984a..bce70eb83 100644 --- a/web/src/utils/i18n.ts +++ b/web/src/utils/i18n.ts @@ -53,6 +53,7 @@ i18n "views/exports", "views/explore", // Config section translations + "config/cameras", "config/detect", "config/record", "config/snapshots", diff --git a/web/src/views/settings/CameraConfigView.tsx b/web/src/views/settings/CameraConfigView.tsx index a9d34621f..4efb2beda 100644 --- a/web/src/views/settings/CameraConfigView.tsx +++ b/web/src/views/settings/CameraConfigView.tsx @@ -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({
    {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 (