separate and consolidate global and camera i18n namespaces

This commit is contained in:
Josh Hawkins
2026-02-27 09:40:20 -06:00
parent 8f681d5689
commit 3f7f5e3253
37 changed files with 1666 additions and 220 deletions
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const AudioSection = createConfigSection({
sectionPath: "audio",
i18nNamespace: "config/audio",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"enabled",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const AudioTranscriptionSection = createConfigSection({
sectionPath: "audio_transcription",
i18nNamespace: "config/audio_transcription",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
hiddenFields: ["enabled_in_config"],
@@ -460,31 +460,26 @@ export function createConfigSection({
return null;
}
// 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.
// Get section title from config namespace
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;
// For camera-level sections, keys live under `config/cameras` and are
// nested under the section name (e.g., `audio.label`). For global-level
// sections, keys are nested under the section name in `config/global`.
const configNamespace =
level === "camera" ? "config/cameras" : "config/global";
const title = t(`${sectionPath}.label`, {
ns: configNamespace,
defaultValue: defaultTitle,
});
const sectionDescription = i18n.exists(`${sectionPath}.description`, {
ns: configNamespace,
})
? t(`${sectionPath}.description`, { ns: configNamespace })
: undefined;
const sectionContent = (
<div className="space-y-6">
@@ -502,7 +497,7 @@ export function createConfigSection({
disabled={disabled || isSaving}
readonly={readonly}
showSubmit={false}
i18nNamespace={i18nNamespace}
i18nNamespace={configNamespace}
formContext={{
level,
cameraName,
@@ -516,7 +511,10 @@ export function createConfigSection({
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,
// When using a consolidated global namespace, keys are nested
// under the section name (e.g., `audio.label`) so provide the
// section prefix to templates so they can attempt `${section}.${field}` lookups.
sectionI18nPrefix: sectionPath,
t,
}}
/>
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const BirdseyeSection = createConfigSection({
sectionPath: "birdseye",
i18nNamespace: "config/birdseye",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["enabled", "mode", "order"],
hiddenFields: [],
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const CameraMqttSection = createConfigSection({
sectionPath: "mqtt",
i18nNamespace: "config/camera_mqtt",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"enabled",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const CameraUiSection = createConfigSection({
sectionPath: "ui",
i18nNamespace: "config/camera_ui",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["dashboard", "order"],
hiddenFields: [],
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const DetectSection = createConfigSection({
sectionPath: "detect",
i18nNamespace: "config/detect",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"enabled",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const FaceRecognitionSection = createConfigSection({
sectionPath: "face_recognition",
i18nNamespace: "config/face_recognition",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["enabled", "min_area"],
hiddenFields: [],
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const FfmpegSection = createConfigSection({
sectionPath: "ffmpeg",
i18nNamespace: "config/ffmpeg",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"inputs",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const LiveSection = createConfigSection({
sectionPath: "live",
i18nNamespace: "config/live",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["stream_name", "height", "quality"],
fieldGroups: {},
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const LprSection = createConfigSection({
sectionPath: "lpr",
i18nNamespace: "config/lpr",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"],
hiddenFields: [],
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const MotionSection = createConfigSection({
sectionPath: "motion",
i18nNamespace: "config/motion",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"enabled",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const NotificationsSection = createConfigSection({
sectionPath: "notifications",
i18nNamespace: "config/notifications",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["enabled", "email"],
fieldGroups: {},
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const ObjectsSection = createConfigSection({
sectionPath: "objects",
i18nNamespace: "config/objects",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["track", "alert", "detect", "filters"],
fieldGroups: {
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const OnvifSection = createConfigSection({
sectionPath: "onvif",
i18nNamespace: "config/onvif",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"host",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const RecordSection = createConfigSection({
sectionPath: "record",
i18nNamespace: "config/record",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"enabled",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const ReviewSection = createConfigSection({
sectionPath: "review",
i18nNamespace: "config/review",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["alerts", "detections", "genai"],
fieldGroups: {},
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const SemanticSearchSection = createConfigSection({
sectionPath: "semantic_search",
i18nNamespace: "config/semantic_search",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["triggers"],
hiddenFields: [],
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const SnapshotsSection = createConfigSection({
sectionPath: "snapshots",
i18nNamespace: "config/snapshots",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: [
"enabled",
@@ -5,7 +5,7 @@ import { createConfigSection } from "./BaseSection";
export const TimestampSection = createConfigSection({
sectionPath: "timestamp_style",
i18nNamespace: "config/timestamp_style",
i18nNamespace: "config/global",
defaultConfig: {
fieldOrder: ["position", "format", "color", "thickness"],
hiddenFields: ["effect", "enabled_in_config"],
@@ -21,7 +21,8 @@ export function DescriptionFieldTemplate(props: DescriptionFieldProps) {
let resolvedDescription = description;
if (isCameraLevel && sectionI18nPrefix && effectiveNamespace) {
// Support nested keys for both camera-level and consolidated global namespace
if (sectionI18nPrefix && effectiveNamespace) {
const descriptionKey = `${sectionI18nPrefix}.description`;
if (i18n.exists(descriptionKey, { ns: effectiveNamespace })) {
resolvedDescription = t(descriptionKey, { ns: effectiveNamespace });
@@ -199,7 +199,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
const label = domain
? t(`${domain}.${groupKey}`, {
ns: "config/groups",
ns: "config/global",
defaultValue: toTitle(groupKey),
})
: t(`groups.${groupKey}`, {