feat: add more toast i18n keys

This commit is contained in:
ZhaiSoul 2025-03-15 01:10:32 +08:00
parent 394b5ee33e
commit f4bd490a12
29 changed files with 457 additions and 129 deletions

View File

@ -137,6 +137,7 @@
} }
}, },
"toast": { "toast": {
"copyUrlToClipboard": "Copied URL to clipboard.",
"save": { "save": {
"error": "Failed to save config changes: {{errorMessage}}", "error": "Failed to save config changes: {{errorMessage}}",
"error.noMessage": "Failed to save config changes" "error.noMessage": "Failed to save config changes"

View File

@ -27,5 +27,13 @@
"droppedFrames.short.value": "{{droppedFrames}} frames", "droppedFrames.short.value": "{{droppedFrames}} frames",
"decodedFrames": "Decoded Frames:", "decodedFrames": "Decoded Frames:",
"droppedFrameRate": "Dropped Frame Rate:" "droppedFrameRate": "Dropped Frame Rate:"
},
"toast": {
"success": {
"submittedFrigatePlus": "Successfully submitted frame to Frigate+"
},
"error": {
"submitFrigatePlusFailed": "Failed to submit frame to Frigate+"
}
} }
} }

View File

@ -2,5 +2,14 @@
"configEditor": "Config Editor", "configEditor": "Config Editor",
"copyConfig": "Copy Config", "copyConfig": "Copy Config",
"saveAndRestart": "Save & Restart", "saveAndRestart": "Save & Restart",
"saveOnly": "Save Only" "saveOnly": "Save Only",
"toast": {
"success": {
"copyToClipboard": "Config copied to clipboard."
},
"error": {
"savingError": "Error saving config"
}
}
} }

View File

@ -1,4 +1,5 @@
{ {
"generativeAI": "Generative AI",
"exploreIsUnavailable": { "exploreIsUnavailable": {
"title": "Explore is Unavailable", "title": "Explore is Unavailable",
"embeddingsReindexing": { "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_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.", "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: <em>{{objects}}</em>" "hasMissingObjects": "Adjust your configuration if you want Frigate to save tracked objects for the following labels: <em>{{objects}}</em>"
},
"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", "label": "Label",
"editSubLable": "Edit sub label", "editSubLable": "Edit sub label",

View File

@ -8,6 +8,11 @@
"title": "Rename Export", "title": "Rename Export",
"desc": "Enter a new name for this export.", "desc": "Enter a new name for this export.",
"saveExport": "Save Export" "saveExport": "Save Export"
},
"toast": {
"error": {
"renameExportFailed": "Failed to rename export: {{errorMessage}}"
}
} }
} }

View File

@ -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}}"
}
}
}

View File

@ -47,6 +47,16 @@
"sunday": "Sunday", "sunday": "Sunday",
"monday": "Monday" "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": { "explore": {
@ -107,6 +117,14 @@
"filter": { "filter": {
"all": "All Masks and Zones" "all": "All Masks and Zones"
}, },
"toast": {
"success": {
"copyCoordinates": "Copied coordinates for {{polyName}} to clipboard."
},
"error": {
"copyCoordinatesFailed": "Could not copy coordinates to clipboard."
}
},
"form": { "form": {
"zoneName": { "zoneName": {
"error": { "error": {
@ -273,12 +291,14 @@
"success": { "success": {
"createUser": "User {{user}} created successfully", "createUser": "User {{user}} created successfully",
"deleteUser": "User {{user}} deleted successfully", "deleteUser": "User {{user}} deleted successfully",
"updatePassword": "Password updated successfully." "updatePassword": "Password updated successfully.",
"roleUpdated": "Role updated for {{user}}"
}, },
"error": { "error": {
"setPasswordFailed": "Failed to save password: {{errorMessage}}", "setPasswordFailed": "Failed to save password: {{errorMessage}}",
"createUserFailed": "Failed to create user: {{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": { "table": {
@ -335,9 +355,17 @@
}, },
"notification": { "notification": {
"title": "Notifications", "title": "Notifications",
"notificationSettings": "Notification Settings", "notificationSettings": {
"desc": "Frigate can send native push notifications to your device when running in the browser or installed as a PWA.", "title": "Notification Settings",
"documentation": "Read the Documentation", "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 (<code>https://...</code>). This is a browser limitation. Access Frigate securely to use notifications.",
"documentation": "Read the Documentation"
},
"email": "Email", "email": "Email",
"email.placeholder": "e.g. example@email.com", "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.", "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.", "cameras.desc": "Select which cameras to enable notifications for.",
"deviceSpecific": "Device Specific Settings", "deviceSpecific": "Device Specific Settings",
"registerDevice": "Register This Device", "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."
}
}
} }
} }

View File

@ -16,7 +16,13 @@
"tag": "Tag", "tag": "Tag",
"message": "Message" "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": { "general": {
"title": "General", "title": "General",
@ -47,7 +53,10 @@
"vbios": "VBios Info: {{vbios}}" "vbios": "VBios Info: {{vbios}}"
}, },
"closeInfo.label": "Close GPU info", "closeInfo.label": "Close GPU info",
"copyInfo.label": "Close GPU info" "copyInfo.label": "Copy GPU info",
"toast": {
"success": "Copied GPU info to clipboard"
}
} }
}, },
"otherProcesses": { "otherProcesses": {
@ -98,6 +107,14 @@
"skipped": "skipped", "skipped": "skipped",
"ffmpeg": "ffmpeg", "ffmpeg": "ffmpeg",
"capture": "capture" "capture": "capture"
},
"toast": {
"success": {
"copyToClipboard": "Copied probe data to clipboard."
},
"error": {
"unableToProbeCamera": "Unable to probe camera: {{errorMessage}}"
}
} }
}, },
"lastRefreshed": "Last refreshed: ", "lastRefreshed": "Last refreshed: ",

