Refactor and clean up i18n (#17198)

* clean up i18n

* fix key

* fix key
This commit is contained in:
Josh Hawkins
2025-03-17 06:26:01 -06:00
committed by GitHub
parent 6e3ae0afc2
commit 03da70cb81
74 changed files with 1243 additions and 799 deletions
+3 -1
View File
@@ -293,7 +293,9 @@ export default function ReviewCard({
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Trans ns="components/dialog">recording.confirmDelete.desc</Trans>
<Trans ns="components/dialog">
recording.confirmDelete.desc.selected
</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
@@ -33,7 +33,7 @@ export default function SearchThumbnailFooter({
searchResult.start_time,
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
: t("time.formattedTimestampExcludeSeconds.12hour", { ns: "common" }),
config?.ui.timezone,
);
+3 -1
View File
@@ -41,7 +41,7 @@ const timeAgo = ({
const elapsed: number = elapsedTime / 1000;
if (elapsed < 10) {
return t("time.justNow");
return t("time.justNow", { ns: "common" });
}
for (let i = 0; i < timeUnits.length; i++) {
@@ -66,6 +66,7 @@ const timeAgo = ({
if (monthDiff > 0) {
const unitAmount = monthDiff;
return t("time.ago", {
ns: "common",
timeAgo: t(`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, {
time: unitAmount,
}),
@@ -74,6 +75,7 @@ const timeAgo = ({
} else if (elapsed >= timeUnits[i].value) {
const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
return t("time.ago", {
ns: "common",
timeAgo: t(`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, {
time: unitAmount,
}),
@@ -28,7 +28,7 @@ export default function CalendarFilterButton({
day,
updateSelectedDay,
}: CalendarFilterButtonProps) {
const { t } = useTranslation(["components/filter"]);
const { t } = useTranslation(["components/filter", "views/events"]);
const [open, setOpen] = useState(false);
const selectedDate = useFormattedTimestamp(
day == undefined ? 0 : day?.getTime() / 1000 + 1,
@@ -38,7 +38,7 @@ export default function CalendarFilterButton({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label={t("date.selectDateBy.label")}
aria-label={t("explore.date.selectDateBy.label")}
variant={day == undefined ? "default" : "select"}
size="sm"
>
@@ -109,7 +109,7 @@ export function CalendarRangeFilterButton({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label={t("date.selectDateBy.label")}
aria-label={t("explore.date.selectDateBy.label")}
variant={range == undefined ? "default" : "select"}
size="sm"
>
@@ -279,7 +279,7 @@ function NewGroupDialog({
setOpen(false);
setEditState("none");
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -296,9 +296,12 @@ function NewGroupDialog({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{
position: "top-center",
},
);
})
.finally(() => {
setIsLoading(false);
@@ -485,9 +488,7 @@ export function EditGroupDialog({
<div className="scrollbar-container flex flex-col overflow-y-auto md:my-4">
<Header className="mt-2" onClose={() => setOpen(false)}>
<Title>{t("group.edit")}</Title>
<Description className="sr-only">
{t("group.edit.desc")}
</Description>
<Description className="sr-only">{t("group.edit")}</Description>
</Header>
<CameraGroupEdit
@@ -538,7 +539,9 @@ export function CameraGroupRow({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("group.delete.confirm")}</AlertDialogTitle>
<AlertDialogTitle>
{t("group.delete.confirm.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Trans ns="components/camera" values={{ name: group[0] }}>
@@ -734,7 +737,7 @@ export function CameraGroupEdit({
.then(async (res) => {
if (res.status === 200) {
toast.success(
t("group.toast.success", {
t("group.success", {
name: values.name,
}),
{
@@ -748,7 +751,7 @@ export function CameraGroupEdit({
setAllGroupsStreamingSettings(updatedSettings);
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -764,7 +767,7 @@ export function CameraGroupEdit({
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage,
ns: "common",
}),
@@ -38,17 +38,17 @@ export function CamerasFilterButton({
const buttonText = useMemo(() => {
if (isMobile) {
return t("menu.live.cameras", { ns: "common" });
return t("menu.live.cameras.title", { ns: "common" });
}
if (!selectedCameras || selectedCameras.length == 0) {
return t("menu.live.allCameras", { ns: "common" });
}
return t("menu.live.cameras.count", {
ns: "common",
count: selectedCameras.includes("birdseye")
? selectedCameras.length - 1
: selectedCameras.length,
ns: "common",
});
}, [selectedCameras, t]);
@@ -158,7 +158,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")}
label={t("cameras.all.title")}
onCheckedChange={(isChecked) => {
if (isChecked) {
setCurrentCameras(undefined);
@@ -53,7 +53,7 @@ export function LogSettingsButton({
<DropdownMenuSeparator />
<div className="space-y-4">
<div className="space-y-0.5">
<div className="text-md">{t("logSettings.loading")}</div>
<div className="text-md">{t("logSettings.loading.title")}</div>
<div className="mt-2.5 flex flex-col gap-2.5">
<div className="space-y-1 text-xs text-muted-foreground">
{t("logSettings.loading.desc")}
@@ -442,7 +442,7 @@ export function GeneralFilterContent({
onReset,
onClose,
}: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
const { t } = useTranslation(["components/filter", "views/events"]);
return (
<>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
@@ -476,7 +476,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
{t("labels.all")}
{t("labels.all.title")}
</Label>
<Switch
className="ml-1"
@@ -523,7 +523,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allZones"
>
{t("zones.all")}
{t("zones.all.title")}
</Label>
<Switch
className="ml-1"
@@ -600,7 +600,7 @@ function ShowMotionOnlyButton({
motionOnly,
setMotionOnly,
}: ShowMotionOnlyButtonProps) {
const { t } = useTranslation(["views/events"]);
const { t } = useTranslation(["views/events", "components/filter"]);
const [motionOnlyButton, setMotionOnlyButton] = useOptimisticState(
motionOnly,
setMotionOnly,
@@ -627,7 +627,7 @@ function ShowMotionOnlyButton({
<Button
size="sm"
className="duration-0"
aria-label={t("motion.showMotionOnly", { ns: "components/filter" })}
aria-label={t("motion.only")}
variant={motionOnlyButton ? "select" : "default"}
onClick={() => setMotionOnlyButton(!motionOnlyButton)}
>
@@ -27,7 +27,7 @@ export default function SearchActionGroup({
setSelectedObjects,
pullLatestData,
}: SearchActionGroupProps) {
const { t } = useTranslation(["views/filter"]);
const { t } = useTranslation(["components/filter"]);
const onClearSelected = useCallback(() => {
setSelectedObjects([]);
}, [setSelectedObjects]);
@@ -198,7 +198,7 @@ export default function SearchFilterGroup({
to: new Date(filter.before * 1000),
}
}
defaultText={isMobile ? t("dates.all.short") : t("dates.all")}
defaultText={isMobile ? t("dates.all.short") : t("dates.all.title")}
updateSelectedRange={onUpdateSelectedRange}
/>
)}
@@ -244,7 +244,7 @@ function GeneralFilterButton({
}
if (!selectedLabels || selectedLabels.length == 0) {
return t("labels.all");
return t("labels.all.title");
}
if (selectedLabels.length == 1) {
@@ -338,7 +338,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
{t("labels.all")}
{t("labels.all.title")}
</Label>
<Switch
className="ml-1"
+1 -1
View File
@@ -78,7 +78,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
{t("labels.all")}
{t("labels.all.title")}
</Label>
<Switch
className="ml-1"
@@ -198,7 +198,7 @@ export function CombinedStorageGraph({
style={{ backgroundColor: item.color }}
></div>
{item.name === "Unused"
? t("storage.cameraStorage.unused")
? t("storage.cameraStorage.unused.title")
: item.name.replaceAll("_", " ")}
{item.name === "Unused" && (
<Popover>
+2 -2
View File
@@ -248,7 +248,7 @@ export default function InputWithTags({
filters.before &&
timestamp >= filters.before * 1000
) {
toast.error(t("afterDatebeEarlierBefore"), {
toast.error(t("filter.toast.error.afterDatebeEarlierBefore"), {
position: "top-center",
});
return;
@@ -727,7 +727,7 @@ export default function InputWithTags({
<div className="space-y-2">
<h3 className="font-medium">{t("filter.tips.title")}</h3>
<p className="text-sm text-muted-foreground">
{t("filter.tips.desc")}
{t("filter.tips.desc.text")}
</p>
<Trans
ns="views/search"
+47 -33
View File
@@ -15,9 +15,13 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { DialogClose } from "../ui/dialog";
} from "@/components/ui/dropdown-menu";
import {
Drawer,
DrawerContent,
DrawerTrigger,
DrawerClose,
} from "@/components/ui/drawer";
import { LuLogOut, LuSquarePen } from "react-icons/lu";
import useSWR from "swr";
@@ -32,7 +36,7 @@ type AccountSettingsProps = {
};
export default function AccountSettings({ className }: AccountSettingsProps) {
const { t } = useTranslation(["views/settings"]);
const { t } = useTranslation(["views/settings", "common"]);
const { data: profile } = useSWR("profile");
const { data: config } = useSWR("config");
const logoutUrl = config?.proxy?.logout_url || `${baseUrl}api/logout`;
@@ -42,7 +46,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
const Container = isDesktop ? DropdownMenu : Drawer;
const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
const Content = isDesktop ? DropdownMenuContent : DrawerContent;
const MenuItem = isDesktop ? DropdownMenuItem : DialogClose;
const MenuItem = isDesktop ? DropdownMenuItem : DrawerClose;
const handlePasswordSave = async (password: string) => {
if (!profile?.username || profile.username === "anonymous") return;
@@ -74,8 +78,8 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
return (
<Container modal={!isDesktop}>
<Trigger>
<Tooltip>
<Tooltip>
<Trigger asChild>
<TooltipTrigger asChild>
<div
className={cn(
@@ -89,34 +93,41 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
<VscAccount className="size-5 md:m-[6px]" />
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="right">
<p>{t("menu.user.account", { ns: "common" })}</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>
</Trigger>
</Trigger>
<TooltipPortal>
<TooltipContent side="right" sideOffset={5}>
<p>{t("menu.user.account", { ns: "common" })}</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Content
className={
isDesktop ? "mr-5 w-72" : "max-h-[75dvh] overflow-hidden p-2"
}
className={cn(
isDesktop ? "mr-5 w-72" : "max-h-[75dvh] overflow-hidden p-4",
)}
>
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
<DropdownMenuLabel>
{t("menu.user.current", {
ns: "common",
user:
profile?.username || t("menu.user.anonymous", { ns: "common" }),
})}{" "}
{t("role." + profile?.role) &&
`(${t("role." + profile?.role, { ns: "common" })})`}
<DropdownMenuLabel className="flex flex-col gap-1.5">
<div>
{t("menu.user.current", {
ns: "common",
user:
profile?.username ||
t("menu.user.anonymous", { ns: "common" }),
})}{" "}
{t("role." + profile?.role) &&
`(${t("role." + profile?.role, { ns: "common" })})`}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} />
<DropdownMenuSeparator className={isDesktop ? "my-2" : "my-2"} />
{profile?.username && profile.username !== "anonymous" && (
<MenuItem
className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
}
className={cn(
"flex w-full items-center gap-2",
isDesktop ? "cursor-pointer" : "p-2 text-sm",
)}
aria-label={t("menu.user.setPassword", { ns: "common" })}
onClick={() => setPasswordDialogOpen(true)}
>
@@ -124,13 +135,16 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
<span>{t("menu.user.setPassword", { ns: "common" })}</span>
</MenuItem>
)}
<MenuItem
className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
}
className={cn(
"flex w-full items-center gap-2",
isDesktop ? "cursor-pointer" : "p-2 text-sm",
)}
asChild
aria-label={t("menu.user.logout", { ns: "common" })}
>
<a className="flex" href={logoutUrl}>
<a href={logoutUrl} className="flex items-center gap-2">
<LuLogOut className="mr-2 size-4" />
<span>{t("menu.user.logout", { ns: "common" })}</span>
</a>
+7 -5
View File
@@ -68,7 +68,7 @@ type GeneralSettingsProps = {
};
export default function GeneralSettings({ className }: GeneralSettingsProps) {
const { t } = useTranslation(["common"]);
const { t } = useTranslation(["common", "views/settings"]);
const { data: profile } = useSWR("profile");
const { data: config } = useSWR<FrigateConfig>("config");
const logoutUrl = config?.proxy?.logout_url || "/api/logout";
@@ -100,7 +100,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
if (response.status === 200) {
setPasswordDialogOpen(false);
toast.success(
t("users.toast.success.updatePassword", { ns: "views/settings" }),
t("users.toast.success.updatePassword", {
ns: "views/settings",
}),
{
position: "top-center",
},
@@ -184,11 +186,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label={t("menu.user.setPassword")}
aria-label={t("menu.user.setPassword", { ns: "common" })}
onClick={() => setPasswordDialogOpen(true)}
>
<LuSquarePen className="mr-2 size-4" />
<span>{t("menu.user.setPassword")}</span>
<span>{t("menu.user.setPassword", { ns: "common" })}</span>
</MenuItem>
)}
<MenuItem
@@ -501,7 +503,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label={t("menu.documentation.label")}
>
<LuLifeBuoy className="mr-2 size-4" />
<span>{t("menu.documentation")}</span>
<span>{t("menu.documentation.title")}</span>
</MenuItem>
</a>
<a
+4 -2
View File
@@ -246,7 +246,7 @@ export default function LiveContextMenu({
strftime_fmt:
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
: t("time.formattedTimestampExcludeSeconds.12hour", { ns: "common" }),
});
return t("time.untilForTime", { ns: "common", time });
};
@@ -343,7 +343,9 @@ export default function LiveContextMenu({
}
>
<div className="text-primary">
{t("streaming.debugView", { ns: "components/dialog" })}
{t("streaming.debugView", {
ns: "components/dialog",
})}
</div>
</div>
</ContextMenuItem>
@@ -149,7 +149,7 @@ export default function SearchResultActions({
onClick={showSnapshot}
>
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
<span>{t("itemMenu.submitToPlus")}</span>
<span>{t("itemMenu.submitToPlus.label")}</span>
</MenuItem>
)}
<MenuItem
@@ -170,7 +170,9 @@ export default function SearchResultActions({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("dialog.confirmDelete")}</AlertDialogTitle>
<AlertDialogTitle>
{t("dialog.confirmDelete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Trans ns="views/explore">dialog.confirmDelete.desc</Trans>
@@ -128,7 +128,7 @@ export default function CreateUserDialog({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t("users.dialog.form.user")}
{t("users.dialog.form.user.title")}
</FormLabel>
<FormControl>
<Input
@@ -150,7 +150,7 @@ export default function CreateUserDialog({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t("users.dialog.form.password")}
{t("users.dialog.form.password.title")}
</FormLabel>
<FormControl>
<Input
@@ -170,7 +170,7 @@ export default function CreateUserDialog({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t("users.dialog.form.password.confirm")}
{t("users.dialog.form.password.confirm.title")}
</FormLabel>
<FormControl>
<Input
+7 -7
View File
@@ -267,7 +267,7 @@ export function ExportContent({
{isDesktop && (
<>
<DialogHeader>
<DialogTitle>{t("menu.export")}</DialogTitle>
<DialogTitle>{t("menu.export", { ns: "common" })}</DialogTitle>
</DialogHeader>
<SelectSeparator className="my-4 bg-secondary" />
</>
@@ -404,14 +404,14 @@ function CustomTimeSelector({
const formattedStart = useFormattedTimestamp(
startTime,
config?.ui.time_format == "24hour"
? t("time.formattedTimestamp.24hour")
: t("time.formattedTimestamp"),
? t("time.formattedTimestamp.24hour", { ns: "common" })
: t("time.formattedTimestamp.12hour", { ns: "common" }),
);
const formattedEnd = useFormattedTimestamp(
endTime,
config?.ui.time_format == "24hour"
? t("time.formattedTimestamp.24hour")
: t("time.formattedTimestamp"),
? t("time.formattedTimestamp.24hour", { ns: "common" })
: t("time.formattedTimestamp.12hour", { ns: "common" }),
);
const startClock = useMemo(() => {
@@ -444,7 +444,7 @@ function CustomTimeSelector({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label={t("export.time.start")}
aria-label={t("export.time.start.title")}
variant={startOpen ? "select" : "default"}
size="sm"
onClick={() => {
@@ -510,7 +510,7 @@ function CustomTimeSelector({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label={t("export.time.end")}
aria-label={t("export.time.end.title")}
variant={endOpen ? "select" : "default"}
size="sm"
onClick={() => {
@@ -27,7 +27,7 @@ export default function MobileCameraDrawer({
<DrawerTrigger asChild>
<Button
className="rounded-lg capitalize"
aria-label={t("menu.live.cameras")}
aria-label={t("menu.live.cameras.title")}
size="sm"
>
<FaVideo className="text-secondary-foreground" />
@@ -19,7 +19,6 @@ import { toast } from "sonner";
import axios from "axios";
import SaveExportOverlay from "./SaveExportOverlay";
import { isIOS, isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
@@ -70,7 +69,7 @@ export default function MobileReviewSettingsDrawer({
setMode,
setShowExportPreview,
}: MobileReviewSettingsDrawerProps) {
const { t } = useTranslation(["views/recording"]);
const { t } = useTranslation(["views/recording", "components/dialog"]);
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
// exports
@@ -68,13 +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>{t("role.admin")}</span>
<span>{t("role.admin", { ns: "common" })}</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>{t("role.viewer")}</span>
<span>{t("role.viewer", { ns: "common" })}</span>
</div>
</SelectItem>
</SelectContent>
@@ -117,7 +117,7 @@ export default function SetPasswordDialog({
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="password">
{t("users.dialog.form.newPassword")}
{t("users.dialog.form.newPassword.title")}
</Label>
<Input
id="password"
@@ -142,7 +142,7 @@ export default function SetPasswordDialog({
/>
</div>
<p className="text-xs text-muted-foreground">
{t("users.dialog.form.password.strength")}
{t("users.dialog.form.password.strength.title")}
<span className="font-medium">{getStrengthLabel()}</span>
</p>
</div>
@@ -151,7 +151,7 @@ export default function SetPasswordDialog({
<div className="space-y-2">
<Label htmlFor="confirm-password">
{t("users.dialog.form.password.confirm")}
{t("users.dialog.form.password.confirm.title")}
</Label>
<Input
id="confirm-password"
@@ -85,7 +85,7 @@ export function AnnotationSettingsPane({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -100,9 +100,12 @@ export function AnnotationSettingsPane({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{
position: "top-center",
},
);
})
.finally(() => {
setIsLoading(false);
@@ -145,7 +148,7 @@ export function AnnotationSettingsPane({
onCheckedChange={setShowZones}
/>
<Label className="cursor-pointer" htmlFor="show-zones">
{t("objectLifecycle.annotationSettings.showAllZones")}
{t("objectLifecycle.annotationSettings.showAllZones.title")}
</Label>
</div>
<div className="text-sm text-muted-foreground">
@@ -573,7 +573,7 @@ export default function ObjectLifecycle({
? t("time.formattedTimestamp2.24hour", {
ns: "common",
})
: t("time.formattedTimestamp2", {
: t("time.formattedTimestamp2.12hour", {
ns: "common",
}),
time_style: "medium",
@@ -98,7 +98,7 @@ export default function ReviewDetailDialog({
review?.start_time ?? 0,
config?.ui.time_format == "24hour"
? t("time.formattedTimestampWithYear.24hour", { ns: "common" })
: t("time.formattedTimestampWithYear", { ns: "common" }),
: t("time.formattedTimestampWithYear.12hour", { ns: "common" }),
config?.ui.timezone,
);
@@ -195,7 +195,9 @@ export default function SearchDetailDialog({
>
<Header>
<Title>{t("trackedObjectDetails")}</Title>
<Description className="sr-only">{t("details")}</Description>
<Description className="sr-only">
{t("trackedObjectDetails")}
</Description>
</Header>
<ScrollArea
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
@@ -311,7 +313,7 @@ function ObjectDetailsTab({
search?.start_time ?? 0,
config?.ui.time_format == "24hour"
? t("time.formattedTimestampWithYear.24hour", { ns: "common" })
: t("time.formattedTimestampWithYear", { ns: "common" }),
: t("time.formattedTimestampWithYear.12hour", { ns: "common" }),
config?.ui.timezone,
);
@@ -575,7 +577,9 @@ function ObjectDetailsTab({
</span>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{t("details.editSubLable")}</TooltipContent>
<TooltipContent>
{t("details.editSubLabel.title")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
@@ -597,7 +601,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">
{t("details.topScore")}
{t("details.topScore.label")}
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
@@ -727,7 +731,7 @@ function ObjectDetailsTab({
aria-label={t("details.button.regenerate.label")}
onClick={() => regenerateDescription("thumbnails")}
>
{t("details.button.regenerate")}
{t("details.button.regenerate.title")}
</Button>
{search.has_snapshot && (
<DropdownMenu>
@@ -772,13 +776,13 @@ function ObjectDetailsTab({
<TextEntryDialog
open={isSubLabelDialogOpen}
setOpen={setIsSubLabelDialogOpen}
title={t("details.editSubLable")}
title={t("details.editSubLabel.title")}
description={
search.label
? t("details.editSubLable.desc", {
label: t(search.label, { ns: "objects" }),
? t("details.editSubLabel.desc", {
label: t(search.label, { an: "objects" }),
})
: t("details.editSubLable.desc.noLabel")
: t("details.editSubLabel.descNoLabel")
}
onSave={handleSubLabelSave}
defaultValue={search?.sub_label || ""}
@@ -921,10 +925,10 @@ export function ObjectSnapshotTab({
}}
>
{/^[aeiou]/i.test(search?.label || "")
? t("explore.plus.review.true_other", {
? t("explore.plus.review.true.true_other", {
label: search?.label,
})
: t("explore.plus.review.true_one", {
: t("explore.plus.review.true.true_one", {
label: search?.label,
})}
</Button>
@@ -938,10 +942,10 @@ export function ObjectSnapshotTab({
}}
>
{/^[aeiou]/i.test(search?.label || "")
? t("explore.plus.review.false_other", {
? t("explore.plus.review.false.false_other", {
label: search?.label,
})
: t("explore.plus.review.false_one", {
: t("explore.plus.review.false.false_one", {
label: search?.label,
})}
</Button>
@@ -251,7 +251,7 @@ function TimeRangeFilterContent({
timeRange,
updateTimeRange,
}: TimeRangeFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
const { t } = useTranslation(["components/filter", "components/dialog"]);
const [startOpen, setStartOpen] = useState(false);
const [endOpen, setEndOpen] = useState(false);
@@ -406,7 +406,7 @@ export function ZoneFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allZones"
>
{t("zones.all")}
{t("zones.all.title")}
</Label>
<Switch
className="ml-1"
@@ -581,6 +581,7 @@ export function SpeedFilterContent({
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">
{t("estimatedSpeed", {
ns: "components/filter",
unit:
config?.ui.unit_system == "metric"
? t("unit.speed.kph", { ns: "common" })
+10 -6
View File
@@ -12,16 +12,16 @@ export function PlayerStats({ stats, minimal }: PlayerStatsProps) {
const fullStatsContent = (
<>
<p>
<span className="text-white/70">{t("stats.streamType")}</span>{" "}
<span className="text-white/70">{t("stats.streamType.title")}</span>{" "}
<span className="text-white">{stats.streamType}</span>
</p>
<p>
<span className="text-white/70">{t("stats.bandwidth")}</span>{" "}
<span className="text-white/70">{t("stats.bandwidth.title")}</span>{" "}
<span className="text-white">{stats.bandwidth.toFixed(2)} kbps</span>
</p>
{stats.latency != undefined && (
<p>
<span className="text-white/70">{t("stats.latency")}</span>{" "}
<span className="text-white/70">{t("stats.latency.title")}</span>{" "}
<span
className={`text-white ${stats.latency > 2 ? "text-danger" : ""}`}
>
@@ -35,7 +35,9 @@ export function PlayerStats({ stats, minimal }: PlayerStatsProps) {
</p>
{stats.droppedFrames != undefined && (
<p>
<span className="text-white/70">{t("stats.droppedFrames")}</span>{" "}
<span className="text-white/70">
{t("stats.droppedFrames.title")}
</span>{" "}
<span className="text-white">{stats.droppedFrames}</span>
</p>
)}
@@ -68,7 +70,9 @@ export function PlayerStats({ stats, minimal }: PlayerStatsProps) {
</div>
{stats.latency != undefined && (
<div className="hidden flex-col items-center gap-1 md:flex">
<span className="text-white/70">{t("stats.latency.short")}</span>
<span className="text-white/70">
{t("stats.latency.short.title")}
</span>
<span
className={`text-white ${stats.latency >= 2 ? "text-danger" : ""}`}
>
@@ -81,7 +85,7 @@ export function PlayerStats({ stats, minimal }: PlayerStatsProps) {
{stats.droppedFrames != undefined && (
<div className="flex flex-col items-center justify-end gap-1">
<span className="text-white/70">
{t("stats.droppedFrames.short")}
{t("stats.droppedFrames.short.title")}
</span>
<span className="text-white">
{t("stats.droppedFrames.short.value", {
@@ -171,7 +171,7 @@ export default function PreviewThumbnailPlayer({
review.start_time,
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
: t("time.formattedTimestampExcludeSeconds.12hour", { ns: "common" }),
config?.ui?.timezone,
);
@@ -50,7 +50,7 @@ export function CameraStreamingDialog({
setIsDialogOpen,
onSave,
}: CameraStreamingDialogProps) {
const { t } = useTranslation(["components/camera"]);
const { t } = useTranslation(["components/camera", "components/dialog"]);
const { data: config } = useSWR<FrigateConfig>("config");
const [isLoading, setIsLoading] = useState(false);
@@ -198,7 +198,9 @@ export function CameraStreamingDialog({
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
{t("streaming.restreaming.desc", { ns: "components/dialog" })}
{t("streaming.restreaming.desc.title", {
ns: "components/dialog",
})}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -206,7 +208,7 @@ export function CameraStreamingDialog({
rel="noopener noreferrer"
className="inline"
>
{t("streaming.restreaming.readTheDocumentation", {
{t("streaming.restreaming.desc.readTheDocumentation", {
ns: "components/dialog",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
@@ -257,7 +259,7 @@ export function CameraStreamingDialog({
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
{t("group.camera.setting.audio.desc")}
{t("group.camera.setting.audio.tips.title")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -265,7 +267,7 @@ export function CameraStreamingDialog({
rel="noopener noreferrer"
className="inline"
>
{t("group.camera.setting.audio.desc.document")}
{t("group.camera.setting.audio.tips.document")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -322,7 +324,7 @@ export function CameraStreamingDialog({
<>
<p className="text-sm text-muted-foreground">
{t(
"group.camera.setting.streamMethod.method.continuousStreaming.desc",
"group.camera.setting.streamMethod.method.continuousStreaming.desc.title",
)}
</p>
<div className="flex items-center gap-2">
@@ -107,7 +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"),
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
path: ["polygon.isFinished"],
});
@@ -166,7 +166,7 @@ export default function MotionMaskEditPane({
if (res.status === 200) {
toast.success(
polygon.name
? t("masksAndZones.motionMasks.toast.success", {
? t("masksAndZones.motionMasks.toast.success.title", {
polygonName: polygon.name,
})
: t("masksAndZones.motionMasks.toast.success.noName"),
@@ -177,7 +177,7 @@ export default function MotionMaskEditPane({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -192,9 +192,12 @@ export default function MotionMaskEditPane({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{
position: "top-center",
},
);
})
.finally(() => {
setIsLoading(false);
@@ -283,7 +286,7 @@ export default function MotionMaskEditPane({
{polygonArea && polygonArea >= 0.35 && (
<>
<div className="mb-3 text-sm text-danger">
{t("masksAndZones.motionMasks.polygonAreaTooLarge", {
{t("masksAndZones.motionMasks.polygonAreaTooLarge.title", {
polygonArea: Math.round(polygonArea * 100),
})}
</div>
@@ -109,7 +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"),
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
path: ["polygon.isFinished"],
});
@@ -198,7 +198,7 @@ export default function ObjectMaskEditPane({
if (res.status === 200) {
toast.success(
polygon.name
? t("masksAndZones.objectMasks.toast.success", {
? t("masksAndZones.objectMasks.toast.success.title", {
polygonName: polygon.name,
})
: t("masksAndZones.objectMasks.toast.success.noName"),
@@ -209,7 +209,7 @@ export default function ObjectMaskEditPane({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -225,7 +225,7 @@ export default function ObjectMaskEditPane({
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage,
ns: "common",
}),
@@ -327,7 +327,7 @@ export default function ObjectMaskEditPane({
render={({ field }) => (
<FormItem>
<FormLabel>
{t("masksAndZones.objectMasks.objects")}
{t("masksAndZones.objectMasks.objects.title")}
</FormLabel>
<Select
onValueChange={field.onChange}
+8 -5
View File
@@ -188,9 +188,9 @@ export default function PolygonItem({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
t("toast.save.error.title", {
ns: "common",
errorMessage: res.statusText,
}),
{
position: "top-center",
@@ -203,9 +203,12 @@ export default function PolygonItem({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{
position: "top-center",
},
);
})
.finally(() => {
setIsLoading(false);
@@ -59,7 +59,9 @@ export default function ExploreSettings({
<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">{t("explore.settings.defaultView")}</div>
<div className="text-md">
{t("explore.settings.defaultView.title")}
</div>
<div className="space-y-1 text-xs text-muted-foreground">
{t("explore.settings.defaultView.desc")}
</div>
@@ -95,7 +97,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">{t("explore.settings.gridColumns")}</div>
<div className="text-md">
{t("explore.settings.gridColumns.title")}
</div>
<div className="space-y-1 text-xs text-muted-foreground">
{t("explore.settings.gridColumns.desc")}
</div>
+21 -18
View File
@@ -161,7 +161,7 @@ export default function ZoneEditPane({
.optional()
.or(z.literal("")),
isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.polygonDrawing.error.mustBeFinished"),
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
}),
objects: z.array(z.string()).optional(),
review_alerts: z.boolean().default(false).optional(),
@@ -170,28 +170,28 @@ export default function ZoneEditPane({
lineA: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error"),
message: t("masksAndZones.form.distance.error.text"),
})
.optional()
.or(z.literal("")),
lineB: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error"),
message: t("masksAndZones.form.distance.error.text"),
})
.optional()
.or(z.literal("")),
lineC: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error"),
message: t("masksAndZones.form.distance.error.text"),
})
.optional()
.or(z.literal("")),
lineD: z.coerce
.number()
.min(0.1, {
message: t("masksAndZones.form.distance.error"),
message: t("masksAndZones.form.distance.error.text"),
})
.optional()
.or(z.literal("")),
@@ -422,7 +422,7 @@ export default function ZoneEditPane({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -438,7 +438,7 @@ export default function ZoneEditPane({
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage,
ns: "common",
}),
@@ -496,7 +496,7 @@ export default function ZoneEditPane({
: t("masksAndZones.zones.add")}
</Heading>
<div className="my-2 text-sm text-muted-foreground">
<p>{t("masksAndZones.zones.desc")}</p>
<p>{t("masksAndZones.zones.desc.title")}</p>
</div>
<Separator className="my-3 bg-secondary" />
{polygons && activePolygonIndex !== undefined && (
@@ -532,7 +532,7 @@ export default function ZoneEditPane({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("masksAndZones.zones.name")}</FormLabel>
<FormLabel>{t("masksAndZones.zones.name.title")}</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]"
@@ -553,7 +553,7 @@ export default function ZoneEditPane({
name="inertia"
render={({ field }) => (
<FormItem>
<FormLabel>{t("masksAndZones.zones.inertia")}</FormLabel>
<FormLabel>{t("masksAndZones.zones.inertia.title")}</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]"
@@ -576,7 +576,9 @@ export default function ZoneEditPane({
name="loitering_time"
render={({ field }) => (
<FormItem>
<FormLabel>{t("masksAndZones.zones.loiteringTime")}</FormLabel>
<FormLabel>
{t("masksAndZones.zones.loiteringTime.title")}
</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]"
@@ -595,7 +597,7 @@ export default function ZoneEditPane({
/>
<Separator className="my-2 flex bg-secondary" />
<FormItem>
<FormLabel>{t("masksAndZones.zones.objects")}</FormLabel>
<FormLabel>{t("masksAndZones.zones.objects.title")}</FormLabel>
<FormDescription>
{t("masksAndZones.zones.objects.desc")}
</FormDescription>
@@ -631,7 +633,7 @@ export default function ZoneEditPane({
className="cursor-pointer text-primary"
htmlFor="allLabels"
>
{t("masksAndZones.zones.speedEstimation")}
{t("masksAndZones.zones.speedEstimation.title")}
</FormLabel>
<Switch
checked={field.value}
@@ -644,7 +646,7 @@ export default function ZoneEditPane({
) {
toast.error(
t(
"masksAndZones.zones.speedEstimation.pointLengthError",
"masksAndZones.zones.speedThreshold.toast.error.pointLengthError",
),
);
return;
@@ -655,7 +657,7 @@ export default function ZoneEditPane({
if (checked && loiteringTime && loiteringTime > 0) {
toast.error(
t(
"masksAndZones.zones.speedEstimation.loiteringTimeError",
"masksAndZones.zones.speedThreshold.toast.error.loiteringTimeError",
),
);
}
@@ -778,11 +780,12 @@ export default function ZoneEditPane({
render={({ field }) => (
<FormItem>
<FormLabel>
{t("masksAndZones.zones.speedThreshold", {
{t("masksAndZones.zones.speedThreshold.title", {
ns: "views/settings",
unit:
config?.ui.unit_system == "imperial"
? t("unit.speed.mph")
: t("unit.speed.kph"),
? t("unit.speed.mph", { ns: "common" })
: t("unit.speed.kph", { ns: "common" }),
})}
</FormLabel>
<FormControl>
+9 -9
View File
@@ -60,15 +60,15 @@ interface Preset {
// Define presets
const PRESETS: Preset[] = [
{ name: "today", label: t("time.today") },
{ name: "yesterday", label: t("time.yesterday") },
{ name: "last7", label: t("time.last7") },
{ name: "last14", label: t("time.last14") },
{ name: "last30", label: t("time.last30") },
{ name: "thisWeek", label: t("time.thisWeek") },
{ name: "lastWeek", label: t("time.lastWeek") },
{ name: "thisMonth", label: t("time.thisMonth") },
{ name: "lastMonth", label: t("time.lastMonth") },
{ name: "today", label: t("time.today", { ns: "common" }) },
{ name: "yesterday", label: t("time.yesterday", { ns: "common" }) },
{ name: "last7", label: t("time.last7", { ns: "common" }) },
{ name: "last14", label: t("time.last14", { ns: "common" }) },
{ name: "last30", label: t("time.last30", { ns: "common" }) },
{ name: "thisWeek", label: t("time.thisWeek", { ns: "common" }) },
{ name: "lastWeek", label: t("time.lastWeek", { ns: "common" }) },
{ name: "thisMonth", label: t("time.thisMonth", { ns: "common" }) },
{ name: "lastMonth", label: t("time.lastMonth", { ns: "common" }) },
];
/** The DateRangePicker component allows a user to select a range of dates */
+6 -6
View File
@@ -8,7 +8,7 @@ import { t } from "i18next";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label={t("pagination.label")}
aria-label={t("pagination.label", { ns: "common" })}
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
@@ -65,13 +65,13 @@ const PaginationPrevious = ({
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label={t("pagination.previous.label")}
aria-label={t("pagination.previous.label", { ns: "common" })}
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>{t("pagination.previous")}</span>
<span>{t("pagination.previous.title", { ns: "common" })}</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
@@ -81,12 +81,12 @@ const PaginationNext = ({
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label={t("pagination.next.label")}
aria-label={t("pagination.next.label", { ns: "common" })}
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>{t("pagination.next")}</span>
<span>{t("pagination.next.title", { ns: "common" })}</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
@@ -102,7 +102,7 @@ const PaginationEllipsis = ({
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">{t("pagination.more")}</span>
<span className="sr-only">{t("pagination.more", { ns: "common" })}</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
+1 -1
View File
@@ -31,7 +31,7 @@ export default function useNavigation(
id: ID_LIVE,
variant,
icon: FaVideo,
title: "menu.live",
title: "menu.live.title",
url: "/",
},
{
+1 -1
View File
@@ -216,8 +216,8 @@ export default function Settings() {
value={item}
data-nav-item={item}
aria-label={t("selectItem", {
item: t("menu." + item),
ns: "common",
item: t("menu." + item),
})}
>
<div className="capitalize">{t("menu." + item)}</div>
+1 -1
View File
@@ -10,7 +10,7 @@ export function shareOrCopy(url: string, title?: string) {
});
} else {
copy(url);
toast.success(t("toast.copyUrlToClipboard"), {
toast.success(t("toast.copyUrlToClipboard", { ns: "common" }), {
position: "top-center",
});
}
+9 -9
View File
@@ -14,24 +14,24 @@ export function getLifecycleItemDescription(
switch (lifecycleItem.class_type) {
case "visible":
return t("objectLifecycle.lifecycleItemDesc.visible", {
label,
ns: "views/explore",
label,
});
case "entered_zone":
return t("objectLifecycle.lifecycleItemDesc.entered_zone", {
label,
ns: "views/explore",
label,
zones: lifecycleItem.data.zones.join(" and ").replaceAll("_", " "),
});
case "active":
return t("objectLifecycle.lifecycleItemDesc.active", {
label,
ns: "views/explore",
label,
});
case "stationary":
return t("objectLifecycle.lifecycleItemDesc.stationary", {
label,
ns: "views/explore",
label,
});
case "attribute": {
let title = "";
@@ -42,34 +42,34 @@ export function getLifecycleItemDescription(
title = t(
"objectLifecycle.lifecycleItemDesc.attribute.faceOrLicense_plate",
{
ns: "views/explore",
label,
attribute: lifecycleItem.data.attribute.replaceAll("_", " "),
ns: "views/explore",
},
);
} else {
title = t("objectLifecycle.lifecycleItemDesc.attribute.other", {
ns: "views/explore",
label: lifecycleItem.data.label,
attribute: lifecycleItem.data.attribute.replaceAll("_", " "),
ns: "views/explore",
});
}
return title;
}
case "gone":
return t("objectLifecycle.lifecycleItemDesc.gone", {
label,
ns: "views/explore",
label,
});
case "heard":
return t("objectLifecycle.lifecycleItemDesc.heard", {
label,
ns: "views/explore",
label,
});
case "external":
return t("objectLifecycle.lifecycleItemDesc.external", {
label,
ns: "views/explore",
label,
});
}
}
+36 -20
View File
@@ -134,7 +134,7 @@ export default function LiveCameraView({
fullscreen,
toggleFullscreen,
}: LiveCameraViewProps) {
const { t } = useTranslation(["views/live"]);
const { t } = useTranslation(["views/live", "components/dialog"]);
const navigate = useNavigate();
const { isPortrait } = useMobileOrientation();
const mainRef = useRef<HTMLDivElement | null>(null);
@@ -530,7 +530,13 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"}
Icon={mic ? FaMicrophone : FaMicrophoneSlash}
isActive={mic}
title={`${mic ? "Disable" : "Enable"} Two Way Talk`}
title={
(mic
? t("button.disable", { ns: "common" })
: t("button.enable", { ns: "common" })) +
" " +
t("button.twoWayTalk", { ns: "common" })
}
onClick={() => {
setMic(!mic);
if (!mic && !audio) {
@@ -546,7 +552,13 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"}
Icon={audio ? GiSpeaker : GiSpeakerOff}
isActive={audio ?? false}
title={`${audio ? "Disable" : "Enable"} Camera Audio`}
title={
(audio
? t("button.disable", { ns: "common" })
: t("button.enable", { ns: "common" })) +
" " +
t("button.cameraAudio", { ns: "common" })
}
onClick={() => setAudio(!audio)}
disabled={!cameraEnabled}
/>
@@ -931,7 +943,7 @@ function PtzControlPanel({
}
function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
const { t } = useTranslation(["views/live"]);
const { t } = useTranslation(["views/live", "views/events"]);
const rankMap = { all: 0, motion: 1, active_objects: 2 };
const getValidMode = (retain?: { mode?: string }): keyof typeof rankMap => {
const mode = retain?.mode;
@@ -946,10 +958,7 @@ function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
? recordRetainMode
: alertsRetainMode;
const source =
effectiveRetainMode === recordRetainMode
? t("camera", { ns: "views/events" })
: t("alerts", { ns: "views/events" });
const source = effectiveRetainMode === recordRetainMode ? "camera" : "alerts";
return effectiveRetainMode !== "all" ? (
<div>
@@ -1007,7 +1016,7 @@ function FrigateCameraFeatures({
supports2WayTalk,
cameraEnabled,
}: FrigateCameraFeaturesProps) {
const { t } = useTranslation(["views/live"]);
const { t } = useTranslation(["views/live", "components/dialog"]);
const { payload: detectState, send: sendDetect } = useDetectState(
camera.name,
@@ -1243,7 +1252,7 @@ function FrigateCameraFeatures({
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" />
<div>
{t("streaming.restreaming.NotEnabled", {
{t("streaming.restreaming.disabled", {
ns: "components/dialog",
})}
</div>
@@ -1257,7 +1266,7 @@ function FrigateCameraFeatures({
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
{t("streaming.restreaming.desc", {
{t("streaming.restreaming.desc.title", {
ns: "components/dialog",
})}
<div className="mt-2 flex items-center text-primary">
@@ -1267,9 +1276,12 @@ function FrigateCameraFeatures({
rel="noopener noreferrer"
className="inline"
>
{t("streaming.restreaming.readTheDocumentation", {
ns: "components/dialog",
})}
{t(
"streaming.restreaming.desc.readTheDocumentation",
{
ns: "components/dialog",
},
)}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@@ -1334,7 +1346,7 @@ function FrigateCameraFeatures({
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
{t("stream.audio.tips")}
{t("stream.audio.tips.title")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -1462,12 +1474,16 @@ function FrigateCameraFeatures({
/>
</div>
<p className="text-sm text-muted-foreground">
{t("streaming.showStats.desc", { ns: "components/dialog" })}
{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">
{t("streaming.debugView", { ns: "components/dialog" })}
{t("streaming.debugView", {
ns: "components/dialog",
})}
<LuExternalLink
onClick={() =>
navigate(`/settings?page=debug&camera=${camera.name}`)
@@ -1558,7 +1574,7 @@ function FrigateCameraFeatures({
<div className="mt-3 flex flex-col gap-5">
{!isRestreamed && (
<div className="flex flex-col gap-2 p-2">
<Label>{t("streaming", { ns: "components/dialog" })}</Label>
<Label>{t("streaming.title", { 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>
@@ -1649,7 +1665,7 @@ function FrigateCameraFeatures({
</div>
</PopoverTrigger>
<PopoverContent className="w-52 text-xs">
{t("stream.audio.tips")}
{t("stream.audio.tips.title")}
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
@@ -1717,7 +1733,7 @@ function FrigateCameraFeatures({
</div>
<Button
className={`flex items-center gap-2.5 rounded-lg`}
aria-label="Reset the stream"
aria-label={t("stream.lowBandwidth.resetStream")}
variant="outline"
size="sm"
onClick={() => setLowBandwidth(false)}
+1 -1
View File
@@ -417,7 +417,7 @@ export function RecordingView({
<FaVideo className="size-5 text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
{t("menu.live", { ns: "common" })}
{t("menu.live.title", { ns: "common" })}
</div>
)}
</Button>
@@ -199,7 +199,7 @@ 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">
{t("users.management")}
{t("users.management.title")}
</Heading>
<p className="text-sm text-muted-foreground">
{t("users.management.desc")}
@@ -166,7 +166,7 @@ export default function CameraSettingsView({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -182,7 +182,7 @@ export default function CameraSettingsView({
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage,
ns: "common",
}),
@@ -574,7 +574,7 @@ export default function CameraSettingsView({
watchedDetectionsZones.length > 0 ? (
!selectDetections ? (
<Trans
i18nKey="camera.reviewClassification.zoneObjectDetectionsTips"
i18nKey="camera.reviewClassification.zoneObjectDetectionsTips.text"
values={{
detectionsLabels,
zone: watchedDetectionsZones
@@ -162,9 +162,12 @@ export default function ClassificationSettingsView({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{
position: "top-center",
},
);
})
.finally(() => {
setIsLoading(false);
@@ -319,7 +322,11 @@ export default function ClassificationSettingsView({
className="cursor-pointer"
value={size}
>
{t("classification.semanticSearch.modelSize." + size)}
{t(
"classification.semanticSearch.modelSize." +
size +
".title",
)}
</SelectItem>
))}
</SelectGroup>
+6 -4
View File
@@ -502,7 +502,7 @@ export default function MasksAndZonesView({
</HoverCardTrigger>
<HoverCardContent>
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
<p>{t("masksAndZones.zones.desc")}</p>
<p>{t("masksAndZones.zones.desc.title")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/zones"
@@ -568,7 +568,7 @@ export default function MasksAndZonesView({
</HoverCardTrigger>
<HoverCardContent>
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
<p>{t("masksAndZones.motionMasks.desc")}</p>
<p>{t("masksAndZones.motionMasks.desc.title")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/masks#motion-masks"
@@ -638,7 +638,7 @@ export default function MasksAndZonesView({
</HoverCardTrigger>
<HoverCardContent>
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
<p>{t("masksAndZones.objectMasks.desc")}</p>
<p>{t("masksAndZones.objectMasks.desc.title")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/masks#object-filter-masks"
@@ -646,7 +646,9 @@ export default function MasksAndZonesView({
rel="noopener noreferrer"
className="inline"
>
{t("masksAndZones.objectMasks.documentation")}{" "}
{t(
"masksAndZones.objectMasks.desc.documentation",
)}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
+6 -6
View File
@@ -126,7 +126,7 @@ export default function MotionTunerView({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -138,7 +138,7 @@ export default function MotionTunerView({
})
.catch((error) => {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: error.response.data.message,
ns: "common",
}),
@@ -194,7 +194,7 @@ export default function MotionTunerView({
{t("motionDetectionTuner.title")}
</Heading>
<div className="my-3 space-y-3 text-sm text-muted-foreground">
<p>{t("motionDetectionTuner.desc")}</p>
<p>{t("motionDetectionTuner.desc.title")}</p>
<div className="flex items-center text-primary">
<Link
@@ -213,7 +213,7 @@ export default function MotionTunerView({
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md">
{t("motionDetectionTuner.Threshold")}
{t("motionDetectionTuner.Threshold.title")}
</Label>
<div className="my-2 text-sm text-muted-foreground">
<Trans ns="views/settings">
@@ -242,7 +242,7 @@ export default function MotionTunerView({
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md">
{t("motionDetectionTuner.contourArea")}
{t("motionDetectionTuner.contourArea.title")}
</Label>
<div className="my-2 text-sm text-muted-foreground">
<p>
@@ -274,7 +274,7 @@ export default function MotionTunerView({
<div className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="improve-contrast">
{t("motionDetectionTuner.improveContrast")}
{t("motionDetectionTuner.improveContrast.title")}
</Label>
<div className="text-sm text-muted-foreground">
<Trans ns="views/settings">
@@ -264,7 +264,7 @@ export default function NotificationView({
updateConfig();
} else {
toast.error(
t("toast.save.error", {
t("toast.save.error.title", {
errorMessage: res.statusText,
ns: "common",
}),
@@ -279,9 +279,12 @@ export default function NotificationView({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{
position: "top-center",
},
);
})
.finally(() => {
setIsLoading(false);
@@ -389,7 +392,7 @@ export default function NotificationView({
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("notification.email")}</FormLabel>
<FormLabel>{t("notification.email.title")}</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"
@@ -414,7 +417,7 @@ export default function NotificationView({
<>
<div className="mb-2">
<FormLabel className="flex flex-row items-center text-base">
{t("notification.cameras")}
{t("notification.cameras.title")}
</FormLabel>
</div>
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
@@ -423,7 +426,7 @@ export default function NotificationView({
name="allEnabled"
render={({ field }) => (
<FilterSwitch
label={t("cameras.all", {
label={t("cameras.all.title", {
ns: "components/filter",
})}
isChecked={field.value}
@@ -514,7 +517,7 @@ export default function NotificationView({
{t("notification.deviceSpecific")}
</Heading>
<Button
aria-label="Register or unregister notifications for this device"
aria-label={t("notification.registerDevice")}
disabled={
!config?.notifications.enabled || publicKey == undefined
}
@@ -653,7 +656,7 @@ export function CameraNotificationSwitch({
strftime_fmt:
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
: t("time.formattedTimestampExcludeSeconds.12hour", { ns: "common" }),
});
return t("time.untilForTime", { ns: "common", time });
};
@@ -709,10 +712,10 @@ export function CameraNotificationSwitch({
{t("notification.suspendTime.1hour")}
</SelectItem>
<SelectItem value="840">
{t("notification.suspendTime.12hour")}
{t("notification.suspendTime.12hours")}
</SelectItem>
<SelectItem value="1440">
{t("notification.suspendTime.24hour")}
{t("notification.suspendTime.24hours")}
</SelectItem>
<SelectItem value="off">
{t("notification.suspendTime.untilRestart")}
+1 -1
View File
@@ -581,7 +581,7 @@ export default function GeneralMetrics({
{gpuMemSeries && (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
{t("general.hardwareInfo.gpuMemroy")}
{t("general.hardwareInfo.gpuMemory")}
</div>
{gpuMemSeries.map((series) => (
<ThresholdBarGraph