refactor: change Trans tag for t function

This commit is contained in:
ZhaiSoul 2025-03-10 13:57:38 +08:00
parent 507ce019ff
commit ef646203a7
56 changed files with 877 additions and 1571 deletions

View File

@ -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 ? (
<div className="flex items-center gap-2 text-sm">
<FaCheck className="size-3 text-green-500" />
<Trans ns="views/system">stats.healthy</Trans>
{t("stats.healthy", { ns: "views/system" })}
</div>
) : (
Object.entries(messages).map(([key, messageArray]) => (

View File

@ -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);
}}
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</>

View File

@ -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<FrigateConfig>("config");
// tooltip
@ -163,7 +165,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="" side="right">
<Trans>menu.live.allCameras</Trans>
{t("menu.live.allCameras")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
@ -233,6 +235,7 @@ function NewGroupDialog({
setGroup,
deleteGroup,
}: NewGroupDialogProps) {
const { t } = useTranslation(["components/camera"]);
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
// editing group and state
@ -354,12 +357,8 @@ function NewGroupDialog({
className={cn(isDesktop && "mt-5", "justify-center")}
onClose={() => setOpen(false)}
>
<Title>
<Trans ns="components/camera">group.label</Trans>
</Title>
<Description className="sr-only">
<Trans ns="components/camera">group.edit</Trans>
</Description>
<Title>{t("group.label")}</Title>
<Description className="sr-only">{t("group.edit")}</Description>
<div
className={cn(
"absolute",
@ -406,11 +405,7 @@ function NewGroupDialog({
}}
>
<Title>
{editState == "add" ? (
<Trans ns="components/camera">group.add</Trans>
) : (
<Trans ns="components/camera">group.edit</Trans>
)}
{editState == "add" ? t("group.add") : t("group.edit")}
</Title>
<Description className="sr-only">
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({
>
<div className="scrollbar-container flex flex-col overflow-y-auto md:my-4">
<Header className="mt-2" onClose={() => setOpen(false)}>
<Title>
<Trans ns="components/camera">group.edit</Trans>
</Title>
<Title>{t("group.edit")}</Title>
<Description className="sr-only">
<Trans ns="components/camera">group.edit.desc</Trans>
{t("group.edit.desc")}
</Description>
</Header>
@ -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({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans ns="components/camera">group.delete.confirm</Trans>
</AlertDialogTitle>
<AlertDialogTitle>{t("group.delete.confirm")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Trans values={{ name: group[0] }}>
group.delete.confirm.desc
</Trans>
{t("group.delete.confirm.desc", { name: group[0] })}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>button.cancel</Trans>
</AlertDialogCancel>
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={onDeleteGroup}
>
<Trans>button.delete</Trans>
{t("button.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -573,13 +562,13 @@ export function CameraGroupRow({
aria-label="Edit group"
onClick={onEditGroup}
>
<Trans>button.edit</Trans>
{t("button.edit")}
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Delete group"
onClick={() => setDeleteDialogOpen(true)}
>
<Trans>button.delete</Trans>
{t("button.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
@ -596,9 +585,7 @@ export function CameraGroupRow({
onClick={onEditGroup}
/>
</TooltipTrigger>
<TooltipContent>
<Trans>button.edit</Trans>
</TooltipContent>
<TooltipContent>{t("button.edit")}</TooltipContent>
</Tooltip>
<Tooltip>
@ -609,9 +596,7 @@ export function CameraGroupRow({
onClick={() => setDeleteDialogOpen(true)}
/>
</TooltipTrigger>
<TooltipContent>
<Trans>button.delete</Trans>
</TooltipContent>
<TooltipContent>{t("button.delete")}</TooltipContent>
</Tooltip>
</div>
)}
@ -637,6 +622,7 @@ export function CameraGroupEdit({
onSave,
onCancel,
}: CameraGroupEditProps) {
const { t } = useTranslation(["components/camera"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("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 }) => (
<FormItem>
<FormLabel>
<Trans ns="components/camera">group.name.label</Trans>
</FormLabel>
<FormLabel>{t("group.name.label")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder={t("group.name.placeholder", {
ns: "components/camera",
})}
placeholder={t("group.name.placeholder")}
{...field}
/>
</FormControl>
@ -836,12 +812,8 @@ export function CameraGroupEdit({
name="cameras"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans ns="components/camera">group.cameras.label</Trans>
</FormLabel>
<FormDescription>
<Trans ns="components/camera">group.cameras.desc</Trans>
</FormDescription>
<FormLabel>{t("group.cameras.label")}</FormLabel>
<FormDescription>{t("group.cameras.desc")}</FormDescription>
<FormMessage />
{[
...(birdseyeConfig?.enabled ? ["birdseye"] : []),
@ -925,9 +897,7 @@ export function CameraGroupEdit({
name="icon"
render={({ field }) => (
<FormItem className="flex flex-col space-y-2">
<FormLabel>
<Trans ns="components/camera">group.icon</Trans>
</FormLabel>
<FormLabel>{t("group.icon")}</FormLabel>
<FormControl>
<IconPicker
selectedIcon={{
@ -955,7 +925,7 @@ export function CameraGroupEdit({
aria-label="Cancel"
onClick={onCancel}
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -967,12 +937,10 @@ export function CameraGroupEdit({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>

View File

@ -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<string[] | undefined>(
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 && (
<>
<DropdownMenuLabel className="flex justify-center">
<Trans ns="components/filter">cameras.all.short</Trans>
{t("cameras.all.short")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
@ -153,7 +154,7 @@ export function CamerasFilterContent({
<div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
<FilterSwitch
isChecked={currentCameras == undefined}
label={t("cameras.all", { ns: "components/filter" })}
label={t("cameras.all")}
onCheckedChange={(isChecked) => {
if (isChecked) {
setCurrentCameras(undefined);
@ -235,7 +236,7 @@ export function CamerasFilterContent({
setOpen(false);
}}
>
<Trans>button.apply</Trans>
{t("button.apply")}
</Button>
<Button
aria-label="Reset"
@ -244,7 +245,7 @@ export function CamerasFilterContent({
updateCameraFilter(undefined);
}}
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</>

View File

@ -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({
}
/>
<Label className="ml-2 cursor-pointer text-primary" htmlFor="reviewed">
<Trans ns="components/filter">review.showReviewed</Trans>
{t("review.showReviewed", { ns: "components/filter" })}
</Label>
</div>
@ -366,7 +366,7 @@ function GeneralFilterButton({
: "text-primary"
}`}
>
<Trans ns="components/filter">label</Trans>
{t("label", { ns: "components/filter" })}
</div>
</Button>
);
@ -474,7 +474,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
<Trans ns="components/filter">labels.all</Trans>
{t("labels.all", { ns: "components/filter" })}
</Label>
<Switch
className="ml-1"
@ -521,7 +521,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allZones"
>
<Trans ns="components/filter">zones.all</Trans>
{t("zones.all", { ns: "components/filter" })}
</Label>
<Switch
className="ml-1"
@ -577,10 +577,10 @@ export function GeneralFilterContent({
onClose();
}}
>
<Trans>button.apply</Trans>
{t("button.apply")}
</Button>
<Button aria-label="Reset" onClick={onReset}>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</>
@ -613,7 +613,7 @@ function ShowMotionOnlyButton({
className="mx-2 cursor-pointer text-primary"
htmlFor="collapse-motion"
>
<Trans ns="views/events">motion.only</Trans>
{t("motion.only", { ns: "views/events" })}
</Label>
</div>

View File

@ -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<FrigateConfig>("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<string[] | undefined>(
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 (
<>
<div className="overflow-x-hidden">
@ -340,7 +339,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
<Trans ns="components/filter">labels.all</Trans>
{t("labels.all")}
</Label>
<Switch
className="ml-1"
@ -392,7 +391,7 @@ export function GeneralFilterContent({
onClose();
}}
>
<Trans>button.apply</Trans>
{t("button.apply")}
</Button>
<Button
aria-label="Reset"
@ -401,7 +400,7 @@ export function GeneralFilterContent({
updateLabelFilter(undefined);
}}
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</>
@ -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({
<div
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`}
>
<Trans ns="components/filter">sort.label</Trans>
{t("sort.label")}
</div>
</Button>
);
@ -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();
}}
>
<Trans>button.apply</Trans>
{t("button.apply")}
</Button>
<Button
aria-label="Reset"
@ -575,7 +576,7 @@ export function SortTypeContent({
updateSortType(undefined);
}}
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</>

View File

@ -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 = (
<Button
size="sm"
@ -30,7 +31,7 @@ export function ZoneMaskFilterButton({
<div
className={`hidden md:block ${selectedZoneMask?.length ? "text-selected-foreground" : "text-primary"}`}
>
<Trans ns="components/filter">label</Trans>
{t("label")}
</div>
</Button>
);
@ -68,6 +69,7 @@ export function GeneralFilterContent({
selectedZoneMask,
updateZoneMaskFilter,
}: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
return (
<>
<div className="h-auto overflow-y-auto overflow-x-hidden">
@ -76,7 +78,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
<Trans ns="components/filter">labels.all</Trans>
{t("labels.all")}
</Label>
<Switch
className="ml-1"
@ -97,11 +99,14 @@ export function GeneralFilterContent({
className="mx-2 w-full cursor-pointer capitalize text-primary"
htmlFor={item}
>
<Trans ns="views/settings">
masksAndZones.
{item.replace(/_([a-z])/g, (letter) => letter.toUpperCase()) +
"s"}
</Trans>
{t(
"masksAndZones." +
item.replace(/_([a-z])/g, (letter) =>
letter.toUpperCase(),
) +
"s",
{ ns: "views/settings" },
)}
</Label>
<Switch
key={item}

View File

@ -16,9 +16,9 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { getUnitSize } from "@/utils/storageUtil";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { CiCircleAlert } from "react-icons/ci";
import { useTranslation } from "react-i18next";
type CameraStorage = {
[key: string]: {
@ -43,6 +43,8 @@ export function CombinedStorageGraph({
cameraStorage,
totalStorage,
}: CombinedStorageGraphProps) {
const { t } = useTranslation(["views/system"]);
const { theme, systemTheme } = useTheme();
const entities = Object.keys(cameraStorage);
@ -178,22 +180,12 @@ export function CombinedStorageGraph({
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("storage.cameraStorage.camera")}</TableHead>
<TableHead>{t("storage.cameraStorage.storageUsed")}</TableHead>
<TableHead>
<Trans ns="views/system">storage.cameraStorage.camera</Trans>
</TableHead>
<TableHead>
<Trans ns="views/system">
storage.cameraStorage.storageUsed
</Trans>
</TableHead>
<TableHead>
<Trans ns="views/system">
storage.cameraStorage.percentageOfTotalUsed
</Trans>
</TableHead>
<TableHead>
<Trans ns="views/system">storage.cameraStorage.bandwidth</Trans>
{t("storage.cameraStorage.percentageOfTotalUsed")}
</TableHead>
<TableHead>{t("storage.cameraStorage.bandwidth")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -206,9 +198,7 @@ export function CombinedStorageGraph({
style={{ backgroundColor: item.color }}
></div>
{item.name === "Unused"
? t("storage.cameraStorage.unused", {
ns: "views/system",
})
? t("storage.cameraStorage.unused")
: item.name.replaceAll("_", " ")}
{item.name === "Unused" && (
<Popover>
@ -225,9 +215,7 @@ export function CombinedStorageGraph({
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
<Trans ns="views/system">
storage.cameraStorage.unused.tips
</Trans>
{t("storage.cameraStorage.unused.tips")}
</div>
</PopoverContent>
</Popover>

View File

@ -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<HTMLDivElement>(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"
>
<Trans ns="components/icons">iconPicker.selectIcon</Trans>
{t("iconPicker.selectIcon")}
</Button>
) : (
<div className="hover:cursor-pointer">
@ -103,9 +104,7 @@ export default function IconPicker({
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
>
<div className="mb-3 flex flex-row items-center justify-between">
<Heading as="h4">
<Trans ns="components/icons">iconPicker.selectIcon</Trans>
</Heading>
<Heading as="h4">{t("iconPicker.selectIcon")}</Heading>
<span tabIndex={0} className="sr-only" />
<IoClose
size={15}

View File

@ -12,8 +12,7 @@ import { Input } from "@/components/ui/input";
import { useMemo, useState } from "react";
import { isMobile } from "react-device-detect";
import { toast } from "sonner";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type SaveSearchDialogProps = {
existingNames: string[];
@ -28,6 +27,8 @@ export function SaveSearchDialog({
onClose,
onSave,
}: SaveSearchDialogProps) {
const { t } = useTranslation(["components/dialog"]);
const [searchName, setSearchName] = useState("");
const handleSave = () => {
@ -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({
}}
>
<DialogHeader>
<DialogTitle>
<Trans ns="components/dialog">search.saveSearch.label</Trans>
</DialogTitle>
<DialogTitle>{t("search.saveSearch.label")}</DialogTitle>
<DialogDescription className="sr-only">
<Trans ns="components/dialog">search.saveSearch.desc</Trans>
{t("search.saveSearch.desc")}
</DialogDescription>
</DialogHeader>
<Input
value={searchName}
className="text-md"
onChange={(e) => setSearchName(e.target.value)}
placeholder={t("search.saveSearch.placeholder", {
ns: "components/dialog",
})}
placeholder={t("search.saveSearch.placeholder")}
/>
{overwrite && (
<div className="ml-1 text-sm text-danger">
<Trans ns="components/dialog" values={{ searchName }}>
search.saveSearch.overwrite
</Trans>
{t("search.saveSearch.overwrite", { searchName })}
</div>
)}
<DialogFooter>
<Button aria-label="Cancel" onClick={onClose}>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
onClick={handleSave}
@ -94,7 +88,7 @@ export function SaveSearchDialog({
className="mb-2 md:mb-0"
aria-label="Save this search"
>
<Trans>button.save</Trans>
{t("button.save")}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -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) {
>
<a className="flex" href={logoutUrl}>
<LuLogOut className="mr-2 size-4" />
<span>
<Trans>menu.user.logout</Trans>
</span>
<span>{t("menu.user.logout")}</span>
</a>
</MenuItem>
</div>

View File

@ -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) {
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="right">
<p>
<Trans>menu.settings</Trans>
</p>
<p>{t("menu.settings")}</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>
@ -196,9 +194,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
)}
{isAdmin && (
<>
<DropdownMenuLabel>
<Trans>menu.system</Trans>
</DropdownMenuLabel>
<DropdownMenuLabel>{t("menu.system")}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
<Link to="/system#general">
@ -211,9 +207,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="System metrics"
>
<LuActivity className="mr-2 size-4" />
<span>
<Trans>menu.systemMetrics</Trans>
</span>
<span>{t("menu.systemMetrics")}</span>
</MenuItem>
</Link>
<Link to="/logs">
@ -226,9 +220,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="System logs"
>
<LuList className="mr-2 size-4" />
<span>
<Trans>menu.systemLogs</Trans>
</span>
<span>{t("menu.systemLogs")}</span>
</MenuItem>
</Link>
</DropdownMenuGroup>
@ -237,7 +229,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<DropdownMenuLabel
className={isDesktop && isAdmin ? "mt-3" : "mt-1"}
>
<Trans>menu.configuration</Trans>
{t("menu.configuration")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
@ -251,9 +243,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="Settings"
>
<LuSettings className="mr-2 size-4" />
<span>
<Trans>menu.settings</Trans>
</span>
<span>{t("menu.settings")}</span>
</MenuItem>
</Link>
{isAdmin && (
@ -268,16 +258,14 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="Configuration editor"
>
<LuSquarePen className="mr-2 size-4" />
<span>
<Trans>menu.configurationEditor</Trans>
</span>
<span>{t("menu.configurationEditor")}</span>
</MenuItem>
</Link>
</>
)}
</DropdownMenuGroup>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
<Trans>menu.appearance</Trans>
{t("menu.appearance")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<SubItem>
@ -287,9 +275,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
}
>
<LuLanguages className="mr-2 size-4" />
<span>
<Trans>menu.languages</Trans>
</span>
<span>{t("menu.languages")}</span>
</SubItemTrigger>
<Portal>
<SubItemContent
@ -310,12 +296,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{language.trim() === "en" ? (
<>
<LuLanguages className="mr-2 size-4" />
<Trans>menu.language.en</Trans>
{t("menu.language.en")}
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.language.en</Trans>
</span>
<span className="ml-6 mr-2">{t("menu.language.en")}</span>
)}
</MenuItem>
<MenuItem
@ -330,11 +314,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{language === "zh-CN" ? (
<>
<LuLanguages className="mr-2 size-4" />
<Trans>menu.language.zhCN</Trans>
{t("menu.language.zhCN")}
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.language.zhCN</Trans>
{t("menu.language.zhCN")}
</span>
)}
</MenuItem>
@ -350,12 +334,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{language === systemLanguage ? (
<>
<LuEarth className="mr-2 size-4 scale-100 transition-all" />
<Trans>menu.withSystem</Trans>
{t("menu.withSystem")}
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.withSystem</Trans>
</span>
<span className="ml-6 mr-2">{t("menu.withSystem")}</span>
)}
</MenuItem>
</SubItemContent>
@ -368,9 +350,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
}
>
<LuSunMoon className="mr-2 size-4" />
<span>
<Trans>menu.darkMode.label</Trans>
</span>
<span>{t("menu.darkMode.label")}</span>
</SubItemTrigger>
<Portal>
<SubItemContent
@ -391,11 +371,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{theme === "light" ? (
<>
<LuSun className="mr-2 size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Trans>menu.darkMode.light</Trans>
{t("menu.darkMode.light")}
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.darkMode.light</Trans>
{t("menu.darkMode.light")}
</span>
)}
</MenuItem>
@ -411,11 +391,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{theme === "dark" ? (
<>
<LuMoon className="mr-2 size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<Trans>menu.darkMode.dark</Trans>
{t("menu.darkMode.dark")}
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.darkMode.dark</Trans>
{t("menu.darkMode.dark")}
</span>
)}
</MenuItem>
@ -431,12 +411,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{theme === "system" ? (
<>
<CgDarkMode className="mr-2 size-4 scale-100 transition-all" />
<Trans>menu.withSystem</Trans>
{t("menu.withSystem")}
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.withSystem</Trans>
</span>
<span className="ml-6 mr-2">{t("menu.withSystem")}</span>
)}
</MenuItem>
</SubItemContent>
@ -449,9 +427,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
}
>
<LuSunMoon className="mr-2 size-4" />
<span>
<Trans>menu.theme.label</Trans>
</span>
<span>{t("menu.theme.label")}</span>
</SubItemTrigger>
<Portal>
<SubItemContent
@ -474,11 +450,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{scheme === colorScheme ? (
<>
<IoColorPalette className="mr-2 size-4 rotate-0 scale-100 transition-all" />
<Trans>{friendlyColorSchemeName(scheme)}</Trans>
{t(friendlyColorSchemeName(scheme))}
</>
) : (
<span className="ml-6 mr-2">
<Trans>{friendlyColorSchemeName(scheme)}</Trans>
{t(friendlyColorSchemeName(scheme))}
</span>
)}
</MenuItem>
@ -487,7 +463,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
</Portal>
</SubItem>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
<Trans>menu.help</Trans>
{t("menu.help")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<a href="https://docs.frigate.video" target="_blank">
@ -498,9 +474,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label={t("menu.documentation.label")}
>
<LuLifeBuoy className="mr-2 size-4" />
<span>
<Trans>menu.documentation</Trans>
</span>
<span>{t("menu.documentation")}</span>
</MenuItem>
</a>
<a
@ -532,9 +506,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
onClick={() => setRestartDialogOpen(true)}
>
<LuRotateCw className="mr-2 size-4" />
<span>
<Trans>menu.restart</Trans>
</span>
<span>{t("menu.restart")}</span>
</MenuItem>
</>
)}

View File

@ -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}
>
<div className="text-primary">
<Trans>button</Trans>
</div>
<div className="text-primary">{t("button")}</div>
</div>
</ContextMenuItem>
</>

View File

@ -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<FrigateConfig>("config");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -92,61 +94,45 @@ export default function SearchResultActions({
const menuItems = (
<>
{searchResult.has_clip && (
<MenuItem
aria-label={t("itemMenu.downloadVideo.aria", { ns: "views/explore" })}
>
<MenuItem aria-label={t("itemMenu.downloadVideo.aria")}>
<a
className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
download={`${searchResult.camera}_${searchResult.label}.mp4`}
>
<LuDownload className="mr-2 size-4" />
<span>
<Trans ns="views/explore">itemMenu.downloadVideo</Trans>
</span>
<span>{t("itemMenu.downloadVideo")}</span>
</a>
</MenuItem>
)}
{searchResult.has_snapshot && (
<MenuItem
aria-label={t("itemMenu.downloadSnapshot.aria", {
ns: "views/explore",
})}
>
<MenuItem aria-label={t("itemMenu.downloadSnapshot.aria")}>
<a
className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
download={`${searchResult.camera}_${searchResult.label}.jpg`}
>
<LuCamera className="mr-2 size-4" />
<span>
<Trans ns="views/explore">itemMenu.downloadSnapshot.label</Trans>
</span>
<span>{t("itemMenu.downloadSnapshot.label")}</span>
</a>
</MenuItem>
)}
{searchResult.data.type == "object" && (
<MenuItem
aria-label={t("itemMenu.viewObjectLifecycle.aria", {
ns: "views/explore",
})}
aria-label={t("itemMenu.viewObjectLifecycle.aria")}
onClick={showObjectLifecycle}
>
<FaArrowsRotate className="mr-2 size-4" />
<span>
<Trans ns="views/explore">itemMenu.viewObjectLifecycle.label</Trans>
</span>
<span>{t("itemMenu.viewObjectLifecycle.label")}</span>
</MenuItem>
)}
{config?.semantic_search?.enabled && isContextMenu && (
<MenuItem
aria-label={t("itemMenu.findSimilar.aria", { ns: "views/explore" })}
aria-label={t("itemMenu.findSimilar.aria")}
onClick={findSimilar}
>
<MdImageSearch className="mr-2 size-4" />
<span>
<Trans ns="views/explore">itemMenu.findSimilar.label</Trans>
</span>
<span>{t("itemMenu.findSimilar.label")}</span>
</MenuItem>
)}
{isMobileOnly &&
@ -156,15 +142,11 @@ export default function SearchResultActions({
searchResult.data.type == "object" &&
!searchResult.plus_id && (
<MenuItem
aria-label={t("itemMenu.submitToPlus.aria", {
ns: "views/explore",
})}
aria-label={t("itemMenu.submitToPlus.aria")}
onClick={showSnapshot}
>
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
<span>
<Trans ns="views/explore">itemMenu.submitToPlus</Trans>
</span>
<span>{t("itemMenu.submitToPlus")}</span>
</MenuItem>
)}
<MenuItem
@ -172,9 +154,7 @@ export default function SearchResultActions({
onClick={() => setDeleteDialogOpen(true)}
>
<LuTrash2 className="mr-2 size-4" />
<span>
<Trans>button.delete</Trans>
</span>
<span>{t("button.delete")}</span>
</MenuItem>
</>
);
@ -187,22 +167,18 @@ export default function SearchResultActions({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans ns="views/explore">dialog.confirmDelete</Trans>
</AlertDialogTitle>
<AlertDialogTitle>{t("dialog.confirmDelete")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Trans ns="views/explore">dialog.confirmDelete.desc</Trans>
{t("dialog.confirmDelete.desc")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>button.cancel</Trans>
</AlertDialogCancel>
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={handleDelete}
>
<Trans>button.delete</Trans>
{t("button.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -224,7 +200,7 @@ export default function SearchResultActions({
/>
</TooltipTrigger>
<TooltipContent>
<Trans ns="views/explore">itemMenu.findSimilar.label</Trans>
{t("itemMenu.findSimilar.label")}
</TooltipContent>
</Tooltip>
)}
@ -243,7 +219,7 @@ export default function SearchResultActions({
/>
</TooltipTrigger>
<TooltipContent>
<Trans ns="views/explore">itemMenu.submitToPlus.label</Trans>
{t("itemMenu.submitToPlus.label")}
</TooltipContent>
</Tooltip>
)}

View File

@ -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({
<TooltipTrigger>{content}</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="right">
<p>
<Trans>{item.title}</Trans>
</p>
<p>{t("{item.title}")}</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>

View File

@ -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<Ffprobe[]>();
useEffect(() => {
@ -76,12 +76,11 @@ export default function CameraInfoDialog({
<DialogTitle className="capitalize">
{t("cameras.info.cameraProbeInfo", {
camera: camera.name.replaceAll("_", " "),
ns: "views/system",
})}
</DialogTitle>
</DialogHeader>
<DialogDescription>
<Trans ns="views/system">cameras.info.streamDataFromFFPROBE</Trans>
{t("cameras.info.streamDataFromFFPROBE")}
</DialogDescription>
<div className="mb-2 p-4">
@ -92,7 +91,6 @@ export default function CameraInfoDialog({
<div className="mb-1 rounded-md bg-secondary p-2 text-lg text-primary">
{t("cameras.info.stream", {
idx: idx + 1,
ns: "views/system",
})}
</div>
{stream.return_code == 0 ? (
@ -102,15 +100,11 @@ export default function CameraInfoDialog({
{codec.width ? (
<div className="text-muted-foreground">
<div className="ml-2">
<Trans ns="views/system">
cameras.info.video
</Trans>
{t("cameras.info.video")}
</div>
<div className="ml-5">
<div>
<Trans ns="views/system">
cameras.info.codec
</Trans>
{t("cameras.info.codec")}
<span className="text-primary">
{" "}
{codec.codec_long_name}
@ -119,9 +113,7 @@ export default function CameraInfoDialog({
<div>
{codec.width && codec.height ? (
<>
<Trans ns="views/system">
cameras.info.resolution
</Trans>{" "}
{t("cameras.info.resolution")}{" "}
<span className="text-primary">
{" "}
{codec.width}x{codec.height} (
@ -135,9 +127,7 @@ export default function CameraInfoDialog({
</>
) : (
<span>
<Trans ns="views/system">
cameras.info.resolution
</Trans>{" "}
{t("cameras.info.resolution")}{" "}
<span className="text-primary">
Unknown
</span>
@ -145,14 +135,10 @@ export default function CameraInfoDialog({
)}
</div>
<div>
<Trans ns="views/system">
cameras.info.fps
</Trans>{" "}
{t("cameras.info.fps")}{" "}
<span className="text-primary">
{codec.avg_frame_rate == "0/0"
? t("cameras.info.unknown", {
ns: "views/system",
})
? t("cameras.info.unknown")
: codec.avg_frame_rate}
</span>
</div>
@ -162,9 +148,7 @@ export default function CameraInfoDialog({
<div className="text-muted-foreground">
<div className="ml-2 mt-1">Audio:</div>
<div className="ml-4">
<Trans ns="views/system">
cameras.info.codec
</Trans>{" "}
{t("cameras.info.codec")}{" "}
<span className="text-primary">
{codec.codec_long_name}
</span>
@ -179,7 +163,6 @@ export default function CameraInfoDialog({
<div>
{t("cameras.info.error", {
error: stream.stderr,
ns: "views/system",
})}
</div>
</div>
@ -190,9 +173,7 @@ export default function CameraInfoDialog({
) : (
<div className="flex flex-col items-center">
<ActivityIndicator />
<div className="mt-2">
<Trans ns="views/system">cameras.info.fetching</Trans>
</div>
<div className="mt-2">{t("cameras.info.fetching")}</div>
</div>
)}
</div>
@ -203,7 +184,7 @@ export default function CameraInfoDialog({
aria-label="Copy"
onClick={() => onCopyFfprobe()}
>
<Trans>button.copy</Trans>
{t("button.copy")}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -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<boolean>(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({
<Dialog open={show} onOpenChange={onCancel}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
<Trans ns="views/settings">users.dialog.createUser.title</Trans>
</DialogTitle>
<DialogTitle>{t("users.dialog.createUser.title")}</DialogTitle>
<DialogDescription>
<Trans ns="views/settings">users.dialog.createUser.desc</Trans>
{t("users.dialog.createUser.desc")}
</DialogDescription>
</DialogHeader>
@ -138,21 +128,17 @@ export default function CreateUserDialog({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
<Trans ns="views/settings">users.dialog.form.user</Trans>
{t("users.dialog.form.user")}
</FormLabel>
<FormControl>
<Input
placeholder={t("users.dialog.form.user.placeholder", {
ns: "views/settings",
})}
placeholder={t("users.dialog.form.user.placeholder")}
className="h-10"
{...field}
/>
</FormControl>
<FormDescription className="text-xs text-muted-foreground">
<Trans ns="views/settings">
users.dialog.form.user.desc
</Trans>
{t("users.dialog.form.user.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -164,15 +150,11 @@ export default function CreateUserDialog({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
<Trans ns="views/settings">
users.dialog.form.password
</Trans>
{t("users.dialog.form.password")}
</FormLabel>
<FormControl>
<Input
placeholder={t("users.dialog.form.password.placeholder", {
ns: "views/settings",
})}
placeholder={t("users.dialog.form.password.placeholder")}
type="password"
className="h-10"
{...field}
@ -188,15 +170,12 @@ export default function CreateUserDialog({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
<Trans ns="views/settings">
users.dialog.form.password.confirm
</Trans>
{t("users.dialog.form.password.confirm")}
</FormLabel>
<FormControl>
<Input
placeholder={t(
"users.dialog.form.password.confirm.placeholder",
{ ns: "views/settings" },
)}
type="password"
className="h-10"
@ -209,18 +188,14 @@ export default function CreateUserDialog({
<>
<LuCheck className="size-3.5 text-green-500" />
<span className="text-green-600">
<Trans ns="views/settings">
users.dialog.form.password.match
</Trans>
{t("users.dialog.form.password.match")}
</span>
</>
) : (
<>
<LuX className="size-3.5 text-red-500" />
<span className="text-red-600">
<Trans ns="views/settings">
users.dialog.form.password.notMatch
</Trans>
{t("users.dialog.form.password.notMatch")}
</span>
</>
)}
@ -236,7 +211,7 @@ export default function CreateUserDialog({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
<Trans>role.title</Trans>
{t("role.title")}
</FormLabel>
<Select
onValueChange={field.onChange}
@ -254,9 +229,7 @@ export default function CreateUserDialog({
>
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-primary" />
<span>
<Trans>role.admin</Trans>
</span>
<span>{t("role.admin")}</span>
</div>
</SelectItem>
<SelectItem
@ -265,15 +238,13 @@ export default function CreateUserDialog({
>
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>
<Trans>role.viewer</Trans>
</span>
<span>{t("role.viewer")}</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<FormDescription className="text-xs text-muted-foreground">
<Trans>role.desc</Trans>
{t("role.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -290,7 +261,7 @@ export default function CreateUserDialog({
onClick={handleCancel}
type="button"
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -302,12 +273,10 @@ export default function CreateUserDialog({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>

View File

@ -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 (
<Dialog open={show} onOpenChange={onCancel}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader className="flex flex-col items-center gap-2 sm:items-start">
<div className="space-y-1 text-center sm:text-left">
<Trans ns="views/settings">users.dialog.deleteUser.title</Trans>
{t("users.dialog.deleteUser.title")}
<DialogDescription>
<Trans ns="views/settings">users.dialog.deleteUser.desc</Trans>
{t("users.dialog.deleteUser.desc")}
</DialogDescription>
</div>
</DialogHeader>
<div className="my-4 rounded-md border border-destructive/20 bg-destructive/5 p-4 text-center text-sm">
<p className="font-medium text-destructive">
<Trans ns="views/settings" values={{ username }}>
users.dialog.deleteUser.warn
</Trans>
{t("users.dialog.deleteUser.warn", { username })}
</p>
</div>
@ -49,7 +48,7 @@ export default function DeleteUserDialog({
onClick={onCancel}
type="button"
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="destructive"
@ -57,7 +56,7 @@ export default function DeleteUserDialog({
className="flex flex-1"
onClick={onDelete}
>
<Trans>button.delete</Trans>
{t("button.delete")}
</Button>
</div>
</div>

View File

@ -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({
>
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
{isDesktop && (
<div className="text-primary">
<Trans>menu.export</Trans>
</div>
<div className="text-primary">{t("menu.export")}</div>
)}
</Button>
</Trigger>
@ -233,6 +217,7 @@ export function ExportContent({
setMode,
onCancel,
}: ExportContentProps) {
const { t } = useTranslation(["components/dialog"]);
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
const onSelectTime = useCallback(
@ -280,9 +265,7 @@ export function ExportContent({
{isDesktop && (
<>
<DialogHeader>
<DialogTitle>
<Trans>menu.export</Trans>
</DialogTitle>
<DialogTitle>{t("menu.export")}</DialogTitle>
</DialogHeader>
<SelectSeparator className="my-4 bg-secondary" />
</>
@ -306,11 +289,10 @@ export function ExportContent({
<Label className="cursor-pointer capitalize" htmlFor={opt}>
{isNaN(parseInt(opt))
? opt == "timeline"
? t("export.time.fromTimeline", { ns: "components/dialog" })
: t("export.time." + opt, { ns: "components/dialog" })
? t("export.time.fromTimeline")
: t("export.time." + opt)
: t("export.time.lastHour", {
count: parseInt(opt),
ns: "components/dialog",
})}
</Label>
</div>
@ -327,7 +309,7 @@ export function ExportContent({
<Input
className="text-md my-6"
type="search"
placeholder={t("export.name.placeholder", { ns: "components/dialog" })}
placeholder={t("export.name.placeholder")}
value={name}
onChange={(e) => setName(e.target.value)}
/>
@ -339,7 +321,7 @@ export function ExportContent({
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
onClick={onCancel}
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</div>
<Button
className={isDesktop ? "" : "w-full"}
@ -358,8 +340,8 @@ export function ExportContent({
}}
>
{selectedOption == "timeline"
? t("export.select", { ns: "components/dialog" })
: t("export.export", { ns: "components/dialog" })}
? t("export.select")
: t("export.export")}
</Button>
</DialogFooter>
</div>
@ -376,6 +358,7 @@ function CustomTimeSelector({
range,
setRange,
}: CustomTimeSelectorProps) {
const { t } = useTranslation(["components/dialog"]);
const { data: config } = useSWR<FrigateConfig>("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({
)}
>
<DialogHeader>
<DialogTitle>
<Trans ns="components/dialog">
export.fromTimeline.previewExport
</Trans>
</DialogTitle>
<DialogTitle>{t("export.fromTimeline.previewExport")}</DialogTitle>
<DialogDescription className="sr-only">
<Trans ns="components/dialog">
export.fromTimeline.previewExport
</Trans>
{t("export.fromTimeline.previewExport")}
</DialogDescription>
</DialogHeader>
<GenericVideoPlayer source={source} />

View File

@ -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({
});
}}
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</div>

View File

@ -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({
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
<Trans ns="views/settings">users.dialog.changeRole.title</Trans>
{t("users.dialog.changeRole.title")}
</DialogTitle>
<DialogDescription>
<Trans ns="views/settings" values={{ username }}>
users.dialog.changeRole.desc
</Trans>
{t("users.dialog.changeRole.desc", { username })}
</DialogDescription>
</DialogHeader>
<div className="py-6">
<div className="mb-4 text-sm text-muted-foreground">
<Trans ns="views/settings">users.dialog.changeRole.roleInfo</Trans>
{t("users.dialog.changeRole.roleInfo")}
</div>
<Select
@ -69,17 +68,13 @@ export default function RoleChangeDialog({
<SelectItem value="admin" className="flex items-center gap-2">
<div className="flex items-center gap-2">
<LuShield className="size-4 text-primary" />
<span>
<Trans>role.admin</Trans>
</span>
<span>{t("role.admin")}</span>
</div>
</SelectItem>
<SelectItem value="viewer" className="flex items-center gap-2">
<div className="flex items-center gap-2">
<LuUser className="size-4 text-primary" />
<span>
<Trans>role.viewer</Trans>
</span>
<span>{t("role.viewer")}</span>
</div>
</SelectItem>
</SelectContent>
@ -95,7 +90,7 @@ export default function RoleChangeDialog({
onClick={onCancel}
type="button"
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -104,7 +99,7 @@ export default function RoleChangeDialog({
onClick={() => onSave(selectedRole)}
disabled={selectedRole === currentRole}
>
<Trans>button.save</Trans>
{t("button.save")}
</Button>
</div>
</div>

View File

@ -2,7 +2,7 @@ import { LuVideo, LuX } from "react-icons/lu";
import { Button } from "../ui/button";
import { FaCompactDisc } from "react-icons/fa";
import { cn } from "@/lib/utils";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type SaveExportOverlayProps = {
className: string;
@ -18,6 +18,7 @@ export default function SaveExportOverlay({
onSave,
onCancel,
}: SaveExportOverlayProps) {
const { t } = useTranslation("components/dialog");
return (
<div className={className}>
<div
@ -34,7 +35,7 @@ export default function SaveExportOverlay({
onClick={onCancel}
>
<LuX />
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
className="flex items-center gap-1"
@ -43,9 +44,7 @@ export default function SaveExportOverlay({
onClick={onPreview}
>
<LuVideo />
<Trans ns="components/dialog">
export.fromTimeline.previewExport
</Trans>
{t("export.fromTimeline.previewExport")}
</Button>
<Button
className="flex items-center gap-1"
@ -55,7 +54,7 @@ export default function SaveExportOverlay({
onClick={onSave}
>
<FaCompactDisc />
<Trans ns="components/dialog">export.fromTimeline.saveExport</Trans>
{t("export.fromTimeline.saveExport", { ns: "components/dialog" })}
</Button>
</div>
</div>

View File

@ -11,10 +11,10 @@ import {
DialogHeader,
DialogTitle,
} from "../ui/dialog";
import { Trans } from "react-i18next";
import { Label } from "../ui/label";
import { LuCheck, LuX } from "react-icons/lu";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type SetPasswordProps = {
show: boolean;
@ -29,6 +29,7 @@ export default function SetPasswordDialog({
onCancel,
username,
}: SetPasswordProps) {
const { t } = useTranslation(["views/settings"]);
const [password, setPassword] = useState<string>("");
const [confirmPassword, setConfirmPassword] = useState<string>("");
const [passwordStrength, setPasswordStrength] = useState<number>(0);
@ -80,20 +81,12 @@ export default function SetPasswordDialog({
const getStrengthLabel = () => {
if (!password) return "";
if (passwordStrength <= 1)
return t("users.dialog.form.password.strength.weak", {
ns: "views/settings",
});
return t("users.dialog.form.password.strength.weak");
if (passwordStrength === 2)
return t("users.dialog.form.password.strength.medium", {
ns: "views/settings",
});
return t("users.dialog.form.password.strength.medium");
if (passwordStrength === 3)
return t("users.dialog.form.password.strength.strong", {
ns: "views/settings",
});
return t("users.dialog.form.password.strength.veryStrong", {
ns: "views/settings",
});
return t("users.dialog.form.password.strength.strong");
return t("users.dialog.form.password.strength.veryStrong");
};
const getStrengthColor = () => {
@ -114,19 +107,17 @@ export default function SetPasswordDialog({
username,
ns: "views/settings",
})
: t("users.dialog.passwordSetting.setPassword", {
ns: "views/settings",
})}
: t("users.dialog.passwordSetting.setPassword")}
</DialogTitle>
<DialogDescription>
<Trans ns="views/settings">users.dialog.passwordSetting.desc</Trans>
{t("users.dialog.passwordSetting.desc")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="password">
<Trans ns="views/settings">users.dialog.form.newPassword</Trans>
{t("users.dialog.form.newPassword")}
</Label>
<Input
id="password"
@ -137,9 +128,7 @@ export default function SetPasswordDialog({
setPassword(event.target.value);
setError(null);
}}
placeholder={t("users.dialog.form.newPassword.placeholder", {
ns: "views/settings",
})}
placeholder={t("users.dialog.form.newPassword.placeholder")}
autoFocus
/>
@ -153,9 +142,7 @@ export default function SetPasswordDialog({
/>
</div>
<p className="text-xs text-muted-foreground">
<Trans ns="views/settings">
users.dialog.form.password.strength
</Trans>
{t("users.dialog.form.password.strength")}
<span className="font-medium">{getStrengthLabel()}</span>
</p>
</div>
@ -164,9 +151,7 @@ export default function SetPasswordDialog({
<div className="space-y-2">
<Label htmlFor="confirm-password">
<Trans ns="views/settings">
users.dialog.form.password.confirm
</Trans>
{t("users.dialog.form.password.confirm")}
</Label>
<Input
id="confirm-password"
@ -179,7 +164,6 @@ export default function SetPasswordDialog({
}}
placeholder={t(
"users.dialog.form.newPassword.confirm.placeholder",
{ ns: "views/settings" },
)}
/>
@ -190,18 +174,14 @@ export default function SetPasswordDialog({
<>
<LuCheck className="size-3.5 text-green-500" />
<span className="text-green-600">
<Trans ns="views/settings">
users.dialog.form.password.match
</Trans>
{t("users.dialog.form.password.match")}
</span>
</>
) : (
<>
<LuX className="size-3.5 text-red-500" />
<span className="text-red-600">
<Trans ns="views/settings">
users.dialog.form.password.notMatch
</Trans>
{t("users.dialog.form.password.notMatch")}
</span>
</>
)}
@ -225,7 +205,7 @@ export default function SetPasswordDialog({
onClick={onCancel}
type="button"
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -234,7 +214,7 @@ export default function SetPasswordDialog({
onClick={handleSave}
disabled={!password || password !== confirmPassword}
>
<Trans>button.save</Trans>
{t("button.save")}
</Button>
</div>
</div>

View File

@ -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<FrigateConfig>("config", {
revalidateOnFocus: false,
});
@ -194,12 +194,8 @@ export default function SearchDetailDialog({
)}
>
<Header>
<Title>
<Trans ns="views/explore">trackedObjectDetails</Trans>
</Title>
<Description className="sr-only">
<Trans ns="views/explore">details</Trans>
</Description>
<Title>{t("trackedObjectDetails")}</Title>
<Description className="sr-only">{t("details")}</Description>
</Header>
<ScrollArea
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
@ -230,9 +226,7 @@ export default function SearchDetailDialog({
{item == "object_lifecycle" && (
<FaRotate className="size-4" />
)}
<div className="capitalize">
<Trans ns="views/explore">type.{item}</Trans>
</div>
<div className="capitalize">{t("type.{item}")}</div>
</ToggleGroupItem>
))}
</ToggleGroup>
@ -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({
<div className="flex w-full flex-row">
<div className="flex w-full flex-col gap-3">
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
<Trans ns="views/explore">details.label</Trans>
</div>
<div className="text-sm text-primary/40">{t("details.label")}</div>
<div className="flex flex-row items-center gap-2 text-sm capitalize">
{getIconForLabel(search.label, "size-4 text-primary")}
<Trans ns="objects">{search.label}</Trans>
{t("{search.label}", { ns: "objects" })}
{search.sub_label && ` (${search.sub_label})`}
<Tooltip>
<TooltipTrigger asChild>
@ -552,9 +542,7 @@ function ObjectDetailsTab({
</span>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
<Trans ns="views/explore">details.editSubLable</Trans>
</TooltipContent>
<TooltipContent>{t("details.editSubLable")}</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
@ -562,7 +550,7 @@ function ObjectDetailsTab({
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
<div className="flex flex-row items-center gap-1">
<Trans ns="views/explore">details.topScore</Trans>
{t("details.topScore")}
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
@ -571,7 +559,7 @@ function ObjectDetailsTab({
</div>
</PopoverTrigger>
<PopoverContent className="w-80">
<Trans ns="views/explore">details.topScore.info</Trans>
{t("details.topScore.info")}
</PopoverContent>
</Popover>
</div>
@ -583,7 +571,7 @@ function ObjectDetailsTab({
{averageEstimatedSpeed && (
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
<Trans ns="views/explore">details.estimatedSpeed</Trans>
{t("details.estimatedSpeed")}
</div>
<div className="flex flex-col space-y-0.5 text-sm">
{averageEstimatedSpeed && (
@ -608,16 +596,14 @@ function ObjectDetailsTab({
</div>
)}
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
<Trans ns="views/explore">details.camera</Trans>
</div>
<div className="text-sm text-primary/40">{t("details.camera")}</div>
<div className="text-sm capitalize">
{search.camera.replaceAll("_", " ")}
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
<Trans ns="views/explore">details.timestamp</Trans>
{t("details.timestamp")}
</div>
<div className="text-sm">{formattedDate}</div>
</div>
@ -669,9 +655,7 @@ function ObjectDetailsTab({
<div className="flex">
<ActivityIndicator />
</div>
<div className="flex">
<Trans ns="views/explore">details.description.aiTips</Trans>
</div>
<div className="flex">{t("details.description.aiTips")}</div>
</div>
</>
) : (
@ -679,9 +663,7 @@ function ObjectDetailsTab({
<div className="text-sm text-primary/40"></div>
<Textarea
className="h-64"
placeholder={t("details.description.placeholder", {
ns: "views/explore",
})}
placeholder={t("details.description.placeholder")}
value={desc}
onChange={(e) => setDesc(e.target.value)}
onFocus={handleDescriptionFocus}
@ -698,7 +680,7 @@ function ObjectDetailsTab({
aria-label="Regenerate tracked object description"
onClick={() => regenerateDescription("thumbnails")}
>
<Trans ns="views/explore">details.button.regenerate</Trans>
{t("details.button.regenerate")}
</Button>
{search.has_snapshot && (
<DropdownMenu>
@ -716,18 +698,14 @@ function ObjectDetailsTab({
aria-label="Regenerate from snapshot"
onClick={() => regenerateDescription("snapshot")}
>
<Trans ns="views/explore">
details.regenerateFromSnapshot
</Trans>
{t("details.regenerateFromSnapshot")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
aria-label="Regenerate from thumbnails"
onClick={() => regenerateDescription("thumbnails")}
>
<Trans ns="views/explore">
details.regenerateFromThumbnails
</Trans>
{t("details.regenerateFromThumbnails")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -741,22 +719,19 @@ function ObjectDetailsTab({
aria-label="Save"
onClick={updateDescription}
>
<Trans>button.save</Trans>
{t("button.save")}
</Button>
)}
<TextEntryDialog
open={isSubLabelDialogOpen}
setOpen={setIsSubLabelDialogOpen}
title={t("details.editSubLable", { ns: "views/explore" })}
title={t("details.editSubLable")}
description={
search.label
? t("details.editSubLable.desc", {
label: t(search.label, { ns: "objects" }),
ns: "views/explore",
})
: t("details.editSubLable.desc.noLabel", {
ns: "views/explore",
})
: t("details.editSubLable.desc.noLabel")
}
onSave={handleSubLabelSave}
defaultValue={search?.sub_label || ""}
@ -776,6 +751,7 @@ export function ObjectSnapshotTab({
search,
onEventUploaded,
}: ObjectSnapshotTabProps) {
const { t } = useTranslation(["components/dialog"]);
type SubmissionState = "reviewing" | "uploading" | "submitted";
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
@ -858,9 +834,7 @@ export function ObjectSnapshotTab({
</a>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
<Trans>button.download</Trans>
</TooltipContent>
<TooltipContent>{t("button.download")}</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>

View File

@ -18,8 +18,8 @@ import {
import { Button } from "@/components/ui/button";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { baseUrl } from "@/api/baseUrl";
import { t } from "i18next";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type RestartDialogProps = {
isOpen: boolean;
@ -32,6 +32,7 @@ export default function RestartDialog({
onClose,
onRestart,
}: RestartDialogProps) {
const { t } = useTranslation("components/dialog");
const [restartDialogOpen, setRestartDialogOpen] = useState(isOpen);
const [restartingSheetOpen, setRestartingSheetOpen] = useState(false);
const [countdown, setCountdown] = useState(60);
@ -80,16 +81,12 @@ export default function RestartDialog({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans ns="components/dialog">restart.title</Trans>
</AlertDialogTitle>
<AlertDialogTitle>{t("restart.title")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>button.cancel</Trans>
</AlertDialogCancel>
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleRestart}>
<Trans ns="components/dialog">restart.button</Trans>
{t("restart.button")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -104,13 +101,12 @@ export default function RestartDialog({
<ActivityIndicator />
<SheetHeader className="mt-5 text-center">
<SheetTitle className="text-center">
<Trans ns="components/dialog">restart.restarting.title</Trans>
{t("restart.restarting.title")}
</SheetTitle>
<SheetDescription className="text-center">
<div>
{t("restart.restarting.content", {
countdown,
ns: "components/dialog",
})}
</div>
</SheetDescription>
@ -121,7 +117,7 @@ export default function RestartDialog({
aria-label="Force reload now"
onClick={handleForceReload}
>
<Trans ns="components/dialog">restart.restarting.button</Trans>
{t("restart.restarting.button")}
</Button>
</div>
</SheetContent>

View File

@ -33,8 +33,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type SearchFilterDialogProps = {
config?: FrigateConfig;
@ -54,7 +53,7 @@ export default function SearchFilterDialog({
onUpdateFilter,
}: SearchFilterDialogProps) {
// data
const { t } = useTranslation(["components/filter"]);
const [currentFilter, setCurrentFilter] = useState(filter ?? {});
const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]);
@ -95,7 +94,7 @@ export default function SearchFilterDialog({
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
)}
/>
<Trans ns="components/filter">more</Trans>
{t("more")}
</Button>
);
const content = (
@ -177,7 +176,7 @@ export default function SearchFilterDialog({
setOpen(false);
}}
>
<Trans>button.apply</Trans>
{t("button.apply")}
</Button>
<Button
aria-label="Reset filters to default values"
@ -197,7 +196,7 @@ export default function SearchFilterDialog({
}));
}}
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</div>
@ -233,6 +232,7 @@ function TimeRangeFilterContent({
timeRange,
updateTimeRange,
}: TimeRangeFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
const [startOpen, setStartOpen] = useState(false);
const [endOpen, setEndOpen] = useState(false);
@ -274,9 +274,7 @@ function TimeRangeFilterContent({
return (
<div className="overflow-x-hidden">
<div className="text-lg">
<Trans ns="components/filter">timeRange</Trans>
</div>
<div className="text-lg">{t("timeRange")}</div>
<div className="mt-3 flex flex-row items-center justify-center gap-2">
<Popover
open={startOpen}
@ -370,13 +368,12 @@ export function ZoneFilterContent({
zones,
updateZones,
}: ZoneFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
return (
<>
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="text-lg">
<Trans ns="components/filter">zones.label</Trans>
</div>
<div className="text-lg">{t("zones.label")}</div>
{allZones && (
<>
<div className="mb-5 mt-2.5 flex items-center justify-between">
@ -384,7 +381,7 @@ export function ZoneFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allZones"
>
<Trans ns="components/filter">zones.all</Trans>
{t("zones.all")}
</Label>
<Switch
className="ml-1"
@ -439,15 +436,14 @@ export function SubFilterContent({
subLabels,
setSubLabels,
}: SubFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="text-lg">
<Trans ns="components/filter">subLabels.label</Trans>
</div>
<div className="text-lg">{t("subLabels.label")}</div>
<div className="mb-5 mt-2.5 flex items-center justify-between">
<Label className="mx-2 cursor-pointer text-primary" htmlFor="allLabels">
<Trans ns="components/filter">subLabels.all</Trans>
{t("subLabels.all")}
</Label>
<Switch
className="ml-1"
@ -499,12 +495,11 @@ export function ScoreFilterContent({
maxScore,
setScoreRange,
}: ScoreFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">
<Trans ns="components/filter">score</Trans>
</div>
<div className="mb-3 text-lg">{t("score")}</div>
<div className="flex items-center gap-1">
<Input
className="w-14 text-center"
@ -555,21 +550,17 @@ export function SpeedFilterContent({
maxSpeed,
setSpeedRange,
}: SpeedFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">
<Trans
values={{
unit:
config?.ui.unit_system == "metric"
? t("unit.speed.kph")
: t("unit.speed.mph"),
}}
ns="components/filter"
>
filter.estimatedSpeed
</Trans>
{t("filter.estimatedSpeed", {
unit:
config?.ui.unit_system == "metric"
? t("unit.speed.kph")
: t("unit.speed.mph"),
})}
</div>
<div className="flex items-center gap-1">
<Input
@ -628,6 +619,7 @@ export function SnapshotClipFilterContent({
submittedToFrigatePlus,
setSnapshotClip,
}: SnapshotClipContentProps) {
const { t } = useTranslation(["components/filter"]);
const [isSnapshotFilterActive, setIsSnapshotFilterActive] = useState(
hasSnapshot !== undefined,
);
@ -656,9 +648,7 @@ export function SnapshotClipFilterContent({
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">
<Trans ns="components/filter">features.label</Trans>
</div>
<div className="mb-3 text-lg">{t("features.label")}</div>
<div className="my-2.5 space-y-1">
<div className="flex items-center justify-between">
@ -680,7 +670,7 @@ export function SnapshotClipFilterContent({
htmlFor="snapshot-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
<Trans ns="components/filter">features.hasSnapshot</Trans>
{t("features.hasSnapshot")}
</Label>
</div>
<ToggleGroup
@ -701,14 +691,14 @@ export function SnapshotClipFilterContent({
aria-label="Yes"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
<Trans>button.yes</Trans>
{t("button.yes")}
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
<Trans>button.no</Trans>
{t("button.no")}
</ToggleGroupItem>
</ToggleGroup>
</div>
@ -742,9 +732,7 @@ export function SnapshotClipFilterContent({
side="left"
sideOffset={5}
>
<Trans ns="components/filter">
features.submittedToFrigatePlus.tips
</Trans>
{t("features.submittedToFrigatePlus.tips")}
</TooltipContent>
)}
</Tooltip>
@ -753,9 +741,7 @@ export function SnapshotClipFilterContent({
htmlFor="plus-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
<Trans ns="components/filter">
features.submittedToFrigatePlus.label
</Trans>
{t("features.submittedToFrigatePlus.label")}
</Label>
</div>
<ToggleGroup
@ -781,14 +767,14 @@ export function SnapshotClipFilterContent({
aria-label="Yes"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
<Trans>button.yes</Trans>
{t("button.yes")}
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
<Trans>button.no</Trans>
{t("button.no")}
</ToggleGroupItem>
</ToggleGroup>
</div>
@ -817,7 +803,7 @@ export function SnapshotClipFilterContent({
htmlFor="clip-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
<Trans ns="components/filter">features.hasVideoClip</Trans>
{t("features.hasVideoClip")}
</Label>
</div>
<ToggleGroup
@ -836,14 +822,14 @@ export function SnapshotClipFilterContent({
aria-label="Yes"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
<Trans>button.yes</Trans>
{t("button.yes")}
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
<Trans>button.no</Trans>
{t("button.no")}
</ToggleGroupItem>
</ToggleGroup>
</div>

View File

@ -12,7 +12,8 @@ import { Input } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useEffect } from "react";
import { useForm } from "react-hook-form";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
import { z } from "zod";
type TextEntryDialogProps = {
@ -38,6 +39,8 @@ export default function TextEntryDialog({
text: z.string(),
});
const { t } = useTranslation("components/dialog");
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { text: defaultValue },
@ -88,10 +91,10 @@ export default function TextEntryDialog({
/>
<DialogFooter className="pt-4">
<Button type="button" onClick={() => setOpen(false)}>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button variant="select" type="submit">
<Trans>button.save</Trans>
{t("button.save")}
</Button>
</DialogFooter>
</form>

View File

@ -20,7 +20,7 @@ import {
getPreviewForTimeRange,
usePreviewForTimeRange,
} from "@/hooks/use-camera-previews";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type PreviewPlayerProps = {
className?: string;
@ -43,6 +43,7 @@ export default function PreviewPlayer({
onControllerReady,
onClick,
}: PreviewPlayerProps) {
const { t } = useTranslation(["components/player"]);
const [currentHourFrame, setCurrentHourFrame] = useState<string>();
const currentPreview = usePreviewForTimeRange(
cameraPreviews,
@ -89,7 +90,7 @@ export default function PreviewPlayer({
className,
)}
>
<Trans ns="components/player">noPreviewFound</Trans>
{t("noPreviewFound")}
</div>
);
}
@ -134,6 +135,7 @@ function PreviewVideoPlayer({
onClick,
setCurrentHourFrame,
}: PreviewVideoPlayerProps) {
const { t } = useTranslation(["components/player"]);
const { data: config } = useSWR<FrigateConfig>("config");
// controlling playback
@ -325,12 +327,7 @@ function PreviewVideoPlayer({
</video>
{cameraPreviews && !currentPreview && (
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
<Trans
ns="components/player"
value={{ camera: camera.replaceAll("_", " ") }}
>
noPreviewFoundFor
</Trans>
{t("noPreviewFoundFor", { camera: camera.replaceAll("_", " ") })}
</div>
)}
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
@ -450,6 +447,8 @@ function PreviewFramesPlayer({
onControllerReady,
onClick,
}: PreviewFramesPlayerProps) {
const { t } = useTranslation(["components/player"]);
// frames data
const { data: previewFrames } = useSWR<string[]>(
@ -550,12 +549,7 @@ function PreviewFramesPlayer({
/>
{previewFrames?.length === 0 && (
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
<Trans
ns="components/player"
values={{ cameraName: camera.replaceAll("_", " ") }}
>
noPreviewFoundFor
</Trans>
{t("noPreviewFoundFor", { cameraName: camera.replaceAll("_", " ") })}
</div>
)}
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}

View File

@ -12,7 +12,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
import { VideoResolutionType } from "@/types/live";
import axios from "axios";
import { cn } from "@/lib/utils";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
/**
* Dynamically switches between video playback and scrubbing preview player.
@ -51,6 +51,7 @@ export default function DynamicVideoPlayer({
toggleFullscreen,
containerRef,
}: DynamicVideoPlayerProps) {
const { t } = useTranslation(["components/player"]);
const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config");
@ -248,7 +249,7 @@ export default function DynamicVideoPlayer({
)}
{!isScrubbing && !isLoading && noRecording && (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<Trans ns="components/player">noRecordingsFoundForThisTime</Trans>
{t("noRecordingsFoundForThisTime")}
</div>
)}
</>

View File

@ -31,7 +31,7 @@ import useSWR from "swr";
import { LuCheck, LuExternalLink, LuInfo, LuX } from "react-icons/lu";
import { Link } from "react-router-dom";
import { LiveStreamMetadata } from "@/types/live";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type CameraStreamingDialogProps = {
camera: string;
@ -50,6 +50,7 @@ export function CameraStreamingDialog({
setIsDialogOpen,
onSave,
}: CameraStreamingDialogProps) {
const { t } = useTranslation(["components/camera"]);
const { data: config } = useSWR<FrigateConfig>("config");
const [isLoading, setIsLoading] = useState(false);
@ -168,16 +169,11 @@ export function CameraStreamingDialog({
<DialogContent className="sm:max-w-[425px]">
<DialogHeader className="mb-4">
<DialogTitle className="capitalize">
<Trans
ns="components/camera"
values={{ cameraName: camera.replaceAll("_", " ") }}
>
group.camera.setting.title
</Trans>
{t("group.camera.setting.title", {
cameraName: camera.replaceAll("_", " "),
})}
</DialogTitle>
<DialogDescription>
<Trans ns="components/camera">group.camera.setting.desc</Trans>
</DialogDescription>
<DialogDescription>{t("group.camera.setting.desc")}</DialogDescription>
</DialogHeader>
<div className="flex flex-col space-y-8">
{!isRestreamed && (
@ -186,23 +182,19 @@ export function CameraStreamingDialog({
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" />
<div>
<Trans ns="components/dialog">
streaming.restreaming.disabled
</Trans>
{t("streaming.restreaming.disabled", {
ns: "components/dialog",
})}
</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">
<Trans>button.info</Trans>
</span>
<span className="sr-only">{t("button.info")}</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
<Trans ns="components/dialog">
streaming.restreaming.desc
</Trans>
{t("streaming.restreaming.desc", { ns: "components/dialog" })}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@ -210,9 +202,9 @@ export function CameraStreamingDialog({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="components/dialog">
streaming.restreaming.readTheDocumentation
</Trans>
{t("streaming.restreaming.readTheDocumentation", {
ns: "components/dialog",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -245,33 +237,21 @@ export function CameraStreamingDialog({
{supportsAudioOutput ? (
<>
<LuCheck className="size-4 text-success" />
<div>
<Trans ns="components/camera">
group.camera.setting.audioIsAvailable
</Trans>
</div>
<div>{t("group.camera.setting.audioIsAvailable")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>
<Trans ns="components/camera">
group.camera.setting.audioIsUnavailable
</Trans>
</div>
<div>{t("group.camera.setting.audioIsUnavailable")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">
<Trans>button.info</Trans>
</span>
<span className="sr-only">{t("button.info")}</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
<Trans ns="components/camera">
group.camera.setting.audio.desc
</Trans>
{t("group.camera.setting.audio.desc")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@ -279,9 +259,7 @@ export function CameraStreamingDialog({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="components/camera">
group.camera.setting.audio.desc.document
</Trans>
{t("group.camera.setting.audio.desc.document")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -295,9 +273,7 @@ export function CameraStreamingDialog({
)}
<div className="flex flex-col items-start gap-2">
<Label htmlFor="streaming-method" className="text-right">
<Trans ns="components/camera">
group.camera.setting.streamMethod
</Trans>
{t("group.camera.setting.streamMethod")}
</Label>
<Select
value={streamType}
@ -308,49 +284,43 @@ export function CameraStreamingDialog({
</SelectTrigger>
<SelectContent>
<SelectItem value="no-streaming">
<Trans ns="components/camera">
group.camera.setting.streamMethod.method.noStreaming
</Trans>
{t("group.camera.setting.streamMethod.method.noStreaming")}
</SelectItem>
<SelectItem value="smart">
<Trans ns="components/camera">
group.camera.setting.streamMethod.method.smartStreaming
</Trans>
{t("group.camera.setting.streamMethod.method.smartStreaming")}
</SelectItem>
<SelectItem value="continuous">
<Trans ns="components/camera">
group.camera.setting.streamMethod.method.continuousStreaming
</Trans>
{t(
"group.camera.setting.streamMethod.method.continuousStreaming",
)}
</SelectItem>
</SelectContent>
</Select>
{streamType === "no-streaming" && (
<p className="text-sm text-muted-foreground">
<Trans ns="components/camera">
group.camera.setting.streamMethod.method.noStreaming.desc
</Trans>
{t("group.camera.setting.streamMethod.method.noStreaming.desc")}
</p>
)}
{streamType === "smart" && (
<p className="text-sm text-muted-foreground">
<Trans ns="components/camera">
group.camera.setting.streamMethod.method.smartStreaming.desc
</Trans>
{t(
"group.camera.setting.streamMethod.method.smartStreaming.desc",
)}
</p>
)}
{streamType === "continuous" && (
<>
<p className="text-sm text-muted-foreground">
<Trans ns="components/camera">
group.camera.setting.streamMethod.method.continuousStreaming.desc
</Trans>
{t(
"group.camera.setting.streamMethod.method.continuousStreaming.desc",
)}
</p>
<div className="flex items-center gap-2">
<IoIosWarning className="mr-2 size-5 text-danger" />
<div className="max-w-[85%] text-sm">
<Trans ns="components/camera">
group.camera.setting.streamMethod.method.continuousStreaming.desc.warning
</Trans>
{t(
"group.camera.setting.streamMethod.method.continuousStreaming.desc.warning",
)}
</div>
</div>
</>
@ -368,16 +338,12 @@ export function CameraStreamingDialog({
htmlFor="compatibility"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<Trans ns="components/camera">
group.camera.setting.compatibilityMode
</Trans>
{t("group.camera.setting.compatibilityMode")}
</Label>
</div>
<div className="flex flex-col gap-2 leading-none">
<p className="text-sm text-muted-foreground">
<Trans ns="components/camera">
group.camera.setting.compatibilityMode.desc
</Trans>
{t("group.camera.setting.compatibilityMode.desc")}
</p>
</div>
</div>
@ -389,7 +355,7 @@ export function CameraStreamingDialog({
aria-label="Cancel"
onClick={handleCancel}
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -401,12 +367,10 @@ export function CameraStreamingDialog({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>

View File

@ -22,8 +22,7 @@ import { Toaster } from "../ui/sonner";
import ActivityIndicator from "../indicators/activity-indicator";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
import { t } from "i18next";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type MotionMaskEditPaneProps = {
polygons?: Polygon[];
@ -52,6 +51,7 @@ export default function MotionMaskEditPane({
snapPoints,
setSnapPoints,
}: MotionMaskEditPaneProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@ -107,9 +107,7 @@ export default function MotionMaskEditPane({
polygon: z.object({ name: z.string(), isFinished: z.boolean() }),
})
.refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
ns: "views/settings",
}),
message: t("masksAndZones.polygonDrawing.error.mustBeFinished"),
path: ["polygon.isFinished"],
});
@ -170,11 +168,8 @@ export default function MotionMaskEditPane({
polygon.name
? t("masksAndZones.motionMasks.toast.success", {
polygonName: polygon.name,
ns: "views/settings",
})
: t("masksAndZones.motionMasks.toast.success.noName", {
ns: "views/settings",
}),
: t("masksAndZones.motionMasks.toast.success.noName"),
{
position: "top-center",
},
@ -205,6 +200,7 @@ export default function MotionMaskEditPane({
scaledHeight,
setIsLoading,
cameraConfig,
t,
]);
function onSubmit(values: z.infer<typeof formSchema>) {
@ -220,10 +216,8 @@ export default function MotionMaskEditPane({
}
useEffect(() => {
document.title = t("masksAndZones.motionMasks.documentTitle", {
ns: "views/settings",
});
}, []);
document.title = t("masksAndZones.motionMasks.documentTitle");
}, [t]);
if (!polygon) {
return;
@ -234,13 +228,11 @@ export default function MotionMaskEditPane({
<Toaster position="top-center" closeButton={true} />
<Heading as="h3" className="my-2">
{polygon.name.length
? t("masksAndZones.motionMasks.edit", { ns: "views/settings" })
: t("masksAndZones.motionMasks.add", { ns: "views/settings" })}
? t("masksAndZones.motionMasks.edit")
: t("masksAndZones.motionMasks.add")}
</Heading>
<div className="my-3 space-y-3 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">masksAndZones.motionMasks.context</Trans>
</p>
<p>{t("masksAndZones.motionMasks.context")}</p>
<div className="flex items-center text-primary">
<Link
@ -249,9 +241,7 @@ export default function MotionMaskEditPane({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
masksAndZones.motionMasks.context.documentation
</Trans>{" "}
{t("masksAndZones.motionMasks.context.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -262,7 +252,6 @@ export default function MotionMaskEditPane({
<div className="my-1 inline-flex">
{t("masksAndZones.motionMasks.point", {
count: polygons[activePolygonIndex].points.length,
ns: "views/settings",
})}
{polygons[activePolygonIndex].isFinished && (
<FaCheckCircle className="ml-2 size-5" />
@ -278,9 +267,7 @@ export default function MotionMaskEditPane({
</div>
)}
<div className="mb-3 text-sm text-muted-foreground">
<Trans ns="views/settings">
masksAndZones.motionMasks.clickDrawPolygon
</Trans>
{t("masksAndZones.motionMasks.clickDrawPolygon")}
</div>
<Separator className="my-3 bg-secondary" />
@ -290,22 +277,17 @@ export default function MotionMaskEditPane({
<div className="mb-3 text-sm text-danger">
{t("masksAndZones.motionMasks.polygonAreaTooLarge", {
polygonArea: Math.round(polygonArea * 100),
ns: "views/settings",
})}
</div>
<div className="mb-3 text-sm text-primary">
<Trans ns="views/settings">
masksAndZones.motionMasks.polygonAreaTooLarge.tips
</Trans>
{t("masksAndZones.motionMasks.polygonAreaTooLarge.tips")}
<Link
to="https://github.com/blakeblackshear/frigate/discussions/13040"
target="_blank"
rel="noopener noreferrer"
className="my-3 block"
>
<Trans ns="views/settings">
masksAndZones.motionMasks.polygonAreaTooLarge.documentation
</Trans>{" "}
{t("masksAndZones.motionMasks.polygonAreaTooLarge.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -342,7 +324,7 @@ export default function MotionMaskEditPane({
aria-label="Cancel"
onClick={onCancel}
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -354,12 +336,10 @@ export default function MotionMaskEditPane({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>

View File

@ -38,8 +38,7 @@ import { toast } from "sonner";
import { Toaster } from "../ui/sonner";
import ActivityIndicator from "../indicators/activity-indicator";
import { getAttributeLabels } from "@/utils/iconUtil";
import { t } from "i18next";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type ObjectMaskEditPaneProps = {
polygons?: Polygon[];
@ -68,6 +67,7 @@ export default function ObjectMaskEditPane({
snapPoints,
setSnapPoints,
}: ObjectMaskEditPaneProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@ -109,9 +109,7 @@ export default function ObjectMaskEditPane({
polygon: z.object({ isFinished: z.boolean(), name: z.string() }),
})
.refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
ns: "views/settings",
}),
message: t("masksAndZones.polygonDrawing.error.mustBeFinished"),
path: ["polygon.isFinished"],
});
@ -202,11 +200,8 @@ export default function ObjectMaskEditPane({
polygon.name
? t("masksAndZones.objectMasks.toast.success", {
polygonName: polygon.name,
ns: "views/settings",
})
: t("masksAndZones.objectMasks.toast.success.noName", {
ns: "views/settings",
}),
: t("masksAndZones.objectMasks.toast.success.noName"),
{
position: "top-center",
},
@ -248,6 +243,7 @@ export default function ObjectMaskEditPane({
scaledHeight,
setIsLoading,
cameraConfig,
t,
],
);
@ -264,10 +260,8 @@ export default function ObjectMaskEditPane({
}
useEffect(() => {
document.title = t("masksAndZones.objectMasks.documentTitle", {
ns: "views/settings",
});
}, []);
document.title = t("masksAndZones.objectMasks.documentTitle");
}, [t]);
if (!polygon) {
return;
@ -278,17 +272,11 @@ export default function ObjectMaskEditPane({
<Toaster position="top-center" closeButton={true} />
<Heading as="h3" className="my-2">
{polygon.name.length
? t("masksAndZones.objectMasks.edit", {
ns: "views/settings",
})
: t("masksAndZones.objectMasks.add", {
ns: "views/settings",
})}
? t("masksAndZones.objectMasks.edit")
: t("masksAndZones.objectMasks.add")}
</Heading>
<div className="my-2 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">masksAndZones.objectMasks.context</Trans>
</p>
<p>{t("masksAndZones.objectMasks.context")}</p>
</div>
<Separator className="my-3 bg-secondary" />
{polygons && activePolygonIndex !== undefined && (
@ -296,7 +284,6 @@ export default function ObjectMaskEditPane({
<div className="my-1 inline-flex">
{t("masksAndZones.objectMasks.point", {
count: polygons[activePolygonIndex].points.length,
ns: "views/settings",
})}
{polygons[activePolygonIndex].isFinished && (
<FaCheckCircle className="ml-2 size-5" />
@ -312,9 +299,7 @@ export default function ObjectMaskEditPane({
</div>
)}
<div className="mb-3 text-sm text-muted-foreground">
<Trans ns="views/settings">
masksAndZones.objectMasks.clickDrawPolygon
</Trans>
{t("masksAndZones.objectMasks.clickDrawPolygon")}
</div>
<Separator className="my-3 bg-secondary" />
@ -340,9 +325,7 @@ export default function ObjectMaskEditPane({
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans ns="views/settings">
masksAndZones.objectMasks.objects
</Trans>
{t("masksAndZones.objectMasks.objects")}
</FormLabel>
<Select
onValueChange={field.onChange}
@ -359,9 +342,7 @@ export default function ObjectMaskEditPane({
</SelectContent>
</Select>
<FormDescription>
<Trans ns="views/settings">
masksAndZones.objectMasks.objects.desc
</Trans>
{t("masksAndZones.objectMasks.objects.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -384,7 +365,7 @@ export default function ObjectMaskEditPane({
aria-label="Cancel"
onClick={onCancel}
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -396,12 +377,10 @@ export default function ObjectMaskEditPane({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>
@ -417,6 +396,7 @@ type ZoneObjectSelectorProps = {
};
export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config } = useSWR<FrigateConfig>("config");
const attributeLabels = useMemo(() => {
@ -461,9 +441,7 @@ export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
<>
<SelectGroup>
<SelectItem value="all_labels">
<Trans ns="views/settings">
masksAndZones.objectMasks.objects.allObjectTypes
</Trans>
{t("masksAndZones.objectMasks.objects.allObjectTypes")}
</SelectItem>
<SelectSeparator className="bg-secondary" />
{allLabels.map((item) => (

View File

@ -36,7 +36,7 @@ import { reviewQueries } from "@/utils/zoneEdutUtil";
import IconWrapper from "../ui/icon-wrapper";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { buttonVariants } from "../ui/button";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type PolygonItemProps = {
polygon: Polygon;
@ -57,6 +57,7 @@ export default function PolygonItem({
setEditPane,
handleCopyCoordinates,
}: PolygonItemProps) {
const { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -318,9 +319,7 @@ export default function PolygonItem({
}}
/>
</TooltipTrigger>
<TooltipContent>
<Trans>button.edit</Trans>
</TooltipContent>
<TooltipContent>{t("button.edit")}</TooltipContent>
</Tooltip>
<Tooltip>
@ -333,9 +332,7 @@ export default function PolygonItem({
onClick={() => handleCopyCoordinates(index)}
/>
</TooltipTrigger>
<TooltipContent>
<Trans>button.copyCoordinates</Trans>
</TooltipContent>
<TooltipContent>{t("button.copyCoordinates")}</TooltipContent>
</Tooltip>
<Tooltip>
@ -349,9 +346,7 @@ export default function PolygonItem({
onClick={() => !isLoading && setDeleteDialogOpen(true)}
/>
</TooltipTrigger>
<TooltipContent>
<Trans>button.delete</Trans>
</TooltipContent>
<TooltipContent>{t("button.delete")}</TooltipContent>
</Tooltip>
</div>
)}

View File

@ -17,8 +17,7 @@ import FilterSwitch from "../filter/FilterSwitch";
import { SearchFilter, SearchSource } from "@/types/search";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type ExploreSettingsProps = {
className?: string;
@ -38,6 +37,7 @@ export default function ExploreSettings({
setDefaultView,
onUpdateFilter,
}: ExploreSettingsProps) {
const { t } = useTranslation(["components/filter"]);
const { data: config } = useSWR<FrigateConfig>("config");
const [open, setOpen] = useState(false);
@ -52,20 +52,16 @@ export default function ExploreSettings({
size="sm"
>
<FaCog className="text-secondary-foreground" />
<Trans ns="components/filter">explore.settings.title</Trans>
{t("explore.settings.title")}
</Button>
);
const content = (
<div className={cn(className, "my-3 space-y-5 py-3 md:mt-0 md:py-0")}>
<div className="space-y-4">
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="components/filter">explore.settings.defaultView</Trans>
</div>
<div className="text-md">{t("explore.settings.defaultView")}</div>
<div className="space-y-1 text-xs text-muted-foreground">
<Trans ns="components/filter">
explore.settings.defaultView.desc
</Trans>
{t("explore.settings.defaultView.desc")}
</div>
</div>
<Select
@ -74,12 +70,8 @@ export default function ExploreSettings({
>
<SelectTrigger className="w-full">
{defaultView == "summary"
? t("explore.settings.defaultView.summary", {
ns: "components/filter",
})
: t("explore.settings.defaultView.unfilteredGrid", {
ns: "components/filter",
})}
? t("explore.settings.defaultView.summary")
: t("explore.settings.defaultView.unfilteredGrid")}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@ -90,12 +82,8 @@ export default function ExploreSettings({
value={value}
>
{value == "summary"
? t("explore.settings.defaultView.summary", {
ns: "components/filter",
})
: t("explore.settings.defaultView.unfilteredGrid", {
ns: "components/filter",
})}
? t("explore.settings.defaultView.summary")
: t("explore.settings.defaultView.unfilteredGrid")}
</SelectItem>
))}
</SelectGroup>
@ -107,15 +95,9 @@ export default function ExploreSettings({
<DropdownMenuSeparator />
<div className="flex w-full flex-col space-y-4">
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="components/filter">
explore.settings.gridColumns
</Trans>
</div>
<div className="text-md">{t("explore.settings.gridColumns")}</div>
<div className="space-y-1 text-xs text-muted-foreground">
<Trans ns="components/filter">
explore.settings.gridColumns.desc
</Trans>
{t("explore.settings.gridColumns.desc")}
</div>
</div>
<div className="flex items-center space-x-4">
@ -171,18 +153,15 @@ export function SearchTypeContent({
searchSources,
setSearchSources,
}: SearchTypeContentProps) {
const { t } = useTranslation(["components/filter"]);
return (
<>
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="components/filter">explore.settings.searchSource</Trans>
</div>
<div className="text-md">{t("explore.settings.searchSource")}</div>
<div className="space-y-1 text-xs text-muted-foreground">
<Trans ns="components/filter">
explore.settings.searchSource.desc
</Trans>
{t("explore.settings.searchSource.desc")}
</div>
</div>
<div className="mt-2.5 flex flex-col gap-2.5">

View File

@ -29,8 +29,7 @@ import { toast } from "sonner";
import { flattenPoints, interpolatePoints } from "@/utils/canvasUtil";
import ActivityIndicator from "../indicators/activity-indicator";
import { getAttributeLabels } from "@/utils/iconUtil";
import { t } from "i18next";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type ZoneEditPaneProps = {
polygons?: Polygon[];
@ -61,6 +60,7 @@ export default function ZoneEditPane({
snapPoints,
setSnapPoints,
}: ZoneEditPaneProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@ -106,7 +106,6 @@ export default function ZoneEditPane({
.min(2, {
message: t(
"masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters",
{ ns: "views/settings" },
),
})
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
@ -117,7 +116,6 @@ export default function ZoneEditPane({
{
message: t(
"masksAndZones.form.zoneName.error.mustNotBeSameWithCamera",
{ ns: "views/settings" },
),
},
)
@ -131,9 +129,7 @@ export default function ZoneEditPane({
return !otherPolygonNames.includes(value);
},
{
message: t("masksAndZones.form.zoneName.error.alreadyExists", {
ns: "views/settings",
}),
message: t("masksAndZones.form.zoneName.error.alreadyExists"),
},
)
.refine(
@ -143,21 +139,16 @@ export default function ZoneEditPane({
{
message: t(
"masksAndZones.form.zoneName.error.mustNotContainPeriod",
{ ns: "views/settings" },
),
},
)
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter", {
ns: "views/settings",
}),
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter"),
}),
inertia: z.coerce
.number()
.min(1, {
message: t("masksAndZones.form.inertia.error.mustBeAboveZero", {
ns: "views/settings",
}),
message: t("masksAndZones.form.inertia.error.mustBeAboveZero"),
})
.or(z.literal("")),
loitering_time: z.coerce
@ -165,15 +156,12 @@ export default function ZoneEditPane({
.min(0, {
message: t(
"masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero",
{ ns: "views/settings" },
),
})
.optional()
.or(z.literal("")),
isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
ns: "views/settings",
}),
message: t("masksAndZones.polygonDrawing.error.mustBeFinished"),
}),
objects: z.array(z.string()).optional(),
review_alerts: z.boolean().default(false).optional(),
@ -182,36 +170,28 @@ export default function ZoneEditPane({
lineA: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
message: t("masksAndZones.form.distance.error"),
})
.optional()
.or(z.literal("")),
lineB: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
message: t("masksAndZones.form.distance.error"),
})
.optional()
.or(z.literal("")),
lineC: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
message: t("masksAndZones.form.distance.error"),
})
.optional()
.or(z.literal("")),
lineD: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
message: t("masksAndZones.form.distance.error"),
})
.optional()
.or(z.literal("")),
@ -231,9 +211,7 @@ export default function ZoneEditPane({
return true;
},
{
message: t("masksAndZones.form.distance.error.mustBeFilled", {
ns: "views/settings",
}),
message: t("masksAndZones.form.distance.error.mustBeFilled"),
path: ["speedEstimation"],
},
)
@ -249,9 +227,6 @@ export default function ZoneEditPane({
{
message: t(
"masksAndZones.zones.speedThreshold.toast.error.loiteringTimeError",
{
ns: "views/settings",
},
),
path: ["loitering_time"],
},
@ -291,13 +266,11 @@ export default function ZoneEditPane({
polygon.points.length !== 4
) {
toast.error(
t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError", {
ns: "views/settings",
}),
t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError"),
);
form.setValue("speedEstimation", false);
}
}, [polygon, form]);
}, [polygon, form, t]);
const saveToConfig = useCallback(
async (
@ -441,7 +414,6 @@ export default function ZoneEditPane({
toast.success(
t("masksAndZones.zones.toast.success", {
zoneName,
ns: "views/settings",
}),
{
position: "top-center",
@ -483,6 +455,7 @@ export default function ZoneEditPane({
scaledHeight,
setIsLoading,
cameraConfig,
t,
],
);
@ -503,10 +476,8 @@ export default function ZoneEditPane({
}
useEffect(() => {
document.title = t("masksAndZones.zones.documentTitle", {
ns: "views/settings",
});
}, []);
document.title = t("masksAndZones.zones.documentTitle");
}, [t]);
if (!polygon) {
return;
@ -517,17 +488,11 @@ export default function ZoneEditPane({
<Toaster position="top-center" closeButton={true} />
<Heading as="h3" className="my-2">
{polygon.name.length
? t("masksAndZones.zones.edit", {
ns: "views/settings",
})
: t("masksAndZones.zones.add", {
ns: "views/settings",
})}
? t("masksAndZones.zones.edit")
: t("masksAndZones.zones.add")}
</Heading>
<div className="my-2 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">masksAndZones.zones.desc</Trans>
</p>
<p>{t("masksAndZones.zones.desc")}</p>
</div>
<Separator className="my-3 bg-secondary" />
{polygons && activePolygonIndex !== undefined && (
@ -535,7 +500,6 @@ export default function ZoneEditPane({
<div className="my-1 inline-flex">
{t("masksAndZones.zones.point", {
count: polygons[activePolygonIndex].points.length,
ns: "views/settings",
})}
{polygons[activePolygonIndex].isFinished && (
@ -552,7 +516,7 @@ export default function ZoneEditPane({
</div>
)}
<div className="mb-3 text-sm text-muted-foreground">
<Trans ns="views/settings">masksAndZones.zones.clickDrawPolygon</Trans>
{t("masksAndZones.zones.clickDrawPolygon")}
</div>
<Separator className="my-3 bg-secondary" />
@ -564,25 +528,16 @@ export default function ZoneEditPane({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans ns="views/settings">masksAndZones.zones.name</Trans>
</FormLabel>
<FormLabel>{t("masksAndZones.zones.name")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder={t(
"masksAndZones.zones.name.inputPlaceHolder",
{
ns: "views/settings",
},
)}
placeholder={t("masksAndZones.zones.name.inputPlaceHolder")}
{...field}
/>
</FormControl>
<FormDescription>
<Trans ns="views/settings">
masksAndZones.zones.name.tips
</Trans>
{t("masksAndZones.zones.name.tips")}
</FormDescription>
<FormMessage />
</FormItem>
@ -594,9 +549,7 @@ export default function ZoneEditPane({
name="inertia"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans ns="views/settings">masksAndZones.zones.inertia</Trans>
</FormLabel>
<FormLabel>{t("masksAndZones.zones.inertia")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
@ -605,9 +558,7 @@ export default function ZoneEditPane({
/>
</FormControl>
<FormDescription>
<Trans ns="views/settings">
masksAndZones.zones.inertia.desc
</Trans>
{t("masksAndZones.zones.inertia.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -619,11 +570,7 @@ export default function ZoneEditPane({
name="loitering_time"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans ns="views/settings">
masksAndZones.zones.loiteringTime
</Trans>
</FormLabel>
<FormLabel>{t("masksAndZones.zones.loiteringTime")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
@ -632,9 +579,7 @@ export default function ZoneEditPane({
/>
</FormControl>
<FormDescription>
<Trans ns="views/settings">
masksAndZones.zones.loiteringTime.desc
</Trans>
{t("masksAndZones.zones.loiteringTime.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -642,13 +587,9 @@ export default function ZoneEditPane({
/>
<Separator className="my-2 flex bg-secondary" />
<FormItem>
<FormLabel>
<Trans ns="views/settings">masksAndZones.zones.objects</Trans>
</FormLabel>
<FormLabel>{t("masksAndZones.zones.objects")}</FormLabel>
<FormDescription>
<Trans ns="views/settings">
masksAndZones.zones.objects.desc
</Trans>
{t("masksAndZones.zones.objects.desc")}
</FormDescription>
<ZoneObjectSelector
camera={polygon.camera}
@ -682,9 +623,7 @@ export default function ZoneEditPane({
className="cursor-pointer text-primary"
htmlFor="allLabels"
>
<Trans ns="views/settings">
masksAndZones.zones.speedEstimation
</Trans>
{t("masksAndZones.zones.speedEstimation")}
</FormLabel>
<Switch
checked={field.value}
@ -698,9 +637,6 @@ export default function ZoneEditPane({
toast.error(
t(
"masksAndZones.zones.speedEstimation.pointLengthError",
{
ns: "views/settings",
},
),
);
return;
@ -712,9 +648,6 @@ export default function ZoneEditPane({
toast.error(
t(
"masksAndZones.zones.speedEstimation.loiteringTimeError",
{
ns: "views/settings",
},
),
);
}
@ -725,9 +658,7 @@ export default function ZoneEditPane({
</FormControl>
</div>
<FormDescription>
<Trans ns="views/settings">
masksAndZones.zones.speedEstimation.desc
</Trans>
{t("masksAndZones.zones.speedEstimation.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -839,17 +770,12 @@ export default function ZoneEditPane({
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans
values={{
unit:
config?.ui.unit_system == "imperial"
? t("unit.speed.mph")
: t("unit.speed.kph"),
}}
ns="views/settings"
>
masksAndZones.zones.speedThreshold
</Trans>
{t("masksAndZones.zones.speedThreshold", {
unit:
config?.ui.unit_system == "imperial"
? t("unit.speed.mph")
: t("unit.speed.kph"),
})}
</FormLabel>
<FormControl>
<Input
@ -858,9 +784,7 @@ export default function ZoneEditPane({
/>
</FormControl>
<FormDescription>
<Trans ns="views/settings">
masksAndZones.zones.speedThreshold.desc
</Trans>
{t("masksAndZones.zones.speedThreshold.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -884,7 +808,7 @@ export default function ZoneEditPane({
aria-label="Cancel"
onClick={onCancel}
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -896,12 +820,10 @@ export default function ZoneEditPane({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>
@ -924,6 +846,7 @@ export function ZoneObjectSelector({
selectedLabels,
updateLabelFilter,
}: ZoneObjectSelectorProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config } = useSWR<FrigateConfig>("config");
const attributeLabels = useMemo(() => {
@ -981,7 +904,7 @@ export function ZoneObjectSelector({
<div className="scrollbar-container h-auto overflow-y-auto overflow-x-hidden">
<div className="my-2.5 flex items-center justify-between">
<Label className="cursor-pointer text-primary" htmlFor="allLabels">
<Trans ns="views/settings">masksAndZones.zones.allObjects</Trans>
{t("masksAndZones.zones.allObjects")}
</Label>
<Switch
className="ml-1"

View File

@ -13,7 +13,7 @@ import { Switch } from "./switch";
import { cn } from "@/lib/utils";
import { LuCheck } from "react-icons/lu";
import { t } from "i18next";
import { Trans } from "react-i18next";
export interface DateRangePickerProps {
/** Click handler for applying the updates from DateRangePicker. */
@ -431,7 +431,7 @@ export function DateRangePicker({
}
}}
>
<Trans>button.apply</Trans>
{t("button.apply")}
</Button>
<Button
onClick={() => {
@ -442,7 +442,7 @@ export function DateRangePicker({
variant="ghost"
aria-label="Reset"
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
</div>
</div>

View File

@ -10,9 +10,11 @@ import useSWR from "swr";
import useDeepMemo from "./use-deep-memo";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { useFrigateStats } from "@/api/ws";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
export default function useStats(stats: FrigateStats | undefined) {
const { t } = useTranslation(["views/system"]);
const { data: config } = useSWR<FrigateConfig>("config");
const memoizedStats = useDeepMemo(stats);
@ -76,7 +78,6 @@ export default function useStats(stats: FrigateStats | undefined) {
text: t("stats.ffmpegHighCpuUsage", {
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
ffmpegAvg,
ns: "views/system",
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
color: "text-danger",
relevantLink: "/system#cameras",
@ -88,7 +89,6 @@ export default function useStats(stats: FrigateStats | undefined) {
text: t("stats.detectHighCpuUsage", {
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
detectAvg,
ns: "views/system",
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
color: "text-danger",
relevantLink: "/system#cameras",
@ -97,7 +97,7 @@ export default function useStats(stats: FrigateStats | undefined) {
});
return problems;
}, [config, memoizedStats]);
}, [config, memoizedStats, t]);
return { potentialProblems };
}

View File

@ -14,11 +14,12 @@ import { toast } from "sonner";
import { LuCopy, LuSave } from "react-icons/lu";
import { MdOutlineRestartAlt } from "react-icons/md";
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type SaveOptions = "saveonly" | "restart";
function ConfigEditor() {
const { t } = useTranslation(["views/configEditor"]);
const apiHost = useApiHost();
useEffect(() => {
@ -191,7 +192,7 @@ function ConfigEditor() {
<div className="relative h-full overflow-hidden">
<div className="mr-1 flex items-center justify-between">
<Heading as="h2" className="mb-0 ml-1 md:ml-0">
<Trans ns="views/configEditor">configEditor</Trans>
{t("configEditor")}
</Heading>
<div className="flex flex-row gap-1">
<Button
@ -201,9 +202,7 @@ function ConfigEditor() {
onClick={() => handleCopyConfig()}
>
<LuCopy className="text-secondary-foreground" />
<span className="hidden md:block">
<Trans ns="views/configEditor">copyConfig</Trans>
</span>
<span className="hidden md:block">{t("copyConfig")}</span>
</Button>
<Button
size="sm"
@ -215,9 +214,7 @@ function ConfigEditor() {
<LuSave className="absolute left-0 top-0 size-3 text-secondary-foreground" />
<MdOutlineRestartAlt className="absolute size-4 translate-x-1 translate-y-1/2 text-secondary-foreground" />
</div>
<span className="hidden md:block">
<Trans ns="views/configEditor">saveAndRestart</Trans>
</span>
<span className="hidden md:block">{t("saveAndRestart")}</span>
</Button>
<Button
size="sm"
@ -226,9 +223,7 @@ function ConfigEditor() {
onClick={() => onHandleSaveConfig("saveonly")}
>
<LuSave className="text-secondary-foreground" />
<span className="hidden md:block">
<Trans ns="views/configEditor">saveOnly</Trans>
</span>
<span className="hidden md:block">{t("saveOnly")}</span>
</Button>
</div>
</div>

View File

@ -17,20 +17,22 @@ import { useSearchEffect } from "@/hooks/use-overlay-state";
import { cn } from "@/lib/utils";
import { DeleteClipType, Export } from "@/types/export";
import axios from "axios";
import { t } from "i18next";
import { useCallback, useEffect, useMemo, useState } from "react";
import { isMobile } from "react-device-detect";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
import { LuFolderX } from "react-icons/lu";
import { toast } from "sonner";
import useSWR from "swr";
function Exports() {
const { t } = useTranslation(["views/exports"]);
const { data: exports, mutate } = useSWR<Export[]>("exports");
useEffect(() => {
document.title = t("documentTitle", { ns: "views/exports" });
}, []);
document.title = t("documentTitle");
}, [t]);
// Search
@ -117,29 +119,20 @@ function Exports() {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans ns="views/exports">deleteExport</Trans>
</AlertDialogTitle>
<AlertDialogTitle>{t("deleteExport")}</AlertDialogTitle>
<AlertDialogDescription>
<Trans
ns="views/exports"
values={{ exportName: deleteClip?.exportName }}
>
deleteExport.desc
</Trans>
{t("deleteExport.desc", { exportName: deleteClip?.exportName })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>button.cancel</Trans>
</AlertDialogCancel>
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
<Button
className="text-white"
aria-label="Delete Export"
variant="destructive"
onClick={() => onHandleDelete()}
>
<Trans>button.delete</Trans>
{t("button.delete")}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
@ -187,7 +180,7 @@ function Exports() {
<div className="flex w-full items-center justify-center p-2">
<Input
className="text-md w-full bg-muted md:w-1/3"
placeholder={t("search", { ns: "views/exports" })}
placeholder={t("search")}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
@ -215,7 +208,7 @@ function Exports() {
) : (
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
<LuFolderX className="size-16" />
<Trans ns="views/exports" i18nKey={"noExports"}></Trans>
{t("noExports")}
</div>
)}
</div>

View File

@ -9,11 +9,13 @@ import { FrigateConfig } from "@/types/frigateConfig";
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
import LiveCameraView from "@/views/live/LiveCameraView";
import LiveDashboardView from "@/views/live/LiveDashboardView";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
import { useEffect, useMemo, useRef } from "react";
import useSWR from "swr";
function Live() {
const { t } = useTranslation(["views/live"]);
const { data: config } = useSWR<FrigateConfig>("config");
// selection
@ -67,17 +69,15 @@ function Live() {
.map((text) => text[0].toUpperCase() + text.substring(1));
document.title = t("documentTitle.withCamera", {
camera: capitalized.join(" "),
ns: "views/live",
});
} else if (cameraGroup && cameraGroup != "default") {
document.title = t("documentTitle.withCamera", {
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
ns: "views/live",
});
} else {
document.title = t("documentTitle", { ns: "views/live" });
}
}, [cameraGroup, selectedCameraName]);
}, [cameraGroup, selectedCameraName, t]);
// settings

View File

@ -14,15 +14,15 @@ import CameraMetrics from "@/views/system/CameraMetrics";
import { useHashState } from "@/hooks/use-overlay-state";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { Toaster } from "@/components/ui/sonner";
import { t } from "i18next";
import { Trans } from "react-i18next";
import { FrigateConfig } from "@/types/frigateConfig";
import FeatureMetrics from "@/views/system/FeatureMetrics";
import { useTranslation } from "react-i18next";
const allMetrics = ["general", "features", "storage", "cameras"] as const;
type SystemMetric = (typeof allMetrics)[number];
function System() {
const { t } = useTranslation(["views/system"]);
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
@ -94,9 +94,7 @@ function System() {
{item == "storage" && <LuHardDrive className="size-4" />}
{item == "cameras" && <FaVideo className="size-4" />}
{isDesktop && (
<div className="capitalize">
{t(item + ".title", { ns: "views/system" })}
</div>
<div className="capitalize">{t(item + ".title")}</div>
)}
</ToggleGroupItem>
))}
@ -105,16 +103,14 @@ function System() {
<div className="flex h-full items-center">
{lastUpdated && (
<div className="h-full content-center text-sm text-muted-foreground">
<Trans ns="views/system">lastRefreshed</Trans>
{t("lastRefreshed")}
<TimeAgo time={lastUpdated * 1000} dense />
</div>
)}
</div>
</div>
<div className="mt-2 flex items-end gap-2">
<div className="h-full content-center font-medium">
<Trans ns="views/system">title</Trans>
</div>
<div className="h-full content-center font-medium">{t("title")}</div>
{statsSnapshot && (
<div className="h-full content-center text-sm text-muted-foreground">
{statsSnapshot.service.version}

View File

@ -54,9 +54,9 @@ import { FilterList, LAST_24_HOURS_KEY } from "@/types/filter";
import { GiSoundWaves } from "react-icons/gi";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
import { useTranslation } from "react-i18next";
type EventViewProps = {
reviewItems?: SegmentedReviewData;
@ -96,6 +96,7 @@ export default function EventView({
pullLatestData,
updateFilter,
}: EventViewProps) {
const { t } = useTranslation(["views/event"]);
const { data: config } = useSWR<FrigateConfig>("config");
const contentRef = useRef<HTMLDivElement | null>(null);
@ -223,7 +224,7 @@ export default function EventView({
);
});
},
[reviewItems],
[reviewItems, t],
);
const [motionOnly, setMotionOnly] = useState(false);
@ -295,7 +296,7 @@ export default function EventView({
<>
<MdCircle className="size-2 text-severity_alert md:mr-[10px]" />
<div className="hidden md:flex md:flex-row md:items-center">
<Trans ns="views/events">alerts</Trans>
{t("alerts")}
{reviewCounts.alert > -1 ? (
`${reviewCounts.alert}`
) : (
@ -331,7 +332,7 @@ export default function EventView({
<>
<MdCircle className="size-2 text-severity_detection md:mr-[10px]" />
<div className="hidden md:flex md:flex-row md:items-center">
<Trans ns="views/events">detections</Trans>
{t("detections")}
{reviewCounts.detection > -1 ? (
`${reviewCounts.detection}`
) : (
@ -354,9 +355,7 @@ export default function EventView({
) : (
<>
<MdCircle className="size-2 text-severity_significant_motion md:mr-[10px]" />
<div className="hidden md:block">
<Trans ns="views/events">motion.label</Trans>
</div>
<div className="hidden md:block">{t("motion.label")}</div>
</>
)}
</ToggleGroupItem>
@ -473,6 +472,8 @@ function DetectionReview({
setSelectedReviews,
pullLatestData,
}: DetectionReviewProps) {
const { t } = useTranslation(["views/event"]);
const reviewTimelineRef = useRef<HTMLDivElement>(null);
// detail
@ -724,7 +725,7 @@ function DetectionReview({
{!loading && currentItems?.length === 0 && (
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
<LuFolderCheck className="size-16" />
<Trans ns="views/events">empty.{severity.replace(/_/g, " ")}</Trans>
{t("empty." + severity.replace(/_/g, " "))}
</div>
)}
@ -869,6 +870,7 @@ function MotionReview({
motionOnly = false,
onOpenRecording,
}: MotionReviewProps) {
const { t } = useTranslation(["views/event"]);
const segmentDuration = 30;
const { data: config } = useSWR<FrigateConfig>("config");
@ -1058,7 +1060,7 @@ function MotionReview({
return (
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
<LuFolderX className="size-16" />
<Trans ns="views/events">empty.motion</Trans>
{t("empty.motion")}
</div>
);
}

View File

@ -103,8 +103,7 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import useSWR from "swr";
import { cn } from "@/lib/utils";
import { useSessionPersistence } from "@/hooks/use-session-persistence";
import { Trans } from "react-i18next";
import { t } from "i18next";
import {
Select,
SelectContent,
@ -119,6 +118,7 @@ import axios from "axios";
import { toast } from "sonner";
import { Toaster } from "@/components/ui/sonner";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { useTranslation } from "react-i18next";
type LiveCameraViewProps = {
config?: FrigateConfig;
@ -134,6 +134,7 @@ export default function LiveCameraView({
fullscreen,
toggleFullscreen,
}: LiveCameraViewProps) {
const { t } = useTranslation(["views/live"]);
const navigate = useNavigate();
const { isPortrait } = useMobileOrientation();
const mainRef = useRef<HTMLDivElement | null>(null);
@ -435,9 +436,7 @@ export default function LiveCameraView({
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
<Trans>button.back</Trans>
</div>
<div className="text-primary">{t("button.back")}</div>
)}
</Button>
<Button
@ -459,9 +458,7 @@ export default function LiveCameraView({
>
<LuHistory className="size-5 text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
<Trans>button.history</Trans>
</div>
<div className="text-primary">{t("button.history")}</div>
)}
</Button>
</div>
@ -482,7 +479,7 @@ export default function LiveCameraView({
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && (
<div className="text-secondary-foreground">
<Trans>button.back</Trans>
{t("button.back")}
</div>
)}
</Button>
@ -681,6 +678,7 @@ function PtzControlPanel({
clickOverlay: boolean;
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { t } = useTranslation(["views/live"]);
const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`);
const { send: sendPtz } = usePtzCommand(camera);
@ -766,7 +764,7 @@ function PtzControlPanel({
{ptz?.features?.includes("pt") && (
<>
<TooltipButton
label={t("ptz.move.left.label", { ns: "views/live" })}
label={t("ptz.move.left.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_LEFT");
@ -781,7 +779,7 @@ function PtzControlPanel({
<FaAngleLeft />
</TooltipButton>
<TooltipButton
label={t("ptz.move.up.label", { ns: "views/live" })}
label={t("ptz.move.up.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_UP");
@ -796,7 +794,7 @@ function PtzControlPanel({
<FaAngleUp />
</TooltipButton>
<TooltipButton
label={t("ptz.move.down.label", { ns: "views/live" })}
label={t("ptz.move.down.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_DOWN");
@ -811,7 +809,7 @@ function PtzControlPanel({
<FaAngleDown />
</TooltipButton>
<TooltipButton
label={t("ptz.move.right.label", { ns: "views/live" })}
label={t("ptz.move.right.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_RIGHT");
@ -830,7 +828,7 @@ function PtzControlPanel({
{ptz?.features?.includes("zoom") && (
<>
<TooltipButton
label={t("ptz.zoom.in.label", { ns: "views/live" })}
label={t("ptz.zoom.in.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("ZOOM_IN");
@ -845,7 +843,7 @@ function PtzControlPanel({
<MdZoomIn />
</TooltipButton>
<TooltipButton
label={t("ptz.zoom.out.label", { ns: "views/live" })}
label={t("ptz.zoom.out.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("ZOOM_OUT");
@ -981,6 +979,8 @@ function FrigateCameraFeatures({
supports2WayTalk,
cameraEnabled,
}: FrigateCameraFeaturesProps) {
const { t } = useTranslation(["views/live"]);
const { payload: detectState, send: sendDetect } = useDetectState(
camera.name,
);
@ -1024,15 +1024,9 @@ function FrigateCameraFeatures({
setIsRecording(true);
const toastId = toast.success(
<div className="flex flex-col space-y-3">
<div className="font-semibold">
<Trans ns="views/live">manualRecording.started</Trans>
</div>
<div className="font-semibold">{t("manualRecording.started")}</div>
{!camera.record.enabled || camera.record.retain.days == 0 ? (
<div>
<Trans ns="views/live">
manualRecording.recordDisabledTips
</Trans>
</div>
<div>{t("manualRecording.recordDisabledTips")}</div>
) : (
<OnDemandRetentionMessage camera={camera} />
)}
@ -1045,11 +1039,11 @@ function FrigateCameraFeatures({
setActiveToastId(toastId);
}
} catch (error) {
toast.error(t("manualRecording.failedToStart", { ns: "views/live" }), {
toast.error(t("manualRecording.failedToStart"), {
position: "top-center",
});
}
}, [camera]);
}, [camera, t]);
const endEvent = useCallback(() => {
if (activeToastId) {
@ -1062,16 +1056,16 @@ function FrigateCameraFeatures({
});
recordingEventIdRef.current = null;
setIsRecording(false);
toast.success(t("manualRecording.ended", { ns: "views/live" }), {
toast.success(t("manualRecording.ended"), {
position: "top-center",
});
}
} catch (error) {
toast.error(t("manualRecording.failedToEnd", { ns: "views/live" }), {
toast.error(t("manualRecording.failedToEnd"), {
position: "top-center",
});
}
}, [activeToastId]);
}, [activeToastId, t]);
const handleEventButtonClick = useCallback(() => {
if (isRecording) {
@ -1174,9 +1168,7 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={isRecording ? TbRecordMail : TbRecordMailOff}
isActive={isRecording}
title={t("manualRecording." + (isRecording ? "stop" : "start"), {
ns: "views/live",
})}
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
onClick={handleEventButtonClick}
disabled={!cameraEnabled}
/>
@ -1197,15 +1189,13 @@ function FrigateCameraFeatures({
<div className="flex flex-col gap-5 p-4">
{!isRestreamed && (
<div className="flex flex-col gap-2">
<Label>
<Trans ns="components/dialog">streaming</Trans>
</Label>
<Label>{t("streaming", { ns: "components/dialog" })}</Label>
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" />
<div>
<Trans ns="components/dialog">
streaming.restreaming.disabled
</Trans>
{t("streaming.restreaming.disabled", {
ns: "components/dialog",
})}
</div>
<Popover>
<PopoverTrigger asChild>
@ -1215,9 +1205,9 @@ function FrigateCameraFeatures({
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
<Trans ns="components/dialog">
streaming.restreaming.desc
</Trans>
{t("streaming.restreaming.desc", {
ns: "components/dialog",
})}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@ -1225,9 +1215,9 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="components/dialog">
streaming.restreaming.readTheDocumentation
</Trans>
{t("streaming.restreaming.readTheDocumentation", {
ns: "components/dialog",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -1408,7 +1398,7 @@ function FrigateCameraFeatures({
className="mx-0 cursor-pointer text-primary"
htmlFor="showstats"
>
<Trans ns="components/dialog">streaming.showStats</Trans>
{t("streaming.showStats", { ns: "components/dialog" })}
</Label>
<Switch
className="ml-1"
@ -1418,12 +1408,12 @@ function FrigateCameraFeatures({
/>
</div>
<p className="text-sm text-muted-foreground">
<Trans ns="components/dialog">streaming.showStats.desc</Trans>
{t("streaming.showStats.desc", { ns: "components/dialog" })}
</p>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between text-sm font-medium leading-none">
<Trans ns="components/dialog">streaming.debugView</Trans>
{t("streaming.debugView", { ns: "components/dialog" })}
<LuExternalLink
onClick={() =>
navigate(`/settings?page=debug&camera=${camera.name}`)
@ -1514,15 +1504,13 @@ function FrigateCameraFeatures({
<div className="mt-3 flex flex-col gap-5">
{!isRestreamed && (
<div className="flex flex-col gap-2 p-2">
<Label>
<Trans ns="components/dialog">streaming</Trans>
</Label>
<Label>{t("streaming", { ns: "components/dialog" })}</Label>
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" />
<div>
<Trans ns="components/dialog">
streaming.restreaming.disabled
</Trans>
{t("streaming.restreaming.disabled", {
ns: "components/dialog",
})}
</div>
<Popover>
<PopoverTrigger asChild>
@ -1532,9 +1520,9 @@ function FrigateCameraFeatures({
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
<Trans ns="components/dialog">
streaming.restreaming.desc
</Trans>
{t("streaming.restreaming.desc", {
ns: "components/dialog",
})}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@ -1542,9 +1530,9 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="components/dialog">
streaming.restreaming.readTheDocumentation
</Trans>
{t("streaming.restreaming.readTheDocumentation", {
ns: "components/dialog",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -1697,9 +1685,7 @@ function FrigateCameraFeatures({
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
)}
>
<Trans ns="views/live">
manualRecording.{isRecording ? "end" : "start"}
</Trans>
{t("manualRecording." + isRecording ? "end" : "start")}
</Button>
<p className="text-sm text-muted-foreground">
Start a manual event based on this camera's recording retention

View File

@ -47,9 +47,9 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
import { useResizeObserver } from "@/hooks/resize-observer";
import { cn } from "@/lib/utils";
import { useFullscreen } from "@/hooks/use-fullscreen";
import { Trans } from "react-i18next";
import { useTimezone } from "@/hooks/use-date-utils";
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
import { useTranslation } from "react-i18next";
type RecordingViewProps = {
startCamera: string;
@ -73,6 +73,7 @@ export function RecordingView({
filter,
updateFilter,
}: RecordingViewProps) {
const { t } = useTranslation(["views/events"]);
const { data: config } = useSWR<FrigateConfig>("config");
const navigate = useNavigate();
const contentRef = useRef<HTMLDivElement | null>(null);
@ -400,9 +401,7 @@ export function RecordingView({
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
<Trans>button.back</Trans>
</div>
<div className="text-primary">{t("button.back")}</div>
)}
</Button>
<Button
@ -414,11 +413,7 @@ export function RecordingView({
}}
>
<FaVideo className="size-5 text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
<Trans>menu.live</Trans>
</div>
)}
{isDesktop && <div className="text-primary">{t("menu.live")}</div>}
</Button>
</div>
<div className="flex items-center justify-end gap-2">
@ -493,18 +488,14 @@ export function RecordingView({
value="timeline"
aria-label="Select timeline"
>
<div className="">
<Trans ns="views/events">timeline</Trans>
</div>
<div className="">{t("timeline")}</div>
</ToggleGroupItem>
<ToggleGroupItem
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
value="events"
aria-label="Select events"
>
<div className="">
<Trans ns="views/events">events.label</Trans>
</div>
<div className="">{t("events.label")}</div>
</ToggleGroupItem>
</ToggleGroup>
) : (
@ -702,6 +693,7 @@ function Timeline({
setScrubbing,
setExportRange,
}: TimelineProps) {
const { t } = useTranslation(["views/events"]);
const internalTimelineRef = useRef<HTMLDivElement>(null);
const selectedTimelineRef = timelineRef || internalTimelineRef;
@ -811,7 +803,7 @@ function Timeline({
>
{mainCameraReviewItems.length === 0 ? (
<div className="mt-5 text-center text-primary">
<Trans ns="views/events">events.noFoundForTimePeriod</Trans>
{t("events.noFoundForTimePeriod")}
</div>
) : (
mainCameraReviewItems.map((review) => {

View File

@ -31,7 +31,7 @@ import {
import Chip from "@/components/indicators/Chip";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import SearchActionGroup from "@/components/filter/SearchActionGroup";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type SearchViewProps = {
search: string;
@ -71,6 +71,7 @@ export default function SearchView({
setColumns,
setDefaultView,
}: SearchViewProps) {
const { t } = useTranslation(["views/explore"]);
const contentRef = useRef<HTMLDivElement | null>(null);
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
@ -521,7 +522,7 @@ export default function SearchView({
{uniqueResults?.length == 0 && !isLoading && (
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
<LuSearchX className="size-16" />
<Trans ns="views/explore">noTrackedObjects</Trans>
{t("noTrackedObjects")}
</div>
)}

View File

@ -13,8 +13,7 @@ import { toast } from "sonner";
import DeleteUserDialog from "@/components/overlay/DeleteUserDialog";
import { HiTrash } from "react-icons/hi";
import { FaUserEdit } from "react-icons/fa";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { LuPlus, LuShield, LuUserCog } from "react-icons/lu";
import {
Table,
@ -32,8 +31,10 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import RoleChangeDialog from "@/components/overlay/RoleChangeDialog";
import { useTranslation } from "react-i18next";
export default function AuthenticationView() {
const { t } = useTranslation("views/settings");
const { data: config } = useSWR<FrigateConfig>("config");
const { data: users, mutate: mutateUsers } = useSWR<User[]>("users");
@ -51,33 +52,35 @@ export default function AuthenticationView() {
document.title = "Authentication Settings - Frigate";
}, []);
const onSavePassword = useCallback((user: string, password: string) => {
axios
.put(`users/${user}/password`, { password })
.then((response) => {
if (response.status === 200) {
setShowSetPassword(false);
toast.success("Password updated successfully", {
position: "top-center",
});
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("users.toast.error.setPasswordFailed", {
ns: "views/settings",
errorMessage,
}),
{
position: "top-center",
},
);
});
}, []);
const onSavePassword = useCallback(
(user: string, password: string) => {
axios
.put(`users/${user}/password`, { password })
.then((response) => {
if (response.status === 200) {
setShowSetPassword(false);
toast.success("Password updated successfully", {
position: "top-center",
});
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("users.toast.error.setPasswordFailed", {
errorMessage,
}),
{
position: "top-center",
},
);
});
},
[t],
);
const onCreate = (
user: string,
@ -93,12 +96,9 @@ export default function AuthenticationView() {
users?.push({ username: user, role: role });
return users;
}, false);
toast.success(
t("users.toast.success.createUser", { ns: "views/settings", user }),
{
position: "top-center",
},
);
toast.success(t("users.toast.success.createUser", { user }), {
position: "top-center",
});
}
})
.catch((error) => {
@ -108,7 +108,6 @@ export default function AuthenticationView() {
"Unknown error";
toast.error(
t("users.toast.error.createUserFailed", {
ns: "views/settings",
errorMessage,
}),
{
@ -128,12 +127,9 @@ export default function AuthenticationView() {
(users) => users?.filter((u) => u.username !== user),
false,
);
toast.success(
t("users.toast.success.deleteUser", { ns: "views/settings", user }),
{
position: "top-center",
},
);
toast.success(t("users.toast.success.deleteUser", { user }), {
position: "top-center",
});
}
})
.catch((error) => {
@ -143,7 +139,6 @@ export default function AuthenticationView() {
"Unknown error";
toast.error(
t("users.toast.error.deleteUserFailed", {
ns: "views/settings",
errorMessage,
}),
{
@ -199,10 +194,10 @@ export default function AuthenticationView() {
<div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start">
<Heading as="h3" className="my-2">
<Trans ns="views/settings">users.management</Trans>
{t("users.management")}
</Heading>
<p className="text-sm text-muted-foreground">
<Trans ns="views/settings">users.management.desc</Trans>
{t("users.management.desc")}
</p>
</div>
<Button
@ -212,7 +207,7 @@ export default function AuthenticationView() {
onClick={() => setShowCreate(true)}
>
<LuPlus className="size-4" />
<Trans ns="views/settings">users.addUser</Trans>
{t("users.addUser")}
</Button>
</div>
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
@ -222,13 +217,11 @@ export default function AuthenticationView() {
<TableHeader className="sticky top-0 bg-muted/50">
<TableRow>
<TableHead className="w-[250px]">
<Trans ns="views/settings">users.table.username</Trans>
</TableHead>
<TableHead>
<Trans ns="views/settings">users.table.role</Trans>
{t("users.table.username")}
</TableHead>
<TableHead>{t("users.table.role")}</TableHead>
<TableHead className="text-right">
<Trans ns="views/settings">users.table.actions</Trans>
{t("users.table.actions")}
</TableHead>
</TableRow>
</TableHeader>
@ -236,7 +229,7 @@ export default function AuthenticationView() {
{users.length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="h-24 text-center">
<Trans ns="views/settings">users.table.noUsers</Trans>
{t("users.table.noUsers")}
</TableCell>
</TableRow>
) : (
@ -263,7 +256,7 @@ export default function AuthenticationView() {
: ""
}
>
<Trans>role.{user.role || "viewer"}</Trans>
{t("role." + user.role || "viewer")}
</Badge>
</TableCell>
<TableCell className="text-right">
@ -287,16 +280,12 @@ export default function AuthenticationView() {
>
<LuUserCog className="size-3.5" />
<span className="ml-1.5 hidden sm:inline-block">
<Trans>role.title</Trans>
{t("role.title")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
<Trans ns="views/settings">
users.table.changeRole
</Trans>
</p>
<p>{t("users.table.changeRole")}</p>
</TooltipContent>
</Tooltip>
)}
@ -314,18 +303,12 @@ export default function AuthenticationView() {
>
<FaUserEdit className="size-3.5" />
<span className="ml-1.5 hidden sm:inline-block">
<Trans ns="views/settings">
users.table.password
</Trans>
{t("users.table.password")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
<Trans ns="views/settings">
users.updatePassword
</Trans>
</p>
<p>{t("users.updatePassword")}</p>
</TooltipContent>
</Tooltip>
@ -343,16 +326,12 @@ export default function AuthenticationView() {
>
<HiTrash className="size-3.5" />
<span className="ml-1.5 hidden sm:inline-block">
<Trans>button.delete</Trans>
{t("button.delete")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
<Trans ns="views/settings">
users.table.deleteUser
</Trans>
</p>
<p>{t("users.table.deleteUser")}</p>
</TooltipContent>
</Tooltip>
)}

View File

@ -27,11 +27,11 @@ import { LuExternalLink } from "react-icons/lu";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { MdCircle } from "react-icons/md";
import { cn } from "@/lib/utils";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useAlertsState, useDetectionsState, useEnabledState } from "@/api/ws";
import { useTranslation } from "react-i18next";
type CameraSettingsViewProps = {
selectedCamera: string;
@ -47,6 +47,7 @@ export default function CameraSettingsView({
selectedCamera,
setUnsavedChanges,
}: CameraSettingsViewProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@ -81,7 +82,7 @@ export default function CameraSettingsView({
.map((label) => t(label, { ns: "objects" }))
.join(", ")
: "";
}, [cameraConfig]);
}, [cameraConfig, t]);
const detectionsLabels = useMemo(() => {
return cameraConfig?.review.detections.labels
@ -89,7 +90,7 @@ export default function CameraSettingsView({
.map((label) => t(label, { ns: "objects" }))
.join(", ")
: "";
}, [cameraConfig]);
}, [cameraConfig, t]);
// form
@ -190,7 +191,7 @@ export default function CameraSettingsView({
setIsLoading(false);
});
},
[updateConfig, setIsLoading, selectedCamera, cameraConfig],
[updateConfig, setIsLoading, selectedCamera, cameraConfig, t],
);
const onCancel = useCallback(() => {
@ -259,13 +260,13 @@ export default function CameraSettingsView({
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
<Heading as="h3" className="my-2">
<Trans ns="views/settings">camera.title</Trans>
{t("camera.title")}
</Heading>
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">camera.streams.title</Trans>
{t("camera.streams.title")}
</Heading>
<div className="flex flex-row items-center">
@ -278,18 +279,16 @@ export default function CameraSettingsView({
}}
/>
<div className="space-y-0.5">
<Label htmlFor="camera-enabled">
<Trans>button.enabled</Trans>
</Label>
<Label htmlFor="camera-enabled">{t("button.enabled")}</Label>
</div>
</div>
<div className="mt-3 text-sm text-muted-foreground">
<Trans ns="views/settings">camera.streams.desc</Trans>
{t("camera.streams.desc")}
</div>
<Separator className="mb-2 mt-4 flex bg-secondary" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">camera.review.title</Trans>
{t("camera.review.title")}
</Heading>
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 space-y-3 text-sm text-primary-variant">
@ -304,7 +303,7 @@ export default function CameraSettingsView({
/>
<div className="space-y-0.5">
<Label htmlFor="alerts-enabled">
<Trans ns="views/settings">camera.review.alerts</Trans>
{t("camera.review.alerts")}
</Label>
</div>
</div>
@ -320,12 +319,12 @@ export default function CameraSettingsView({
/>
<div className="space-y-0.5">
<Label htmlFor="detections-enabled">
<Trans ns="views/settings">camera.review.detections</Trans>
{t("camera.review.detections")}
</Label>
</div>
</div>
<div className="mt-3 text-sm text-muted-foreground">
<Trans ns="views/settings">camera.review.desc</Trans>
{t("camera.review.desc")}
</div>
</div>
</div>
@ -333,16 +332,12 @@ export default function CameraSettingsView({
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">camera.reviewClassification.title</Trans>
{t("camera.reviewClassification.title")}
</Heading>
<div className="max-w-6xl">
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
<p>
<Trans ns="views/settings">
camera.reviewClassification.desc
</Trans>
</p>
<p>{t("camera.reviewClassification.desc")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/review"
@ -350,9 +345,7 @@ export default function CameraSettingsView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
camera.reviewClassification.readTheDocumentation
</Trans>{" "}
{t("camera.reviewClassification.readTheDocumentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -381,15 +374,13 @@ export default function CameraSettingsView({
<>
<div className="mb-2">
<FormLabel className="flex flex-row items-center text-base">
<Trans ns="views/settings">
camera.review.alerts
</Trans>
{t("camera.review.alerts")}
<MdCircle className="ml-3 size-2 text-severity_alert" />
</FormLabel>
<FormDescription>
<Trans ns="views/settings">
camera.reviewClassification.selectAlertsZones
</Trans>
{t(
"camera.reviewClassification.selectAlertsZones",
)}
</FormDescription>
</div>
<div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
@ -438,9 +429,7 @@ export default function CameraSettingsView({
</>
) : (
<div className="font-normal text-destructive">
<Trans ns="views/settings">
camera.reviewClassification.noDefinedZones
</Trans>
{t("camera.reviewClassification.noDefinedZones")}
</div>
)}
<FormMessage />
@ -461,7 +450,6 @@ export default function CameraSettingsView({
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
ns: "views/settings",
},
)
: t("camera.reviewClassification.objectAlertsTips", {
@ -469,7 +457,6 @@ export default function CameraSettingsView({
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
ns: "views/settings",
})}
</div>
</FormItem>
@ -485,16 +472,14 @@ export default function CameraSettingsView({
<>
<div className="mb-2">
<FormLabel className="flex flex-row items-center text-base">
<Trans ns="views/settings">
camera.review.detections
</Trans>
{t("camera.review.detections")}
<MdCircle className="ml-3 size-2 text-severity_detection" />
</FormLabel>
{selectDetections && (
<FormDescription>
<Trans ns="views/settings">
camera.reviewClassification.selectDetectionsZones
</Trans>
{t(
"camera.reviewClassification.selectDetectionsZones",
)}
</FormDescription>
)}
</div>
@ -557,9 +542,9 @@ export default function CameraSettingsView({
htmlFor="select-detections"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<Trans ns="views/settings">
camera.reviewClassification.limitDetections
</Trans>
{t(
"camera.reviewClassification.limitDetections",
)}
</label>
</div>
</div>
@ -568,58 +553,51 @@ export default function CameraSettingsView({
<div className="text-sm">
{watchedDetectionsZones &&
watchedDetectionsZones.length > 0 ? (
!selectDetections ? (
<Trans
i18nKey="camera.reviewClassification.zoneObjectDetectionsTips"
values={{
watchedDetectionsZones.length > 0
? !selectDetections
? t(
"camera.reviewClassification.zoneObjectDetectionsTips",
{
detectionsLabels,
zone: watchedDetectionsZones
.map((zone) =>
capitalizeFirstLetter(zone).replaceAll(
"_",
" ",
),
)
.join(", "),
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
},
)
: t(
"camera.reviewClassification.zoneObjectDetectionsTips.notSelectDetections",
{
detectionsLabels,
zone: watchedDetectionsZones
.map((zone) =>
capitalizeFirstLetter(zone).replaceAll(
"_",
" ",
),
)
.join(", "),
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
},
)
: t(
"camera.reviewClassification.objectDetectionsTips",
{
detectionsLabels,
zone: watchedDetectionsZones
.map((zone) =>
capitalizeFirstLetter(zone).replaceAll(
"_",
" ",
),
)
.join(", "),
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
}}
ns="views/settings"
></Trans>
) : (
<Trans
i18nKey="camera.reviewClassification.zoneObjectDetectionsTips.notSelectDetections"
values={{
detectionsLabels,
zone: watchedDetectionsZones
.map((zone) =>
capitalizeFirstLetter(zone).replaceAll(
"_",
" ",
),
)
.join(", "),
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
}}
ns="views/settings"
/>
)
) : (
<Trans
i18nKey="camera.reviewClassification.objectDetectionsTips"
values={{
detectionsLabels,
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
}}
ns="views/settings"
/>
)}
},
)}
</div>
</FormItem>
)}
@ -634,7 +612,7 @@ export default function CameraSettingsView({
onClick={onCancel}
type="button"
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -646,12 +624,10 @@ export default function CameraSettingsView({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>

View File

@ -37,8 +37,9 @@ import PolygonItem from "@/components/settings/PolygonItem";
import { Link } from "react-router-dom";
import { isDesktop } from "react-device-detect";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { Trans } from "react-i18next";
import { useSearchEffect } from "@/hooks/use-overlay-state";
import { useTranslation } from "react-i18next";
type MasksAndZoneViewProps = {
selectedCamera: string;
@ -51,6 +52,7 @@ export default function MasksAndZonesView({
selectedZoneMask,
setUnsavedChanges,
}: MasksAndZoneViewProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config } = useSWR<FrigateConfig>("config");
const [allPolygons, setAllPolygons] = useState<Polygon[]>([]);
const [editingPolygons, setEditingPolygons] = useState<Polygon[]>([]);
@ -481,7 +483,7 @@ export default function MasksAndZonesView({
{editPane === undefined && (
<>
<Heading as="h3" className="my-2">
<Trans ns="views/settings">menu.masksAndZones</Trans>
{t("menu.masksAndZones")}
</Heading>
<div className="flex w-full flex-col">
{(selectedZoneMask === undefined ||
@ -491,18 +493,12 @@ export default function MasksAndZonesView({
<HoverCard>
<HoverCardTrigger asChild>
<div className="text-md cursor-default">
<Trans ns="views/settings">
masksAndZones.zones.label
</Trans>
{t("masksAndZones.zones.label")}
</div>
</HoverCardTrigger>
<HoverCardContent>
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
<p>
<Trans ns="views/settings">
masksAndZones.zones.desc
</Trans>
</p>
<p>{t("masksAndZones.zones.desc")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/zones"
@ -510,9 +506,7 @@ export default function MasksAndZonesView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
masksAndZones.zones.desc.documentation
</Trans>{" "}
{t("masksAndZones.zones.desc.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -534,9 +528,7 @@ export default function MasksAndZonesView({
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans ns="views/settings">
masksAndZones.zones.add
</Trans>
{t("masksAndZones.zones.add")}
</TooltipContent>
</Tooltip>
</div>
@ -567,18 +559,12 @@ export default function MasksAndZonesView({
<HoverCard>
<HoverCardTrigger asChild>
<div className="text-md cursor-default">
<Trans ns="views/settings">
masksAndZones.motionMasks.label
</Trans>
{t("masksAndZones.motionMasks.label")}
</div>
</HoverCardTrigger>
<HoverCardContent>
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
<p>
<Trans ns="views/settings">
masksAndZones.motionMasks.desc
</Trans>
</p>
<p>{t("masksAndZones.motionMasks.desc")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/masks#motion-masks"
@ -586,9 +572,9 @@ export default function MasksAndZonesView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
masksAndZones.motionMasks.desc.documentation
</Trans>{" "}
{t(
"masksAndZones.motionMasks.desc.documentation",
)}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -610,9 +596,7 @@ export default function MasksAndZonesView({
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans ns="views/settings">
masksAndZones.motionMasks.add
</Trans>
{t("masksAndZones.motionMasks.add")}
</TooltipContent>
</Tooltip>
</div>
@ -645,18 +629,12 @@ export default function MasksAndZonesView({
<HoverCard>
<HoverCardTrigger asChild>
<div className="text-md cursor-default">
<Trans ns="views/settings">
masksAndZones.objectMasks.label
</Trans>
{t("masksAndZones.objectMasks.label")}
</div>
</HoverCardTrigger>
<HoverCardContent>
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
<p>
<Trans ns="views/settings">
masksAndZones.objectMasks.desc
</Trans>
</p>
<p>{t("masksAndZones.objectMasks.desc")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/masks#object-filter-masks"
@ -664,9 +642,7 @@ export default function MasksAndZonesView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
masksAndZones.objectMasks.documentation
</Trans>{" "}
{t("masksAndZones.objectMasks.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -688,9 +664,7 @@ export default function MasksAndZonesView({
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans ns="views/settings">
masksAndZones.objectMasks.add
</Trans>
{t("masksAndZones.objectMasks.add")}
</TooltipContent>
</Tooltip>
</div>

View File

@ -21,8 +21,7 @@ import { Separator } from "@/components/ui/separator";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type MotionTunerViewProps = {
selectedCamera: string;
@ -39,6 +38,7 @@ export default function MotionTunerView({
selectedCamera,
setUnsavedChanges,
}: MotionTunerViewProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
@ -119,12 +119,9 @@ export default function MotionTunerView({
)
.then((res) => {
if (res.status === 200) {
toast.success(
t("motionDetectionTuner.toast.success", { ns: "views/settings" }),
{
position: "top-center",
},
);
toast.success(t("motionDetectionTuner.toast.success"), {
position: "top-center",
});
setChangedValue(false);
updateConfig();
} else {
@ -150,6 +147,7 @@ export default function MotionTunerView({
motionSettings.contour_area,
motionSettings.improve_contrast,
selectedCamera,
t,
]);
const onCancel = useCallback(() => {
@ -186,12 +184,10 @@ export default function MotionTunerView({
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12">
<Heading as="h3" className="my-2">
<Trans ns="views/settings">motionDetectionTuner.title</Trans>
{t("motionDetectionTuner.title")}
</Heading>
<div className="my-3 space-y-3 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">motionDetectionTuner.desc</Trans>
</p>
<p>{t("motionDetectionTuner.desc")}</p>
<div className="flex items-center text-primary">
<Link
@ -200,9 +196,7 @@ export default function MotionTunerView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
motionDetectionTuner.desc.documentation
</Trans>{" "}
{t("motionDetectionTuner.desc.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -212,16 +206,10 @@ export default function MotionTunerView({
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md">
<Trans ns="views/settings">
motionDetectionTuner.Threshold
</Trans>
{t("motionDetectionTuner.Threshold")}
</Label>
<div className="my-2 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
motionDetectionTuner.Threshold.desc
</Trans>
</p>
<p>{t("motionDetectionTuner.Threshold.desc")}</p>
</div>
</div>
<div className="flex flex-row justify-between">
@ -245,16 +233,10 @@ export default function MotionTunerView({
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md">
<Trans ns="views/settings">
motionDetectionTuner.contourArea
</Trans>
{t("motionDetectionTuner.contourArea")}
</Label>
<div className="my-2 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
motionDetectionTuner.contourArea.desc
</Trans>
</p>
<p>{t("motionDetectionTuner.contourArea.desc")}</p>
</div>
</div>
<div className="flex flex-row justify-between">
@ -279,14 +261,10 @@ export default function MotionTunerView({
<div className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="improve-contrast">
<Trans ns="views/settings">
motionDetectionTuner.improveContrast
</Trans>
{t("motionDetectionTuner.improveContrast")}
</Label>
<div className="text-sm text-muted-foreground">
<Trans ns="views/settings">
motionDetectionTuner.improveContrast.desc
</Trans>
{t("motionDetectionTuner.improveContrast.desc")}
</div>
</div>
<Switch
@ -307,7 +285,7 @@ export default function MotionTunerView({
aria-label="Reset"
onClick={onCancel}
>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
<Button
variant="select"
@ -319,12 +297,10 @@ export default function MotionTunerView({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>

View File

@ -18,10 +18,10 @@ import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { FrigateConfig } from "@/types/frigateConfig";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { t } from "i18next";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { Trans } from "react-i18next";
import { LuCheck, LuExternalLink, LuX } from "react-icons/lu";
import { CiCircleAlert } from "react-icons/ci";
import { Link } from "react-router-dom";
@ -43,6 +43,7 @@ import {
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import FilterSwitch from "@/components/filter/FilterSwitch";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useTranslation } from "react-i18next";
const NOTIFICATION_SERVICE_WORKER = "notifications-worker.js";
@ -58,6 +59,8 @@ type NotificationsSettingsViewProps = {
export default function NotificationView({
setUnsavedChanges,
}: NotificationsSettingsViewProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config, mutate: updateConfig } = useSWR<FrigateConfig>(
"config",
{
@ -351,16 +354,12 @@ export default function NotificationView({
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
<div className="col-span-1">
<Heading as="h3" className="my-2">
<Trans ns="views/settings">
notification.notificationSettings
</Trans>
{t("notification.notificationSettings")}
</Heading>
<div className="max-w-6xl">
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
<p>
<Trans ns="views/settings">notification.desc</Trans>
</p>
<p>{t("notification.desc")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/notifications"
@ -368,9 +367,7 @@ export default function NotificationView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
notification.documentation
</Trans>{" "}
{t("notification.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -387,22 +384,16 @@ export default function NotificationView({
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans ns="views/settings">notification.email</Trans>
</FormLabel>
<FormLabel>{t("notification.email")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72"
placeholder={t("notification.email.placeholder", {
ns: "views/settings",
})}
placeholder={t("notification.email.placeholder")}
{...field}
/>
</FormControl>
<FormDescription>
<Trans ns="views/settings">
notification.email.desc
</Trans>
{t("notification.email.desc")}
</FormDescription>
<FormMessage />
</FormItem>
@ -418,9 +409,7 @@ export default function NotificationView({
<>
<div className="mb-2">
<FormLabel className="flex flex-row items-center text-base">
<Trans ns="views/settings">
notification.cameras
</Trans>
{t("notification.cameras")}
</FormLabel>
</div>
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
@ -470,17 +459,13 @@ export default function NotificationView({
</>
) : (
<div className="font-normal text-destructive">
<Trans ns="views/settings">
notification.cameras.noCameras
</Trans>
{t("notification.cameras.noCameras")}
</div>
)}
<FormMessage />
<FormDescription>
<Trans ns="views/settings">
notification.cameras.desc
</Trans>
{t("notification.cameras.desc")}
</FormDescription>
</FormItem>
)}
@ -493,7 +478,7 @@ export default function NotificationView({
onClick={onCancel}
type="button"
>
<Trans>button.cancel</Trans>
{t("button.cancel")}
</Button>
<Button
variant="select"
@ -505,12 +490,10 @@ export default function NotificationView({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>
@ -523,9 +506,7 @@ export default function NotificationView({
<div className="flex flex-col gap-2 md:max-w-[50%]">
<Separator className="my-2 flex bg-secondary md:hidden" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">
notification.deviceSpecific
</Trans>
{t("notification.deviceSpecific")}
</Heading>
<Button
aria-label="Register or unregister notifications for this device"
@ -569,12 +550,8 @@ export default function NotificationView({
}}
>
{registration != null
? t("notification.unregisterDevice", {
ns: "views/settings",
})
: t("notification.registerDevice", {
ns: "views/settings",
})}
? t("notification.unregisterDevice")
: t("notification.registerDevice")}
</Button>
{registration != null && registration.active && (
<Button
@ -634,6 +611,7 @@ export function CameraNotificationSwitch({
config,
camera,
}: CameraNotificationSwitchProps) {
const { t } = useTranslation(["views/settings"]);
const { payload: notificationState, send: sendNotification } =
useNotifications(camera);
const { payload: notificationSuspendUntil, send: sendNotificationSuspend } =

View File

@ -23,11 +23,11 @@ import { getIconForLabel } from "@/utils/iconUtil";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { LuExternalLink, LuInfo } from "react-icons/lu";
import { Link } from "react-router-dom";
import { t } from "i18next";
import { Trans } from "react-i18next";
import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer";
import { Separator } from "@/components/ui/separator";
import { isDesktop } from "react-device-detect";
import { useTranslation } from "react-i18next";
type ObjectSettingsViewProps = {
selectedCamera?: string;
@ -40,6 +40,8 @@ const emptyObject = Object.freeze({});
export default function ObjectSettingsView({
selectedCamera,
}: ObjectSettingsViewProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config } = useSWR<FrigateConfig>("config");
const containerRef = useRef<HTMLDivElement>(null);
@ -47,55 +49,45 @@ export default function ObjectSettingsView({
const DEBUG_OPTIONS = [
{
param: "bbox",
title: t("debug.boundingBoxes.title", { ns: "views/settings" }),
description: t("debug.boundingBoxes.desc", { ns: "views/settings" }),
title: t("debug.boundingBoxes.title"),
description: t("debug.boundingBoxes.desc"),
info: (
<>
<p className="mb-2">
<strong>
<Trans ns="views/settings">debug.boundingBoxes.colors</Trans>
</strong>
<strong>{t("debug.boundingBoxes.colors")}</strong>
</p>
<ul className="list-disc space-y-1 pl-5">
<Trans ns="views/settings">debug.boundingBoxes.colors.info</Trans>
{t("debug.boundingBoxes.colors.info")}
</ul>
</>
),
},
{
param: "timestamp",
title: t("debug.timestamp.title", { ns: "views/settings" }),
description: t("debug.timestamp.desc", { ns: "views/settings" }),
title: t("debug.timestamp.title"),
description: t("debug.timestamp.desc"),
},
{
param: "zones",
title: t("debug.zones.title", { ns: "views/settings" }),
description: t("debug.zones.desc", { ns: "views/settings" }),
title: t("debug.zones.title"),
description: t("debug.zones.desc"),
},
{
param: "mask",
title: t("debug.mask.title", { ns: "views/settings" }),
description: t("debug.mask.desc", { ns: "views/settings" }),
title: t("debug.mask.title"),
description: t("debug.mask.desc"),
},
{
param: "motion",
title: t("debug.motion.title", { ns: "views/settings" }),
description: t("debug.motion.desc", { ns: "views/settings" }),
info: (
<>
<Trans ns="views/settings">debug.motion.tips</Trans>
</>
),
title: t("debug.motion.title"),
description: t("debug.motion.desc"),
info: <>{t("debug.motion.tips")}</>,
},
{
param: "regions",
title: t("debug.regions.title", { ns: "views/settings" }),
description: t("debug.regions.desc", { ns: "views/settings" }),
info: (
<>
<Trans ns="views/settings">debug.regions.tips</Trans>
</>
),
title: t("debug.regions.title"),
description: t("debug.regions.desc"),
info: <>{t("debug.regions.tips")}</>,
},
];
@ -156,7 +148,7 @@ export default function ObjectSettingsView({
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12">
<Heading as="h3" className="my-2">
<Trans ns="views/settings">debug.title</Trans>
{t("debug.title")}
</Heading>
<div className="mb-5 space-y-3 text-sm text-muted-foreground">
<p>
@ -166,12 +158,9 @@ export default function ObjectSettingsView({
.map((detector) => capitalizeFirstLetter(detector))
.join(",")
: "",
ns: "views/settings",
})}
</p>
<p>
<Trans ns="views/settings">debug.desc</Trans>
</p>
<p>{t("debug.desc")}</p>
</div>
{config?.cameras[cameraConfig.name]?.webui_url && (
<div className="mb-5 text-sm text-muted-foreground">
@ -191,11 +180,9 @@ export default function ObjectSettingsView({
<Tabs defaultValue="debug" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="debug">
<Trans ns="views/settings">debug.debugging</Trans>
</TabsTrigger>
<TabsTrigger value="debug">{t("debug.debugging")}</TabsTrigger>
<TabsTrigger value="objectlist">
<Trans ns="views/settings">debug.objectList</Trans>
{t("debug.objectList")}
</TabsTrigger>
</TabsList>
<TabsContent value="debug">
@ -255,9 +242,7 @@ export default function ObjectSettingsView({
className="mb-0 cursor-pointer capitalize text-primary"
htmlFor="debugdraw"
>
<Trans ns="views/settings">
debug.objectShapeFilterDrawing.title
</Trans>
{t("debug.objectShapeFilterDrawing.title")}
</Label>
<Popover>
@ -265,14 +250,12 @@ export default function ObjectSettingsView({
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">
<Trans>button.info</Trans>
{t("button.info")}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-sm">
<Trans ns="views/settings">
debug.objectShapeFilterDrawing.tips
</Trans>
{t("debug.objectShapeFilterDrawing.tips")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/object_filters#object-shape"
@ -280,9 +263,7 @@ export default function ObjectSettingsView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
debug.objectShapeFilterDrawing.document
</Trans>
{t("debug.objectShapeFilterDrawing.document")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -290,9 +271,7 @@ export default function ObjectSettingsView({
</Popover>
</div>
<div className="mt-1 text-xs text-muted-foreground">
<Trans ns="views/settings">
debug.objectShapeFilterDrawing.desc
</Trans>
{t("debug.objectShapeFilterDrawing.desc")}
</div>
</div>
<Switch
@ -348,6 +327,7 @@ type ObjectListProps = {
};
function ObjectList({ cameraConfig, objects }: ObjectListProps) {
const { t } = useTranslation(["views/settings"]);
const { data: config } = useSWR<FrigateConfig>("config");
const colormap = useMemo(() => {
@ -441,9 +421,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
);
})
) : (
<div className="p-3 text-center">
<Trans ns="views/settings">debug.noObjects</Trans>
</div>
<div className="p-3 text-center">{t("debug.noObjects")}</div>
)}
</div>
);

View File

@ -20,8 +20,7 @@ import {
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type ExploreSettingsViewProps = {
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
@ -36,6 +35,7 @@ type ExploreSettings = {
export default function ExploreSettingsView({
setUnsavedChanges,
}: ExploreSettingsViewProps) {
const { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
@ -127,6 +127,7 @@ export default function ExploreSettingsView({
ExploreSettings.enabled,
ExploreSettings.reindex,
ExploreSettings.model_size,
t,
]);
const onCancel = useCallback(() => {
@ -163,17 +164,15 @@ export default function ExploreSettingsView({
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
<Heading as="h3" className="my-2">
<Trans ns="views/settings">explore.title</Trans>
{t("explore.title")}
</Heading>
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">explore.semanticSearch.title</Trans>
{t("explore.semanticSearch.title")}
</Heading>
<div className="max-w-6xl">
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
<p>
<Trans ns="views/settings">explore.semanticSearch.desc</Trans>
</p>
<p>{t("explore.semanticSearch.desc")}</p>
<div className="flex items-center text-primary">
<Link
@ -182,9 +181,7 @@ export default function ExploreSettingsView({
rel="noopener noreferrer"
className="inline"
>
<Trans ns="views/settings">
explore.semanticSearch.readTheDocumentation
</Trans>
{t("explore.semanticSearch.readTheDocumentation")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -203,9 +200,7 @@ export default function ExploreSettingsView({
}}
/>
<div className="space-y-0.5">
<Label htmlFor="enabled">
<Trans>button.enabled</Trans>
</Label>
<Label htmlFor="enabled">{t("button.enabled")}</Label>
</div>
</div>
<div className="flex flex-col">
@ -221,42 +216,24 @@ export default function ExploreSettingsView({
/>
<div className="space-y-0.5">
<Label htmlFor="reindex">
<Trans ns="views/settings">
explore.semanticSearch.reindexOnStartup.label
</Trans>
{t("explore.semanticSearch.reindexOnStartup.label")}
</Label>
</div>
</div>
<div className="mt-3 text-sm text-muted-foreground">
<Trans ns="views/settings">
explore.semanticSearch.reindexOnStartup.desc
</Trans>
{t("explore.semanticSearch.reindexOnStartup.desc")}
</div>
</div>
<div className="mt-2 flex flex-col space-y-6">
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="views/settings">
explore.semanticSearch.modelSize.label
</Trans>
{t("explore.semanticSearch.modelSize.label")}
</div>
<div className="space-y-1 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
explore.semanticSearch.modelSize.desc
</Trans>
</p>
<p>{t("explore.semanticSearch.modelSize.desc")}</p>
<ul className="list-disc pl-5 text-sm">
<li>
<Trans ns="views/settings">
explore.semanticSearch.modelSize.small.desc
</Trans>
</li>
<li>
<Trans ns="views/settings">
explore.semanticSearch.modelSize.large.desc
</Trans>
</li>
<li>{t("explore.semanticSearch.modelSize.small.desc")}</li>
<li>{t("explore.semanticSearch.modelSize.large.desc")}</li>
</ul>
</div>
</div>
@ -272,7 +249,6 @@ export default function ExploreSettingsView({
{t(
"explore.semanticSearch.modelSize." +
ExploreSettings.model_size,
{ ns: "views/settings" },
)}
</SelectTrigger>
<SelectContent>
@ -283,9 +259,7 @@ export default function ExploreSettingsView({
className="cursor-pointer"
value={size}
>
{t("explore.semanticSearch.modelSize." + size, {
ns: "views/settings",
})}
{t("explore.semanticSearch.modelSize." + size)}
</SelectItem>
))}
</SelectGroup>
@ -297,7 +271,7 @@ export default function ExploreSettingsView({
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button className="flex flex-1" aria-label="Reset" onClick={onCancel}>
<Trans>button.reset</Trans>
{t("button.reset")}
</Button>
<Button
variant="select"
@ -309,12 +283,10 @@ export default function ExploreSettingsView({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>
<Trans>button.saving</Trans>
</span>
<span>{t("button.saving")}</span>
</div>
) : (
<Trans>button.save</Trans>
t("button.save")
)}
</Button>
</div>

View File

@ -18,15 +18,14 @@ import {
SelectItem,
SelectTrigger,
} from "../../components/ui/select";
import { Trans } from "react-i18next";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
const WEEK_STARTS_ON = ["Sunday", "Monday"];
export default function UiSettingsView() {
const { data: config } = useSWR<FrigateConfig>("config");
const { t } = useTranslation("views/settings");
const clearStoredLayouts = useCallback(() => {
if (!config) {
return [];
@ -90,13 +89,13 @@ export default function UiSettingsView() {
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
<Heading as="h3" className="my-2">
<Trans ns="views/settings">general.title</Trans>
{t("general.title")}
</Heading>
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">general.liveDashboard.title</Trans>
{t("general.liveDashboard.title")}
</Heading>
<div className="mt-2 space-y-6">
@ -108,17 +107,11 @@ export default function UiSettingsView() {
onCheckedChange={setAutoLive}
/>
<Label className="cursor-pointer" htmlFor="auto-live">
<Trans ns="views/settings">
general.liveDashboard.automaticLiveView.label
</Trans>
{t("general.liveDashboard.automaticLiveView.label")}
</Label>
</div>
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
general.liveDashboard.automaticLiveView.desc
</Trans>
</p>
<p>{t("general.liveDashboard.automaticLiveView.desc")}</p>
</div>
</div>
<div className="space-y-3">
@ -129,17 +122,11 @@ export default function UiSettingsView() {
onCheckedChange={setAlertVideos}
/>
<Label className="cursor-pointer" htmlFor="images-only">
<Trans ns="views/settings">
general.liveDashboard.playAlertVideos.label
</Trans>
{t("general.liveDashboard.playAlertVideos.label")}
</Label>
</div>
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
general.liveDashboard.playAlertVideos.desc
</Trans>
</p>
<p>{t("general.liveDashboard.playAlertVideos.desc")}</p>
</div>
</div>
</div>
@ -148,69 +135,51 @@ export default function UiSettingsView() {
<div className="mt-2 space-y-3">
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="views/settings">general.storedLayouts.title</Trans>
{t("general.storedLayouts.title")}
</div>
<div className="my-2 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
general.storedLayouts.desc
</Trans>
</p>
<p>{t("general.storedLayouts.desc")}</p>
</div>
</div>
<Button
aria-label="Clear all saved layouts"
onClick={clearStoredLayouts}
>
<Trans ns="views/settings">
general.storedLayouts.clearAll
</Trans>
{t("general.storedLayouts.clearAll")}
</Button>
</div>
<div className="mt-2 space-y-3">
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="views/settings">
general.cameraGroupStreaming.title
</Trans>
{t("general.cameraGroupStreaming.title")}
</div>
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
general.cameraGroupStreaming.desc
</Trans>
</p>
<p>{t("general.cameraGroupStreaming.desc")}</p>
</div>
</div>
<Button
aria-label="Clear all group streaming settings"
onClick={clearStreamingSettings}
>
<Trans ns="views/settings">
general.cameraGroupStreaming.clearAll
</Trans>
{t("general.cameraGroupStreaming.clearAll")}
</Button>
</div>
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">general.recordingsViewer.title</Trans>
{t("general.recordingsViewer.title")}
</Heading>
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="views/settings">
general.recordingsViewer.defaultPlaybackRate.label
</Trans>
{t("general.recordingsViewer.defaultPlaybackRate.label")}
</div>
<div className="my-2 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
general.recordingsViewer.defaultPlaybackRate.desc
</Trans>
{t("general.recordingsViewer.defaultPlaybackRate.desc")}
</p>
</div>
</div>
@ -239,22 +208,16 @@ export default function UiSettingsView() {
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
<Trans ns="views/settings">general.calendar.title</Trans>
{t("general.calendar.title")}
</Heading>
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<div className="text-md">
<Trans ns="views/settings">
general.calendar.firstWeekday.label
</Trans>
{t("general.calendar.firstWeekday.label")}
</div>
<div className="my-2 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
general.calendar.firstWeekday.desc
</Trans>
</p>
<p>{t("general.calendar.firstWeekday.desc")}</p>
</div>
</div>
</div>
@ -266,7 +229,6 @@ export default function UiSettingsView() {
{t(
"general.calendar.firstWeekday." +
WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(),
{ ns: "views/settings" },
)}
</SelectTrigger>
<SelectContent>
@ -277,9 +239,7 @@ export default function UiSettingsView() {
className="cursor-pointer"
value={index.toString()}
>
{t("general.calendar.firstWeekday." + day.toLowerCase(), {
ns: "views/settings",
})}
{t("general.calendar.firstWeekday." + day.toLowerCase())}
</SelectItem>
))}
</SelectGroup>

View File

@ -12,7 +12,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import useSWR from "swr";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type CameraMetricsProps = {
lastUpdated: number;
@ -23,7 +23,7 @@ export default function CameraMetrics({
setLastUpdated,
}: CameraMetricsProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const { t } = useTranslation(["views/system"]);
// camera info dialog
const [showCameraInfoDialog, setShowCameraInfoDialog] = useState(false);
@ -225,14 +225,12 @@ export default function CameraMetrics({
return (
<div className="scrollbar-container mt-4 flex size-full flex-col gap-3 overflow-y-auto">
<div className="text-sm font-medium text-muted-foreground">
<Trans ns="views/system">cameras.overview</Trans>
{t("cameras.overview")}
</div>
<div className="grid grid-cols-1 md:grid-cols-3">
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">cameras.framesAndDetections</Trans>
</div>
<div className="mb-5">{t("cameras.framesAndDetections")}</div>
<CameraLineGraph
graphId="overall-stats"
unit=""
@ -300,9 +298,7 @@ export default function CameraMetrics({
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">
cameras.framesAndDetections
</Trans>
{t("cameras.framesAndDetections")}
</div>
<CameraLineGraph
graphId={`${camera.name}-dps`}

View File

@ -15,7 +15,7 @@ import GPUInfoDialog from "@/components/overlay/GPUInfoDialog";
import { Skeleton } from "@/components/ui/skeleton";
import { ThresholdBarGraph } from "@/components/graph/SystemGraph";
import { cn } from "@/lib/utils";
import { Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
type GeneralMetricsProps = {
lastUpdated: number;
@ -26,7 +26,7 @@ export default function GeneralMetrics({
setLastUpdated,
}: GeneralMetricsProps) {
// extra info
const { t } = useTranslation(["views/system"]);
const [showVainfo, setShowVainfo] = useState(false);
// stats
@ -449,7 +449,7 @@ export default function GeneralMetrics({
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
<div className="text-sm font-medium text-muted-foreground">
<Trans ns="views/system">general.detector.title</Trans>
{t("general.detector.title")}
</div>
<div
className={cn(
@ -459,9 +459,7 @@ export default function GeneralMetrics({
>
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">general.detector.inferenceSpeed</Trans>
</div>
<div className="mb-5">{t("general.detector.inferenceSpeed")}</div>
{detInferenceTimeSeries.map((series) => (
<ThresholdBarGraph
key={series.name}
@ -499,9 +497,7 @@ export default function GeneralMetrics({
)}
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">general.detector.cpuUsage</Trans>
</div>
<div className="mb-5">{t("general.detector.cpuUsage")}</div>
{detCpuSeries.map((series) => (
<ThresholdBarGraph
key={series.name}
@ -519,9 +515,7 @@ export default function GeneralMetrics({
)}
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">general.detector.memoryUsage</Trans>
</div>
<div className="mb-5">{t("general.detector.memoryUsage")}</div>
{detMemSeries.map((series) => (
<ThresholdBarGraph
key={series.name}
@ -552,7 +546,7 @@ export default function GeneralMetrics({
size="sm"
onClick={() => setShowVainfo(true)}
>
<Trans ns="views/system">general.hardwareInfo.title</Trans>
{t("general.hardwareInfo.title")}
</Button>
)}
</div>
@ -565,9 +559,7 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">
general.hardwareInfo.gpuUsage
</Trans>
{t("general.hardwareInfo.gpuUsage")}
</div>
{gpuSeries.map((series) => (
<ThresholdBarGraph
@ -589,9 +581,7 @@ export default function GeneralMetrics({
{gpuMemSeries && (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">
general.hardwareInfo.gpuMemroy
</Trans>
{t("general.hardwareInfo.gpuMemroy")}
</div>
{gpuMemSeries.map((series) => (
<ThresholdBarGraph
@ -615,9 +605,7 @@ export default function GeneralMetrics({
{gpuEncSeries && gpuEncSeries?.length != 0 && (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">
general.hardwareInfo.gpuEncoder
</Trans>
{t("general.hardwareInfo.gpuEncoder")}
</div>
{gpuEncSeries.map((series) => (
<ThresholdBarGraph
@ -641,9 +629,7 @@ export default function GeneralMetrics({
{gpuDecSeries && gpuDecSeries?.length != 0 && (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">
general.hardwareInfo.gpuDecoder
</Trans>
{t("general.hardwareInfo.gpuDecoder")}
</div>
{gpuDecSeries.map((series) => (
<ThresholdBarGraph
@ -667,15 +653,13 @@ export default function GeneralMetrics({
)}
<div className="mt-4 text-sm font-medium text-muted-foreground">
<Trans ns="views/system">general.otherProcesses.title</Trans>
{t("general.otherProcesses.title")}
</div>
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">
general.otherProcesses.processCpuUsage
</Trans>
{t("general.otherProcesses.processCpuUsage")}
</div>
{otherProcessCpuSeries.map((series) => (
<ThresholdBarGraph
@ -695,9 +679,7 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
<Trans ns="views/system">
general.otherProcesses.processMemoryUsage
</Trans>
{t("general.otherProcesses.processMemoryUsage")}
</div>
{otherProcessMemSeries.map((series) => (
<ThresholdBarGraph

View File

@ -8,12 +8,12 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import useSWR from "swr";
import { Trans } from "react-i18next";
import { CiCircleAlert } from "react-icons/ci";
import { FrigateConfig } from "@/types/frigateConfig";
import { useTimezone } from "@/hooks/use-date-utils";
import { RecordingsSummary } from "@/types/review";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useTranslation } from "react-i18next";
type CameraStorage = {
[key: string]: {
@ -34,7 +34,7 @@ export default function StorageMetrics({
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const { t } = useTranslation(["views/system"]);
const timezone = useTimezone(config);
const totalStorage = useMemo(() => {
@ -77,12 +77,12 @@ export default function StorageMetrics({
return (
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
<div className="text-sm font-medium text-muted-foreground">
<Trans ns="views/system">storage.overview</Trans>
{t("storage.overview")}
</div>
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
<div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5 flex flex-row items-center justify-between">
<Trans ns="views/system">storage.recordings.title</Trans>
{t("storage.recordings.title")}
<Popover>
<PopoverTrigger asChild>
<button
@ -96,9 +96,7 @@ export default function StorageMetrics({
</button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
<Trans ns="views/system">storage.recordings.tips</Trans>
</div>
<div className="space-y-2">{t("storage.recordings.tips")}</div>
</PopoverContent>
</Popover>
</div>
@ -136,7 +134,7 @@ export default function StorageMetrics({
</div>
</div>
<div className="mt-4 text-sm font-medium text-muted-foreground">
<Trans ns="views/system">storage.cameraStorage.title</Trans>
{t("storage.cameraStorage.title")}
</div>
<div className="mt-4 bg-background_alt p-2.5 md:rounded-2xl">
<CombinedStorageGraph