View File

@ -36,6 +36,8 @@
"second": "{{time}}秒", "second": "{{time}}秒",
"formattedTimestamp": "%m月%-d日 %I:%M:%S %p", "formattedTimestamp": "%m月%-d日 %I:%M:%S %p",
"formattedTimestamp.24hour": "%m月%-d日 %H:%M:%S", "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": "%m月%-d日 %I:%M %p",
"formattedTimestampExcludeSeconds.24hour": "%m月%-d日 %H:%M", "formattedTimestampExcludeSeconds.24hour": "%m月%-d日 %H:%M",
"formattedTimestampWithYear": "%Y年%m月%-d日 %I:%M:%S %p", "formattedTimestampWithYear": "%Y年%m月%-d日 %I:%M:%S %p",
@ -44,10 +46,13 @@
}, },
"unit": { "unit": {
"speed": { "speed": {
"mph": "里/小时", "mph": "里/小时",
"kph": "里/小时" "kph": "里/小时"
} }
}, },
"label": {
"back": "返回"
},
"pagination": { "pagination": {
"label": "分页", "label": "分页",
"previous": "上一页", "previous": "上一页",
@ -140,6 +145,7 @@
"restart": "重启 Frigate" "restart": "重启 Frigate"
}, },
"toast": { "toast": {
"copyUrlToClipboard": "已复制链接到剪贴板。",
"save": { "save": {
"error": "保存配置信息失败: {{errorMessage}}", "error": "保存配置信息失败: {{errorMessage}}",
"error.noMessage": "保存配置信息失败" "error.noMessage": "保存配置信息失败"

View File

@ -27,5 +27,13 @@
"droppedFrames.short.value": "{{droppedFrames}} 帧", "droppedFrames.short.value": "{{droppedFrames}} 帧",
"decodedFrames": "解码帧数:", "decodedFrames": "解码帧数:",
"droppedFrameRate": "丢帧率:" "droppedFrameRate": "丢帧率:"
},
"toast": {
"success": {
"submittedFrigatePlus": "已成功提交帧到 Frigate+"
},
"error": {
"submitFrigatePlusFailed": "提交帧到 Frigate+ 失败"
}
} }
} }

View File

@ -2,5 +2,13 @@
"configEditor": "配置编辑器", "configEditor": "配置编辑器",
"copyConfig": "复制配置", "copyConfig": "复制配置",
"saveAndRestart": "保存并重启", "saveAndRestart": "保存并重启",
"saveOnly": "只保存" "saveOnly": "只保存",
"toast": {
"success": {
"copyToClipboard": "配置已复制到剪贴板。"
},
"error": {
"savingError": "保存配置时出错"
}
}
} }

View File

