From f97629433d6766e9fbcc7a7d96a6b83aa6b00970 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:45:24 -0500 Subject: [PATCH] Fixes (#18877) * Object labels with spaces should use correct i18n keys * Add Hungarian * Ensure onvif move request has a valid speed before removing When autotracking zooming is set to `disabled` (or is left out of the config), move_request["Speed"] may not exist, depending on the camera * Add another frame cache debug log --- frigate/camera/state.py | 2 +- frigate/ptz/onvif.py | 10 ++++++++-- web/src/components/card/SearchThumbnail.tsx | 5 ++--- web/src/components/filter/ReviewFilterGroup.tsx | 3 ++- web/src/components/filter/SearchFilterGroup.tsx | 3 ++- .../components/settings/ObjectMaskEditPane.tsx | 5 +++-- web/src/components/settings/ZoneEditPane.tsx | 3 ++- web/src/lib/const.ts | 1 + web/src/utils/i18n.ts | 8 +++++++- web/src/utils/lifecycleUtil.ts | 17 ++++++----------- web/src/views/explore/ExploreView.tsx | 3 ++- web/src/views/settings/CameraSettingsView.tsx | 9 +++++---- web/src/views/settings/MasksAndZonesView.tsx | 3 ++- web/src/views/settings/ObjectSettingsView.tsx | 3 ++- 14 files changed, 45 insertions(+), 30 deletions(-) diff --git a/frigate/camera/state.py b/frigate/camera/state.py index c05eaa486..620873cba 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -445,7 +445,7 @@ class CameraState: obj.thumbnail_data["frame_time"] if obj.thumbnail_data else None ) logger.debug( - f"{self.name}: Tracked object {obj_id} thumbnail frame_time: {thumb_time}" + f"{self.name}: Tracked object {obj_id} thumbnail frame_time: {thumb_time}, false positive: {obj.false_positive}" ) for t in thumb_frames_to_delete: object_id = self.frame_cache[t].get("object_id", "unknown") diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index edba6222f..81c8b9852 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -265,9 +265,15 @@ class OnvifController: "RelativeZoomTranslationSpace" ][zoom_space_id]["URI"] else: - if "Zoom" in move_request["Translation"]: + if ( + move_request["Translation"] is not None + and "Zoom" in move_request["Translation"] + ): del move_request["Translation"]["Zoom"] - if "Zoom" in move_request["Speed"]: + if ( + move_request["Speed"] is not None + and "Zoom" in move_request["Speed"] + ): del move_request["Speed"]["Zoom"] logger.debug( f"{camera_name}: Relative move request after deleting zoom: {move_request}" diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx index a765a8073..3876a7710 100644 --- a/web/src/components/card/SearchThumbnail.tsx +++ b/web/src/components/card/SearchThumbnail.tsx @@ -12,7 +12,7 @@ import { SearchResult } from "@/types/search"; import { cn } from "@/lib/utils"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import useContextMenu from "@/hooks/use-contextmenu"; -import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; type SearchThumbnailProps = { searchResult: SearchResult; @@ -23,7 +23,6 @@ export default function SearchThumbnail({ searchResult, onClick, }: SearchThumbnailProps) { - const { t } = useTranslation(["views/search"]); const apiHost = useApiHost(); const { data: config } = useSWR("config"); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); @@ -151,7 +150,7 @@ export default function SearchThumbnail({ .filter( (item) => item !== undefined && !item.includes("-verified"), ) - .map((text) => t(text, { ns: "objects" })) + .map((text) => getTranslatedLabel(text)) .sort() .join(", ") .replaceAll("-verified", "")} diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index bc16d2180..f2234b359 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -24,6 +24,7 @@ import CalendarFilterButton from "./CalendarFilterButton"; import { CamerasFilterButton } from "./CamerasFilterButton"; import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; const REVIEW_FILTERS = [ "cameras", @@ -498,7 +499,7 @@ export function GeneralFilterContent({ {allLabels.map((item) => ( { if (isChecked) { diff --git a/web/src/components/filter/SearchFilterGroup.tsx b/web/src/components/filter/SearchFilterGroup.tsx index f2190ee96..4e1f98ebe 100644 --- a/web/src/components/filter/SearchFilterGroup.tsx +++ b/web/src/components/filter/SearchFilterGroup.tsx @@ -26,6 +26,7 @@ import { CalendarRangeFilterButton } from "./CalendarFilterButton"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; type SearchFilterGroupProps = { className: string; @@ -372,7 +373,7 @@ export function GeneralFilterContent({ {allLabels.map((item) => ( { if (isChecked) { diff --git a/web/src/components/settings/ObjectMaskEditPane.tsx b/web/src/components/settings/ObjectMaskEditPane.tsx index 2a4a89a47..8fc1b6338 100644 --- a/web/src/components/settings/ObjectMaskEditPane.tsx +++ b/web/src/components/settings/ObjectMaskEditPane.tsx @@ -38,6 +38,7 @@ import { toast } from "sonner"; import { Toaster } from "../ui/sonner"; import ActivityIndicator from "../indicators/activity-indicator"; import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; type ObjectMaskEditPaneProps = { polygons?: Polygon[]; @@ -101,7 +102,7 @@ export default function ObjectMaskEditPane({ return t("masksAndZones.objectMaskLabel", { number: count + 1, - label: t(objectType, { ns: "objects" }), + label: getTranslatedLabel(objectType), }); }, [polygons, polygon, t]); @@ -438,7 +439,7 @@ export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) { {allLabels.map((item) => ( - {t(item, { ns: "objects" })} + {getTranslatedLabel(item)} ))} diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index c73e1fe9c..bc93fa781 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -33,6 +33,7 @@ import { Trans, useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { getTranslatedLabel } from "@/utils/i18n"; type ZoneEditPaneProps = { polygons?: Polygon[]; @@ -969,7 +970,7 @@ export function ZoneObjectSelector({ className="w-full cursor-pointer text-primary smart-capitalize" htmlFor={item} > - {t(item, { ns: "objects" })} + {getTranslatedLabel(item)} { + if (!label) return ""; + + return t(`${label.replace(/\s+/g, "_").toLowerCase()}`, { ns: "objects" }); +}; + i18n .use(initReactI18next) .use(HttpBackend) diff --git a/web/src/utils/lifecycleUtil.ts b/web/src/utils/lifecycleUtil.ts index af6e4d5bc..0a716d9fc 100644 --- a/web/src/utils/lifecycleUtil.ts +++ b/web/src/utils/lifecycleUtil.ts @@ -1,20 +1,15 @@ import { ObjectLifecycleSequence } from "@/types/timeline"; import { t } from "i18next"; +import { getTranslatedLabel } from "./i18n"; export function getLifecycleItemDescription( lifecycleItem: ObjectLifecycleSequence, ) { - // can't use useTranslation here - const label = t( - ( - (Array.isArray(lifecycleItem.data.sub_label) - ? lifecycleItem.data.sub_label[0] - : lifecycleItem.data.sub_label) || lifecycleItem.data.label - ) - .replace(" ", "_") - .toLowerCase(), - { ns: "objects" }, - ); + const rawLabel = Array.isArray(lifecycleItem.data.sub_label) + ? lifecycleItem.data.sub_label[0] + : lifecycleItem.data.sub_label || lifecycleItem.data.label; + + const label = getTranslatedLabel(rawLabel); switch (lifecycleItem.class_type) { case "visible": diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index afe5001af..1c5613563 100644 --- a/web/src/views/explore/ExploreView.tsx +++ b/web/src/views/explore/ExploreView.tsx @@ -22,6 +22,7 @@ import SearchResultActions from "@/components/menu/SearchResultActions"; import { SearchTab } from "@/components/overlay/detail/SearchDetailDialog"; import { FrigateConfig } from "@/types/frigateConfig"; import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; type ExploreViewProps = { searchDetail: SearchResult | undefined; @@ -152,7 +153,7 @@ function ThumbnailRow({ return (
- {t(objectType, { ns: "objects" })} + {getTranslatedLabel(objectType)} {searchResults && ( {t("trackedObjectsCount", { diff --git a/web/src/views/settings/CameraSettingsView.tsx b/web/src/views/settings/CameraSettingsView.tsx index 6a5f3261a..994936b8f 100644 --- a/web/src/views/settings/CameraSettingsView.tsx +++ b/web/src/views/settings/CameraSettingsView.tsx @@ -32,6 +32,7 @@ import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { useAlertsState, useDetectionsState, useEnabledState } from "@/api/ws"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { getTranslatedLabel } from "@/utils/i18n"; type CameraSettingsViewProps = { selectedCamera: string; @@ -81,18 +82,18 @@ export default function CameraSettingsView({ const alertsLabels = useMemo(() => { return cameraConfig?.review.alerts.labels ? cameraConfig.review.alerts.labels - .map((label) => t(label, { ns: "objects" })) + .map((label) => getTranslatedLabel(label)) .join(", ") : ""; - }, [cameraConfig, t]); + }, [cameraConfig]); const detectionsLabels = useMemo(() => { return cameraConfig?.review.detections.labels ? cameraConfig.review.detections.labels - .map((label) => t(label, { ns: "objects" })) + .map((label) => getTranslatedLabel(label)) .join(", ") : ""; - }, [cameraConfig, t]); + }, [cameraConfig]); // form diff --git a/web/src/views/settings/MasksAndZonesView.tsx b/web/src/views/settings/MasksAndZonesView.tsx index 60d39efb3..eb4afda77 100644 --- a/web/src/views/settings/MasksAndZonesView.tsx +++ b/web/src/views/settings/MasksAndZonesView.tsx @@ -42,6 +42,7 @@ import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { getTranslatedLabel } from "@/utils/i18n"; type MasksAndZoneViewProps = { selectedCamera: string; @@ -332,7 +333,7 @@ export default function MasksAndZonesView({ camera: cameraConfig.name, name: t("masksAndZones.objectMaskLabel", { number: globalObjectMasksCount + index + 1, - label: t(objectName, { ns: "objects" }), + label: getTranslatedLabel(objectName), }), objects: [objectName], points: interpolatePoints( diff --git a/web/src/views/settings/ObjectSettingsView.tsx b/web/src/views/settings/ObjectSettingsView.tsx index 8e1f93330..b666789d7 100644 --- a/web/src/views/settings/ObjectSettingsView.tsx +++ b/web/src/views/settings/ObjectSettingsView.tsx @@ -29,6 +29,7 @@ import { Separator } from "@/components/ui/separator"; import { isDesktop } from "react-device-detect"; import { Trans, useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { getTranslatedLabel } from "@/utils/i18n"; type ObjectSettingsViewProps = { selectedCamera?: string; @@ -371,7 +372,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) { {getIconForLabel(obj.label, "size-5 text-white")}
- {t(obj.label, { ns: "objects" })} + {getTranslatedLabel(obj.label)}