From a53d4654b23b815c374c3a52488bcee80bbdd601 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 19 May 2026 08:22:21 -0500 Subject: [PATCH] i18n for attributes --- generate_config_translations.py | 58 +++++++++++++++++++ web/public/locales/en/config/cameras.json | 2 +- web/public/locales/en/config/global.json | 37 +++++++++++- .../config-form/theme/utils/i18n.ts | 17 +++++- 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/generate_config_translations.py b/generate_config_translations.py index 9bc830855d..7f9c9bc504 100644 --- a/generate_config_translations.py +++ b/generate_config_translations.py @@ -364,6 +364,64 @@ def main(): continue section_data.pop(key, None) + if field_name == "objects": + # Produce a parallel `filters_attribute` block alongside `filters`, + # with object-wording rewritten for attribute filters (face, + # license_plate, courier logos). The frontend's + # buildTranslationPath routes `filters..` lookups to + # `filters_attribute.` when `` is in + # `model.all_attributes`. Keep this rewrite list explicit rather + # than running a blanket s/object/attribute/ so unrelated + # descriptions (e.g. "JSON object") never accidentally flip. + filters_block = section_data.get("filters") + if isinstance(filters_block, dict): + attribute_rewrites = [ + ("Object filters", "Attribute filters"), + ("detected objects", "detected attributes"), + ("object area", "attribute area"), + ("object type", "attribute"), + ("the object", "the attribute"), + ] + + # Per-field overrides for cases where the generic rewrite + # doesn't capture the attribute-specific semantics. Keys + # match the FilterConfig field name; values are partial + # overrides applied AFTER the generic rewrites. + attribute_field_overrides: Dict[str, Dict[str, str]] = { + "min_score": { + "description": ( + "Minimum single-frame detection confidence required " + "to associate this attribute with its parent object." + ), + }, + } + + def rewrite(text: str) -> str: + for source, replacement in attribute_rewrites: + text = text.replace(source, replacement) + return text + + attribute_variant: Dict[str, Any] = {} + for key, value in filters_block.items(): + if key in ("label", "description"): + if isinstance(value, str): + attribute_variant[key] = rewrite(value) + continue + if not isinstance(value, dict): + continue + field_trans: Dict[str, str] = {} + if isinstance(value.get("label"), str): + field_trans["label"] = rewrite(value["label"]) + if isinstance(value.get("description"), str): + field_trans["description"] = rewrite(value["description"]) + overrides = attribute_field_overrides.get(key) + if overrides: + field_trans.update(overrides) + if field_trans: + attribute_variant[key] = field_trans + if attribute_variant: + section_data["filters_attribute"] = attribute_variant + if not section_data: logger.warning(f"No translations found for section: {field_name}") continue diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index f645dd33a1..4f2c0ea01e 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -950,4 +950,4 @@ "label": "Original camera state", "description": "Keep track of original state of camera." } -} \ No newline at end of file +} diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index b10f0a7afc..1f5c39248c 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -921,6 +921,41 @@ "label": "Original GenAI state", "description": "Indicates whether GenAI was enabled in the original static config." } + }, + "filters_attribute": { + "label": "Attribute filters", + "description": "Filters applied to detected attributes to reduce false positives (area, ratio, confidence).", + "min_area": { + "label": "Minimum attribute area", + "description": "Minimum bounding box area (pixels or percentage) required for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum attribute area", + "description": "Maximum bounding box area (pixels or percentage) allowed for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum aspect ratio", + "description": "Minimum width/height ratio required for the bounding box to qualify." + }, + "max_ratio": { + "label": "Maximum aspect ratio", + "description": "Maximum width/height ratio allowed for the bounding box to qualify." + }, + "threshold": { + "label": "Confidence threshold", + "description": "Average detection confidence threshold required for the attribute to be considered a true positive." + }, + "min_score": { + "label": "Minimum confidence", + "description": "Minimum single-frame detection confidence required to associate this attribute with its parent object." + }, + "mask": { + "label": "Filter mask", + "description": "Polygon coordinates defining where this filter applies within the frame." + }, + "raw_mask": { + "label": "Raw Mask" + } } }, "record": { @@ -1597,4 +1632,4 @@ "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication." } } -} \ No newline at end of file +} diff --git a/web/src/components/config-form/theme/utils/i18n.ts b/web/src/components/config-form/theme/utils/i18n.ts index 5a27020655..e0e76eaefa 100644 --- a/web/src/components/config-form/theme/utils/i18n.ts +++ b/web/src/components/config-form/theme/utils/i18n.ts @@ -70,12 +70,23 @@ export function buildTranslationPath( (segment): segment is string => typeof segment === "string", ); - // Handle filters section - skip the dynamic filter object name - // Example: filters.person.threshold -> filters.threshold + // Handle filters section - skip the dynamic filter object name. Route + // to `filters_attribute.` when the dynamic key is an attribute + // label (face, license_plate, courier logos) so attribute filter fields + // pick up the attribute-worded translations emitted by + // generate_config_translations.py. + // Example: filters.person.threshold -> filters.threshold + // Example: filters.face.min_area -> filters_attribute.min_area const filtersIndex = stringSegments.indexOf("filters"); if (filtersIndex !== -1 && stringSegments.length > filtersIndex + 2) { + const filterKey = stringSegments[filtersIndex + 1]; + const allAttributes = formContext?.fullConfig?.model?.all_attributes ?? []; + const sectionWord = allAttributes.includes(filterKey) + ? "filters_attribute" + : "filters"; const normalized = [ - ...stringSegments.slice(0, filtersIndex + 1), + ...stringSegments.slice(0, filtersIndex), + sectionWord, ...stringSegments.slice(filtersIndex + 2), ]; return normalized.join(".");