i18n for attributes

This commit is contained in:
Josh Hawkins 2026-05-19 08:22:21 -05:00
parent 5465482895
commit a53d4654b2
4 changed files with 109 additions and 5 deletions

View File

@ -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.<attr>.<field>` lookups to
# `filters_attribute.<field>` when `<attr>` 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

View File

@ -950,4 +950,4 @@
"label": "Original camera state",
"description": "Keep track of original state of camera."
}
}
}

View File

@ -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."
}
}
}
}

View File

@ -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.<field>` 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(".");