From f4bd490a1294bf265ad2651f1fcbecefee829c28 Mon Sep 17 00:00:00 2001 From: ZhaiSoul <842607283@qq.com> Date: Sat, 15 Mar 2025 01:10:32 +0800 Subject: [PATCH] feat: add more toast i18n keys --- web/public/locales/en/common.json | 1 + web/public/locales/en/components/player.json | 8 +++ web/public/locales/en/views/configEditor.json | 11 ++- web/public/locales/en/views/explore.json | 12 ++++ web/public/locales/en/views/exports.json | 5 ++ web/public/locales/en/views/faceLibrary.json | 40 +++++++++++ web/public/locales/en/views/settings.json | 49 +++++++++++-- web/public/locales/en/views/system.json | 21 +++++- web/public/locales/zh-CN/common.json | 10 ++- .../locales/zh-CN/components/player.json | 8 +++ .../locales/zh-CN/views/configEditor.json | 10 ++- web/public/locales/zh-CN/views/explore.json | 11 +++ web/public/locales/zh-CN/views/exports.json | 5 ++ .../locales/zh-CN/views/faceLibrary.json | 40 +++++++++++ web/public/locales/zh-CN/views/settings.json | 54 +++++++++++++-- web/public/locales/zh-CN/views/system.json | 21 +++++- .../components/overlay/CameraInfoDialog.tsx | 24 +++++-- web/src/components/overlay/GPUInfoDialog.tsx | 2 +- .../overlay/detail/SearchDetailDialog.tsx | 32 ++++++--- web/src/components/player/HlsVideoPlayer.tsx | 6 +- web/src/pages/ConfigEditor.tsx | 10 +-- web/src/pages/Exports.tsx | 4 +- web/src/pages/FaceLibrary.tsx | 69 ++++++++++--------- web/src/pages/Logs.tsx | 21 +++--- web/src/utils/browserUtil.ts | 3 +- web/src/views/settings/AuthenticationView.tsx | 13 ++-- web/src/views/settings/MasksAndZonesView.tsx | 10 ++- .../settings/NotificationsSettingsView.tsx | 49 +++++++------ web/src/views/settings/UiSettingsView.tsx | 37 ++++++---- 29 files changed, 457 insertions(+), 129 deletions(-) create mode 100644 web/public/locales/en/views/faceLibrary.json create mode 100644 web/public/locales/zh-CN/views/faceLibrary.json diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 707052b28..00334cf75 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -137,6 +137,7 @@ } }, "toast": { + "copyUrlToClipboard": "Copied URL to clipboard.", "save": { "error": "Failed to save config changes: {{errorMessage}}", "error.noMessage": "Failed to save config changes" diff --git a/web/public/locales/en/components/player.json b/web/public/locales/en/components/player.json index 8f565e75a..c5e9b6662 100644 --- a/web/public/locales/en/components/player.json +++ b/web/public/locales/en/components/player.json @@ -27,5 +27,13 @@ "droppedFrames.short.value": "{{droppedFrames}} frames", "decodedFrames": "Decoded Frames:", "droppedFrameRate": "Dropped Frame Rate:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Successfully submitted frame to Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Failed to submit frame to Frigate+" + } } } \ No newline at end of file diff --git a/web/public/locales/en/views/configEditor.json b/web/public/locales/en/views/configEditor.json index 417448147..d62a7d376 100644 --- a/web/public/locales/en/views/configEditor.json +++ b/web/public/locales/en/views/configEditor.json @@ -2,5 +2,14 @@ "configEditor": "Config Editor", "copyConfig": "Copy Config", "saveAndRestart": "Save & Restart", - "saveOnly": "Save Only" + "saveOnly": "Save Only", + "toast": { + "success": { + "copyToClipboard": "Config copied to clipboard." + }, + "error": { + "savingError": "Error saving config" + } + + } } \ No newline at end of file diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index a5a78abd6..2024b4956 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -1,4 +1,5 @@ { + "generativeAI": "Generative AI", "exploreIsUnavailable": { "title": "Explore is Unavailable", "embeddingsReindexing": { @@ -83,7 +84,18 @@ "mismatch_one": "{{count}} unavailable object was detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.", "mismatch_other": "{{count}} unavailable objects were detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.", "hasMissingObjects": "Adjust your configuration if you want Frigate to save tracked objects for the following labels: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "A new description has been requested from {{provider}}. Depending on the speed of your provider, the new description may take some time to regenerate.", + "updatedSublabel": "Successfully updated sub label." + }, + "error": { + "regenerate": "Failed to call {{provider}} for a new description: {{errorMessage}}", + "updatedSublabelFailed": "Failed to update sub label: {{errorMessage}}" + } } + }, "label": "Label", "editSubLable": "Edit sub label", diff --git a/web/public/locales/en/views/exports.json b/web/public/locales/en/views/exports.json index 60cf2b7fe..0dba02a29 100644 --- a/web/public/locales/en/views/exports.json +++ b/web/public/locales/en/views/exports.json @@ -8,6 +8,11 @@ "title": "Rename Export", "desc": "Enter a new name for this export.", "saveExport": "Save Export" + }, + "toast": { + "error": { + "renameExportFailed": "Failed to rename export: {{errorMessage}}" + } } } \ No newline at end of file diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json new file mode 100644 index 000000000..ca3cba4e5 --- /dev/null +++ b/web/public/locales/en/views/faceLibrary.json @@ -0,0 +1,40 @@ +{ + "uploadFaceImage": { + "title": "Upload Face Image", + "desc": "Upload an image to scan for faces and include for {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Create Face Library", + "desc": "Create a new face library" + }, + "train": { + "title": "Train", + "aria": "Select train" + }, + "selectItem": "Select {{item}}", + "button": { + "deleteFaceAttempts": "Delete Face Attempts", + "addFace": "Add Face", + "uploadImage": "Upload Image", + "reprocessFace:": "Reprocess Face" + }, + "trainFaceAs:": "Train Face as:", + "trainFaceAsPerson:": "Train Face as Person", + + "toast": { + "success": { + "uploadedImage": "Successfully uploaded image.", + "addFaceLibrary": "Successfully add face library.", + "deletedFace": "Successfully deleted face.", + "trainedFace": "Successfully trained face.", + "updatedFaceScore": "Successfully updated face score." + }, + "error": { + "uploadingImageFailed": "Failed to upload image: {{errorMessage}}", + "addFaceLibraryFailed": "Failed to set face name: {{errorMessage}}", + "deleteFaceFailed": "Failed to delete: {{errorMessage}}", + "trainFailed": "Failed to train: {{errorMessage}}", + "updateFaceScoreFailed": "Failed to update face score: {{errorMessage}}" + } + } +} \ 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 06f024f9d..556c424c4 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -47,6 +47,16 @@ "sunday": "Sunday", "monday": "Monday" } + }, + "toast": { + "success": { + "clearStoredLayout": "Cleared stored layout for {{cameraName}}", + "clearStreamingSettings": "Cleared streaming settings for all camera groups." + }, + "error": { + "clearStoredLayoutFailed": "Failed to clear stored layout: {{errorMessage}}", + "clearStreamingSettingsFailed": "Failed to clear streaming settings: {{errorMessage}}" + } } }, "explore": { @@ -107,6 +117,14 @@ "filter": { "all": "All Masks and Zones" }, + "toast": { + "success": { + "copyCoordinates": "Copied coordinates for {{polyName}} to clipboard." + }, + "error": { + "copyCoordinatesFailed": "Could not copy coordinates to clipboard." + } + }, "form": { "zoneName": { "error": { @@ -273,12 +291,14 @@ "success": { "createUser": "User {{user}} created successfully", "deleteUser": "User {{user}} deleted successfully", - "updatePassword": "Password updated successfully." + "updatePassword": "Password updated successfully.", + "roleUpdated": "Role updated for {{user}}" }, "error": { "setPasswordFailed": "Failed to save password: {{errorMessage}}", "createUserFailed": "Failed to create user: {{errorMessage}}", - "deleteUserFailed": "Failed to delete user: {{errorMessage}}" + "deleteUserFailed": "Failed to delete user: {{errorMessage}}", + "roleUpdateFailed": "Failed to update role: {{errorMessage}}" } }, "table": { @@ -335,9 +355,17 @@ }, "notification": { "title": "Notifications", - "notificationSettings": "Notification Settings", - "desc": "Frigate can send native push notifications to your device when running in the browser or installed as a PWA.", - "documentation": "Read the Documentation", + "notificationSettings": { + "title": "Notification Settings", + "desc": "Frigate can natively send push notifications to your device when it is running in the browser or installed as a PWA.", + "documentation": "Read the Documentation" + }, + "notificationUnavailable": { + "title": "Notifications Unavailable", + "desc": "Web push notifications require a secure context (https://...). This is a browser limitation. Access Frigate securely to use notifications.", + "documentation": "Read the Documentation" + }, + "email": "Email", "email.placeholder": "e.g. example@email.com", "email.desc": "A valid email is required and will be used to notify you if there are any issues with the push service.", @@ -346,6 +374,15 @@ "cameras.desc": "Select which cameras to enable notifications for.", "deviceSpecific": "Device Specific Settings", "registerDevice": "Register This Device", - "unregisterDevice": "Unregister This Device" + "unregisterDevice": "Unregister This Device", + "toast": { + "success": { + "registered": "Successfully registered for notifications. Restarting Frigate is required before any notifications (including a test notification) can be sent.", + "settingSaved": "Notification settings have been saved." + }, + "error": { + "registerFailed": "Failed to save notification registration." + } + } } } diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index c33dff7ef..78efe0719 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -16,7 +16,13 @@ "tag": "Tag", "message": "Message" }, - "tips": "Logs are streaming from the server" + "tips": "Logs are streaming from the server", + "toast": { + "error": { + "fetchingLogsFailed": "Error fetching logs: {{errorMessage}}", + "whileStreamingLogs": "Error while streaming logs: {{errorMessage}}" + } + } }, "general": { "title": "General", @@ -47,7 +53,10 @@ "vbios": "VBios Info: {{vbios}}" }, "closeInfo.label": "Close GPU info", - "copyInfo.label": "Close GPU info" + "copyInfo.label": "Copy GPU info", + "toast": { + "success": "Copied GPU info to clipboard" + } } }, "otherProcesses": { @@ -98,6 +107,14 @@ "skipped": "skipped", "ffmpeg": "ffmpeg", "capture": "capture" + }, + "toast": { + "success": { + "copyToClipboard": "Copied probe data to clipboard." + }, + "error": { + "unableToProbeCamera": "Unable to probe camera: {{errorMessage}}" + } } }, "lastRefreshed": "Last refreshed: ", diff --git a/web/public/locales/zh-CN/common.json b/web/public/locales/zh-CN/common.json index 64d7c84a1..980e40e9f 100644 --- a/web/public/locales/zh-CN/common.json +++ b/web/public/locales/zh-CN/common.json @@ -36,6 +36,8 @@ "second": "{{time}}秒", "formattedTimestamp": "%m月%-d日 %I:%M:%S %p", "formattedTimestamp.24hour": "%m月%-d日 %H:%M:%S", + "formattedTimestamp2": "%m/%d %I:%M:%S%P", + "formattedTimestamp2.24hour": "%d日%m月 %H:%M:%S", "formattedTimestampExcludeSeconds": "%m月%-d日 %I:%M %p", "formattedTimestampExcludeSeconds.24hour": "%m月%-d日 %H:%M", "formattedTimestampWithYear": "%Y年%m月%-d日 %I:%M:%S %p", @@ -44,10 +46,13 @@ }, "unit": { "speed": { - "mph": "公里/小时", - "kph": "英里/小时" + "mph": "英里/小时", + "kph": "公里/小时" } }, + "label": { + "back": "返回" + }, "pagination": { "label": "分页", "previous": "上一页", @@ -140,6 +145,7 @@ "restart": "重启 Frigate" }, "toast": { + "copyUrlToClipboard": "已复制链接到剪贴板。", "save": { "error": "保存配置信息失败: {{errorMessage}}", "error.noMessage": "保存配置信息失败" diff --git a/web/public/locales/zh-CN/components/player.json b/web/public/locales/zh-CN/components/player.json index ffb64e0d3..5b70fd856 100644 --- a/web/public/locales/zh-CN/components/player.json +++ b/web/public/locales/zh-CN/components/player.json @@ -27,5 +27,13 @@ "droppedFrames.short.value": "{{droppedFrames}} 帧", "decodedFrames": "解码帧数:", "droppedFrameRate": "丢帧率:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "已成功提交帧到 Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "提交帧到 Frigate+ 失败" + } } } \ No newline at end of file diff --git a/web/public/locales/zh-CN/views/configEditor.json b/web/public/locales/zh-CN/views/configEditor.json index 9f3931730..a8ad9292d 100644 --- a/web/public/locales/zh-CN/views/configEditor.json +++ b/web/public/locales/zh-CN/views/configEditor.json @@ -2,5 +2,13 @@ "configEditor": "配置编辑器", "copyConfig": "复制配置", "saveAndRestart": "保存并重启", - "saveOnly": "只保存" + "saveOnly": "只保存", + "toast": { + "success": { + "copyToClipboard": "配置已复制到剪贴板。" + }, + "error": { + "savingError": "保存配置时出错" + } + } } \ No newline at end of file diff --git a/web/public/locales/zh-CN/views/explore.json b/web/public/locales/zh-CN/views/explore.json index e35656f24..e221680c2 100644 --- a/web/public/locales/zh-CN/views/explore.json +++ b/web/public/locales/zh-CN/views/explore.json @@ -1,4 +1,5 @@ { + "generativeAI": "生成式 AI", "exploreIsUnavailable": { "title": "探索功能不可用", "embeddingsReindexing": { @@ -83,6 +84,16 @@ "mismatch_one": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。", "mismatch_other": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。", "hasMissingObjects": "如果希望 Frigate 保存以下标签的跟踪对象,请调整您的配置:{{objects}}" + }, + "toast": { + "success": { + "regenerate": "已向 {{provider}} 请求新的描述。根据提供商的速度,生成新描述可能需要一些时间。", + "updatedSublabel": "成功更新子标签。" + }, + "error": { + "regenerate": "调用 {{provider}} 生成新描述失败:{{errorMessage}}", + "updatedSublabelFailed": "更新子标签失败:{{errorMessage}}" + } } }, "label": "标签", diff --git a/web/public/locales/zh-CN/views/exports.json b/web/public/locales/zh-CN/views/exports.json index 776a79cfb..fe3f849ac 100644 --- a/web/public/locales/zh-CN/views/exports.json +++ b/web/public/locales/zh-CN/views/exports.json @@ -8,5 +8,10 @@ "title": "重命名导出", "desc": "为此导出项目输入新名称。", "saveExport": "保存导出" + }, + "toast": { + "error": { + "renameExportFailed": "重命名导出失败:{{errorMessage}}" + } } } \ No newline at end of file diff --git a/web/public/locales/zh-CN/views/faceLibrary.json b/web/public/locales/zh-CN/views/faceLibrary.json new file mode 100644 index 000000000..72b26a95e --- /dev/null +++ b/web/public/locales/zh-CN/views/faceLibrary.json @@ -0,0 +1,40 @@ +{ + "uploadFaceImage": { + "title": "上传人脸图片", + "desc": "上传图片以扫描人脸并包含在{{pageToggle}}中" + }, + "createFaceLibrary": { + "title": "创建人脸库", + "desc": "创建一个新的人脸库" + }, + "train": { + "title": "训练", + "aria": "选择训练" + }, + "selectItem": "选择{{item}}", + "button": { + "deleteFaceAttempts": "尝试删除人脸", + "addFace": "添加人脸", + "uploadImage": "上传图片", + "reprocessFace:": "重新处理人脸" + }, + "trainFaceAs:": "将人脸训练为:", + "trainFaceAsPerson:": "将人脸训练为人物", + + "toast": { + "success": { + "uploadedImage": "图片上传成功。", + "addFaceLibrary": "人脸库添加成功。", + "deletedFace": "人脸删除成功。", + "trainedFace": "人脸训练成功。", + "updatedFaceScore": "人脸分数更新成功。" + }, + "error": { + "uploadingImageFailed": "图片上传失败:{{errorMessage}}", + "addFaceLibraryFailed": "设置人脸名称失败:{{errorMessage}}", + "deleteFaceFailed": "删除失败:{{errorMessage}}", + "trainFailed": "训练失败:{{errorMessage}}", + "updateFaceScoreFailed": "更新人脸分数失败:{{errorMessage}}" + } + } +} diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json index acfbb6c86..a57fc87e0 100644 --- a/web/public/locales/zh-CN/views/settings.json +++ b/web/public/locales/zh-CN/views/settings.json @@ -47,6 +47,16 @@ "sunday": "星期天", "monday": "星期一" } + }, + "toast": { + "success": { + "clearStoredLayout": "已清除 {{cameraName}} 的存储布局", + "clearStreamingSettings": "已清除所有摄像头组的视频流设置。" + }, + "error": { + "clearStoredLayoutFailed": "清除存储布局失败:{{errorMessage}}", + "clearStreamingSettingsFailed": "清除视频流设置失败:{{errorMessage}}" + } } }, "explore": { @@ -107,6 +117,14 @@ "filter": { "all": "所有遮罩和区域" }, + "toast": { + "success": { + "copyCoordinates": "已复制 {{polyName}} 的坐标到剪贴板。" + }, + "error": { + "copyCoordinatesFailed": "无法复制坐标到剪贴板。" + } + }, "form": { "zoneName": { "error": { @@ -122,6 +140,12 @@ "inertia.error.mustBeAboveZero": "惯性必须大于 0。", "loiteringTime.error.mustBeGreaterOrEqualZero": "徘徊时间必须大于或等于 0。", "polygonDrawing": { + "removeLastPoint": "删除最后一个点", + "reset.label": "清除所有点", + "snapPoints": { + "true": "启用点对齐", + "false": "禁用点对齐" + }, "error": { "mustBeFinished": "多边形绘制必须完成闭合后才能保存。" } @@ -262,12 +286,14 @@ "success": { "createUser": "用户 {{user}} 创建成功", "deleteUser": "用户 {{user}} 删除成功", - "updatePassword": "已成功修改密码" + "updatePassword": "已成功修改密码", + "roleUpdated": "已更新 {{user}} 的权限组" }, "error": { "setPasswordFailed": "保存密码出现错误:{{errorMessage}}", "createUserFailed": "创建用户失败:{{errorMessage}}", - "deleteUserFailed": "删除用户失败:{{errorMessage}}" + "deleteUserFailed": "删除用户失败:{{errorMessage}}", + "roleUpdateFailed": "更新权限组失败:{{errorMessage}}" } }, "table": { @@ -326,9 +352,16 @@ }, "notification": { "title": "通知", - "notificationSettings": "通知设置", - "desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。", - "documentation": "阅读文档(英文)", + "notificationSettings": { + "title": "通知设置", + "desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。", + "documentation": "阅读文档(英文)" + }, + "notificationUnavailable": { + "title": "通知功能不可用", + "desc": "网页推送通知需要安全连接(https://...)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。", + "documentation": "阅读文档(英文)" + }, "email": "电子邮箱", "email.placeholder": "例如:example@email.com", "email.desc": "需要输入有效的电子邮件,在推送服务出现问题时,将使用此电子邮件进行通知。", @@ -337,6 +370,15 @@ "cameras.desc": "选择要启用通知的摄像头。", "deviceSpecific": "设备专用设置", "registerDevice": "注册该设备", - "unregisterDevice": "取消注册该设备" + "unregisterDevice": "取消注册该设备", + "toast": { + "success": { + "registered": "已成功注册通知。需要重启 Frigate 才能发送任何通知(包括测试通知)。", + "settingSaved": "通知设置已保存。" + }, + "error": { + "registerFailed": "通知注册失败。" + } + } } } diff --git a/web/public/locales/zh-CN/views/system.json b/web/public/locales/zh-CN/views/system.json index 2b68723fe..20d6bba8d 100644 --- a/web/public/locales/zh-CN/views/system.json +++ b/web/public/locales/zh-CN/views/system.json @@ -16,7 +16,13 @@ "tag": "标签", "message": "消息" }, - "tips": "日志正在从服务器流式传输" + "tips": "日志正在从服务器流式传输", + "toast": { + "error": { + "fetchingLogsFailed": "获取日志出错:{{errorMessage}}", + "whileStreamingLogs": "流式传输日志时出错:{{errorMessage}}" + } + } }, "general": { "title": "常规", @@ -47,7 +53,10 @@ "vbios": "VBios信息:{{vbios}}" }, "closeInfo.label": "关闭GPU信息", - "copyInfo.label": "复制GPU信息" + "copyInfo.label": "复制GPU信息", + "toast": { + "success": "已复制GPU信息到剪贴板" + } } }, "otherProcesses": { @@ -98,6 +107,14 @@ "skipped": "跳过", "ffmpeg": "ffmpeg编码器", "capture": "捕获" + }, + "toast": { + "success": { + "copyToClipboard": "已复制探测数据到剪贴板。" + }, + "error": { + "unableToProbeCamera": "无法探测摄像头:{{errorMessage}}" + } } }, "lastRefreshed": "最后刷新时间:", diff --git a/web/src/components/overlay/CameraInfoDialog.tsx b/web/src/components/overlay/CameraInfoDialog.tsx index 76317dd8c..cc13b23ca 100644 --- a/web/src/components/overlay/CameraInfoDialog.tsx +++ b/web/src/components/overlay/CameraInfoDialog.tsx @@ -41,15 +41,25 @@ export default function CameraInfoDialog({ if (res.status === 200) { setFfprobeInfo(res.data); } else { - toast.error(`Unable to probe camera: ${res.statusText}`, { - position: "top-center", - }); + toast.error( + t("cameras.toast.success.copyToClipboard", { + errorMessage: res.statusText, + }), + { + position: "top-center", + }, + ); } }) .catch((error) => { - toast.error(`Unable to probe camera: ${error.response.data.message}`, { - position: "top-center", - }); + toast.error( + t("cameras.toast.success.copyToClipboard", { + errorMessage: error.response.data.message, + }), + { + position: "top-center", + }, + ); }); // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps @@ -57,7 +67,7 @@ export default function CameraInfoDialog({ const onCopyFfprobe = async () => { copy(JSON.stringify(ffprobeInfo)); - toast.success("Copied probe data to clipboard."); + toast.success(t("cameras.toast.success.copyToClipboard")); }; function gcd(a: number, b: number): number { diff --git a/web/src/components/overlay/GPUInfoDialog.tsx b/web/src/components/overlay/GPUInfoDialog.tsx index d6d227ef6..6a1755c9d 100644 --- a/web/src/components/overlay/GPUInfoDialog.tsx +++ b/web/src/components/overlay/GPUInfoDialog.tsx @@ -38,7 +38,7 @@ export default function GPUInfoDialog({ .replace(/\\t/g, "\t") .replace(/\\n/g, "\n"), ); - toast.success("Copied GPU info to clipboard."); + toast.success(t("general.hardwareInfo.gpuInfo.toast.success")); }; if (gpuType == "vainfo") { diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index dd775f582..9bc384861 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -443,7 +443,12 @@ function ObjectDetailsTab({ .then((resp) => { if (resp.status == 200) { toast.success( - `A new description has been requested from ${capitalizeAll(config?.genai.provider.replaceAll("_", " ") ?? "Generative AI")}. Depending on the speed of your provider, the new description may take some time to regenerate.`, + t("details.item.toast.success.regenerate", { + provider: capitalizeAll( + config?.genai.provider.replaceAll("_", " ") ?? + t("generativeAI"), + ), + }), { position: "top-center", duration: 7000, @@ -457,12 +462,18 @@ function ObjectDetailsTab({ error.response?.data?.detail || "Unknown error"; toast.error( - `Failed to call ${capitalizeAll(config?.genai.provider.replaceAll("_", " ") ?? "Generative AI")} for a new description: ${errorMessage}`, + t("details.item.toast.error.regenerate", { + provider: capitalizeAll( + config?.genai.provider.replaceAll("_", " ") ?? + t("generativeAI"), + ), + errorMessage, + }), { position: "top-center" }, ); }); }, - [search, config], + [search, config, t], ); const handleSubLabelSave = useCallback( @@ -481,7 +492,7 @@ function ObjectDetailsTab({ }) .then((response) => { if (response.status === 200) { - toast.success("Successfully updated sub label.", { + toast.success(t("details.item.toast.success.updatedSublabel"), { position: "top-center", }); @@ -529,12 +540,17 @@ function ObjectDetailsTab({ error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to update sub label: ${errorMessage}`, { - position: "top-center", - }); + toast.error( + t("details.item.toast.error.updatedSublabelFailed", { + errorMessage, + }), + { + position: "top-center", + }, + ); }); }, - [search, apiHost, mutate, setSearch], + [search, apiHost, mutate, setSearch, t], ); return ( diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 6c04bb6dd..1350a70b1 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -18,6 +18,7 @@ import { useOverlayState } from "@/hooks/use-overlay-state"; import { usePersistence } from "@/hooks/use-persistence"; import { cn } from "@/lib/utils"; import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record"; +import { useTranslation } from "react-i18next"; // Android native hls does not seek correctly const USE_NATIVE_HLS = !isAndroid; @@ -63,6 +64,7 @@ export default function HlsVideoPlayer({ toggleFullscreen, onError, }: HlsVideoPlayerProps) { + const { t } = useTranslation("components/player"); const { data: config } = useSWR("config"); // playback @@ -236,11 +238,11 @@ export default function HlsVideoPlayer({ const resp = await onUploadFrame(videoRef.current.currentTime); if (resp && resp.status == 200) { - toast.success("Successfully submitted frame to Frigate+", { + toast.success(t("toast.success.submittedFrigatePlus"), { position: "top-center", }); } else { - toast.success("Failed to submit frame to Frigate+", { + toast.success(t("toast.error.submitFrigatePlusFailed"), { position: "top-center", }); } diff --git a/web/src/pages/ConfigEditor.tsx b/web/src/pages/ConfigEditor.tsx index 0bbfcdfdf..03e02c51a 100644 --- a/web/src/pages/ConfigEditor.tsx +++ b/web/src/pages/ConfigEditor.tsx @@ -66,7 +66,7 @@ function ConfigEditor() { toast.success(response.data.message, { position: "top-center" }); } } catch (error) { - toast.error("Error saving config", { position: "top-center" }); + toast.error(t("toast.error.savingError"), { position: "top-center" }); const axiosError = error as AxiosError; const errorMessage = @@ -78,7 +78,7 @@ function ConfigEditor() { throw new Error(errorMessage); } }, - [editorRef], + [editorRef, t], ); const handleCopyConfig = useCallback(async () => { @@ -87,8 +87,10 @@ function ConfigEditor() { } copy(editorRef.current.getValue()); - toast.success("Config copied to clipboard.", { position: "top-center" }); - }, [editorRef]); + toast.success(t("toast.success.copyToClipboard"), { + position: "top-center", + }); + }, [editorRef, t]); const handleSaveAndRestart = useCallback(async () => { try { diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx index a4f1f97e2..e1ed08a8d 100644 --- a/web/src/pages/Exports.tsx +++ b/web/src/pages/Exports.tsx @@ -101,12 +101,12 @@ function Exports() { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to rename export: ${errorMessage}`, { + toast.error(t("toast.error.renameExportFailed", { errorMessage }), { position: "top-center", }); }); }, - [mutate], + [mutate, t], ); return ( diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index fbb75c681..91cfa54a9 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -25,11 +25,14 @@ import { cn } from "@/lib/utils"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu"; import { toast } from "sonner"; import useSWR from "swr"; export default function FaceLibrary() { + const { t } = useTranslation(["views/faceLibrary"]); + const { data: config } = useSWR("config"); // title @@ -94,7 +97,7 @@ export default function FaceLibrary() { if (resp.status == 200) { setUpload(false); refreshFaces(); - toast.success("Successfully uploaded image.", { + toast.success(t("toast.success.uploadedImage"), { position: "top-center", }); } @@ -104,12 +107,12 @@ export default function FaceLibrary() { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to upload image: ${errorMessage}`, { + toast.error(t("toast.error.uploadingImageFailed", { errorMessage }), { position: "top-center", }); }); }, - [pageToggle, refreshFaces], + [pageToggle, refreshFaces, t], ); const onAddName = useCallback( @@ -124,7 +127,7 @@ export default function FaceLibrary() { if (resp.status == 200) { setAddFace(false); refreshFaces(); - toast.success("Successfully add face library.", { + toast.success(t("toast.success.addFaceLibrary"), { position: "top-center", }); } @@ -134,12 +137,12 @@ export default function FaceLibrary() { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to set face name: ${errorMessage}`, { + toast.error(t("toast.error.addFaceLibraryFailed", { errorMessage }), { position: "top-center", }); }); }, - [refreshFaces], + [refreshFaces, t], ); // face multiselect @@ -176,7 +179,7 @@ export default function FaceLibrary() { setSelectedFaces([]); if (resp.status == 200) { - toast.success(`Successfully deleted face.`, { + toast.success(t("toast.success.deletedFace"), { position: "top-center", }); refreshFaces(); @@ -187,11 +190,11 @@ export default function FaceLibrary() { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to delete: ${errorMessage}`, { + toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), { position: "top-center", }); }); - }, [selectedFaces, refreshFaces]); + }, [selectedFaces, refreshFaces, t]); // keyboard @@ -219,15 +222,15 @@ export default function FaceLibrary() { -
Train
+
{t("train.title")}
|
@@ -267,7 +270,7 @@ export default function FaceLibrary() { className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`} value={item} data-nav-item={item} - aria-label={`Select ${item}`} + aria-label={t("selectItem", { item })} >
{item} ({faceData[item].length}) @@ -282,19 +285,19 @@ export default function FaceLibrary() {
) : (
{pageToggle != "train" && ( )}
@@ -370,6 +373,7 @@ function FaceAttempt({ onClick, onRefresh, }: FaceAttemptProps) { + const { t } = useTranslation(["views/faceLibrary"]); const data = useMemo(() => { const parts = image.split("-"); @@ -386,7 +390,7 @@ function FaceAttempt({ .post(`/faces/train/${trainName}/classify`, { training_file: image }) .then((resp) => { if (resp.status == 200) { - toast.success(`Successfully trained face.`, { + toast.success(t("toast.success.trainedFace"), { position: "top-center", }); onRefresh(); @@ -397,12 +401,12 @@ function FaceAttempt({ error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to train: ${errorMessage}`, { + toast.error(t("toast.error.trainFailed", { errorMessage }), { position: "top-center", }); }); }, - [image, onRefresh], + [image, onRefresh, t], ); const onReprocess = useCallback(() => { @@ -410,7 +414,7 @@ function FaceAttempt({ .post(`/faces/reprocess`, { training_file: image }) .then((resp) => { if (resp.status == 200) { - toast.success(`Successfully updated face score.`, { + toast.success(t("toast.success.updatedFaceScore"), { position: "top-center", }); onRefresh(); @@ -421,11 +425,11 @@ function FaceAttempt({ error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to update face score: ${errorMessage}`, { + toast.error(t("toast.error.updateFaceScoreFailed", { errorMessage }), { position: "top-center", }); }); - }, [image, onRefresh]); + }, [image, onRefresh, t]); return (
- Train Face as: + {t("trainFaceAs")} {faceNames.map((faceName) => ( - Train Face as Person + {t("trainFaceAsPerson")} @@ -484,7 +488,7 @@ function FaceAttempt({ onClick={() => onReprocess()} /> - Reprocess Face + {t("button.reprocessFace")}
@@ -519,12 +523,13 @@ type FaceImageProps = { onRefresh: () => void; }; function FaceImage({ name, image, onRefresh }: FaceImageProps) { + const { t } = useTranslation(["views/faceLibrary"]); const onDelete = useCallback(() => { axios .post(`/faces/${name}/delete`, { ids: [image] }) .then((resp) => { if (resp.status == 200) { - toast.success(`Successfully deleted face.`, { + toast.success(t("toast.success.deletedFace"), { position: "top-center", }); onRefresh(); @@ -535,11 +540,11 @@ function FaceImage({ name, image, onRefresh }: FaceImageProps) { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to delete: ${errorMessage}`, { + toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), { position: "top-center", }); }); - }, [name, image, onRefresh]); + }, [name, image, onRefresh, t]); return (
@@ -559,7 +564,7 @@ function FaceImage({ name, image, onRefresh }: FaceImageProps) { onClick={onDelete} /> - Delete Face Attempt + {t("button.deleteFaceAttempts")}
diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index 08e9fc8e7..f9fb3c88c 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -106,13 +106,16 @@ function Logs() { } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; - toast.error(`Error fetching logs: ${errorMessage}`, { - position: "top-center", - }); + toast.error( + t("logs.toast.error.fetchingLogsFailed", { errorMessage }), + { + position: "top-center", + }, + ); } return []; }, - [logService, filterLines], + [logService, filterLines, t], ); const fetchInitialLogs = useCallback(async () => { @@ -134,13 +137,13 @@ function Logs() { } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; - toast.error(`Error fetching logs: ${errorMessage}`, { + toast.error(t("logs.toast.error.fetchingLogsFailed", { errorMessage }), { position: "top-center", }); } finally { setIsLoading(false); } - }, [logService, filterLines, filterSeverity]); + }, [logService, filterLines, filterSeverity, t]); const abortControllerRef = useRef(null); @@ -205,10 +208,12 @@ function Logs() { error instanceof Error ? error.message : "An unknown error occurred"; - toast.error(`Error while streaming logs: ${errorMessage}`); + toast.error( + t("logs.toast.error.whileStreamingLogs", { errorMessage }), + ); } }); - }, [logService, filterSeverity]); + }, [logService, filterSeverity, t]); useEffect(() => { setIsLoading(true); diff --git a/web/src/utils/browserUtil.ts b/web/src/utils/browserUtil.ts index b6a82fb54..13589752e 100644 --- a/web/src/utils/browserUtil.ts +++ b/web/src/utils/browserUtil.ts @@ -1,4 +1,5 @@ import copy from "copy-to-clipboard"; +import { t } from "i18next"; import { toast } from "sonner"; export function shareOrCopy(url: string, title?: string) { @@ -9,7 +10,7 @@ export function shareOrCopy(url: string, title?: string) { }); } else { copy(url); - toast.success("Copied URL to clipboard.", { + toast.success(t("toast.copyUrlToClipboard"), { position: "top-center", }); } diff --git a/web/src/views/settings/AuthenticationView.tsx b/web/src/views/settings/AuthenticationView.tsx index 646100103..2a3f13217 100644 --- a/web/src/views/settings/AuthenticationView.tsx +++ b/web/src/views/settings/AuthenticationView.tsx @@ -163,7 +163,7 @@ export default function AuthenticationView() { ), false, ); - toast.success(`Role updated for ${user}`, { + toast.success(t("users.toast.success.roleUpdated", { user }), { position: "top-center", }); } @@ -173,9 +173,14 @@ export default function AuthenticationView() { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to update role: ${errorMessage}`, { - position: "top-center", - }); + toast.error( + t("users.toast.error.roleUpdateFailed", { + errorMessage, + }), + { + position: "top-center", + }, + ); }); }; diff --git a/web/src/views/settings/MasksAndZonesView.tsx b/web/src/views/settings/MasksAndZonesView.tsx index 55ebef3b0..87e1233a1 100644 --- a/web/src/views/settings/MasksAndZonesView.tsx +++ b/web/src/views/settings/MasksAndZonesView.tsx @@ -221,12 +221,16 @@ export default function MasksAndZonesView({ .map((point) => `${point[0]},${point[1]}`) .join(","), ); - toast.success(`Copied coordinates for ${poly.name} to clipboard.`); + toast.success( + t("masksAndZones.toast.success.copyCoordinates", { + polyName: poly.name, + }), + ); } else { - toast.error("Could not copy coordinates to clipboard."); + toast.error(t("masksAndZones.toast.error.copyCoordinatesFailed")); } }, - [allPolygons, scaledHeight, scaledWidth], + [allPolygons, scaledHeight, scaledWidth, t], ); useEffect(() => { diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index 4c1710336..f0e0d349b 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -43,7 +43,7 @@ import { import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import FilterSwitch from "@/components/filter/FilterSwitch"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; const NOTIFICATION_SERVICE_WORKER = "notifications-worker.js"; @@ -143,23 +143,20 @@ export default function NotificationView({ sub: pushSubscription, }) .catch(() => { - toast.error("Failed to save notification registration.", { + toast.error(t("notification.toast.error.registerFailed"), { position: "top-center", }); pushSubscription.unsubscribe(); registration.unregister(); setRegistration(null); }); - toast.success( - "Successfully registered for notifications. Restarting Frigate is required before any notifications (including a test notification) can be sent.", - { - position: "top-center", - }, - ); + toast.success(t("notification.toast.success.registered"), { + position: "top-center", + }); }); } }, - [publicKey, addMessage], + [publicKey, addMessage, t], ); // notification state @@ -261,7 +258,7 @@ export default function NotificationView({ ) .then((res) => { if (res.status === 200) { - toast.success("Notification settings have been saved.", { + toast.success(t("notification.toast.success.settingSaved"), { position: "top-center", }); updateConfig(); @@ -304,14 +301,11 @@ export default function NotificationView({
- Notification Settings + {t("notification.notificationSettings.title")}
-

- Frigate can natively send push notifications to your device - when it is running in the browser or installed as a PWA. -

+

{t("notification.notificationSettings.desc")}

- Read the Documentation{" "} +

+ {t("notification.notificationSettings.documentation")} +

{" "}
@@ -327,12 +323,13 @@ export default function NotificationView({
- Notifications Unavailable - + + {t("notification.notificationUnavailable.title")} + - Web push notifications require a secure context ( - https://...). This is a browser limitation. Access - Frigate securely to use notifications. + + notification.notificationUnavailable.desc +
- Read the Documentation{" "} +

+ {t("notification.notificationUnavailable.documentation")} +

{" "}
@@ -360,12 +359,12 @@ export default function NotificationView({
- {t("notification.notificationSettings")} + {t("notification.notificationSettings.title")}
-

{t("notification.desc")}

+

{t("notification.notificationSettings.desc")}

- {t("notification.documentation")}{" "} + {t("notification.notificationSettings.documentation")}{" "}
diff --git a/web/src/views/settings/UiSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx index 27937204d..14d727523 100644 --- a/web/src/views/settings/UiSettingsView.tsx +++ b/web/src/views/settings/UiSettingsView.tsx @@ -34,21 +34,29 @@ export default function UiSettingsView() { Object.entries(config.camera_groups).forEach(async (value) => { await delData(`${value[0]}-draggable-layout`) .then(() => { - toast.success(`Cleared stored layout for ${value[0]}`, { - position: "top-center", - }); + toast.success( + t("general.toast.success.clearStoredLayout", { + cameraName: value[0], + }), + { + position: "top-center", + }, + ); }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to clear stored layout: ${errorMessage}`, { - position: "top-center", - }); + toast.error( + t("general.toast.error.clearStoredLayoutFailed", { errorMessage }), + { + position: "top-center", + }, + ); }); }); - }, [config]); + }, [config, t]); const clearStreamingSettings = useCallback(async () => { if (!config) { @@ -57,7 +65,7 @@ export default function UiSettingsView() { await delData(`streaming-settings`) .then(() => { - toast.success(`Cleared streaming settings for all camera groups.`, { + toast.success(t("general.toast.success.clearStreamingSettings"), { position: "top-center", }); }) @@ -66,11 +74,16 @@ export default function UiSettingsView() { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(`Failed to clear streaming settings: ${errorMessage}`, { - position: "top-center", - }); + toast.error( + t("general.toast.error.clearStreamingSettingsFailed", { + errorMessage, + }), + { + position: "top-center", + }, + ); }); - }, [config]); + }, [config, t]); useEffect(() => { document.title = "General Settings - Frigate";