chore: add more i18n keys again

This commit is contained in:
ZhaiSoul 2025-03-11 13:07:54 +08:00
parent b35fbef325
commit a52592bcac
30 changed files with 330 additions and 143 deletions

View File

@ -1,5 +1,8 @@
{
"time": {
"untilForTime": "Until {{time}}",
"untilForRestart": "Until Frigate restarts.",
"untilRestart": "Until restart",
"ago": "{{timeAgo}} ago",
"justNow": "Just now",
"today": "Today",
@ -11,6 +14,12 @@
"lastWeek": "Last Week",
"thisMonth": "This Month",
"lastMonth": "Last Month",
"5minutes": "5 minutes",
"10minutes": "10 minutes",
"30minutes": "30 minutes",
"1hour": "1 hour",
"12hours": "12 hours",
"24hours": "24 hours",
"pm": "pm",
"am": "am",
"yr": "{{time}}yr",
@ -43,6 +52,9 @@
"apply": "Apply",
"reset": "Reset",
"enabled": "Enabled",
"enable": "Enable",
"disabled": "Disabled",
"disable": "Disable",
"save": "Save",
"saving": "Saving...",
"cancel": "Cancel",
@ -61,7 +73,9 @@
"yes": "Yes",
"no": "No",
"download": "Download",
"info": "Info"
"info": "Info",
"suspended": "Suspended",
"unsuspended": "Unsuspend"
},
"menu": {
"system": "System",
@ -103,9 +117,11 @@
"uiPlayground": "UI Playground",
"faceLibrary": "Face Library",
"user": {
"account": "Account",
"current": "Current User: {{user}}",
"anonymous": "anonymous",
"logout": "Logout"
"logout": "Logout",
"setPassword": "Set Password"
}
},
"toast": {

View File

@ -72,5 +72,16 @@
"overwrite": "{{searchName}} already exists. Saving will overwrite the existing value.",
"success": "Search ({{searchName}}) has been saved."
}
},
"recording": {
"confirmDelete": {
"title": "Confirm Delete",
"desc": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future."
},
"button": {
"export": "Export",
"markAsReviewed": "Mark as reviewed",
"deleteNow": "Delete Now"
}
}
}

View File

