diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json
index 973f3f1d6..b426c6f7c 100644
--- a/web/public/locales/en/common.json
+++ b/web/public/locales/en/common.json
@@ -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": {
diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json
index 38a0ff22d..85666eef4 100644
--- a/web/public/locales/en/components/dialog.json
+++ b/web/public/locales/en/components/dialog.json
@@ -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?
Hold the Shift key to bypass this dialog in the future."
+ },
+ "button": {
+ "export": "Export",
+ "markAsReviewed": "Mark as reviewed",
+ "deleteNow": "Delete Now"
+ }
}
}
diff --git a/web/public/locales/en/components/filter.json b/web/public/locales/en/components/filter.json
index 00dc6e8ac..401d405d6 100644
--- a/web/public/locales/en/components/filter.json
+++ b/web/public/locales/en/components/filter.json
@@ -1,5 +1,5 @@
{
- "label": "Filter",
+ "filter": "Filter",
"labels": {
"all": "All Labels",
"all.short": "Labels",
diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json
index b82dc17c2..91bf80893 100644
--- a/web/public/locales/en/views/live.json
+++ b/web/public/locales/en/views/live.json
@@ -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: "
}
}
diff --git a/web/public/locales/en/views/recording.json b/web/public/locales/en/views/recording.json
new file mode 100644
index 000000000..d8bd373a9
--- /dev/null
+++ b/web/public/locales/en/views/recording.json
@@ -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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index 64fd734db..6d778c792 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -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}}",
diff --git a/web/public/locales/zh-CN/common.json b/web/public/locales/zh-CN/common.json
index 18cf5cc73..bdd740cd5 100644
--- a/web/public/locales/zh-CN/common.json
+++ b/web/public/locales/zh-CN/common.json
@@ -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"
},
diff --git a/web/public/locales/zh-CN/components/dialog.json b/web/public/locales/zh-CN/components/dialog.json
index 7d24d214d..28041b01d 100644
--- a/web/public/locales/zh-CN/components/dialog.json
+++ b/web/public/locales/zh-CN/components/dialog.json
@@ -72,5 +72,16 @@
"overwrite": "{{searchName}} 已存在。保存将覆盖现有值。",
"success": "搜索 ({{searchName}}) 已保存。"
}
+ },
+ "recording": {
+ "confirmDelete": {
+ "title": "确认删除",
+ "desc": "您确定要删除与此审核项相关的所有录制视频吗?
提示:按住 Shift 键点击删除可跳过此对话框。"
+ },
+ "button": {
+ "export": "导出",
+ "markAsReviewed": "标记为已审核",
+ "deleteNow": "立即删除"
+ }
}
}
diff --git a/web/public/locales/zh-CN/components/filter.json b/web/public/locales/zh-CN/components/filter.json
index b64f1c737..00990e41f 100644
--- a/web/public/locales/zh-CN/components/filter.json
+++ b/web/public/locales/zh-CN/components/filter.json
@@ -1,5 +1,5 @@
{
- "label": "过滤器",
+ "filter": "过滤器",
"labels": {
"all": "所有标签",
"all.short": "标签",
diff --git a/web/public/locales/zh-CN/views/live.json b/web/public/locales/zh-CN/views/live.json
index 3499449f6..3638d4cdb 100644
--- a/web/public/locales/zh-CN/views/live.json
+++ b/web/public/locales/zh-CN/views/live.json
@@ -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": "暂停时长:"
}
}
diff --git a/web/public/locales/zh-CN/views/recording.json b/web/public/locales/zh-CN/views/recording.json
new file mode 100644
index 000000000..a27769af1
--- /dev/null
+++ b/web/public/locales/zh-CN/views/recording.json
@@ -0,0 +1,11 @@
+{
+ "export": "导出",
+ "calendar": "日历",
+ "filter": "筛选",
+ "toast": {
+ "error": {
+ "noValidTimeSelected": "未选择有效的时间范围",
+ "endTimeMustAfterStartTime": "结束时间必须晚于开始时间"
+ }
+ }
+}
diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json
index 746768b15..19be3e44f 100644
--- a/web/public/locales/zh-CN/views/settings.json
+++ b/web/public/locales/zh-CN/views/settings.json
@@ -258,7 +258,8 @@
"toast": {
"success": {
"createUser": "用户 {{user}} 创建成功",
- "deleteUser": "用户 {{user}} 删除成功"
+ "deleteUser": "用户 {{user}} 删除成功",
+ "updatePassword": "已成功修改密码"
},
"error": {
"setPasswordFailed": "保存密码出现错误:{{errorMessage}}",
@@ -317,7 +318,7 @@
"title": "更改用户权限组",
"desc": "更新 {{username}} 的权限",
"roleInfo": "
请选择此用户的适当角色:
- • 管理员 (Admin): 拥有所有功能的完整访问权限。
- • 查看者 (Viewer): 仅限访问实时监控、回放、探测和导出功能。
"
- }
+ }
}
},
"notification": {
diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx
index 131fdb2eb..88a862e3b 100644
--- a/web/src/components/Statusbar.tsx
+++ b/web/src/components/Statusbar.tsx
@@ -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 ? (
- {t("stats.healthy", { ns: "views/system" })}
+ {t("stats.healthy")}
) : (
Object.entries(messages).map(([key, messageArray]) => (
diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx
index 6975f7d9e..d1939dfe4 100644
--- a/web/src/components/card/ReviewCard.tsx
+++ b/web/src/components/card/ReviewCard.tsx
@@ -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("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({
>
- Confirm Delete
+
+ {t("recording.confirmDelete.title")}
+
- Are you sure you want to delete all recorded video associated with
- this review item?
-
-
- Hold the Shift key to bypass this dialog in the future.
+
+ recording.confirmDelete.title
+
setOptionsOpen(false)}>
- Cancel
+ {t("button.cancel", { ns: "common" })}
- Delete
+ {t("button.delete", { ns: "common" })}
@@ -250,7 +243,9 @@ export default function ReviewCard({
onClick={onExport}
>
- Export
+
+ {t("recording.button.export")}
+
{!event.has_been_reviewed && (
@@ -260,7 +255,9 @@ export default function ReviewCard({
onClick={onMarkAsReviewed}
>
- Mark as reviewed
+
+ {t("recording.button.markAsReviewed")}
+
)}
@@ -271,7 +268,9 @@ export default function ReviewCard({
>
- {bypassDialogRef.current ? "Delete Now" : "Delete"}
+ {bypassDialogRef.current
+ ? t("recording.button.deleteNow")
+ : t("button.delete", { ns: "common" })}
diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx
index aff94680a..1ee7e0ab8 100644
--- a/web/src/components/filter/CameraGroupSelector.tsx
+++ b/web/src/components/filter/CameraGroupSelector.tsx
@@ -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" },
);
diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx
index 888f6da5c..f038e0daa 100644
--- a/web/src/components/filter/ReviewFilterGroup.tsx
+++ b/web/src/components/filter/ReviewFilterGroup.tsx
@@ -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({
}
/>
@@ -322,6 +322,7 @@ function GeneralFilterButton({
selectedZones,
onUpdateFilter,
}: GeneralFilterButtonProps) {
+ const { t } = useTranslation(["components/filter"]);
const [open, setOpen] = useState(false);
const [currentFilter, setCurrentFilter] = useState({
labels: selectedLabels,
@@ -366,7 +367,7 @@ function GeneralFilterButton({
: "text-primary"
}`}
>
- {t("label", { ns: "components/filter" })}
+ {t("filter")}
);
@@ -441,6 +442,7 @@ export function GeneralFilterContent({
onReset,
onClose,
}: GeneralFilterContentProps) {
+ const { t } = useTranslation(["components/filter"]);
return (
<>
@@ -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")}
- {t("zones.all", { ns: "components/filter" })}
+ {t("zones.all")}
- {t("motion.only", { ns: "views/events" })}
+ {t("motion.only")}
diff --git a/web/src/components/graph/CameraGraph.tsx b/web/src/components/graph/CameraGraph.tsx
index 1ed970354..a347c2d37 100644
--- a/web/src/components/graph/CameraGraph.tsx
+++ b/web/src/components/graph/CameraGraph.tsx
@@ -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("config", {
revalidateOnFocus: false,
});
@@ -128,7 +129,7 @@ export function CameraLineGraph({
style={{ color: GRAPH_COLORS[labelIdx] }}
/>
- {t("cameras.label." + label, { ns: "views/settings" })}
+ {t("cameras.label." + label)}
{lastValues[labelIdx]}
diff --git a/web/src/components/menu/AccountSettings.tsx b/web/src/components/menu/AccountSettings.tsx
index 14069c1bb..c15544cbb 100644
--- a/web/src/components/menu/AccountSettings.tsx
+++ b/web/src/components/menu/AccountSettings.tsx
@@ -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) {
- Account
+ {t("menu.user.account")}
@@ -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)})`}
{profile?.username && profile.username !== "anonymous" && (
@@ -112,7 +121,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
onClick={() => setPasswordDialogOpen(true)}
>
-
Set Password
+
{t("menu.user.setPassword")}
)}
@@ -338,7 +342,9 @@ export default function LiveContextMenu({
: undefined
}
>
- Debug View
+
+ {t("streaming.debugView", { ns: "components/dialog" })}
+
{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}
>
- Streaming Settings
+ {t("streamingSettings")}
>
@@ -362,7 +368,9 @@ export default function LiveContextMenu({
className="flex w-full cursor-pointer items-center justify-start gap-2"
onClick={isEnabled ? resetPreferredLiveMode : undefined}
>
- {t("button")}
+
+ {t("button.reset", { ns: "common" })}
+
>
@@ -373,7 +381,7 @@ export default function LiveContextMenu({
- Notifications
+ {t("notifications")}
@@ -384,25 +392,29 @@ export default function LiveContextMenu({
{isSuspended ? (
<>
- Suspended
+
+ {t("button.suspended", { ns: "common" })}
+
>
) : (
<>
- Enabled
+
+ {t("button.enabled", { ns: "common" })}
+
>
)}
>
) : (
<>
- Disabled
+ {t("button.disabled", { ns: "common" })}
>
)}
{isSuspended && (
- Until {formatSuspendedUntil(notificationSuspendUntil)}
+ {formatSuspendedUntil(notificationSuspendUntil)}
)}
@@ -423,9 +435,11 @@ export default function LiveContextMenu({
>
{notificationState === "ON" ? (
- Unsuspend
+
+ {t("button.unsuspended", { ns: "common" })}
+
) : (
- Enable
+ {t("button.enable", { ns: "common" })}
)}
@@ -436,7 +450,7 @@ export default function LiveContextMenu({
- Suspend for:
+ {t("suspend.forTime")}
handleSuspend("5") : undefined
}
>
- 5 minutes
+ {t("time.5minutes", { ns: "common" })}
- 10 minutes
+ {t("time.10minutes", { ns: "common" })}
- 30 minutes
+ {t("time.30minutes", { ns: "common" })}
- 1 hour
+ {t("time.1hour", { ns: "common" })}
- 12 hours
+ {t("time.12hours", { ns: "common" })}
- 24 hours
+ {t("time.24hours", { ns: "common" })}
- Until restart
+ {t("time.untilRestart", { ns: "common" })}
diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx
index 801c56f51..ec1cd0131 100644
--- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx
+++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx
@@ -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("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({
}}
>
- Export
+ {t("export")}
)}
{features.includes("calendar") && (
@@ -160,7 +169,7 @@ export default function MobileReviewSettingsDrawer({
- Calendar
+ {t("calendar")}
)}
{features.includes("filter") && (
@@ -173,7 +182,7 @@ export default function MobileReviewSettingsDrawer({
- Filter
+ {t("filter")}
)}
@@ -210,10 +219,10 @@ export default function MobileReviewSettingsDrawer({
className="absolute left-0 text-selected"
onClick={() => setDrawerMode("select")}
>
- Back
+ {t("button.back", { ns: "common" })}
- Calendar
+ {t("calendar")}
@@ -260,7 +269,7 @@ export default function MobileReviewSettingsDrawer({
className="absolute left-0 text-selected"
onClick={() => setDrawerMode("select")}
>
- Back
+ {t("button.back", { ns: "common" })}
Filter
diff --git a/web/src/components/settings/ObjectMaskEditPane.tsx b/web/src/components/settings/ObjectMaskEditPane.tsx
index aa4f1b63a..80b36ec95 100644
--- a/web/src/components/settings/ObjectMaskEditPane.tsx
+++ b/web/src/components/settings/ObjectMaskEditPane.tsx
@@ -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",
diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx
index d9988b161..ab72a894f 100644
--- a/web/src/components/settings/ZoneEditPane.tsx
+++ b/web/src/components/settings/ZoneEditPane.tsx
@@ -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",
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx
index 107268d93..d477f5693 100644
--- a/web/src/pages/Events.tsx
+++ b/web/src/pages/Events.tsx
@@ -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
("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
diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx
index eea047bef..452abf41f 100644
--- a/web/src/pages/Settings.tsx
+++ b/web/src/pages/Settings.tsx
@@ -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("uiSettings");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const tabsRef = useRef(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}`}
>
-
- {t("menu." + item, { ns: "views/settings" })}
-
+ {t("menu." + item)}
))}
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index d7ef32646..b588fb1ff 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -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,
}),
diff --git a/web/src/views/settings/CameraSettingsView.tsx b/web/src/views/settings/CameraSettingsView.tsx
index 4dd34590c..2b052f20f 100644
--- a/web/src/views/settings/CameraSettingsView.tsx
+++ b/web/src/views/settings/CameraSettingsView.tsx
@@ -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("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",
})}
diff --git a/web/src/views/settings/MotionTunerView.tsx b/web/src/views/settings/MotionTunerView.tsx
index 3ce691bb6..4f75021d9 100644
--- a/web/src/views/settings/MotionTunerView.tsx
+++ b/web/src/views/settings/MotionTunerView.tsx
@@ -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" },
);
diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx
index dd3490624..ebe9ad187 100644
--- a/web/src/views/settings/NotificationsSettingsView.tsx
+++ b/web/src/views/settings/NotificationsSettingsView.tsx
@@ -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({
) : (
- Notifications suspended until{" "}
+ Notifications suspended{" "}
{formatSuspendedUntil(notificationSuspendUntil)}
)}
diff --git a/web/src/views/settings/SearchSettingsView.tsx b/web/src/views/settings/SearchSettingsView.tsx
index 79ac7e3f4..1ddee7a11 100644
--- a/web/src/views/settings/SearchSettingsView.tsx
+++ b/web/src/views/settings/SearchSettingsView.tsx
@@ -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",