@ -1,4 +1,5 @@
{ {
"generativeAI": "生成式 AI",
"exploreIsUnavailable": { "exploreIsUnavailable": {
"title": "探索功能不可用", "title": "探索功能不可用",
"embeddingsReindexing": { "embeddingsReindexing": {
@ -83,6 +84,16 @@
"mismatch_one": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。", "mismatch_one": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。",
"mismatch_other": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。", "mismatch_other": "检测到 {{count}} 个不可用的对象,并已包含在此审核项中。这些对象可能未达到警告或检测标准,或者已被清理/删除。",
"hasMissingObjects": "如果希望 Frigate 保存以下标签的跟踪对象,请调整您的配置:<em>{{objects}}</em>" "hasMissingObjects": "如果希望 Frigate 保存以下标签的跟踪对象,请调整您的配置:<em>{{objects}}</em>"
},
"toast": {
"success": {
"regenerate": "已向 {{provider}} 请求新的描述。根据提供商的速度,生成新描述可能需要一些时间。",
"updatedSublabel": "成功更新子标签。"
},
"error": {
"regenerate": "调用 {{provider}} 生成新描述失败:{{errorMessage}}",
"updatedSublabelFailed": "更新子标签失败:{{errorMessage}}"
}
} }
}, },
"label": "标签", "label": "标签",

View File

@ -8,5 +8,10 @@
"title": "重命名导出", "title": "重命名导出",
"desc": "为此导出项目输入新名称。", "desc": "为此导出项目输入新名称。",
"saveExport": "保存导出" "saveExport": "保存导出"
},
"toast": {
"error": {
"renameExportFailed": "重命名导出失败:{{errorMessage}}"
}
} }
} }

View File

@ -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}}"
}
}
}

View File

@ -47,6 +47,16 @@
"sunday": "星期天", "sunday": "星期天",
"monday": "星期一" "monday": "星期一"
} }
},
"toast": {
"success": {
"clearStoredLayout": "已清除 {{cameraName}} 的存储布局",
"clearStreamingSettings": "已清除所有摄像头组的视频流设置。"
},
"error": {
"clearStoredLayoutFailed": "清除存储布局失败:{{errorMessage}}",
"clearStreamingSettingsFailed": "清除视频流设置失败:{{errorMessage}}"
}
} }
}, },
"explore": { "explore": {
@ -107,6 +117,14 @@
"filter": { "filter": {
"all": "所有遮罩和区域" "all": "所有遮罩和区域"
}, },
"toast": {
"success": {
"copyCoordinates": "已复制 {{polyName}} 的坐标到剪贴板。"
},
"error": {
"copyCoordinatesFailed": "无法复制坐标到剪贴板。"
}
},
"form": { "form": {
"zoneName": { "zoneName": {
"error": { "error": {
@ -122,6 +140,12 @@
"inertia.error.mustBeAboveZero": "惯性必须大于 0。", "inertia.error.mustBeAboveZero": "惯性必须大于 0。",
"loiteringTime.error.mustBeGreaterOrEqualZero": "徘徊时间必须大于或等于 0。", "loiteringTime.error.mustBeGreaterOrEqualZero": "徘徊时间必须大于或等于 0。",
"polygonDrawing": { "polygonDrawing": {
"removeLastPoint": "删除最后一个点",
"reset.label": "清除所有点",
"snapPoints": {
"true": "启用点对齐",
"false": "禁用点对齐"
},
"error": { "error": {
"mustBeFinished": "多边形绘制必须完成闭合后才能保存。" "mustBeFinished": "多边形绘制必须完成闭合后才能保存。"
} }
@ -262,12 +286,14 @@
"success": { "success": {
"createUser": "用户 {{user}} 创建成功", "createUser": "用户 {{user}} 创建成功",
"deleteUser": "用户 {{user}} 删除成功", "deleteUser": "用户 {{user}} 删除成功",
"updatePassword": "已成功修改密码" "updatePassword": "已成功修改密码",
"roleUpdated": "已更新 {{user}} 的权限组"
}, },
"error": { "error": {
"setPasswordFailed": "保存密码出现错误:{{errorMessage}}", "setPasswordFailed": "保存密码出现错误:{{errorMessage}}",
"createUserFailed": "创建用户失败:{{errorMessage}}", "createUserFailed": "创建用户失败:{{errorMessage}}",
"deleteUserFailed": "删除用户失败:{{errorMessage}}" "deleteUserFailed": "删除用户失败:{{errorMessage}}",
"roleUpdateFailed": "更新权限组失败:{{errorMessage}}"
} }
}, },
"table": { "table": {
@ -326,9 +352,16 @@
}, },
"notification": { "notification": {
"title": "通知", "title": "通知",
"notificationSettings": "通知设置", "notificationSettings": {
"desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。", "title": "通知设置",
"documentation": "阅读文档(英文)", "desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。",
"documentation": "阅读文档(英文)"
},
"notificationUnavailable": {
"title": "通知功能不可用",
"desc": "网页推送通知需要安全连接(<code>https://...</code>)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。",
"documentation": "阅读文档(英文)"
},
"email": "电子邮箱", "email": "电子邮箱",
"email.placeholder": "例如example@email.com", "email.placeholder": "例如example@email.com",
"email.desc": "需要输入有效的电子邮件,在推送服务出现问题时,将使用此电子邮件进行通知。", "email.desc": "需要输入有效的电子邮件,在推送服务出现问题时,将使用此电子邮件进行通知。",
@ -337,6 +370,15 @@
"cameras.desc": "选择要启用通知的摄像头。", "cameras.desc": "选择要启用通知的摄像头。",
"deviceSpecific": "设备专用设置", "deviceSpecific": "设备专用设置",
"registerDevice": "注册该设备", "registerDevice": "注册该设备",
"unregisterDevice": "取消注册该设备" "unregisterDevice": "取消注册该设备",
"toast": {
"success": {
"registered": "已成功注册通知。需要重启 Frigate 才能发送任何通知(包括测试通知)。",
"settingSaved": "通知设置已保存。"
},
"error": {
"registerFailed": "通知注册失败。"
}
}
} }
} }

View File

@ -16,7 +16,13 @@
"tag": "标签", "tag": "标签",
"message": "消息" "message": "消息"
}, },
"tips": "日志正在从服务器流式传输" "tips": "日志正在从服务器流式传输",
"toast": {
"error": {
"fetchingLogsFailed": "获取日志出错:{{errorMessage}}",
"whileStreamingLogs": "流式传输日志时出错:{{errorMessage}}"
}
}
}, },
"general": { "general": {
"title": "常规", "title": "常规",
@ -47,7 +53,10 @@
"vbios": "VBios信息{{vbios}}" "vbios": "VBios信息{{vbios}}"
}, },
"closeInfo.label": "关闭GPU信息", "closeInfo.label": "关闭GPU信息",
"copyInfo.label": "复制GPU信息" "copyInfo.label": "复制GPU信息",
"toast": {
"success": "已复制GPU信息到剪贴板"
}
} }
}, },
"otherProcesses": { "otherProcesses": {
@ -98,6 +107,14 @@
"skipped": "跳过", "skipped": "跳过",
"ffmpeg": "ffmpeg编码器", "ffmpeg": "ffmpeg编码器",
"capture": "捕获" "capture": "捕获"
},
"toast": {
"success": {
"copyToClipboard": "已复制探测数据到剪贴板。"
},
"error": {
"unableToProbeCamera": "无法探测摄像头:{{errorMessage}}"
}
} }
}, },
"lastRefreshed": "最后刷新时间:", "lastRefreshed": "最后刷新时间:",

