From a52592bcac077da252962088226873e73ce29408 Mon Sep 17 00:00:00 2001 From: ZhaiSoul <842607283@qq.com> Date: Tue, 11 Mar 2025 13:07:54 +0800 Subject: [PATCH] chore: add more i18n keys again --- web/public/locales/en/common.json | 20 ++++- web/public/locales/en/components/dialog.json | 11 +++ web/public/locales/en/components/filter.json | 2 +- web/public/locales/en/views/live.json | 15 ++++ web/public/locales/en/views/recording.json | 11 +++ web/public/locales/en/views/settings.json | 3 +- web/public/locales/zh-CN/common.json | 20 ++++- .../locales/zh-CN/components/dialog.json | 11 +++ .../locales/zh-CN/components/filter.json | 2 +- web/public/locales/zh-CN/views/live.json | 15 ++++ web/public/locales/zh-CN/views/recording.json | 11 +++ web/public/locales/zh-CN/views/settings.json | 5 +- web/src/components/Statusbar.tsx | 6 +- web/src/components/card/ReviewCard.tsx | 57 +++++++------- .../components/filter/CameraGroupSelector.tsx | 6 +- .../components/filter/ReviewFilterGroup.tsx | 17 +++-- web/src/components/graph/CameraGraph.tsx | 5 +- web/src/components/menu/AccountSettings.tsx | 27 ++++--- web/src/components/menu/GeneralSettings.tsx | 30 +++++--- web/src/components/menu/LiveContextMenu.tsx | 74 +++++++++++-------- .../overlay/MobileReviewSettingsDrawer.tsx | 35 +++++---- .../settings/ObjectMaskEditPane.tsx | 2 + web/src/components/settings/ZoneEditPane.tsx | 8 +- web/src/pages/Events.tsx | 10 ++- web/src/pages/Settings.tsx | 11 ++- web/src/views/events/EventView.tsx | 2 +- web/src/views/settings/CameraSettingsView.tsx | 19 +++-- web/src/views/settings/MotionTunerView.tsx | 13 +++- .../settings/NotificationsSettingsView.tsx | 12 +-- web/src/views/settings/SearchSettingsView.tsx | 13 +++- 30 files changed, 330 insertions(+), 143 deletions(-) create mode 100644 web/public/locales/en/views/recording.json create mode 100644 web/public/locales/zh-CN/views/recording.json 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": "

请选择此用户的适当角色:

" - } + } } }, "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")} )} { 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 && (
- 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)})`} setPasswordDialogOpen(true)} > - Set Password + {t("menu.user.setPassword")} )} { - 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 && (
-

Low-bandwidth mode

+

{t("lowBandwidthMode")}

)}
@@ -270,7 +272,7 @@ export default function LiveContextMenu({
-

Audio

+

{t("audio")}

sendEnabled(isEnabled ? "OFF" : "ON")} >
- {isEnabled ? "Disable" : "Enable"} Camera + {isEnabled ? t("camera.disable") : t("camera.enable")}
@@ -307,7 +309,7 @@ export default function LiveContextMenu({ className="flex w-full cursor-pointer items-center justify-start gap-2" onClick={isEnabled ? muteAll : undefined} > -
Mute All Cameras
+
{t("muteCameras.enable")}
@@ -315,7 +317,7 @@ export default function LiveContextMenu({ className="flex w-full cursor-pointer items-center justify-start gap-2" onClick={isEnabled ? unmuteAll : undefined} > -
Unmute All Cameras
+
{t("muteCameras.disable")}
@@ -325,7 +327,9 @@ export default function LiveContextMenu({ onClick={isEnabled ? toggleStats : undefined} >
- {statsState ? "Hide" : "Show"} Stream Stats + {statsState + ? t("streamStats.disable") + : t("streamStats.enable")}
@@ -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",