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": {
"copyUrlToClipboard": "Copied URL to clipboard.",
"save": {
"error": "Failed to save config changes: {{errorMessage}}",
"error.noMessage": "Failed to save config changes"

View File

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

View File

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

View File

@ -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: <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",
"editSubLable": "Edit sub label",

View File

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

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",
"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 (<code>https://...</code>). 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."
}
}
}
}

View File

@ -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: ",

View File

@ -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": "保存配置信息失败"

View File

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

View File

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

View File

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

View File

@ -8,5 +8,10 @@
"title": "重命名导出",
"desc": "为此导出项目输入新名称。",
"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": "星期天",
"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": "网页推送通知需要安全连接(<code>https://...</code>)。这是浏览器的限制。请通过安全方式访问 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": "通知注册失败。"
}
}
}
}

View File

@ -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": "最后刷新时间:",

View File

@ -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 {

View File

@ -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") {

View File

@ -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 (

View File

@ -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<FrigateConfig>("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",
});
}

View File

@ -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<ApiErrorResponse>;
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 {

View File

@ -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 (

View File

@ -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<FrigateConfig>("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() {
<UploadImageDialog
open={upload}
title="Upload Face Image"
description={`Upload an image to scan for faces and include for ${pageToggle}`}
title={t("uploadFaceImage.title")}
description={t("uploadFaceImage.desc", { pageToggle })}
setOpen={setUpload}
onSave={onUploadImage}
/>
<TextEntryDialog
title="Create Face Library"
description="Create a new face library"
title={t("createFaceLibrary.title")}
description={t("createFaceLibrary.desc")}
open={addFace}
setOpen={setAddFace}
onSave={onAddName}
@ -253,9 +256,9 @@ export default function FaceLibrary() {
value="train"
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == "train" ? "" : "*:text-muted-foreground"}`}
data-nav-item="train"
aria-label="Select train"
aria-label={t("train.aria")}
>
<div>Train</div>
<div>{t("train.title")}</div>
</ToggleGroupItem>
<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"}`}
value={item}
data-nav-item={item}
aria-label={`Select ${item}`}
aria-label={t("selectItem", { item })}
>
<div className="capitalize">
{item} ({faceData[item].length})
@ -282,19 +285,19 @@ export default function FaceLibrary() {
<div className="flex items-center justify-center gap-2">
<Button className="flex gap-2" onClick={() => onDelete()}>
<LuTrash2 className="size-7 rounded-md p-1 text-secondary-foreground" />
Delete Face Attempts
{t("button.deleteFaceAttempts")}
</Button>
</div>
) : (
<div className="flex items-center justify-center gap-2">
<Button className="flex gap-2" onClick={() => setAddFace(true)}>
<LuScanFace className="size-7 rounded-md p-1 text-secondary-foreground" />
Add Face
{t("button.addFace")}
</Button>
{pageToggle != "train" && (
<Button className="flex gap-2" onClick={() => setUpload(true)}>
<LuImagePlus className="size-7 rounded-md p-1 text-secondary-foreground" />
Upload Image
{t("button.uploadImage")}
</Button>
)}
</div>
@ -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 (
<div
@ -463,7 +467,7 @@ function FaceAttempt({
</TooltipTrigger>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Train Face as:</DropdownMenuLabel>
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
{faceNames.map((faceName) => (
<DropdownMenuItem
key={faceName}
@ -475,7 +479,7 @@ function FaceAttempt({
))}
</DropdownMenuContent>
</DropdownMenu>
<TooltipContent>Train Face as Person</TooltipContent>
<TooltipContent>{t("trainFaceAsPerson")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
@ -484,7 +488,7 @@ function FaceAttempt({
onClick={() => onReprocess()}
/>
</TooltipTrigger>
<TooltipContent>Reprocess Face</TooltipContent>
<TooltipContent>{t("button.reprocessFace")}</TooltipContent>
</Tooltip>
</div>
</div>
@ -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 (
<div className="relative flex flex-col rounded-lg">
@ -559,7 +564,7 @@ function FaceImage({ name, image, onRefresh }: FaceImageProps) {
onClick={onDelete}
/>
</TooltipTrigger>
<TooltipContent>Delete Face Attempt</TooltipContent>
<TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>
</Tooltip>
</div>
</div>

View File

@ -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<AbortController | null>(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);

View File

@ -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",
});
}

View File

@ -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",
},
);
});
};

View File

@ -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(() => {

View File

@ -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({
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
<div className="col-span-1">
<Heading as="h3" className="my-2">
Notification Settings
{t("notification.notificationSettings.title")}
</Heading>
<div className="max-w-6xl">
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
<p>
Frigate can natively send push notifications to your device
when it is running in the browser or installed as a PWA.
</p>
<p>{t("notification.notificationSettings.desc")}</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/notifications"
@ -319,7 +313,9 @@ export default function NotificationView({
rel="noopener noreferrer"
className="inline"
>
Read the Documentation{" "}
<p>
{t("notification.notificationSettings.documentation")}
</p>{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -327,12 +323,13 @@ export default function NotificationView({
</div>
<Alert variant="destructive">
<CiCircleAlert className="size-5" />
<AlertTitle>Notifications Unavailable</AlertTitle>
<AlertTitle>
{t("notification.notificationUnavailable.title")}
</AlertTitle>
<AlertDescription>
Web push notifications require a secure context (
<code>https://...</code>). This is a browser limitation. Access
Frigate securely to use notifications.
<Trans ns="views/settings">
notification.notificationUnavailable.desc
</Trans>
<div className="mt-3 flex items-center">
<Link
to="https://docs.frigate.video/configuration/authentication"
@ -340,7 +337,9 @@ export default function NotificationView({
rel="noopener noreferrer"
className="inline"
>
Read the Documentation{" "}
<p>
{t("notification.notificationUnavailable.documentation")}
</p>{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</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="col-span-1">
<Heading as="h3" className="my-2">
{t("notification.notificationSettings")}
{t("notification.notificationSettings.title")}
</Heading>
<div className="max-w-6xl">
<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">
<Link
to="https://docs.frigate.video/configuration/notifications"
@ -373,7 +372,7 @@ export default function NotificationView({
rel="noopener noreferrer"
className="inline"
>
{t("notification.documentation")}{" "}
{t("notification.notificationSettings.documentation")}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>

View File

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