View File

@ -41,15 +41,25 @@ export default function CameraInfoDialog({
if (res.status === 200) { if (res.status === 200) {
setFfprobeInfo(res.data); setFfprobeInfo(res.data);
} else { } else {
toast.error(`Unable to probe camera: ${res.statusText}`, { toast.error(
position: "top-center", t("cameras.toast.success.copyToClipboard", {
}); errorMessage: res.statusText,
}),
{
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {
toast.error(`Unable to probe camera: ${error.response.data.message}`, { toast.error(
position: "top-center", t("cameras.toast.success.copyToClipboard", {
}); errorMessage: error.response.data.message,
}),
{
position: "top-center",
},
);
}); });
// we know that these deps are correct // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -57,7 +67,7 @@ export default function CameraInfoDialog({
const onCopyFfprobe = async () => { const onCopyFfprobe = async () => {
copy(JSON.stringify(ffprobeInfo)); 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 { function gcd(a: number, b: number): number {

View File

@ -38,7 +38,7 @@ export default function GPUInfoDialog({
.replace(/\\t/g, "\t") .replace(/\\t/g, "\t")
.replace(/\\n/g, "\n"), .replace(/\\n/g, "\n"),
); );
toast.success("Copied GPU info to clipboard."); toast.success(t("general.hardwareInfo.gpuInfo.toast.success"));
}; };
if (gpuType == "vainfo") { if (gpuType == "vainfo") {

View File

@ -443,7 +443,12 @@ function ObjectDetailsTab({
.then((resp) => { .then((resp) => {
if (resp.status == 200) { if (resp.status == 200) {
toast.success( 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", position: "top-center",
duration: 7000, duration: 7000,
@ -457,12 +462,18 @@ function ObjectDetailsTab({
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.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" }, { position: "top-center" },
); );
}); });
}, },
[search, config], [search, config, t],
); );
const handleSubLabelSave = useCallback( const handleSubLabelSave = useCallback(
@ -481,7 +492,7 @@ function ObjectDetailsTab({
}) })
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
toast.success("Successfully updated sub label.", { toast.success(t("details.item.toast.success.updatedSublabel"), {
position: "top-center", position: "top-center",
}); });
@ -529,12 +540,17 @@ function ObjectDetailsTab({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to update sub label: ${errorMessage}`, { toast.error(
position: "top-center", t("details.item.toast.error.updatedSublabelFailed", {
}); errorMessage,
}),
{
position: "top-center",
},
);
}); });
}, },
[search, apiHost, mutate, setSearch], [search, apiHost, mutate, setSearch, t],
); );
return ( return (

View File

@ -18,6 +18,7 @@ import { useOverlayState } from "@/hooks/use-overlay-state";
import { usePersistence } from "@/hooks/use-persistence"; import { usePersistence } from "@/hooks/use-persistence";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record"; import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record";
import { useTranslation } from "react-i18next";
// Android native hls does not seek correctly // Android native hls does not seek correctly
const USE_NATIVE_HLS = !isAndroid; const USE_NATIVE_HLS = !isAndroid;
@ -63,6 +64,7 @@ export default function HlsVideoPlayer({
toggleFullscreen, toggleFullscreen,
onError, onError,
}: HlsVideoPlayerProps) { }: HlsVideoPlayerProps) {
const { t } = useTranslation("components/player");
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
// playback // playback
@ -236,11 +238,11 @@ export default function HlsVideoPlayer({
const resp = await onUploadFrame(videoRef.current.currentTime); const resp = await onUploadFrame(videoRef.current.currentTime);
if (resp && resp.status == 200) { if (resp && resp.status == 200) {
toast.success("Successfully submitted frame to Frigate+", { toast.success(t("toast.success.submittedFrigatePlus"), {
position: "top-center", position: "top-center",
}); });
} else { } else {
toast.success("Failed to submit frame to Frigate+", { toast.success(t("toast.error.submitFrigatePlusFailed"), {
position: "top-center", position: "top-center",
}); });
} }

View File

@ -66,7 +66,7 @@ function ConfigEditor() {
toast.success(response.data.message, { position: "top-center" }); toast.success(response.data.message, { position: "top-center" });
} }
} catch (error) { } catch (error) {
toast.error("Error saving config", { position: "top-center" }); toast.error(t("toast.error.savingError"), { position: "top-center" });
const axiosError = error as AxiosError<ApiErrorResponse>; const axiosError = error as AxiosError<ApiErrorResponse>;
const errorMessage = const errorMessage =
@ -78,7 +78,7 @@ function ConfigEditor() {
throw new Error(errorMessage); throw new Error(errorMessage);
} }
}, },
[editorRef], [editorRef, t],
); );
const handleCopyConfig = useCallback(async () => { const handleCopyConfig = useCallback(async () => {
@ -87,8 +87,10 @@ function ConfigEditor() {
} }
copy(editorRef.current.getValue()); copy(editorRef.current.getValue());
toast.success("Config copied to clipboard.", { position: "top-center" }); toast.success(t("toast.success.copyToClipboard"), {
}, [editorRef]); position: "top-center",
});
}, [editorRef, t]);
const handleSaveAndRestart = useCallback(async () => { const handleSaveAndRestart = useCallback(async () => {
try { try {

View File

@ -101,12 +101,12 @@ function Exports() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to rename export: ${errorMessage}`, { toast.error(t("toast.error.renameExportFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); });
}, },
[mutate], [mutate, t],
); );
return ( return (

View File

@ -25,11 +25,14 @@ import { cn } from "@/lib/utils";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import axios from "axios"; import axios from "axios";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu"; import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu";
import { toast } from "sonner"; import { toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
export default function FaceLibrary() { export default function FaceLibrary() {
const { t } = useTranslation(["views/faceLibrary"]);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
// title // title
@ -94,7 +97,7 @@ export default function FaceLibrary() {
if (resp.status == 200) { if (resp.status == 200) {
setUpload(false); setUpload(false);
refreshFaces(); refreshFaces();
toast.success("Successfully uploaded image.", { toast.success(t("toast.success.uploadedImage"), {
position: "top-center", position: "top-center",
}); });
} }
@ -104,12 +107,12 @@ export default function FaceLibrary() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to upload image: ${errorMessage}`, { toast.error(t("toast.error.uploadingImageFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); });
}, },
[pageToggle, refreshFaces], [pageToggle, refreshFaces, t],
); );
const onAddName = useCallback( const onAddName = useCallback(
@ -124,7 +127,7 @@ export default function FaceLibrary() {
if (resp.status == 200) { if (resp.status == 200) {
setAddFace(false); setAddFace(false);
refreshFaces(); refreshFaces();
toast.success("Successfully add face library.", { toast.success(t("toast.success.addFaceLibrary"), {
position: "top-center", position: "top-center",
}); });
} }
@ -134,12 +137,12 @@ export default function FaceLibrary() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to set face name: ${errorMessage}`, { toast.error(t("toast.error.addFaceLibraryFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); });
}, },
[refreshFaces], [refreshFaces, t],
); );
// face multiselect // face multiselect
@ -176,7 +179,7 @@ export default function FaceLibrary() {
setSelectedFaces([]); setSelectedFaces([]);
if (resp.status == 200) { if (resp.status == 200) {
toast.success(`Successfully deleted face.`, { toast.success(t("toast.success.deletedFace"), {
position: "top-center", position: "top-center",
}); });
refreshFaces(); refreshFaces();
@ -187,11 +190,11 @@ export default function FaceLibrary() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to delete: ${errorMessage}`, { toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); });
}, [selectedFaces, refreshFaces]); }, [selectedFaces, refreshFaces, t]);
// keyboard // keyboard
@ -219,15 +222,15 @@ export default function FaceLibrary() {
<UploadImageDialog <UploadImageDialog
open={upload} open={upload}
title="Upload Face Image" title={t("uploadFaceImage.title")}
description={`Upload an image to scan for faces and include for ${pageToggle}`} description={t("uploadFaceImage.desc", { pageToggle })}
setOpen={setUpload} setOpen={setUpload}
onSave={onUploadImage} onSave={onUploadImage}
/> />
<TextEntryDialog <TextEntryDialog
title="Create Face Library" title={t("createFaceLibrary.title")}
description="Create a new face library" description={t("createFaceLibrary.desc")}
open={addFace} open={addFace}
setOpen={setAddFace} setOpen={setAddFace}
onSave={onAddName} onSave={onAddName}
@ -253,9 +256,9 @@ export default function FaceLibrary() {
value="train" value="train"
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == "train" ? "" : "*:text-muted-foreground"}`} className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == "train" ? "" : "*:text-muted-foreground"}`}
data-nav-item="train" data-nav-item="train"
aria-label="Select train" aria-label={t("train.aria")}
> >
<div>Train</div> <div>{t("train.title")}</div>
</ToggleGroupItem> </ToggleGroupItem>
<div>|</div> <div>|</div>
</> </>
@ -267,7 +270,7 @@ export default function FaceLibrary() {
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`} className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
value={item} value={item}
data-nav-item={item} data-nav-item={item}
aria-label={`Select ${item}`} aria-label={t("selectItem", { item })}
> >
<div className="capitalize"> <div className="capitalize">
{item} ({faceData[item].length}) {item} ({faceData[item].length})
@ -282,19 +285,19 @@ export default function FaceLibrary() {
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Button className="flex gap-2" onClick={() => onDelete()}> <Button className="flex gap-2" onClick={() => onDelete()}>
<LuTrash2 className="size-7 rounded-md p-1 text-secondary-foreground" /> <LuTrash2 className="size-7 rounded-md p-1 text-secondary-foreground" />
Delete Face Attempts {t("button.deleteFaceAttempts")}
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Button className="flex gap-2" onClick={() => setAddFace(true)}> <Button className="flex gap-2" onClick={() => setAddFace(true)}>
<LuScanFace className="size-7 rounded-md p-1 text-secondary-foreground" /> <LuScanFace className="size-7 rounded-md p-1 text-secondary-foreground" />
Add Face {t("button.addFace")}
</Button> </Button>
{pageToggle != "train" && ( {pageToggle != "train" && (
<Button className="flex gap-2" onClick={() => setUpload(true)}> <Button className="flex gap-2" onClick={() => setUpload(true)}>
<LuImagePlus className="size-7 rounded-md p-1 text-secondary-foreground" /> <LuImagePlus className="size-7 rounded-md p-1 text-secondary-foreground" />
Upload Image {t("button.uploadImage")}
</Button> </Button>
)} )}
</div> </div>
@ -370,6 +373,7 @@ function FaceAttempt({
onClick, onClick,
onRefresh, onRefresh,
}: FaceAttemptProps) { }: FaceAttemptProps) {
const { t } = useTranslation(["views/faceLibrary"]);
const data = useMemo(() => { const data = useMemo(() => {
const parts = image.split("-"); const parts = image.split("-");
@ -386,7 +390,7 @@ function FaceAttempt({
.post(`/faces/train/${trainName}/classify`, { training_file: image }) .post(`/faces/train/${trainName}/classify`, { training_file: image })
.then((resp) => { .then((resp) => {
if (resp.status == 200) { if (resp.status == 200) {
toast.success(`Successfully trained face.`, { toast.success(t("toast.success.trainedFace"), {
position: "top-center", position: "top-center",
}); });
onRefresh(); onRefresh();
@ -397,12 +401,12 @@ function FaceAttempt({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to train: ${errorMessage}`, { toast.error(t("toast.error.trainFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); });
}, },
[image, onRefresh], [image, onRefresh, t],
); );
const onReprocess = useCallback(() => { const onReprocess = useCallback(() => {
@ -410,7 +414,7 @@ function FaceAttempt({
.post(`/faces/reprocess`, { training_file: image }) .post(`/faces/reprocess`, { training_file: image })
.then((resp) => { .then((resp) => {
if (resp.status == 200) { if (resp.status == 200) {
toast.success(`Successfully updated face score.`, { toast.success(t("toast.success.updatedFaceScore"), {
position: "top-center", position: "top-center",
}); });
onRefresh(); onRefresh();
@ -421,11 +425,11 @@ function FaceAttempt({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to update face score: ${errorMessage}`, { toast.error(t("toast.error.updateFaceScoreFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); });
}, [image, onRefresh]); }, [image, onRefresh, t]);
return ( return (
<div <div
@ -463,7 +467,7 @@ function FaceAttempt({
</TooltipTrigger> </TooltipTrigger>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuLabel>Train Face as:</DropdownMenuLabel> <DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
{faceNames.map((faceName) => ( {faceNames.map((faceName) => (
<DropdownMenuItem <DropdownMenuItem
key={faceName} key={faceName}
@ -475,7 +479,7 @@ function FaceAttempt({
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<TooltipContent>Train Face as Person</TooltipContent> <TooltipContent>{t("trainFaceAsPerson")}</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
@ -484,7 +488,7 @@ function FaceAttempt({
onClick={() => onReprocess()} onClick={() => onReprocess()}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Reprocess Face</TooltipContent> <TooltipContent>{t("button.reprocessFace")}</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@ -519,12 +523,13 @@ type FaceImageProps = {
onRefresh: () => void; onRefresh: () => void;
}; };
function FaceImage({ name, image, onRefresh }: FaceImageProps) { function FaceImage({ name, image, onRefresh }: FaceImageProps) {
const { t } = useTranslation(["views/faceLibrary"]);
const onDelete = useCallback(() => { const onDelete = useCallback(() => {
axios axios
.post(`/faces/${name}/delete`, { ids: [image] }) .post(`/faces/${name}/delete`, { ids: [image] })
.then((resp) => { .then((resp) => {
if (resp.status == 200) { if (resp.status == 200) {
toast.success(`Successfully deleted face.`, { toast.success(t("toast.success.deletedFace"), {
position: "top-center", position: "top-center",
}); });
onRefresh(); onRefresh();
@ -535,11 +540,11 @@ function FaceImage({ name, image, onRefresh }: FaceImageProps) {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to delete: ${errorMessage}`, { toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); });
}, [name, image, onRefresh]); }, [name, image, onRefresh, t]);
return ( return (
<div className="relative flex flex-col rounded-lg"> <div className="relative flex flex-col rounded-lg">
@ -559,7 +564,7 @@ function FaceImage({ name, image, onRefresh }: FaceImageProps) {
onClick={onDelete} onClick={onDelete}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Delete Face Attempt</TooltipContent> <TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>

View File

@ -106,13 +106,16 @@ function Logs() {
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred"; error instanceof Error ? error.message : "An unknown error occurred";
toast.error(`Error fetching logs: ${errorMessage}`, { toast.error(
position: "top-center", t("logs.toast.error.fetchingLogsFailed", { errorMessage }),
}); {
position: "top-center",
},
);
} }
return []; return [];
}, },
[logService, filterLines], [logService, filterLines, t],
); );
const fetchInitialLogs = useCallback(async () => { const fetchInitialLogs = useCallback(async () => {
@ -134,13 +137,13 @@ function Logs() {
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred"; 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", position: "top-center",
}); });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [logService, filterLines, filterSeverity]); }, [logService, filterLines, filterSeverity, t]);
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
@ -205,10 +208,12 @@ function Logs() {
error instanceof Error error instanceof Error
? error.message ? error.message
: "An unknown error occurred"; : "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(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);

View File

@ -1,4 +1,5 @@
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { t } from "i18next";
import { toast } from "sonner"; import { toast } from "sonner";
export function shareOrCopy(url: string, title?: string) { export function shareOrCopy(url: string, title?: string) {
@ -9,7 +10,7 @@ export function shareOrCopy(url: string, title?: string) {
}); });
} else { } else {
copy(url); copy(url);
toast.success("Copied URL to clipboard.", { toast.success(t("toast.copyUrlToClipboard"), {
position: "top-center", position: "top-center",
}); });
} }

View File

@ -163,7 +163,7 @@ export default function AuthenticationView() {
), ),
false, false,
); );
toast.success(`Role updated for ${user}`, { toast.success(t("users.toast.success.roleUpdated", { user }), {
position: "top-center", position: "top-center",
}); });
} }
@ -173,9 +173,14 @@ export default function AuthenticationView() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(`Failed to update role: ${errorMessage}`, { toast.error(
position: "top-center", t("users.toast.error.roleUpdateFailed", {
}); errorMessage,
}),
{
position: "top-center",
},
);
}); });
}; };

View File

@ -221,12 +221,16 @@ export default function MasksAndZonesView({
.map((point) => `${point[0]},${point[1]}`) .map((point) => `${point[0]},${point[1]}`)
.join(","), .join(","),
); );
toast.success(`Copied coordinates for ${poly.name} to clipboard.`); toast.success(
t("masksAndZones.toast.success.copyCoordinates", {
polyName: poly.name,
}),
);
} else { } else {
toast.error("Could not copy coordinates to clipboard."); toast.error(t("masksAndZones.toast.error.copyCoordinatesFailed"));
} }
}, },
[allPolygons, scaledHeight, scaledWidth], [allPolygons, scaledHeight, scaledWidth, t],
); );
useEffect(() => { useEffect(() => {

View File

@ -43,7 +43,7 @@ import {
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import FilterSwitch from "@/components/filter/FilterSwitch"; import FilterSwitch from "@/components/filter/FilterSwitch";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 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"; const NOTIFICATION_SERVICE_WORKER = "notifications-worker.js";
@ -143,23 +143,20 @@ export default function NotificationView({
sub: pushSubscription, sub: pushSubscription,
}) })
.catch(() => { .catch(() => {
toast.error("Failed to save notification registration.", { toast.error(t("notification.toast.error.registerFailed"), {
position: "top-center", position: "top-center",
}); });
pushSubscription.unsubscribe(); pushSubscription.unsubscribe();
registration.unregister(); registration.unregister();
setRegistration(null); setRegistration(null);
}); });
toast.success( toast.success(t("notification.toast.success.registered"), {
"Successfully registered for notifications. Restarting Frigate is required before any notifications (including a test notification) can be sent.", position: "top-center",
{ });
position: "top-center",
},
);
}); });
} }
}, },
[publicKey, addMessage], [publicKey, addMessage, t],
); );
// notification state // notification state
@ -261,7 +258,7 @@ export default function NotificationView({
) )
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
toast.success("Notification settings have been saved.", { toast.success(t("notification.toast.success.settingSaved"), {
position: "top-center", position: "top-center",
}); });
updateConfig(); updateConfig();
@ -304,14 +301,11 @@ export default function NotificationView({
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
Notification Settings {t("notification.notificationSettings.title")}
</Heading> </Heading>
<div className="max-w-6xl"> <div className="max-w-6xl">
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant"> <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
<p> <p>{t("notification.notificationSettings.desc")}</p>
Frigate can natively send push notifications to your device
when it is running in the browser or installed as a PWA.
</p>
<div className="flex items-center text-primary"> <div className="flex items-center text-primary">
<Link <Link
to="https://docs.frigate.video/configuration/notifications" to="https://docs.frigate.video/configuration/notifications"
@ -319,7 +313,9 @@ export default function NotificationView({
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
Read the Documentation{" "} <p>
{t("notification.notificationSettings.documentation")}
</p>{" "}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </div>
@ -327,12 +323,13 @@ export default function NotificationView({
</div> </div>
<Alert variant="destructive"> <Alert variant="destructive">
<CiCircleAlert className="size-5" /> <CiCircleAlert className="size-5" />
<AlertTitle>Notifications Unavailable</AlertTitle> <AlertTitle>
{t("notification.notificationUnavailable.title")}
</AlertTitle>
<AlertDescription> <AlertDescription>
Web push notifications require a secure context ( <Trans ns="views/settings">
<code>https://...</code>). This is a browser limitation. Access notification.notificationUnavailable.desc
Frigate securely to use notifications. </Trans>
<div className="mt-3 flex items-center"> <div className="mt-3 flex items-center">
<Link <Link
to="https://docs.frigate.video/configuration/authentication" to="https://docs.frigate.video/configuration/authentication"
@ -340,7 +337,9 @@ export default function NotificationView({
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
Read the Documentation{" "} <p>
{t("notification.notificationUnavailable.documentation")}
</p>{" "}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </div>
@ -360,12 +359,12 @@ export default function NotificationView({
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
{t("notification.notificationSettings")} {t("notification.notificationSettings.title")}
</Heading> </Heading>
<div className="max-w-6xl"> <div className="max-w-6xl">
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant"> <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
<p>{t("notification.desc")}</p> <p>{t("notification.notificationSettings.desc")}</p>
<div className="flex items-center text-primary"> <div className="flex items-center text-primary">
<Link <Link
to="https://docs.frigate.video/configuration/notifications" to="https://docs.frigate.video/configuration/notifications"
@ -373,7 +372,7 @@ export default function NotificationView({
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
{t("notification.documentation")}{" "} {t("notification.notificationSettings.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </div>

View File

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