@ -1,5 +1,5 @@
{
"label": "Filter",
"filter": "Filter",
"labels": {
"all": "All Labels",
"all.short": "Labels",

View File

@ -1,6 +1,7 @@
{
"documentTitle": "Live - Frigate",
"documentTitle.withCamera": "{{camera}} - Live - Frigate",
"lowBandwidthMode": "Low-bandwidth Mode",
"twoWayTalk": {
"enable": "Enable Two Way Talk",
"disable": "Disable Two Way Talk"
@ -42,6 +43,10 @@
"enable": "Enable Camera",
"disable": "Disable Camera"
},
"muteCameras": {
"enable": "Mute All Cameras",
"disable": "Unmute All Cameras"
},
"detect": {
"enable": "Enable Detect",
"disable": "Disable Detect"
@ -62,6 +67,10 @@
"enable": "Enable Autotracking",
"disable": "Disable Autotracking"
},
"streamStats": {
"enable": "Show Stream Stats",
"disable": "Hide Stream Stats"
},
"manualRecording": {
"start": "Start on-demand recording",
"started": "Started manual on-demand recording.",
@ -70,5 +79,11 @@
"end": "End on-demand recording",
"ended": "Ended manual on-demand recording.",
"failedToEnd": "Failed to end manual on-demand recording."
},
"streamingSettings": "Streaming Settings",
"notifications": "Notifications",
"audio": "Audio",
"suspend:": {
"forTime": "Suspend for: "
}
}

View File

@ -0,0 +1,11 @@
{
"export": "Export",
"calendar": "Calendar",
"filter": "Filter",
"toast": {
"error": {
"noValidTimeSelected": "No valid time range selected",
"endTimeMustAfterStartTime": "End time must be after start time"
}
}
}

View File

@ -258,7 +258,8 @@
"toast": {
"success": {
"createUser": "User {{user}} created successfully",
"deleteUser": "User {{user}} deleted successfully"
"deleteUser": "User {{user}} deleted successfully",
"updatePassword": "Password updated successfully."
},
"error": {
"setPasswordFailed": "Failed to save password: {{errorMessage}}",

View File

@ -1,5 +1,8 @@
{
"time": {
"untilForTime": "直到 {{time}}",
"untilForRestart": "直到 Frigate 重启。",
"untilRestart": "直到重启",
"ago": "{{timeAgo}} 前",
"justNow": "刚才",
"today": "今天",
@ -11,6 +14,12 @@
"lastWeek": "上个周",
"thisMonth": "本月",
"lastMonth": "上个月",
"5minutes": "5 分钟",
"10minutes": "10 分钟",
"30minutes": "30 分钟",
"1hour": "1 小时",
"12hours": "12 小时",
"24hours": "24 小时",
"pm": "上午",
"am": "下午",
"yr": "{{time}}年",
@ -43,6 +52,9 @@
"apply": "应用",
"reset": "重置",
"enabled": "启用",
"enable": "启用",
"disabled": "禁用",
"disable": "禁用",
"save": "保存",
"saving": "保存中……",
"cancel": "取消",
@ -61,7 +73,9 @@
"yes": "是",
"no": "否",
"download": "下载",
"info": "信息"
"info": "信息",
"suspended": "已暂停",
"unsuspended": "取消暂停"
},
"menu": {
"system": "系统",
@ -102,9 +116,11 @@
"uiPlayground": "UI Playground",
"faceLibrary": "人脸管理",
"user": {
"account": "账号",
"current": "当前用户:{{user}}",
"anonymous": "匿名",
"logout": "登出"
"logout": "登出",
"setPassword": "设置密码"
},
"restart": "重启 Frigate"
},

View File

@ -72,5 +72,16 @@
"overwrite": "{{searchName}} 已存在。保存将覆盖现有值。",
"success": "搜索 ({{searchName}}) 已保存。"
}
},
"recording": {
"confirmDelete": {
"title": "确认删除",
"desc": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。"
},
"button": {
"export": "导出",
"markAsReviewed": "标记为已审核",
"deleteNow": "立即删除"
}
}
}

View File

@ -1,5 +1,5 @@
{
"label": "过滤器",
"filter": "过滤器",
"labels": {
"all": "所有标签",
"all.short": "标签",

View File

@ -1,6 +1,7 @@
{
"documentTitle": "实时监控 - Frigate",
"documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate",
"lowBandwidthMode": "低带宽模式",
"twoWayTalk": {
"enable": "开启双向对话",
"disable": "关闭双向对话"
@ -42,6 +43,10 @@
"enable": "开启摄像头",
"disable": "关闭摄像头"
},
"muteCameras": {
"enable": "屏蔽所有摄像头",
"disable": "取消屏蔽所有摄像头"
},
"detect": {
"enable": "启用检测",
"disable": "关闭检测"
@ -62,6 +67,10 @@
"enable": "启用自动追踪",
"disable": "关闭自动追踪"
},
"streamStats": {
"enable": "显示视频流统计信息",
"disable": "隐藏视频流统计信息"
},
"manualRecording": {
"start": "开始手动按需录制",
"started": "已启用手动按需录制",
@ -70,5 +79,11 @@
"end": "停止手动按需录制",
"ended": "已完成手动按需录制",
"failedToEnd": "停止手动录制失败"
},
"streamingSettings": "视频流设置",
"notifications": "通知",
"audio": "音频",
"suspend": {
"forTime": "暂停时长:"
}
}

View File

@ -0,0 +1,11 @@
{
"export": "导出",
"calendar": "日历",
"filter": "筛选",
"toast": {
"error": {
"noValidTimeSelected": "未选择有效的时间范围",
"endTimeMustAfterStartTime": "结束时间必须晚于开始时间"
}
}
}

View File

@ -258,7 +258,8 @@
"toast": {
"success": {
"createUser": "用户 {{user}} 创建成功",
"deleteUser": "用户 {{user}} 删除成功"
"deleteUser": "用户 {{user}} 删除成功",
"updatePassword": "已成功修改密码"
},
"error": {
"setPasswordFailed": "保存密码出现错误:{{errorMessage}}",
@ -317,7 +318,7 @@
"title": "更改用户权限组",
"desc": "更新 <span className=\"font-medium\">{{username}}</span> 的权限",
"roleInfo": "<p>请选择此用户的适当角色:</p><ul className=\"mt-2 space-y-1 pl-5\"><li> • <span className=\"font-medium\">管理员 (Admin)</span> 拥有所有功能的完整访问权限。</li><li> • <span className=\"font-medium\">查看者 (Viewer)</span> 仅限访问实时监控、回放、探测和导出功能。</li></ul>"
}
}
}
},
"notification": {

View File

@ -4,8 +4,8 @@ import {
StatusMessage,
} from "@/context/statusbar-provider";
import useStats, { useAutoFrigateStats } from "@/hooks/use-stats";
import { t } from "i18next";
import { useContext, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { FaCheck } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io";
@ -13,6 +13,8 @@ import { MdCircle } from "react-icons/md";
import { Link } from "react-router-dom";
export default function Statusbar() {
const { t } = useTranslation(["views/system"]);
const { messages, addMessage, clearMessages } = useContext(
StatusBarMessagesContext,
)!;
@ -131,7 +133,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" />
{t("stats.healthy", { ns: "views/system" })}
{t("stats.healthy")}
</div>
) : (
Object.entries(messages).map(([key, messageArray]) => (

View File

@ -35,7 +35,7 @@ import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { buttonVariants } from "../ui/button";
import { t } from "i18next";
import { Trans, useTranslation } from "react-i18next";
type ReviewCardProps = {
event: ReviewSegment;
@ -47,6 +47,7 @@ export default function ReviewCard({
currentTime,
onClick,
}: ReviewCardProps) {
const { t } = useTranslation(["components/dialog"]);
const { data: config } = useSWR<FrigateConfig>("config");
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
const formattedDate = useFormattedTimestamp(
@ -83,28 +84,20 @@ export default function ReviewCard({
)
.then((response) => {
if (response.status == 200) {
toast.success(
t("export.toast.success", { ns: "components/dialog" }),
{
position: "top-center",
},
);
}
})
.catch((error) => {
if (error.response?.data?.message) {
toast.error(
`Failed to start export: ${error.response.data.message}`,
{ position: "top-center" },
);
} else {
toast.error(`Failed to start export: ${error.message}`, {
toast.success(t("export.toast.success"), {
position: "top-center",
});
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message || error.message || "Unknown error";
toast.error(t("export.toast.error.failed", { error: errorMessage }), {
position: "top-center",
});
});
setOptionsOpen(false);
}, [event]);
}, [event, t]);
const onDelete = useCallback(async () => {
await axios.post(`reviews/delete`, { ids: [event.id] });
@ -219,24 +212,24 @@ export default function ReviewCard({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
<AlertDialogTitle>
{t("recording.confirmDelete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
this review item?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
<Trans ns="components/dialog">
recording.confirmDelete.title
</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
Cancel
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={onDelete}
>
Delete
{t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -250,7 +243,9 @@ export default function ReviewCard({
onClick={onExport}
>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
<div className="text-primary">
{t("recording.button.export")}
</div>
</div>
</ContextMenuItem>
{!event.has_been_reviewed && (
@ -260,7 +255,9 @@ export default function ReviewCard({
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
<div className="text-primary">
{t("recording.button.markAsReviewed")}
</div>
</div>
</ContextMenuItem>
)}
@ -271,7 +268,9 @@ export default function ReviewCard({
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">
{bypassDialogRef.current ? "Delete Now" : "Delete"}
{bypassDialogRef.current
? t("recording.button.deleteNow")
: t("button.delete", { ns: "common" })}
</div>
</div>
</ContextMenuItem>

View File

@ -743,7 +743,10 @@ export function CameraGroupEdit({
setAllGroupsStreamingSettings(updatedSettings);
} else {
toast.error(
t("toast.save.error", { errorMessage: res.statusText }),
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
@ -758,6 +761,7 @@ export function CameraGroupEdit({
toast.error(
t("toast.save.error", {
errorMessage,
ns: "common",
}),
{ position: "top-center" },
);

View File

@ -23,8 +23,7 @@ import { FilterList, GeneralFilter } from "@/types/filter";
import CalendarFilterButton from "./CalendarFilterButton";
import { CamerasFilterButton } from "./CamerasFilterButton";
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
const REVIEW_FILTERS = [
"cameras",
@ -265,6 +264,7 @@ function ShowReviewFilter({
showReviewed,
setShowReviewed,
}: ShowReviewedFilterProps) {
const { t } = useTranslation(["components/filter"]);
const [showReviewedSwitch, setShowReviewedSwitch] = useOptimisticState(
showReviewed,
setShowReviewed,
@ -280,7 +280,7 @@ function ShowReviewFilter({
}
/>
<Label className="ml-2 cursor-pointer text-primary" htmlFor="reviewed">
{t("review.showReviewed", { ns: "components/filter" })}
{t("review.showReviewed")}
</Label>
</div>
@ -322,6 +322,7 @@ function GeneralFilterButton({
selectedZones,
onUpdateFilter,
}: GeneralFilterButtonProps) {
const { t } = useTranslation(["components/filter"]);
const [open, setOpen] = useState(false);
const [currentFilter, setCurrentFilter] = useState<GeneralFilter>({
labels: selectedLabels,
@ -366,7 +367,7 @@ function GeneralFilterButton({
: "text-primary"
}`}
>
{t("label", { ns: "components/filter" })}
{t("filter")}
</div>
</Button>
);
@ -441,6 +442,7 @@ export function GeneralFilterContent({
onReset,
onClose,
}: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
return (
<>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
@ -474,7 +476,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
{t("labels.all", { ns: "components/filter" })}
{t("labels.all")}
</Label>
<Switch
className="ml-1"
@ -521,7 +523,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allZones"
>
{t("zones.all", { ns: "components/filter" })}
{t("zones.all")}
</Label>
<Switch
className="ml-1"
@ -595,6 +597,7 @@ function ShowMotionOnlyButton({
motionOnly,
setMotionOnly,
}: ShowMotionOnlyButtonProps) {
const { t } = useTranslation(["views/events"]);
const [motionOnlyButton, setMotionOnlyButton] = useOptimisticState(
motionOnly,
setMotionOnly,
@ -613,7 +616,7 @@ function ShowMotionOnlyButton({
className="mx-2 cursor-pointer text-primary"
htmlFor="collapse-motion"
>
{t("motion.only", { ns: "views/events" })}
{t("motion.only")}
</Label>
</div>

View File

@ -1,10 +1,10 @@
import { useTheme } from "@/context/theme-provider";
import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { t } from "i18next";
import { useCallback, useEffect, useMemo } from "react";
import Chart from "react-apexcharts";
import { isMobileOnly } from "react-device-detect";
import { useTranslation } from "react-i18next";
import { MdCircle } from "react-icons/md";
import useSWR from "swr";
@ -24,6 +24,7 @@ export function CameraLineGraph({
updateTimes,
data,
}: CameraLineGraphProps) {
const { t } = useTranslation(["views/system"]);
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
@ -128,7 +129,7 @@ export function CameraLineGraph({
style={{ color: GRAPH_COLORS[labelIdx] }}
/>
<div className="text-xs text-muted-foreground">
{t("cameras.label." + label, { ns: "views/settings" })}
{t("cameras.label." + label)}
</div>
<div className="text-xs text-primary">
{lastValues[labelIdx]}

View File

@ -50,9 +50,12 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
.then((response) => {
if (response.status === 200) {
setPasswordDialogOpen(false);
toast.success("Password updated successfully.", {
position: "top-center",
});
toast.success(
t("users.toast.success.updatePassword", { ns: "views/settings" }),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -60,9 +63,15 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Error setting password: ${errorMessage}`, {
position: "top-center",
});
toast.error(
t("users.toast.error.setPasswordFailed", {
ns: "views/settings",
errorMessage,
}),
{
position: "top-center",
},
);
});
};
@ -85,7 +94,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="right">
<p>Account</p>
<p>{t("menu.user.account")}</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>
@ -100,7 +109,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
{t("menu.user.current", {
user: profile?.username || t("menu.user.anonymous"),
})}{" "}
{profile?.role && `(${profile.role})`}
{t("role." + profile?.role) && `(${t("role." + profile.role)})`}
</DropdownMenuLabel>
<DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} />
{profile?.username && profile.username !== "anonymous" && (
@ -112,7 +121,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
onClick={() => setPasswordDialogOpen(true)}
>
<LuSquarePen className="mr-2 size-4" />
<span>Set Password</span>
<span>{t("menu.user.setPassword")}</span>
</MenuItem>
)}
<MenuItem

View File

@ -97,9 +97,12 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
.then((response) => {
if (response.status === 200) {
setPasswordDialogOpen(false);
toast.success("Password updated successfully.", {
position: "top-center",
});
toast.success(
t("users.toast.success.updatePassword", { ns: "views/settings" }),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -107,9 +110,15 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Error setting password: ${errorMessage}`, {
position: "top-center",
});
toast.error(
t("users.toast.error.setPasswordFailed", {
ns: "views/settings",
errorMessage,
}),
{
position: "top-center",
},
);
});
};
@ -157,8 +166,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{isMobile && (
<div className="mb-2">
<DropdownMenuLabel>
Current User: {profile?.username || "anonymous"}{" "}
{profile?.role && `(${profile.role})`}
{t("menu.user.current", {
user: profile?.username || t("menu.user.anonymous"),
})}{" "}
{t("role." + profile?.role) &&
`(${t("role." + profile.role)})`}
</DropdownMenuLabel>
<DropdownMenuSeparator
className={isDesktop ? "mt-3" : "mt-1"}
@ -174,7 +186,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
onClick={() => setPasswordDialogOpen(true)}
>
<LuSquarePen className="mr-2 size-4" />
<span>Set Password</span>
<span>{t("menu.user.setPassword")}</span>
</MenuItem>
)}
<MenuItem

View File

@ -44,8 +44,7 @@ import {
useNotifications,
useNotificationSuspend,
} from "@/api/ws";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type LiveContextMenuProps = {
className?: string;
@ -87,6 +86,7 @@ export default function LiveContextMenu({
config,
children,
}: LiveContextMenuProps) {
const { t } = useTranslation("views/live");
const [showSettings, setShowSettings] = useState(false);
// camera enabled
@ -236,17 +236,19 @@ export default function LiveContextMenu({
};
const formatSuspendedUntil = (timestamp: string) => {
if (timestamp === "0") return "Frigate restarts.";
// Some languages require a change in word order
if (timestamp === "0") return t("time.untilForRestart", { ns: "common" });
return formatUnixTimestampToDateTime(Number.parseInt(timestamp), {
const time = formatUnixTimestampToDateTime(Number.parseInt(timestamp), {
time_style: "medium",
date_style: "medium",
timezone: config?.ui.timezone,
strftime_fmt:
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour")
: t("time.formattedTimestampExcludeSeconds"),
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
});
return t("time.untilForTime", { ns: "common", time });
};
return (
@ -261,7 +263,7 @@ export default function LiveContextMenu({
{preferredLiveMode == "jsmpeg" && isRestreamed && (
<div className="flex flex-row items-center gap-1">
<IoIosWarning className="mr-1 size-4 text-danger" />
<p className="mr-2 text-xs">Low-bandwidth mode</p>
<p className="mr-2 text-xs">{t("lowBandwidthMode")}</p>
</div>
)}
</div>
@ -270,7 +272,7 @@ export default function LiveContextMenu({
<ContextMenuSeparator className="mb-1" />
<div className="p-2 text-sm">
<div className="flex w-full flex-col gap-1">
<p>Audio</p>
<p>{t("audio")}</p>
<div className="flex flex-row items-center gap-1">
<VolumeIcon
className="size-5"
@ -297,7 +299,7 @@ export default function LiveContextMenu({
onClick={() => sendEnabled(isEnabled ? "OFF" : "ON")}
>
<div className="text-primary">
{isEnabled ? "Disable" : "Enable"} Camera
{isEnabled ? t("camera.disable") : t("camera.enable")}
</div>
</div>
</ContextMenuItem>
@ -307,7 +309,7 @@ export default function LiveContextMenu({
className="flex w-full cursor-pointer items-center justify-start gap-2"
onClick={isEnabled ? muteAll : undefined}
>
<div className="text-primary">Mute All Cameras</div>
<div className="text-primary">{t("muteCameras.enable")}</div>
</div>
</ContextMenuItem>
<ContextMenuItem disabled={!isEnabled}>
@ -315,7 +317,7 @@ export default function LiveContextMenu({
className="flex w-full cursor-pointer items-center justify-start gap-2"
onClick={isEnabled ? unmuteAll : undefined}
>
<div className="text-primary">Unmute All Cameras</div>
<div className="text-primary">{t("muteCameras.disable")}</div>
</div>
</ContextMenuItem>
<ContextMenuSeparator />
@ -325,7 +327,9 @@ export default function LiveContextMenu({
onClick={isEnabled ? toggleStats : undefined}
>
<div className="text-primary">
{statsState ? "Hide" : "Show"} Stream Stats
{statsState
? t("streamStats.disable")
: t("streamStats.enable")}
</div>
</div>
</ContextMenuItem>
@ -338,7 +342,9 @@ export default function LiveContextMenu({
: undefined
}
>
<div className="text-primary">Debug View</div>
<div className="text-primary">
{t("streaming.debugView", { ns: "components/dialog" })}
</div>
</div>
</ContextMenuItem>
{cameraGroup && cameraGroup !== "default" && (
@ -349,7 +355,7 @@ export default function LiveContextMenu({
className="flex w-full cursor-pointer items-center justify-start gap-2"
onClick={isEnabled ? () => setShowSettings(true) : undefined}
>
<div className="text-primary">Streaming Settings</div>
<div className="text-primary">{t("streamingSettings")}</div>
</div>
</ContextMenuItem>
</>
@ -362,7 +368,9 @@ export default function LiveContextMenu({
className="flex w-full cursor-pointer items-center justify-start gap-2"
onClick={isEnabled ? resetPreferredLiveMode : undefined}
>
<div className="text-primary">{t("button")}</div>
<div className="text-primary">
{t("button.reset", { ns: "common" })}
</div>
</div>
</ContextMenuItem>
</>
@ -373,7 +381,7 @@ export default function LiveContextMenu({
<ContextMenuSub>
<ContextMenuSubTrigger disabled={!isEnabled}>
<div className="flex items-center gap-2">
<span>Notifications</span>
<span>{t("notifications")}</span>
</div>
</ContextMenuSubTrigger>
<ContextMenuSubContent>
@ -384,25 +392,29 @@ export default function LiveContextMenu({
{isSuspended ? (
<>
<IoIosNotificationsOff className="size-5 text-muted-foreground" />
<span>Suspended</span>
<span>
{t("button.suspended", { ns: "common" })}
</span>
</>
) : (
<>
<IoIosNotifications className="size-5 text-muted-foreground" />
<span>Enabled</span>
<span>
{t("button.enabled", { ns: "common" })}
</span>
</>
)}
</>
) : (
<>
<IoIosNotificationsOff className="size-5 text-danger" />
<span>Disabled</span>
<span>{t("button.disabled", { ns: "common" })}</span>
</>
)}
</div>
{isSuspended && (
<span className="text-xs text-primary-variant">
Until {formatSuspendedUntil(notificationSuspendUntil)}
{formatSuspendedUntil(notificationSuspendUntil)}
</span>
)}
</div>
@ -423,9 +435,11 @@ export default function LiveContextMenu({
>
<div className="flex w-full flex-col gap-2">
{notificationState === "ON" ? (
<span>Unsuspend</span>
<span>
{t("button.unsuspended", { ns: "common" })}
</span>
) : (
<span>Enable</span>
<span>{t("button.enable", { ns: "common" })}</span>
)}
</div>
</ContextMenuItem>
@ -436,7 +450,7 @@ export default function LiveContextMenu({
<ContextMenuSeparator />
<div className="px-2 py-1.5">
<p className="mb-2 text-sm font-medium text-muted-foreground">
Suspend for:
{t("suspend.forTime")}
</p>
<div className="space-y-1">
<ContextMenuItem
@ -445,7 +459,7 @@ export default function LiveContextMenu({
isEnabled ? () => handleSuspend("5") : undefined
}
>
5 minutes
{t("time.5minutes", { ns: "common" })}
</ContextMenuItem>
<ContextMenuItem
disabled={!isEnabled}
@ -455,7 +469,7 @@ export default function LiveContextMenu({
: undefined
}
>
10 minutes
{t("time.10minutes", { ns: "common" })}
</ContextMenuItem>
<ContextMenuItem
disabled={!isEnabled}
@ -465,7 +479,7 @@ export default function LiveContextMenu({
: undefined
}
>
30 minutes
{t("time.30minutes", { ns: "common" })}
</ContextMenuItem>
<ContextMenuItem
disabled={!isEnabled}
@ -475,7 +489,7 @@ export default function LiveContextMenu({
: undefined
}
>
1 hour
{t("time.1hour", { ns: "common" })}
</ContextMenuItem>
<ContextMenuItem
disabled={!isEnabled}
@ -485,7 +499,7 @@ export default function LiveContextMenu({
: undefined
}
>
12 hours
{t("time.12hours", { ns: "common" })}
</ContextMenuItem>
<ContextMenuItem
disabled={!isEnabled}
@ -495,7 +509,7 @@ export default function LiveContextMenu({
: undefined
}
>
24 hours
{t("time.24hours", { ns: "common" })}
</ContextMenuItem>
<ContextMenuItem
disabled={!isEnabled}
@ -505,7 +519,7 @@ export default function LiveContextMenu({
: undefined
}
>
Until restart
{t("time.untilRestart", { ns: "common" })}
</ContextMenuItem>
</div>
</div>

View File

@ -20,7 +20,7 @@ import axios from "axios";
import SaveExportOverlay from "./SaveExportOverlay";
import { isIOS, isMobile } from "react-device-detect";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
@ -70,6 +70,7 @@ export default function MobileReviewSettingsDrawer({
setMode,
setShowExportPreview,
}: MobileReviewSettingsDrawerProps) {
const { t } = useTranslation(["views/recording"]);
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
// exports
@ -77,12 +78,14 @@ export default function MobileReviewSettingsDrawer({
const [name, setName] = useState("");
const onStartExport = useCallback(() => {
if (!range) {
toast.error("No valid time range selected", { position: "top-center" });
toast.error(t("toast.error.noValidTimeSelected"), {
position: "top-center",
});
return;
}
if (range.before < range.after) {
toast.error("End time must be after start time", {
toast.error(t("toast.error.endTimeMustAfterStartTime"), {
position: "top-center",
});
return;
@ -114,11 +117,17 @@ export default function MobileReviewSettingsDrawer({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Failed to start export: ${errorMessage}`, {
position: "top-center",
});
toast.error(
t("export.toast.error.failed", {
ns: "components/dialog",
errorMessage,
}),
{
position: "top-center",
},
);
});
}, [camera, name, range, setRange, setName, setMode]);
}, [camera, name, range, setRange, setName, setMode, t]);
// filters
@ -147,7 +156,7 @@ export default function MobileReviewSettingsDrawer({
}}
>
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
Export
{t("export")}
</Button>
)}
{features.includes("calendar") && (
@ -160,7 +169,7 @@ export default function MobileReviewSettingsDrawer({
<FaCalendarAlt
className={`${filter?.after ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
Calendar
{t("calendar")}
</Button>
)}
{features.includes("filter") && (
@ -173,7 +182,7 @@ export default function MobileReviewSettingsDrawer({
<FaFilter
className={`${filter?.labels || filter?.zones ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
Filter
{t("filter")}
</Button>
)}
</div>
@ -210,10 +219,10 @@ export default function MobileReviewSettingsDrawer({
className="absolute left-0 text-selected"
onClick={() => setDrawerMode("select")}
>
Back
{t("button.back", { ns: "common" })}
</div>
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
Calendar
{t("calendar")}
</div>
</div>
<div className="flex w-full flex-row justify-center">
@ -260,7 +269,7 @@ export default function MobileReviewSettingsDrawer({
className="absolute left-0 text-selected"
onClick={() => setDrawerMode("select")}
>
Back
{t("button.back", { ns: "common" })}
</div>
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
Filter

View File

@ -211,6 +211,7 @@ export default function ObjectMaskEditPane({
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
@ -226,6 +227,7 @@ export default function ObjectMaskEditPane({
toast.error(
t("toast.save.error", {
errorMessage,
ns: "common",
}),
{
position: "top-center",

View File

@ -330,7 +330,7 @@ export default function ZoneEditPane({
// Wait for the config to be updated
mutatedConfig = await updateConfig();
} catch (error) {
toast.error(t("toast.save.error.noMessage"), {
toast.error(t("toast.save.error.noMessage", { ns: "common" }), {
position: "top-center",
});
return;
@ -422,7 +422,10 @@ export default function ZoneEditPane({
updateConfig();
} else {
toast.error(
t("toast.save.error", { errorMessage: res.statusText }),
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
@ -437,6 +440,7 @@ export default function ZoneEditPane({
toast.error(
t("toast.save.error", {
errorMessage,
ns: "common",
}),
{
position: "top-center",

View File

@ -22,11 +22,13 @@ import {
import EventView from "@/views/events/EventView";
import { RecordingView } from "@/views/recording/RecordingView";
import axios from "axios";
import { t } from "i18next";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
export default function Events() {
const { t } = useTranslation(["views/events"]);
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
@ -78,11 +80,11 @@ export default function Events() {
useEffect(() => {
if (recording) {
document.title = t("recordings.documentTitle", { ns: "views/events" });
document.title = t("recordings.documentTitle");
} else {
document.title = t("documentTitle", { ns: "views/events" });
document.title = t("documentTitle");
}
}, [recording, severity]);
}, [recording, severity, t]);
// review filter

View File

@ -37,13 +37,13 @@ import AuthenticationView from "@/views/settings/AuthenticationView";
import NotificationView from "@/views/settings/NotificationsSettingsView";
import SearchSettingsView from "@/views/settings/SearchSettingsView";
import UiSettingsView from "@/views/settings/UiSettingsView";
import { t } from "i18next";
import { useSearchEffect } from "@/hooks/use-overlay-state";
import { useSearchParams } from "react-router-dom";
import { useInitialCameraState } from "@/api/ws";
import { isInIframe } from "@/utils/isIFrame";
import { isPWA } from "@/utils/isPWA";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { useTranslation } from "react-i18next";
const allSettingsViews = [
"uiSettings",
@ -58,6 +58,7 @@ const allSettingsViews = [
type SettingsType = (typeof allSettingsViews)[number];
export default function Settings() {
const { t } = useTranslation(["views/settings"]);
const [page, setPage] = useState<SettingsType>("uiSettings");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const tabsRef = useRef<HTMLDivElement | null>(null);
@ -164,7 +165,7 @@ export default function Settings() {
useSearchEffect("page", (page: string) => {
if (allSettingsViews.includes(page as SettingsType)) {
// Restrict viewer to UI settings
if (!isAdmin && !["UI settings", "debug"].includes(page)) {
if (!isAdmin && !["uiSettings", "debug"].includes(page)) {
setPage("uiSettings");
} else {
setPage(page as SettingsType);
@ -200,7 +201,7 @@ export default function Settings() {
onValueChange={(value: SettingsType) => {
if (value) {
// Restrict viewer navigation
if (!isAdmin && !["UI settings", "debug"].includes(value)) {
if (!isAdmin && !["uiSettings", "debug"].includes(value)) {
setPageToggle("uiSettings");
} else {
setPageToggle(value);
@ -216,9 +217,7 @@ export default function Settings() {
data-nav-item={item}
aria-label={`Select ${item}`}
>
<div className="capitalize">
{t("menu." + item, { ns: "views/settings" })}
</div>
<div className="capitalize">{t("menu." + item)}</div>
</ToggleGroupItem>
))}
</ToggleGroup>

View File

@ -214,7 +214,7 @@ export default function EventView({
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("export.toast.error", {
t("export.toast.error.failed", {
ns: "components/dialog",
message: errorMessage,
}),

View File

@ -27,8 +27,7 @@ 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 { Trans, useTranslation } from "react-i18next";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useAlertsState, useDetectionsState, useEnabledState } from "@/api/ws";
@ -47,6 +46,8 @@ 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
@ -165,7 +166,10 @@ export default function CameraSettingsView({
updateConfig();
} else {
toast.error(
t("toast.save.error", { errorMessage: res.statusText }),
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
@ -180,6 +184,7 @@ export default function CameraSettingsView({
toast.error(
t("toast.save.error", {
errorMessage,
ns: "common",
}),
{
position: "top-center",
@ -190,7 +195,7 @@ export default function CameraSettingsView({
setIsLoading(false);
});
},
[updateConfig, setIsLoading, selectedCamera, cameraConfig],
[updateConfig, setIsLoading, selectedCamera, cameraConfig, t],
);
const onCancel = useCallback(() => {
@ -461,7 +466,6 @@ export default function CameraSettingsView({
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
ns: "views/settings",
},
)
: t("camera.reviewClassification.objectAlertsTips", {
@ -469,7 +473,6 @@ export default function CameraSettingsView({
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
ns: "views/settings",
})}
</div>
</FormItem>

View File

@ -125,15 +125,22 @@ export default function MotionTunerView({
setChangedValue(false);
updateConfig();
} else {
toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
position: "top-center",
});
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
toast.error(
t("toast.save.error", {
errorMessage: error.response.data.message,
ns: "common",
}),
{ position: "top-center" },
);

View File

@ -641,17 +641,19 @@ export function CameraNotificationSwitch({
};
const formatSuspendedUntil = (timestamp: string) => {
if (timestamp === "0") return "Frigate restarts.";
// Some languages require a change in word order
if (timestamp === "0") return t("time.untilForRestart", { ns: "common" });
return formatUnixTimestampToDateTime(parseInt(timestamp), {
const time = formatUnixTimestampToDateTime(parseInt(timestamp), {
time_style: "medium",
date_style: "medium",
timezone: config?.ui.timezone,
strftime_fmt:
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour")
: t("time.formattedTimestampExcludeSeconds"),
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
});
return t("time.untilForTime", { ns: "common", time });
};
return (
@ -677,7 +679,7 @@ export function CameraNotificationSwitch({
</div>
) : (
<div className="flex flex-row items-center gap-2 text-sm text-danger">
Notifications suspended until{" "}
Notifications suspended{" "}
{formatSuspendedUntil(notificationSuspendUntil)}
</div>
)}

View File

@ -100,9 +100,15 @@ export default function ExploreSettingsView({
setChangedValue(false);
updateConfig();
} else {
toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
position: "top-center",
});
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -113,6 +119,7 @@ export default function ExploreSettingsView({
toast.error(
t("toast.save.error", {
errorMessage,
ns: "common",
}),
{
position: "top-center",