diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx index 7a1cee4c5..131fdb2eb 100644 --- a/web/src/components/Statusbar.tsx +++ b/web/src/components/Statusbar.tsx @@ -4,8 +4,9 @@ import { StatusMessage, } from "@/context/statusbar-provider"; import useStats, { useAutoFrigateStats } from "@/hooks/use-stats"; +import { t } from "i18next"; import { useContext, useEffect, useMemo } from "react"; -import { Trans } from "react-i18next"; + import { FaCheck } from "react-icons/fa"; import { IoIosWarning } from "react-icons/io"; import { MdCircle } from "react-icons/md"; @@ -130,7 +131,7 @@ export default function Statusbar() { {Object.entries(messages).length === 0 ? (
- stats.healthy + {t("stats.healthy", { ns: "views/system" })}
) : ( Object.entries(messages).map(([key, messageArray]) => ( diff --git a/web/src/components/filter/CalendarFilterButton.tsx b/web/src/components/filter/CalendarFilterButton.tsx index 2975b110e..0ef8207da 100644 --- a/web/src/components/filter/CalendarFilterButton.tsx +++ b/web/src/components/filter/CalendarFilterButton.tsx @@ -15,7 +15,6 @@ import { DateRange } from "react-day-picker"; import { useState } from "react"; import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import { t } from "i18next"; -import { Trans } from "react-i18next"; type CalendarFilterButtonProps = { reviewSummary?: ReviewSummary; @@ -70,7 +69,7 @@ export default function CalendarFilterButton({ updateSelectedDay(undefined); }} > - button.reset + {t("button.reset")} diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 43d54a53b..630e6a1da 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -70,18 +70,20 @@ import { MobilePageHeader, MobilePageTitle, } from "../mobile/MobilePage"; -import { Trans } from "react-i18next"; -import { t } from "i18next"; + import { Label } from "../ui/label"; import { Switch } from "../ui/switch"; import { CameraStreamingDialog } from "../settings/CameraStreamingDialog"; import { DialogTrigger } from "@radix-ui/react-dialog"; import { useStreamingSettings } from "@/context/streaming-settings-provider"; +import { useTranslation } from "react-i18next"; type CameraGroupSelectorProps = { className?: string; }; + export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { + const { t } = useTranslation(["components/camera"]); const { data: config } = useSWR("config"); // tooltip @@ -163,7 +165,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { - menu.live.allCameras + {t("menu.live.allCameras")} @@ -233,6 +235,7 @@ function NewGroupDialog({ setGroup, deleteGroup, }: NewGroupDialogProps) { + const { t } = useTranslation(["components/camera"]); const { mutate: updateConfig } = useSWR("config"); // editing group and state @@ -354,12 +357,8 @@ function NewGroupDialog({ className={cn(isDesktop && "mt-5", "justify-center")} onClose={() => setOpen(false)} > - - <Trans ns="components/camera">group.label</Trans> - - - group.edit - + {t("group.label")} + {t("group.edit")}
- {editState == "add" ? ( - <Trans ns="components/camera">group.add</Trans> - ) : ( - <Trans ns="components/camera">group.edit</Trans> - )} + {editState == "add" ? t("group.add") : t("group.edit")} Edit camera groups @@ -444,6 +439,7 @@ export function EditGroupDialog({ currentGroups, activeGroup, }: EditGroupDialogProps) { + const { t } = useTranslation(["components/camera"]); const Overlay = isDesktop ? Dialog : MobilePage; const Content = isDesktop ? DialogContent : MobilePageContent; const Header = isDesktop ? DialogHeader : MobilePageHeader; @@ -483,11 +479,9 @@ export function EditGroupDialog({ >
setOpen(false)}> - - <Trans ns="components/camera">group.edit</Trans> - + {t("group.edit")} - group.edit.desc + {t("group.edit.desc")}
@@ -517,6 +511,7 @@ export function CameraGroupRow({ onDeleteGroup, onEditGroup, }: CameraGroupRowProps) { + const { t } = useTranslation(["components/camera"]); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); if (!group) { @@ -538,24 +533,18 @@ export function CameraGroupRow({ > - - group.delete.confirm - + {t("group.delete.confirm")} - - group.delete.confirm.desc - + {t("group.delete.confirm.desc", { name: group[0] })} - - button.cancel - + {t("button.cancel")} - button.delete + {t("button.delete")} @@ -573,13 +562,13 @@ export function CameraGroupRow({ aria-label="Edit group" onClick={onEditGroup} > - button.edit + {t("button.edit")} setDeleteDialogOpen(true)} > - button.delete + {t("button.delete")} @@ -596,9 +585,7 @@ export function CameraGroupRow({ onClick={onEditGroup} /> - - button.edit - + {t("button.edit")} @@ -609,9 +596,7 @@ export function CameraGroupRow({ onClick={() => setDeleteDialogOpen(true)} /> - - button.delete - + {t("button.delete")}
)} @@ -637,6 +622,7 @@ export function CameraGroupEdit({ onSave, onCancel, }: CameraGroupEditProps) { + const { t } = useTranslation(["components/camera"]); const { data: config, mutate: updateConfig } = useSWR("config"); @@ -656,9 +642,7 @@ export function CameraGroupEdit({ name: z .string() .min(2, { - message: t("group.name.errorMessage.mustLeastCharacters", { - ns: "components/camera", - }), + message: t("group.name.errorMessage.mustLeastCharacters"), }) .transform((val: string) => val.trim().replace(/\s+/g, "_")) .refine( @@ -669,9 +653,7 @@ export function CameraGroupEdit({ ); }, { - message: t("group.name.errorMessage.exists", { - ns: "components/camera", - }), + message: t("group.name.errorMessage.exists"), }, ) .refine( @@ -683,9 +665,7 @@ export function CameraGroupEdit({ }, ) .refine((value: string) => value.toLowerCase() !== "default", { - message: t("group.name.errorMessage.invalid", { - ns: "components/camera", - }), + message: t("group.name.errorMessage.invalid"), }), cameras: z.array(z.string()), @@ -743,7 +723,6 @@ export function CameraGroupEdit({ toast.success( t("group.toast.success", { name: values.name, - ns: "components/camera", }), { position: "top-center", @@ -788,6 +767,7 @@ export function CameraGroupEdit({ groupStreamingSettings, allGroupsStreamingSettings, setAllGroupsStreamingSettings, + t, ], ); @@ -812,15 +792,11 @@ export function CameraGroupEdit({ name="name" render={({ field }) => ( - - group.name.label - + {t("group.name.label")} @@ -836,12 +812,8 @@ export function CameraGroupEdit({ name="cameras" render={({ field }) => ( - - group.cameras.label - - - group.cameras.desc - + {t("group.cameras.label")} + {t("group.cameras.desc")} {[ ...(birdseyeConfig?.enabled ? ["birdseye"] : []), @@ -925,9 +897,7 @@ export function CameraGroupEdit({ name="icon" render={({ field }) => ( - - group.icon - + {t("group.icon")} - button.cancel + {t("button.cancel")}
) : ( - button.save + t("button.save") )} diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx index 48a736388..a9b4a4f15 100644 --- a/web/src/components/filter/CamerasFilterButton.tsx +++ b/web/src/components/filter/CamerasFilterButton.tsx @@ -12,8 +12,7 @@ import { isMobile } from "react-device-detect"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import FilterSwitch from "./FilterSwitch"; import { FaVideo } from "react-icons/fa"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; +import { useTranslation } from "react-i18next"; type CameraFilterButtonProps = { allCameras: string[]; @@ -31,6 +30,7 @@ export function CamerasFilterButton({ mainCamera, updateCameraFilter, }: CameraFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); const [open, setOpen] = useState(false); const [currentCameras, setCurrentCameras] = useState( selectedCameras, @@ -46,7 +46,7 @@ export function CamerasFilterButton({ } return `${selectedCameras.includes("birdseye") ? selectedCameras.length - 1 : selectedCameras.length} Camera${selectedCameras.length !== 1 ? "s" : ""}`; - }, [selectedCameras]); + }, [selectedCameras, t]); // ui @@ -140,12 +140,13 @@ export function CamerasFilterContent({ setOpen, updateCameraFilter, }: CamerasFilterContentProps) { + const { t } = useTranslation(["components/filter"]); return ( <> {isMobile && ( <> - cameras.all.short + {t("cameras.all.short")} @@ -153,7 +154,7 @@ export function CamerasFilterContent({
{ if (isChecked) { setCurrentCameras(undefined); @@ -235,7 +236,7 @@ export function CamerasFilterContent({ setOpen(false); }} > - button.apply + {t("button.apply")}
diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 7140335a5..85a236bcc 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -23,7 +23,7 @@ import { FilterList, GeneralFilter } from "@/types/filter"; import CalendarFilterButton from "./CalendarFilterButton"; import { CamerasFilterButton } from "./CamerasFilterButton"; import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; -import { Trans } from "react-i18next"; + import { t } from "i18next"; const REVIEW_FILTERS = [ @@ -280,7 +280,7 @@ function ShowReviewFilter({ } /> @@ -366,7 +366,7 @@ function GeneralFilterButton({ : "text-primary" }`} > - label + {t("label", { ns: "components/filter" })} ); @@ -474,7 +474,7 @@ export function GeneralFilterContent({ className="mx-2 cursor-pointer text-primary" htmlFor="allLabels" > - labels.all + {t("labels.all", { ns: "components/filter" })} - zones.all + {t("zones.all", { ns: "components/filter" })} - button.apply + {t("button.apply")} @@ -613,7 +613,7 @@ function ShowMotionOnlyButton({ className="mx-2 cursor-pointer text-primary" htmlFor="collapse-motion" > - motion.only + {t("motion.only", { ns: "views/events" })} diff --git a/web/src/components/filter/SearchFilterGroup.tsx b/web/src/components/filter/SearchFilterGroup.tsx index 344ca7245..fb410fe33 100644 --- a/web/src/components/filter/SearchFilterGroup.tsx +++ b/web/src/components/filter/SearchFilterGroup.tsx @@ -24,8 +24,8 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog"; import { CalendarRangeFilterButton } from "./CalendarFilterButton"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Trans } from "react-i18next"; -import { t } from "i18next"; + +import { useTranslation } from "react-i18next"; type SearchFilterGroupProps = { className: string; @@ -41,6 +41,7 @@ export default function SearchFilterGroup({ filterList, onUpdateFilter, }: SearchFilterGroupProps) { + const { t } = useTranslation(["components/filter"]); const { data: config } = useSWR("config", { revalidateOnFocus: false, }); @@ -197,11 +198,7 @@ export default function SearchFilterGroup({ to: new Date(filter.before * 1000), } } - defaultText={ - isMobile - ? t("dates.all.short", { ns: "components/filter" }) - : t("dates.all", { ns: "components/filter" }) - } + defaultText={isMobile ? t("dates.all.short") : t("dates.all")} updateSelectedRange={onUpdateSelectedRange} /> )} @@ -235,6 +232,7 @@ function GeneralFilterButton({ selectedLabels, updateLabelFilter, }: GeneralFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); const [open, setOpen] = useState(false); const [currentLabels, setCurrentLabels] = useState( selectedLabels, @@ -242,11 +240,11 @@ function GeneralFilterButton({ const buttonText = useMemo(() => { if (isMobile) { - return t("labels.all.short", { ns: "components/filter" }); + return t("labels.all.short"); } if (!selectedLabels || selectedLabels.length == 0) { - return t("labels.all", { ns: "components/filter" }); + return t("labels.all"); } if (selectedLabels.length == 1) { @@ -257,7 +255,7 @@ function GeneralFilterButton({ count: selectedLabels.length, ns: "components/filter", }); - }, [selectedLabels]); + }, [selectedLabels, t]); // ui @@ -332,6 +330,7 @@ export function GeneralFilterContent({ setCurrentLabels, onClose, }: GeneralFilterContentProps) { + const { t } = useTranslation(["components/filter"]); return ( <>
@@ -340,7 +339,7 @@ export function GeneralFilterContent({ className="mx-2 cursor-pointer text-primary" htmlFor="allLabels" > - labels.all + {t("labels.all")} - button.apply + {t("button.apply")}
@@ -420,6 +419,7 @@ function SortTypeButton({ selectedSortType, updateSortType, }: SortTypeButtonProps) { + const { t } = useTranslation(["components/filter"]); const [open, setOpen] = useState(false); const [currentSortType, setCurrentSortType] = useState< SearchSortType | undefined @@ -450,7 +450,7 @@ function SortTypeButton({
- sort.label + {t("sort.label")}
); @@ -505,14 +505,15 @@ export function SortTypeContent({ setCurrentSortType, onClose, }: SortTypeContentProps) { + const { t } = useTranslation(["components/filter"]); const sortLabels = { - date_asc: t("sort.dateAsc", { ns: "components/filter" }), - date_desc: t("sort.dateDesc", { ns: "components/filter" }), - score_asc: t("sort.scoreAsc", { ns: "components/filter" }), - score_desc: t("sort.scoreDesc", { ns: "components/filter" }), - speed_asc: t("sort.speedAsc", { ns: "components/filter" }), - speed_desc: t("sort.speedDesc", { ns: "components/filter" }), - relevance: t("sort.relevance", { ns: "components/filter" }), + date_asc: t("sort.dateAsc"), + date_desc: t("sort.dateDesc"), + score_asc: t("sort.scoreAsc"), + score_desc: t("sort.scoreDesc"), + speed_asc: t("sort.speedAsc"), + speed_desc: t("sort.speedDesc"), + relevance: t("sort.relevance"), }; return ( <> @@ -566,7 +567,7 @@ export function SortTypeContent({ onClose(); }} > - button.apply + {t("button.apply")} diff --git a/web/src/components/filter/ZoneMaskFilter.tsx b/web/src/components/filter/ZoneMaskFilter.tsx index 4f767d8f4..9d1d26d7f 100644 --- a/web/src/components/filter/ZoneMaskFilter.tsx +++ b/web/src/components/filter/ZoneMaskFilter.tsx @@ -7,7 +7,7 @@ import { PolygonType } from "@/types/canvas"; import { Label } from "../ui/label"; import { Switch } from "../ui/switch"; import { DropdownMenuSeparator } from "../ui/dropdown-menu"; -import { Trans } from "react-i18next"; +import { useTranslation } from "react-i18next"; type ZoneMaskFilterButtonProps = { selectedZoneMask?: PolygonType[]; @@ -17,6 +17,7 @@ export function ZoneMaskFilterButton({ selectedZoneMask, updateZoneMaskFilter, }: ZoneMaskFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); const trigger = ( ); @@ -68,6 +69,7 @@ export function GeneralFilterContent({ selectedZoneMask, updateZoneMaskFilter, }: GeneralFilterContentProps) { + const { t } = useTranslation(["components/filter"]); return ( <>
@@ -76,7 +78,7 @@ export function GeneralFilterContent({ className="mx-2 cursor-pointer text-primary" htmlFor="allLabels" > - labels.all + {t("labels.all")} - - masksAndZones. - {item.replace(/_([a-z])/g, (letter) => letter.toUpperCase()) + - "s"} - + {t( + "masksAndZones." + + item.replace(/_([a-z])/g, (letter) => + letter.toUpperCase(), + ) + + "s", + { ns: "views/settings" }, + )} + {t("storage.cameraStorage.camera")} + {t("storage.cameraStorage.storageUsed")} - storage.cameraStorage.camera - - - - storage.cameraStorage.storageUsed - - - - - storage.cameraStorage.percentageOfTotalUsed - - - - storage.cameraStorage.bandwidth + {t("storage.cameraStorage.percentageOfTotalUsed")} + {t("storage.cameraStorage.bandwidth")} @@ -206,9 +198,7 @@ export function CombinedStorageGraph({ style={{ backgroundColor: item.color }} >
{item.name === "Unused" - ? t("storage.cameraStorage.unused", { - ns: "views/system", - }) + ? t("storage.cameraStorage.unused") : item.name.replaceAll("_", " ")} {item.name === "Unused" && ( @@ -225,9 +215,7 @@ export function CombinedStorageGraph({
- - storage.cameraStorage.unused.tips - + {t("storage.cameraStorage.unused.tips")}
diff --git a/web/src/components/icons/IconPicker.tsx b/web/src/components/icons/IconPicker.tsx index a01461b32..4e83cbb93 100644 --- a/web/src/components/icons/IconPicker.tsx +++ b/web/src/components/icons/IconPicker.tsx @@ -11,8 +11,8 @@ import { IoClose } from "react-icons/io5"; import Heading from "../ui/heading"; import { cn } from "@/lib/utils"; import { Button } from "../ui/button"; -import { Trans } from "react-i18next"; -import { t } from "i18next"; + +import { useTranslation } from "react-i18next"; export type IconName = keyof typeof LuIcons; @@ -32,6 +32,7 @@ export default function IconPicker({ selectedIcon, setSelectedIcon, }: IconPickerProps) { + const { t } = useTranslation(["components/icons"]); const [open, setOpen] = useState(false); const containerRef = useRef(null); const [searchTerm, setSearchTerm] = useState(""); @@ -72,7 +73,7 @@ export default function IconPicker({ className="mt-2 w-full text-muted-foreground" aria-label="Select an icon" > - iconPicker.selectIcon + {t("iconPicker.selectIcon")} ) : (
@@ -103,9 +104,7 @@ export default function IconPicker({ className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]" >
- - iconPicker.selectIcon - + {t("iconPicker.selectIcon")} { @@ -37,7 +38,6 @@ export function SaveSearchDialog({ toast.success( t("search.saveSearch.success", { searchName: searchName.trim(), - ns: "components/dialog", }), { position: "top-center", @@ -62,31 +62,25 @@ export function SaveSearchDialog({ }} > - - search.saveSearch.label - + {t("search.saveSearch.label")} - search.saveSearch.desc + {t("search.saveSearch.desc")} setSearchName(e.target.value)} - placeholder={t("search.saveSearch.placeholder", { - ns: "components/dialog", - })} + placeholder={t("search.saveSearch.placeholder")} /> {overwrite && (
- - search.saveSearch.overwrite - + {t("search.saveSearch.overwrite", { searchName })}
)} diff --git a/web/src/components/menu/AccountSettings.tsx b/web/src/components/menu/AccountSettings.tsx index b5f535914..14069c1bb 100644 --- a/web/src/components/menu/AccountSettings.tsx +++ b/web/src/components/menu/AccountSettings.tsx @@ -21,7 +21,7 @@ import { DialogClose } from "../ui/dialog"; import { LuLogOut, LuSquarePen } from "react-icons/lu"; import useSWR from "swr"; import { t } from "i18next"; -import { Trans } from "react-i18next"; + import { useState } from "react"; import axios from "axios"; import { toast } from "sonner"; @@ -123,9 +123,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) { > - - menu.user.logout - + {t("menu.user.logout")}
diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index c6905b31d..ef148d612 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -55,7 +55,7 @@ import { cn } from "@/lib/utils"; import useSWR from "swr"; import RestartDialog from "../overlay/dialog/RestartDialog"; import { t } from "i18next"; -import { Trans } from "react-i18next"; + import { useLanguage } from "@/context/language-provider"; import { useIsAdmin } from "@/hooks/use-is-admin"; import SetPasswordDialog from "../overlay/SetPasswordDialog"; @@ -133,9 +133,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { -

- menu.settings -

+

{t("menu.settings")}

@@ -196,9 +194,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { )} {isAdmin && ( <> - - menu.system - + {t("menu.system")} @@ -211,9 +207,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { aria-label="System metrics" > - - menu.systemMetrics - + {t("menu.systemMetrics")} @@ -226,9 +220,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { aria-label="System logs" > - - menu.systemLogs - + {t("menu.systemLogs")} @@ -237,7 +229,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { - menu.configuration + {t("menu.configuration")} @@ -251,9 +243,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { aria-label="Settings" > - - menu.settings - + {t("menu.settings")} {isAdmin && ( @@ -268,16 +258,14 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { aria-label="Configuration editor" > - - menu.configurationEditor - + {t("menu.configurationEditor")} )} - menu.appearance + {t("menu.appearance")} @@ -287,9 +275,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { } > - - menu.languages - + {t("menu.languages")} - menu.language.en + {t("menu.language.en")} ) : ( - - menu.language.en - + {t("menu.language.en")} )} - menu.language.zhCN + {t("menu.language.zhCN")} ) : ( - menu.language.zhCN + {t("menu.language.zhCN")} )} @@ -350,12 +334,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { {language === systemLanguage ? ( <> - menu.withSystem + {t("menu.withSystem")} ) : ( - - menu.withSystem - + {t("menu.withSystem")} )} @@ -368,9 +350,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { } > - - menu.darkMode.label - + {t("menu.darkMode.label")} - menu.darkMode.light + {t("menu.darkMode.light")} ) : ( - menu.darkMode.light + {t("menu.darkMode.light")} )} @@ -411,11 +391,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { {theme === "dark" ? ( <> - menu.darkMode.dark + {t("menu.darkMode.dark")} ) : ( - menu.darkMode.dark + {t("menu.darkMode.dark")} )} @@ -431,12 +411,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { {theme === "system" ? ( <> - menu.withSystem + {t("menu.withSystem")} ) : ( - - menu.withSystem - + {t("menu.withSystem")} )} @@ -449,9 +427,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { } > - - menu.theme.label - + {t("menu.theme.label")} - {friendlyColorSchemeName(scheme)} + {t(friendlyColorSchemeName(scheme))} ) : ( - {friendlyColorSchemeName(scheme)} + {t(friendlyColorSchemeName(scheme))} )} @@ -487,7 +463,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { - menu.help + {t("menu.help")} @@ -498,9 +474,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { aria-label={t("menu.documentation.label")} > - - menu.documentation - + {t("menu.documentation")} setRestartDialogOpen(true)} > - - menu.restart - + {t("menu.restart")} )} diff --git a/web/src/components/menu/LiveContextMenu.tsx b/web/src/components/menu/LiveContextMenu.tsx index ab590a357..bc3093c72 100644 --- a/web/src/components/menu/LiveContextMenu.tsx +++ b/web/src/components/menu/LiveContextMenu.tsx @@ -44,7 +44,7 @@ import { useNotifications, useNotificationSuspend, } from "@/api/ws"; -import { Trans } from "react-i18next"; + import { t } from "i18next"; type LiveContextMenuProps = { @@ -362,9 +362,7 @@ export default function LiveContextMenu({ className="flex w-full cursor-pointer items-center justify-start gap-2" onClick={isEnabled ? resetPreferredLiveMode : undefined} > -
- button -
+
{t("button")}
diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index 5859cf92c..b8e5c6e58 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -39,8 +39,8 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import useSWR from "swr"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; + +import { useTranslation } from "react-i18next"; type SearchResultActionsProps = { searchResult: SearchResult; @@ -61,6 +61,8 @@ export default function SearchResultActions({ isContextMenu = false, children, }: SearchResultActionsProps) { + const { t } = useTranslation(["views/explore"]); + const { data: config } = useSWR("config"); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -92,61 +94,45 @@ export default function SearchResultActions({ const menuItems = ( <> {searchResult.has_clip && ( - + - - itemMenu.downloadVideo - + {t("itemMenu.downloadVideo")} )} {searchResult.has_snapshot && ( - + - - itemMenu.downloadSnapshot.label - + {t("itemMenu.downloadSnapshot.label")} )} {searchResult.data.type == "object" && ( - - itemMenu.viewObjectLifecycle.label - + {t("itemMenu.viewObjectLifecycle.label")} )} {config?.semantic_search?.enabled && isContextMenu && ( - - itemMenu.findSimilar.label - + {t("itemMenu.findSimilar.label")} )} {isMobileOnly && @@ -156,15 +142,11 @@ export default function SearchResultActions({ searchResult.data.type == "object" && !searchResult.plus_id && ( - - itemMenu.submitToPlus - + {t("itemMenu.submitToPlus")} )} setDeleteDialogOpen(true)} > - - button.delete - + {t("button.delete")} ); @@ -187,22 +167,18 @@ export default function SearchResultActions({ > - - dialog.confirmDelete - + {t("dialog.confirmDelete")} - dialog.confirmDelete.desc + {t("dialog.confirmDelete.desc")} - - button.cancel - + {t("button.cancel")} - button.delete + {t("button.delete")} @@ -224,7 +200,7 @@ export default function SearchResultActions({ /> - itemMenu.findSimilar.label + {t("itemMenu.findSimilar.label")} )} @@ -243,7 +219,7 @@ export default function SearchResultActions({ /> - itemMenu.submitToPlus.label + {t("itemMenu.submitToPlus.label")} )} diff --git a/web/src/components/navigation/NavItem.tsx b/web/src/components/navigation/NavItem.tsx index dd0a494f2..2b425d678 100644 --- a/web/src/components/navigation/NavItem.tsx +++ b/web/src/components/navigation/NavItem.tsx @@ -9,7 +9,7 @@ import { TooltipPortal } from "@radix-ui/react-tooltip"; import { NavData } from "@/types/navigation"; import { IconType } from "react-icons"; import { cn } from "@/lib/utils"; -import { Trans } from "react-i18next"; +import { t } from "i18next"; const variants = { primary: { @@ -61,9 +61,7 @@ export default function NavItem({ {content} -

- {item.title} -

+

{t("{item.title}")}

diff --git a/web/src/components/overlay/CameraInfoDialog.tsx b/web/src/components/overlay/CameraInfoDialog.tsx index ed0bb9f9a..8f11bf179 100644 --- a/web/src/components/overlay/CameraInfoDialog.tsx +++ b/web/src/components/overlay/CameraInfoDialog.tsx @@ -15,8 +15,7 @@ import { useEffect, useState } from "react"; import axios from "axios"; import { toast } from "sonner"; import { Toaster } from "../ui/sonner"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; +import { useTranslation } from "react-i18next"; type CameraInfoDialogProps = { camera: CameraConfig; @@ -28,6 +27,7 @@ export default function CameraInfoDialog({ showCameraInfoDialog, setShowCameraInfoDialog, }: CameraInfoDialogProps) { + const { t } = useTranslation(["views/system"]); const [ffprobeInfo, setFfprobeInfo] = useState(); useEffect(() => { @@ -76,12 +76,11 @@ export default function CameraInfoDialog({ {t("cameras.info.cameraProbeInfo", { camera: camera.name.replaceAll("_", " "), - ns: "views/system", })} - cameras.info.streamDataFromFFPROBE + {t("cameras.info.streamDataFromFFPROBE")}
@@ -92,7 +91,6 @@ export default function CameraInfoDialog({
{t("cameras.info.stream", { idx: idx + 1, - ns: "views/system", })}
{stream.return_code == 0 ? ( @@ -102,15 +100,11 @@ export default function CameraInfoDialog({ {codec.width ? (
- - cameras.info.video - + {t("cameras.info.video")}
- - cameras.info.codec - + {t("cameras.info.codec")} {" "} {codec.codec_long_name} @@ -119,9 +113,7 @@ export default function CameraInfoDialog({
{codec.width && codec.height ? ( <> - - cameras.info.resolution - {" "} + {t("cameras.info.resolution")}{" "} {" "} {codec.width}x{codec.height} ( @@ -135,9 +127,7 @@ export default function CameraInfoDialog({ ) : ( - - cameras.info.resolution - {" "} + {t("cameras.info.resolution")}{" "} Unknown @@ -145,14 +135,10 @@ export default function CameraInfoDialog({ )}
- - cameras.info.fps - {" "} + {t("cameras.info.fps")}{" "} {codec.avg_frame_rate == "0/0" - ? t("cameras.info.unknown", { - ns: "views/system", - }) + ? t("cameras.info.unknown") : codec.avg_frame_rate}
@@ -162,9 +148,7 @@ export default function CameraInfoDialog({
Audio:
- - cameras.info.codec - {" "} + {t("cameras.info.codec")}{" "} {codec.codec_long_name} @@ -179,7 +163,6 @@ export default function CameraInfoDialog({
{t("cameras.info.error", { error: stream.stderr, - ns: "views/system", })}
@@ -190,9 +173,7 @@ export default function CameraInfoDialog({ ) : (
-
- cameras.info.fetching -
+
{t("cameras.info.fetching")}
)}
@@ -203,7 +184,7 @@ export default function CameraInfoDialog({ aria-label="Copy" onClick={() => onCopyFfprobe()} > - button.copy + {t("button.copy")} diff --git a/web/src/components/overlay/CreateUserDialog.tsx b/web/src/components/overlay/CreateUserDialog.tsx index 5da5530f8..0a22378d8 100644 --- a/web/src/components/overlay/CreateUserDialog.tsx +++ b/web/src/components/overlay/CreateUserDialog.tsx @@ -22,8 +22,7 @@ import { DialogHeader, DialogTitle, } from "../ui/dialog"; -import { Trans } from "react-i18next"; -import { t } from "i18next"; + import { Select, SelectContent, @@ -33,6 +32,7 @@ import { } from "../ui/select"; import { Shield, User } from "lucide-react"; import { LuCheck, LuX } from "react-icons/lu"; +import { useTranslation } from "react-i18next"; type CreateUserOverlayProps = { show: boolean; @@ -45,31 +45,23 @@ export default function CreateUserDialog({ onCreate, onCancel, }: CreateUserOverlayProps) { + const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); const formSchema = z .object({ user: z .string() - .min( - 1, - t("users.dialog.form.usernameIsRequired", { - ns: "views/settings", - }), - ) + .min(1, t("users.dialog.form.usernameIsRequired")) .regex(/^[A-Za-z0-9._]+$/, { - message: t("users.dialog.createUser.usernameOnlyInclude", { - ns: "views/settings", - }), + message: t("users.dialog.createUser.usernameOnlyInclude"), }), password: z.string().min(1, "Password is required"), confirmPassword: z.string().min(1, "Please confirm your password"), role: z.enum(["admin", "viewer"]), }) .refine((data) => data.password === data.confirmPassword, { - message: t("users.dialog.form.password.notMatch", { - ns: "views/settings", - }), + message: t("users.dialog.form.password.notMatch"), path: ["confirmPassword"], }); @@ -120,11 +112,9 @@ export default function CreateUserDialog({ - - users.dialog.createUser.title - + {t("users.dialog.createUser.title")} - users.dialog.createUser.desc + {t("users.dialog.createUser.desc")} @@ -138,21 +128,17 @@ export default function CreateUserDialog({ render={({ field }) => ( - users.dialog.form.user + {t("users.dialog.form.user")} - - users.dialog.form.user.desc - + {t("users.dialog.form.user.desc")} @@ -164,15 +150,11 @@ export default function CreateUserDialog({ render={({ field }) => ( - - users.dialog.form.password - + {t("users.dialog.form.password")} ( - - users.dialog.form.password.confirm - + {t("users.dialog.form.password.confirm")} - - users.dialog.form.password.match - + {t("users.dialog.form.password.match")} ) : ( <> - - users.dialog.form.password.notMatch - + {t("users.dialog.form.password.notMatch")} )} @@ -236,7 +211,7 @@ export default function CreateUserDialog({ render={({ field }) => ( - role.title + {t("role.title")} - role.desc + {t("role.desc")} @@ -290,7 +261,7 @@ export default function CreateUserDialog({ onClick={handleCancel} type="button" > - button.cancel + {t("button.cancel")}
) : ( - button.save + t("button.save") )}
diff --git a/web/src/components/overlay/DeleteUserDialog.tsx b/web/src/components/overlay/DeleteUserDialog.tsx index 74ca3cad1..d24fcd5b1 100644 --- a/web/src/components/overlay/DeleteUserDialog.tsx +++ b/web/src/components/overlay/DeleteUserDialog.tsx @@ -1,4 +1,4 @@ -import { Trans } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { Button } from "../ui/button"; import { Dialog, @@ -20,23 +20,22 @@ export default function DeleteUserDialog({ onDelete, onCancel, }: DeleteUserDialogProps) { + const { t } = useTranslation(["views/settings"]); return (
- users.dialog.deleteUser.title + {t("users.dialog.deleteUser.title")} - users.dialog.deleteUser.desc + {t("users.dialog.deleteUser.desc")}

- - users.dialog.deleteUser.warn - + {t("users.dialog.deleteUser.warn", { username })}

@@ -49,7 +48,7 @@ export default function DeleteUserDialog({ onClick={onCancel} type="button" > - button.cancel + {t("button.cancel")}
diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index bddc6b620..ac4045326 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -30,8 +30,7 @@ import { getUTCOffset } from "@/utils/dateUtil"; import { baseUrl } from "@/api/baseUrl"; import { cn } from "@/lib/utils"; import { GenericVideoPlayer } from "../player/GenericVideoPlayer"; -import { Trans } from "react-i18next"; -import { t } from "i18next"; +import { useTranslation } from "react-i18next"; const EXPORT_OPTIONS = [ "1", @@ -66,30 +65,21 @@ export default function ExportDialog({ setMode, setShowPreview, }: ExportDialogProps) { + const { t } = useTranslation(["components/dialog"]); const [name, setName] = useState(""); const onStartExport = useCallback(() => { if (!range) { - toast.error( - t("export.toast.error.noVaildTimeSelected", { - ns: "components/dialog", - }), - { - position: "top-center", - }, - ); + toast.error(t("export.toast.error.noVaildTimeSelected"), { + position: "top-center", + }); return; } if (range.before < range.after) { - toast.error( - t("export.toast.error.endTimeMustAfterStartTime", { - ns: "components/dialog", - }), - { - position: "top-center", - }, - ); + toast.error(t("export.toast.error.endTimeMustAfterStartTime"), { + position: "top-center", + }); return; } @@ -103,12 +93,9 @@ export default function ExportDialog({ ) .then((response) => { if (response.status == 200) { - toast.success( - t("export.toast.success", { ns: "components/dialog" }), - { - position: "top-center", - }, - ); + toast.success(t("export.toast.success"), { + position: "top-center", + }); setName(""); setRange(undefined); setMode("none"); @@ -122,12 +109,11 @@ export default function ExportDialog({ toast.error( t("export.toast.error.failed", { error: errorMessage, - ns: "components/dialog", }), { position: "top-center" }, ); }); - }, [camera, name, range, setRange, setName, setMode]); + }, [camera, name, range, setRange, setName, setMode, t]); const handleCancel = useCallback(() => { setName(""); @@ -181,9 +167,7 @@ export default function ExportDialog({ > {isDesktop && ( -
- menu.export -
+
{t("menu.export")}
)} @@ -233,6 +217,7 @@ export function ExportContent({ setMode, onCancel, }: ExportContentProps) { + const { t } = useTranslation(["components/dialog"]); const [selectedOption, setSelectedOption] = useState("1"); const onSelectTime = useCallback( @@ -280,9 +265,7 @@ export function ExportContent({ {isDesktop && ( <> - - menu.export - + {t("menu.export")} @@ -306,11 +289,10 @@ export function ExportContent({ @@ -327,7 +309,7 @@ export function ExportContent({ setName(e.target.value)} /> @@ -339,7 +321,7 @@ export function ExportContent({ className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`} onClick={onCancel} > - button.cancel + {t("button.cancel")} @@ -376,6 +358,7 @@ function CustomTimeSelector({ range, setRange, }: CustomTimeSelectorProps) { + const { t } = useTranslation(["components/dialog"]); const { data: config } = useSWR("config"); // times @@ -596,6 +579,7 @@ export function ExportPreviewDialog({ showPreview, setShowPreview, }: ExportPreviewDialogProps) { + const { t } = useTranslation(["components/dialog"]); if (!range) { return null; } @@ -613,15 +597,9 @@ export function ExportPreviewDialog({ )} > - - - export.fromTimeline.previewExport - - + {t("export.fromTimeline.previewExport")} - - export.fromTimeline.previewExport - + {t("export.fromTimeline.previewExport")} diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 9a869a8eb..3e672f1be 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -19,7 +19,7 @@ import { toast } from "sonner"; import axios from "axios"; import SaveExportOverlay from "./SaveExportOverlay"; import { isIOS, isMobile } from "react-device-detect"; -import { Trans } from "react-i18next"; + import { t } from "i18next"; type DrawerMode = "none" | "select" | "export" | "calendar" | "filter"; @@ -247,7 +247,7 @@ export default function MobileReviewSettingsDrawer({ }); }} > - button.reset + {t("button.reset")} diff --git a/web/src/components/overlay/RoleChangeDialog.tsx b/web/src/components/overlay/RoleChangeDialog.tsx index 468a72f0a..265971e3d 100644 --- a/web/src/components/overlay/RoleChangeDialog.tsx +++ b/web/src/components/overlay/RoleChangeDialog.tsx @@ -1,4 +1,4 @@ -import { Trans } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { Button } from "../ui/button"; import { Dialog, @@ -33,6 +33,7 @@ export default function RoleChangeDialog({ onSave, onCancel, }: RoleChangeDialogProps) { + const { t } = useTranslation(["views/settings"]); const [selectedRole, setSelectedRole] = useState<"admin" | "viewer">( currentRole, ); @@ -42,18 +43,16 @@ export default function RoleChangeDialog({ - users.dialog.changeRole.title + {t("users.dialog.changeRole.title")} - - users.dialog.changeRole.desc - + {t("users.dialog.changeRole.desc", { username })}
- users.dialog.changeRole.roleInfo + {t("users.dialog.changeRole.roleInfo")}
@@ -153,9 +142,7 @@ export default function SetPasswordDialog({ />

- - users.dialog.form.password.strength - + {t("users.dialog.form.password.strength")} {getStrengthLabel()}

@@ -164,9 +151,7 @@ export default function SetPasswordDialog({
@@ -190,18 +174,14 @@ export default function SetPasswordDialog({ <> - - users.dialog.form.password.match - + {t("users.dialog.form.password.match")} ) : ( <> - - users.dialog.form.password.notMatch - + {t("users.dialog.form.password.notMatch")} )} @@ -225,7 +205,7 @@ export default function SetPasswordDialog({ onClick={onCancel} type="button" > - button.cancel + {t("button.cancel")}
diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 08948dfaf..051387525 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -73,8 +73,7 @@ import { LuInfo } from "react-icons/lu"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { FaPencilAlt } from "react-icons/fa"; import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; -import { Trans } from "react-i18next"; -import { t } from "i18next"; +import { useTranslation } from "react-i18next"; const SEARCH_TABS = [ "details", @@ -100,6 +99,7 @@ export default function SearchDetailDialog({ setSimilarity, setInputFocused, }: SearchDetailDialogProps) { + const { t } = useTranslation(["views/explore"]); const { data: config } = useSWR("config", { revalidateOnFocus: false, }); @@ -194,12 +194,8 @@ export default function SearchDetailDialog({ )} >
- - <Trans ns="views/explore">trackedObjectDetails</Trans> - - - details - + {t("trackedObjectDetails")} + {t("details")}
)} -
- type.{item} -
+
{t("type.{item}")}
))} @@ -289,6 +283,8 @@ function ObjectDetailsTab({ setSimilarity, setInputFocused, }: ObjectDetailsTabProps) { + const { t } = useTranslation(["views/explore"]); + const apiHost = useApiHost(); // mutation / revalidation @@ -374,12 +370,9 @@ function ObjectDetailsTab({ .post(`events/${search.id}/description`, { description: desc }) .then((resp) => { if (resp.status == 200) { - toast.success( - t("details.tips.descriptionSaved", { ns: "views/explore" }), - { - position: "top-center", - }, - ); + toast.success(t("details.tips.descriptionSaved"), { + position: "top-center", + }); } mutate( (key) => @@ -412,7 +405,6 @@ function ObjectDetailsTab({ "Unknown error"; toast.error( t("details.tips.saveDescriptionFailed", { - ns: "views/explore", errorMessage, }), { @@ -421,7 +413,7 @@ function ObjectDetailsTab({ ); setDesc(search.data.description); }); - }, [desc, search, mutate]); + }, [desc, search, mutate, t]); const regenerateDescription = useCallback( (source: "snapshot" | "thumbnails") => { @@ -533,12 +525,10 @@ function ObjectDetailsTab({
-
- details.label -
+
{t("details.label")}
{getIconForLabel(search.label, "size-4 text-primary")} - {search.label} + {t("{search.label}", { ns: "objects" })} {search.sub_label && ` (${search.sub_label})`} @@ -552,9 +542,7 @@ function ObjectDetailsTab({ - - details.editSubLable - + {t("details.editSubLable")}
@@ -562,7 +550,7 @@ function ObjectDetailsTab({
- details.topScore + {t("details.topScore")}
@@ -571,7 +559,7 @@ function ObjectDetailsTab({
- details.topScore.info + {t("details.topScore.info")}
@@ -583,7 +571,7 @@ function ObjectDetailsTab({ {averageEstimatedSpeed && (
- details.estimatedSpeed + {t("details.estimatedSpeed")}
{averageEstimatedSpeed && ( @@ -608,16 +596,14 @@ function ObjectDetailsTab({
)}
-
- details.camera -
+
{t("details.camera")}
{search.camera.replaceAll("_", " ")}
- details.timestamp + {t("details.timestamp")}
{formattedDate}
@@ -669,9 +655,7 @@ function ObjectDetailsTab({
-
- details.description.aiTips -
+
{t("details.description.aiTips")}
) : ( @@ -679,9 +663,7 @@ function ObjectDetailsTab({