feat: add more i18n keys

This commit is contained in:
ZhaiSoul 2025-03-13 17:12:51 +08:00
parent 9e6272d065
commit 94d0c66402
92 changed files with 1040 additions and 398 deletions

View File

@ -36,6 +36,8 @@
"second": "{{time}} seconds",
"formattedTimestamp": "%b %-d, %I:%M:%S %p",
"formattedTimestamp.24hour": "%b %-d, %H:%M:%S",
"formattedTimestamp2": "%m/%d %I:%M:%S%P",
"formattedTimestamp2.24hour": "%d %b %H:%M:%S",
"formattedTimestampExcludeSeconds": "%b %-d, %I:%M %p",
"formattedTimestampExcludeSeconds.24hour": "%b %-d, %H:%M",
"formattedTimestampWithYear": "%b %-d %Y, %I:%M %p",
@ -48,6 +50,9 @@
"kph": "kph"
}
},
"label": {
"back": "Go back"
},
"button": {
"apply": "Apply",
"reset": "Reset",
@ -75,7 +80,11 @@
"download": "Download",
"info": "Info",
"suspended": "Suspended",
"unsuspended": "Unsuspend"
"unsuspended": "Unsuspend",
"play": "Play",
"unselect": "Unselect",
"export": "Export",
"deleteNow": "Delete Now"
},
"menu": {
"system": "System",
@ -87,13 +96,15 @@
"languages": "Languages",
"language": {
"en": "English",
"zhCN": "简体中文(Simplified Chinese)"
"zhCN": "简体中文(Simplified Chinese)",
"withSystem.label": "Use the system settings for languag"
},
"appearance": "Appearance",
"darkMode": {
"label": "Dark Mode",
"light": "Light",
"dark": "Dark"
"dark": "Dark",
"withSystem.label": "Use the system settings for light or dark mode"
},
"withSystem": "System",
"theme": {
@ -136,5 +147,13 @@
"admin": "Admin",
"viewer": "Viewer",
"desc": "Admins have full access to all features in the Frigate UI. Viewers are limited to viewing cameras, review items, and historical footage in the UI."
},
"pagination": {
"label": "pagination",
"previous": "Previous",
"previous.label": "Go to previous page",
"next": "Next",
"next.label": "Go to next page",
"more": "More pages"
}
}

View File

@ -0,0 +1,15 @@
{
"form": {
"user": "Username",
"password": "Password",
"login": "Login",
"errors": {
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"rateLimit": "Exceeded rate limit. Try again later.",
"loginFailed": "Login failed",
"unknownError": "Unknown error. Check logs.",
"webUnkownError": "Unknown error. Check console logs."
}
}
}

View File

@ -4,6 +4,7 @@
"add": "Add camera groups",
"edit": "Edit camera groups",
"delete": {
"label": "Delete Camera Group",
"confirm": "Confirm Delete",
"confirm.desc": "Are you sure you want to delete the camera group <em>{{name}}</em>?"
},
@ -25,6 +26,7 @@
"success": "Camera group ({{name}}) has been saved.",
"camera": {
"setting": {
"label": "Camera Streaming Settings",
"title": "{{cameraName}} Streaming Settings",
"desc": "Change the live streaming options for this camera group's dashboard. <em>These settings are device/browser-specific.</em>",
"audioIsAvailable": "Audio is available for this stream",
@ -57,5 +59,19 @@
}
}
}
},
"debug": {
"options": {
"label": "Settings",
"title": "Options",
"showOptions": "Show Options",
"hideOptions": "Hide Options"
},
"boundingBox": "Bounding Box",
"timestamp": "Timestamp",
"zones": "Zones",
"mask": "Mask",
"motion": "Motion",
"regions": "Regions"
}
}

View File

@ -15,10 +15,12 @@
"desc": "Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model."
},
"review": {
"true.label": "Confirm this label for Frigate Plus",
"true_one": "This is a {{label}}",
"true_other": "This is an {{label}}",
"false_one": "This is not a {{label}}",
"false_other": "This is not an {{label}}",
"false.label": "Do not confirm this label for Frigate Plus",
"state.submitted": "Submitted"
}
},
@ -31,13 +33,18 @@
"fromTimeline": "Select from Timeline",
"lastHour_one": "Last Hour",
"lastHour_other": "Last {{count}} Hours",
"custom": "Custom"
"custom": "Custom",
"start": "Start Time",
"start.label": "Select Start Time",
"end": "End Time",
"end.label": "Select End Time"
},
"name": {
"placeholder": "Name the Export"
},
"select": "Select",
"export": "Export",
"selectOrExport": "Select or Export",
"toast": {
"success": "Successfully started export. View the file in the /exports folder.",
"error": {
@ -70,13 +77,15 @@
"desc": "Provide a name for this saved search.",
"placeholder": "Enter a name for your search",
"overwrite": "{{searchName}} already exists. Saving will overwrite the existing value.",
"success": "Search ({{searchName}}) has been saved."
"success": "Search ({{searchName}}) has been saved.",
"button.save.label": "Save this search"
}
},
"recording": {
"confirmDelete": {
"title": "Confirm Delete",
"desc": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future."
"desc": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future.",
"desc.selected": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future."
},
"button": {
"export": "Export",

View File

@ -1,6 +1,7 @@
{
"filter": "Filter",
"labels": {
"label": "Labels",
"all": "All Labels",
"all.short": "Labels",
"count": "{{count}} Labels"
@ -14,6 +15,7 @@
"all.short": "Dates"
},
"more": "More Filters",
"reset.label": "Reset filters to default values",
"timeRange": "Time Range",
"zones.label": "Zones",
"subLabels": {
@ -42,12 +44,16 @@
"relevance": "Relevance"
},
"cameras": {
"label": "Cameras Filter",
"all": "All Cameras",
"all.short": "Cameras"
},
"review": {
"showReviewed": "Show Reviewed"
},
"motion": {
"showMotionOnly": "Show Motion Only"
},
"explore": {
"settings": {
"title": "Settings",
@ -65,6 +71,30 @@
"description": "Description"
}
}
},
"date": {
"selectDateBy": {
"label": "Select a date to filter by"
}
}
},
"logSettings": {
"label": "Filter log level",
"filterBySeverity": "Filter logs by severity",
"loading": "Loading",
"loading.desc": "When the log pane is scrolled to the bottom, new logs automatically stream as they are added.",
"disableLogStreaming": "Disable log streaming",
"allLogs": "All logs"
},
"trackedObjectDelete": {
"title": "Confirm Delete",
"desc": "Deleting these {{objectLength}} tracked objects removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of these tracked objects in History view will <em>NOT</em> be deleted.<br /><br />Are you sure you want to proceed?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future.",
"toast": {
"success": "Tracked objects deleted successfully.",
"error": "Failed to delete tracked objects: {{errorMessage}}"
}
},
"zoneMask": {
"filterBy": "Filter by zone mask"
}
}

View File

@ -0,0 +1,10 @@
{
"button": {
"downloadVideo": {
"label": "Download Video",
"toast": {
"success": "Your review item video has started downloading."
}
}
}
}

View File

@ -1,5 +1,9 @@
{
"noRecordingsFoundForThisTime": "No recordings found for this time",
"noPreviewFound": "No Preview Found",
"noPreviewFoundFor": "No Preview Found for {{cameraName}}"
"noPreviewFoundFor": "No Preview Found for {{cameraName}}",
"submitFrigatePlus": {
"title": "Submit this frame to Frigate+?",
"submit": "Submit"
}
}

View File

@ -12,8 +12,10 @@
"motion": "No motion data found"
},
"timeline": "Timeline",
"timeline.aria": "Select timeline",
"events": {
"label": "Events",
"aria": "Select events",
"noFoundForTimePeriod": "No events found for this time period."
},
"documentTitle": "Review - Frigate",
@ -22,5 +24,12 @@
},
"calendarFilter": {
"last24Hours": "Last 24 Hours"
}
},
"markAsReviewed": "Mark as Reviewed",
"markTheseItemsAsReviewed": "Mark these items as reviewed",
"newReviewItems": {
"label": "View new review items",
"button": "New Items To Review"
},
"camera": "Camera"
}

View File

@ -34,6 +34,43 @@
"video": "video",
"object_lifecycle": "object lifecycle"
},
"objectLifecycle": {
"title": "Object Lifecycle",
"noImageFound": "No image found for this timestamp.",
"createObjectMask": "Create Object Mask",
"adjustAnnotationSettings": "Adjust annotation settings",
"scrollViewTips": "Scroll to view the significant moments of this object's lifecycle.",
"autoTrackingTips": "Bounding box positions will be inaccurate for autotracking cameras.",
"lifecycleItemDesc": {
"visible": "{{label}} detected",
"entered_zone": "{{label}} entered {{zones}}",
"active": "{{label}} became active",
"stationary": "{{label}} became stationary",
"attribute": {
"faceOrLicense_plate": "{{attribute}} detected for {{label}}",
"other": "{{label}} recognized as {{attribute}}"
},
"gone": "{{label}} left",
"heard": "{{label}} heard",
"external": "{{label}} detected"
},
"annotationSettings": {
"title": "Annotation Settings",
"showAllZones": "Show All Zones",
"showAllZones.desc": "Always show zones on frames where objects have entered a zone.",
"offset": {
"label": "Annotation Offset",
"desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. However, the <code>annotation_offset</code> field can be used to adjust this.",
"documentation": "Read the documentation ",
"millisecondsToOffset": "Milliseconds to offset detect annotations by. <em>Default: 0</em>",
"tips": "TIP: Imagine there is an event clip with a person walking from left to right. If the event timeline bounding box is consistently to the left of the person then the value should be decreased. Similarly, if a person is walking from left to right and the bounding box is consistently ahead of the person then the value should be increased."
}
},
"carousel": {
"previous": "Previous slide",
"next": "Next slide"
}
},
"details": {
"item": {
"title": "Review Item Details",
@ -68,6 +105,8 @@
"aiTips": "Frigate will not request a description from your Generative AI provider until the tracked object's lifecycle has ended."
},
"button.regenerate": "Regenerate",
"button.regenerate.label": "Regenerate tracked object description",
"expandRegenerationMenu": "Expand regeneration menu",
"regenerateFromSnapshot": "Regenerate from Snapshot",
"regenerateFromThumbnails": "Regenerate from Thumbnails",
"tips": {
@ -99,6 +138,9 @@
"viewInHistory": {
"label": "View in History",
"aria": "View in History"
},
"deleteTrackedObject": {
"label": "Delete this tracked object"
}
},
"dialog": {

View File

@ -3,5 +3,11 @@
"search": "Search",
"noExports": "No exports found",
"deleteExport": "Delete Export",
"deleteExport.desc": "Are you sure you want to delete {{exportName}}?"
"deleteExport.desc": "Are you sure you want to delete {{exportName}}?",
"editExport": {
"title": "Rename Export",
"desc": "Enter a new name for this export.",
"saveExport": "Save Export"
}
}

View File

@ -12,6 +12,11 @@
},
"ptz": {
"move": {
"clickMove": {
"label": "Click in the frame to center the camera",
"enable": "Enable click to move",
"disable": "Disable click to move"
},
"left": {
"label": "Move PTZ camera to the left"
},
@ -37,7 +42,8 @@
"center": {
"label": "Click in the frame to center the PTZ camera"
}
}
},
"presets": "PTZ camera presets"
},
"camera": {
"enable": "Enable Camera",
@ -118,8 +124,7 @@
"playInBackground": {
"label": "Play in background",
"tips": "Enable this option to continue streaming when the player is hidden."
},
"": ""
}
},
"cameraSettings": {
"title": "{{camera}} Settings",
@ -129,5 +134,16 @@
"snapshots": "Snapshots",
"audioDetection": "Audio Detection",
"autotracking": "Autotracking"
},
"history": {
"label": "Show historical footage"
},
"effectiveRetainMode": {
"modes": {
"all": "All",
"motion": "Motion",
"active_objects": "Active Objects"
},
"notAllTips": "Your {{source}} recording retention configuration is set to <code>mode: {{effectiveRetainMode}}</code>, so this on-demand recording will only keep segments with {{effectiveRetainModeName}}."
}
}

View File

@ -2,6 +2,7 @@
"export": "Export",
"calendar": "Calendar",
"filter": "Filter",
"filters": "Filters",
"toast": {
"error": {
"noValidTimeSelected": "No valid time range selected",

View File

@ -122,6 +122,17 @@
"inertia.error.mustBeAboveZero": "Inertia must be above 0.",
"loiteringTime.error.mustBeGreaterOrEqualZero": "Loitering time must be greater than or equal to 0.",
"polygonDrawing": {
"removeLastPoint": "Remove last point",
"reset.label": "Clear all points",
"snapPoints": {
"true": "Snap points",
"false": "Don't Snap points"
},
"delete": {
"title": "Confirm Delete",
"desc": "Are you sure you want to delete the {{type}} <em>{{name}}</em>?",
"success": "{{name}} has been deleted."
},
"error": {
"mustBeFinished": "Polygon drawing must be finished before saving."
}

View File

@ -1,7 +1,23 @@
{
"title": "System",
"metrics": "System metrics",
"logs": "System logs",
"logs": {
"download": {
"label": "Download Logs"
},
"copy": {
"label": "Copy to Clipboard",
"success": "Copied logs to clipboard",
"error": "Could not copy logs to clipboard"
},
"type": {
"label": "Type",
"timestamp": "Timestamp",
"tag": "Tag",
"message": "Message"
},
"tips": "Logs are streaming from the server"
},
"general": {
"title": "General",
"detector": {
@ -15,7 +31,24 @@
"gpuUsage": "GPU Usage",
"gpuMemory": "GPU Memory",
"gpuEncoder": "GPU Encoder",
"gpuDecoder": "GPU Decoder"
"gpuDecoder": "GPU Decoder",
"gpuInfo": {
"vainfoOutput": {
"title": "Vainfo Output",
"returnCode": "Return Code: {{code}}",
"processOutput": "Process Output:",
"processError": "Process Error:"
},
"nvidiaSMIOutput": {
"title": "Nvidia SMI Output",
"name": "Name: {{name}}",
"driver": "Driver: {{driver}}",
"cudaComputerCapability": "CUDA Compute Capability: {{cuda_compute}}",
"vbios": "VBios Info: {{vbios}}"
},
"closeInfo.label": "Close GPU info",
"copyInfo.label": "Close GPU info"
}
},
"otherProcesses": {
"title": "Other Processes",
@ -28,12 +61,14 @@
"overview": "Overview",
"recordings": {
"title": "Recordings",
"tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk."
"tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.",
"earliestRecording": "Earliest recording available:"
},
"cameraStorage": {
"title": "Camera Storage",
"camera": "Camera",
"unused": "Unused",
"unusedStorageInformation": "Unused Storage Information",
"storageUsed": "Storage Used",
"percentageOfTotalUsed": "Percentage of Total Used",
"bandwidth": "Bandwidth",

View File

@ -48,6 +48,14 @@
"kph": "英里/小时"
}
},
"pagination": {
"label": "分页",
"previous": "上一页",
"previous.label": "转到上一页",
"next": "下一页",
"next.label": "转到下一页",
"more": "更多页面"
},
"button": {
"apply": "应用",
"reset": "重置",
@ -75,7 +83,11 @@
"download": "下载",
"info": "信息",
"suspended": "已暂停",
"unsuspended": "取消暂停"
"unsuspended": "取消暂停",
"play": "播放",
"unselect": "取消选择",
"export": "导出",
"deleteNow": "立即删除"
},
"menu": {
"system": "系统",
@ -87,13 +99,15 @@
"languages": "languages / 语言",
"language": {
"en": "English",
"zhCN": "简体中文"
"zhCN": "简体中文",
"withSystem.label": "使用系统语言设置"
},
"appearance": "外观",
"darkMode": {
"label": "深色模式",
"light": "浅色",
"dark": "深色"
"dark": "深色",
"withSystem.label": "使用系统深色模式设置"
},
"withSystem": "跟随系统",
"theme": {

View File

@ -0,0 +1,15 @@
{
"form": {
"user": "用户名",
"password": "密码",
"login": "登录",
"errors": {
"usernameRequired": "用户名不能为空",
"passwordRequired": "密码不能为空",
"rateLimit": "超出请求限制,请稍后再试。",
"loginFailed": "登录失败",
"unknownError": "未知错误,请检查日志。",
"webUnkownError": "未知错误,请检查控制台日志。"
}
}
}

View File

@ -4,6 +4,7 @@
"add": "添加摄像头组",
"edit": "编辑摄像头组",
"delete": {
"label": "删除摄像头组",
"confirm": "确认删除",
"confirm.desc": "你确定要删除摄像头组 <em>{{name}}</em> 吗?"
},
@ -25,6 +26,7 @@
"success": "摄像头组({{name}})保存成功。",
"camera": {
"setting": {
"label": "摄像头视频流设置",
"title": "{{cameraName}} 视频流设置",
"desc": "更改此摄像头组仪表板的实时视频流选项。<em>这些设置特定于设备/浏览器。</em>",
"audioIsAvailable": "此视频流支持音频",
@ -57,5 +59,19 @@
}
}
}
},
"debug": {
"options": {
"label": "设置",
"title": "选项",
"showOptions": "显示选项",
"hideOptions": "隐藏选项"
},
"boundingBox": "边界框",
"timestamp": "时间戳",
"zones": "区域",
"mask": "遮罩",
"motion": "运动",
"regions": "区域"
}
}

View File

@ -15,8 +15,10 @@
"desc": "您希望避开的地点中的物体不应被视为误报。若将其作为误报提交可能会导致AI模型容易混淆相关物体的识别。"
},
"review": {
"true.label": "为 Frigate Plus 确认此标签",
"true_one": "这是 {{label}}",
"true_other": "这是 {{label}}",
"false.label": "不为 Frigate Plus 确认此标签",
"false_one": "这不是 {{label}}",
"false_other": "这不是 {{label}}",
"state.submitted": "已提交"
@ -31,13 +33,18 @@
"fromTimeline": "从时间线选择",
"lastHour_one": "最后1小时",
"lastHour_other": "最后 {{count}} 小时",
"custom": "自定义"
"custom": "自定义",
"start": "开始时间",
"start.label": "选择开始时间",
"end": "结束时间",
"end.label": "选择结束时间"
},
"name": {
"placeholder": "导出项目的名字"
},
"select": "选择",
"export": "导出",
"selectOrExport": "选择或导出",
"toast": {
"success": "导出成功。进入 /exports 目录查看文件。",
"error": {
@ -70,13 +77,15 @@
"desc": "请为此已保存的搜索提供一个名称。",
"placeholder": "请输入搜索名称",
"overwrite": "{{searchName}} 已存在。保存将覆盖现有值。",
"success": "搜索 ({{searchName}}) 已保存。"
"success": "搜索 ({{searchName}}) 已保存。",
"button.save.label": "保存此搜索"
}
},
"recording": {
"confirmDelete": {
"title": "确认删除",
"desc": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。"
"desc": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。",
"desc.selected": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。"
},
"button": {
"export": "导出",

View File

@ -1,6 +1,7 @@
{
"filter": "过滤器",
"labels": {
"label": "标签",
"all": "所有标签",
"all.short": "标签",
"count": "{{count}} 个标签"
@ -14,6 +15,7 @@
"all.short": "日期"
},
"more": "更多筛选项",
"reset.label": "重置筛选器为默认值",
"timeRange": "时间范围",
"zones.label": "区域",
"subLabels": {
@ -42,12 +44,16 @@
"relevance": "关联性"
},
"cameras": {
"label": "摄像头筛选",
"all": "所有摄像头",
"all.short": "摄像头"
},
"review": {
"showReviewed": "显示已查看的项目"
},
"motion": {
"showMotionOnly": "仅显示运动"
},
"explore": {
"settings": {
"title": "设置",
@ -64,8 +70,31 @@
"thumbnailImage": "缩略图",
"description": "描述"
}
},
"date": {
"selectDateBy": {
"label": "选择日期进行筛选"
}
}
}
},
"logSettings": {
"label": "日志级别筛选",
"filterBySeverity": "按严重程度筛选日志",
"loading": "加载中",
"loading.desc": "当日志面板滚动到底部时,新的日志会自动流式加载。",
"disableLogStreaming": "禁用日志流式加载",
"allLogs": "所有日志"
},
"trackedObjectDelete": {
"title": "确认删除",
"desc": "删除这 {{objectLength}} 个跟踪对象将移除快照、任何已保存的嵌入和任何相关的对象生命周期条目。历史视图中这些跟踪对象的录制片段将<em>不会</em>被删除。<br /><br />您确定要继续吗?<br /><br />按住 <em>Shift</em> 键可在将来跳过此对话框。",
"toast": {
"success": "跟踪对象删除成功。",
"error": "删除跟踪对象失败:{{errorMessage}}"
}
},
"zoneMask": {
"filterBy": "按区域遮罩筛选"
}
}

View File

@ -1,3 +1,10 @@
{
"button": {
"downloadVideo": {
"label": "下载视频",
"toast": {
"success": "下载成功"
}
}
}
}

View File

@ -1,5 +1,9 @@
{
"noRecordingsFoundForThisTime": "找不到此次录制",
"noPreviewFound": "没有找到预览",
"noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览"
"noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览",
"submitFrigatePlus": {
"title": "提交此帧到 Frigate+",
"submit": "提交"
}
}

View File

@ -12,8 +12,10 @@
"motion": "还没有运动类数据"
},
"timeline": "时间线",
"timeline.aria": "选择时间线",
"events": {
"label": "事件",
"aria": "选择事件",
"noFoundForTimePeriod": "未找到该时间段的事件。"
},
"documentTitle": "预览 - Frigate",
@ -22,5 +24,12 @@
},
"calendarFilter": {
"last24Hours": "过去24小时"
}
},
"markAsReviewed": "标记为已审核",
"markTheseItemsAsReviewed": "将这些项目标记为已审核",
"newReviewItems": {
"label": "查看新的审核项目",
"button": "新的待审核项目"
},
"camera": "摄像头"
}

View File

@ -34,6 +34,43 @@
"video": "视频",
"object_lifecycle": "对象生命周期"
},
"objectLifecycle": {
"title": "对象生命周期",
"noImageFound": "未找到此时间戳的图像。",
"createObjectMask": "创建对象遮罩",
"adjustAnnotationSettings": "调整标注设置",
"scrollViewTips": "滚动查看此对象生命周期的重要时刻。",
"autoTrackingTips": "自动跟踪摄像头的边界框位置可能不准确。",
"lifecycleItemDesc": {
"visible": "检测到 {{label}}",
"entered_zone": "{{label}} 进入 {{zones}}",
"active": "{{label}} 变为活动状态",
"stationary": "{{label}} 变为静止状态",
"attribute": {
"faceOrLicense_plate": "检测到 {{label}} 的 {{attribute}}",
"other": "{{label}} 识别为 {{attribute}}"
},
"gone": "{{label}} 离开",
"heard": "听到 {{label}}",
"external": "检测到 {{label}}"
},
"annotationSettings": {
"title": "标注设置",
"showAllZones": "显示所有区域",
"showAllZones.desc": "在对象进入区域的帧上始终显示区域。",
"offset": {
"label": "标注偏移",
"desc": "这些数据来自摄像头的检测源,但是叠加在录制源的图像上。这两个流不太可能完全同步。因此,边界框和录像不会完全对齐。但是,可以使用 <code>annotation_offset</code> 字段来调整这个问题。",
"documentation": "阅读文档(英文) ",
"millisecondsToOffset": "检测标注的偏移毫秒数。<em>默认值0</em>",
"tips": "提示:假设有一个人从左向右走的事件片段。如果事件时间线上的边界框始终在人的左侧,则应该减小该值。同样,如果一个人从左向右走,而边界框始终在人的前面,则应该增加该值。"
}
},
"carousel": {
"previous": "上一张",
"next": "下一张"
}
},
"details": {
"item": {
"title": "回放项目详情",
@ -68,6 +105,8 @@
"aiTips": "在跟踪对象的生命周期结束之前Frigate 不会向您的生成式 AI 提供商请求描述。"
},
"button.regenerate": "重新生成",
"button.regenerate.label": "重新生成跟踪对象描述",
"expandRegenerationMenu": "展开重新生成菜单",
"regenerateFromSnapshot": "从快照重新生成",
"regenerateFromThumbnails": "从缩略图重新生成",
"tips": {
@ -99,6 +138,9 @@
"viewInHistory": {
"label": "在历史记录中查看",
"aria": "在历史记录中查看"
},
"deleteTrackedObject": {
"label": "删除此跟踪对象"
}
},
"dialog": {

View File

@ -3,5 +3,10 @@
"search": "搜索",
"noExports": "没有找到导出的项目",
"deleteExport": "删除导出的项目",
"deleteExport.desc": "你确定要删除 {{exportName}} 吗?"
"deleteExport.desc": "你确定要删除 {{exportName}} 吗?",
"editExport": {
"title": "重命名导出",
"desc": "为此导出项目输入新名称。",
"saveExport": "保存导出"
}
}

View File

@ -12,6 +12,11 @@
},
"ptz": {
"move": {
"clickMove": {
"label": "点击画面以使摄像头居中",
"enable": "启用点击移动",
"disable": "禁用点击移动"
},
"left": {
"label": "PTZ摄像头向左移动"
},
@ -37,7 +42,8 @@
"center": {
"label": "点击将PTZ摄像头画面居中"
}
}
},
"presets": "PTZ摄像头预设"
},
"camera": {
"enable": "开启摄像头",
@ -128,5 +134,16 @@
"snapshots": "快照",
"audioDetection": "音频检测",
"autotracking": "自动跟踪"
},
"history": {
"label": "显示历史录像"
},
"effectiveRetainMode": {
"modes": {
"all": "全部",
"motion": "运动",
"active_objects": "活动对象"
},
"notAllTips": "您的 {{source}} 录制保留配置设置为 <code>mode: {{effectiveRetainMode}}</code>,因此此按需录制将仅保留包含 {{effectiveRetainModeName}} 的片段。"
}
}

View File

@ -2,6 +2,7 @@
"export": "导出",
"calendar": "日历",
"filter": "筛选",
"filters": "筛选条件",
"toast": {
"error": {
"noValidTimeSelected": "未选择有效的时间范围",

View File

@ -1,7 +1,23 @@
{
"title": "系统",
"metrics": "系统指标",
"logs": "系统日志",
"logs": {
"download": {
"label": "下载日志"
},
"copy": {
"label": "复制到剪贴板",
"success": "已复制日志到剪贴板",
"error": "无法复制日志到剪贴板"
},
"type": {
"label": "类型",
"timestamp": "时间戳",
"tag": "标签",
"message": "消息"
},
"tips": "日志正在从服务器流式传输"
},
"general": {
"title": "常规",
"detector": {
@ -15,7 +31,24 @@
"gpuUsage": "GPU使用率",
"gpuMemory": "GPU显存",
"gpuEncoder": "GPU编码",
"gpuDecoder": "GPU解码"
"gpuDecoder": "GPU解码",
"gpuInfo": {
"vainfoOutput": {
"title": "Vainfo 输出",
"returnCode": "返回代码:{{code}}",
"processOutput": "进程输出:",
"processError": "进程错误:"
},
"nvidiaSMIOutput": {
"title": "Nvidia SMI 输出",
"name": "名称:{{name}}",
"driver": "驱动:{{driver}}",
"cudaComputerCapability": "CUDA计算能力{{cuda_compute}}",
"vbios": "VBios信息{{vbios}}"
},
"closeInfo.label": "关闭GPU信息",
"copyInfo.label": "复制GPU信息"
}
},
"otherProcesses": {
"title": "其他进程",
@ -28,12 +61,14 @@
"overview": "概览",
"recordings": {
"title": "录制内容",
"tips": "该值表示 Frigate 数据库中录制内容所使用的总存储空间。Frigate 不会追踪磁盘上所有文件的存储使用情况。"
"tips": "该值表示 Frigate 数据库中录制内容所使用的总存储空间。Frigate 不会追踪磁盘上所有文件的存储使用情况。",
"earliestRecording": "最早的可用录制:"
},
"cameraStorage": {
"title": "摄像头存储",
"camera": "摄像头",
"unused": "未使用",
"unusedStorageInformation": "未使用存储信息",
"storageUsed": "存储使用",
"percentageOfTotalUsed": "总使用率",
"bandwidth": "带宽",

View File

@ -21,16 +21,18 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { AuthContext } from "@/context/auth-context";
import { useTranslation } from "react-i18next";
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const { t } = useTranslation(["components/auth"]);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const { login } = React.useContext(AuthContext);
const formSchema = z.object({
user: z.string().min(1, "Username is required"),
password: z.string().min(1, "Password is required"),
user: z.string().min(1, t("form.errors.usernameRequired")),
password: z.string().min(1, t("form.errors.passwordRequired")),
});
const form = useForm<z.infer<typeof formSchema>>({
@ -62,20 +64,20 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
if (err.response?.status === 429) {
toast.error("Exceeded rate limit. Try again later.", {
toast.error(t("form.errors.rateLimit"), {
position: "top-center",
});
} else if (err.response?.status === 401) {
toast.error("Login failed", {
toast.error(t("form.errors.loginFailed"), {
position: "top-center",
});
} else {
toast.error("Unknown error. Check logs.", {
toast.error(t("form.errors.unknownError"), {
position: "top-center",
});
}
} else {
toast.error("Unknown error. Check console logs.", {
toast.error(t("form.errors.webUnkownError"), {
position: "top-center",
});
}
@ -92,7 +94,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
name="user"
render={({ field }) => (
<FormItem>
<FormLabel>User</FormLabel>
<FormLabel>{t("form.user")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
@ -107,7 +109,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t("form.password")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
@ -123,10 +125,10 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Login"
aria-label={t("form.login")}
>
{isLoading && <ActivityIndicator className="mr-2 h-4 w-4" />}
Login
{t("form.login")}
</Button>
</div>
</form>

View File

@ -3,6 +3,7 @@ import { toast } from "sonner";
import { FaDownload } from "react-icons/fa";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
type DownloadVideoButtonProps = {
source: string;
@ -17,6 +18,7 @@ export function DownloadVideoButton({
startTime,
className,
}: DownloadVideoButtonProps) {
const { t } = useTranslation(["components/input"]);
const formattedDate = formatUnixTimestampToDateTime(startTime, {
strftime_fmt: "%D-%T",
time_style: "medium",
@ -25,7 +27,7 @@ export function DownloadVideoButton({
const filename = `${camera}_${formattedDate}.mp4`;
const handleDownloadStart = () => {
toast.success("Your review item video has started downloading.", {
toast.success(t("button.downloadVideo.toast.success"), {
position: "top-center",
});
};
@ -36,7 +38,7 @@ export function DownloadVideoButton({
asChild
className="flex items-center gap-2"
size="sm"
aria-label="Download Video"
aria-label={t("button.downloadVideo.label")}
>
<a href={source} download={filename} onClick={handleDownloadStart}>
<FaDownload

View File

@ -7,6 +7,7 @@ import { useCallback, useMemo, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { usePersistence } from "@/hooks/use-persistence";
import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
import { useTranslation } from "react-i18next";
type Options = { [key: string]: boolean };
@ -21,6 +22,7 @@ export default function DebugCameraImage({
className,
cameraConfig,
}: DebugCameraImageProps) {
const { t } = useTranslation(["components/camera"]);
const [showSettings, setShowSettings] = useState(false);
const [options, setOptions] = usePersistence<Options>(
`${cameraConfig?.name}-feed`,
@ -59,17 +61,21 @@ export default function DebugCameraImage({
onClick={handleToggleSettings}
variant="link"
size="sm"
aria-label="Settings"
aria-label={t("debug.options.label")}
>
<span className="h-5 w-5">
<LuSettings />
</span>{" "}
<span>{showSettings ? "Hide" : "Show"} Options</span>
<span>
{showSettings
? t("debug.options.hideOptions")
: t("debug.options.showOptions")}
</span>
</Button>
{showSettings ? (
<Card>
<CardHeader>
<CardTitle>Options</CardTitle>
<CardTitle>{t("debug.options.title")}</CardTitle>
</CardHeader>
<CardContent>
<DebugSettings
@ -89,6 +95,7 @@ type DebugSettingsProps = {
};
function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
const { t } = useTranslation(["components/camera"]);
return (
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
<div className="flex items-center space-x-2">
@ -99,7 +106,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
handleSetOption("bbox", isChecked);
}}
/>
<Label htmlFor="bbox">Bounding Box</Label>
<Label htmlFor="bbox">{t("debug.boundingBox")}</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
@ -109,7 +116,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
handleSetOption("timestamp", isChecked);
}}
/>
<Label htmlFor="timestamp">Timestamp</Label>
<Label htmlFor="timestamp">{t("debug.timestamp")}</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
@ -119,7 +126,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
handleSetOption("zones", isChecked);
}}
/>
<Label htmlFor="zones">Zones</Label>
<Label htmlFor="zones">{t("debug.zones")}</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
@ -129,7 +136,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
handleSetOption("mask", isChecked);
}}
/>
<Label htmlFor="mask">Mask</Label>
<Label htmlFor="mask">{t("debug.mask")}</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
@ -139,7 +146,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
handleSetOption("motion", isChecked);
}}
/>
<Label htmlFor="motion">Motion</Label>
<Label htmlFor="motion">{t("debug.motion")}</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
@ -149,7 +156,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
handleSetOption("regions", isChecked);
}}
/>
<Label htmlFor="regions">Regions</Label>
<Label htmlFor="regions">{t("debug.regions")}</Label>
</div>
</div>
);

View File

@ -18,6 +18,7 @@ import { Skeleton } from "../ui/skeleton";
import { Button } from "../ui/button";
import { FaCircleCheck } from "react-icons/fa6";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
type AnimatedEventCardProps = {
event: ReviewSegment;
@ -29,6 +30,7 @@ export function AnimatedEventCard({
selectedGroup,
updateEvents,
}: AnimatedEventCardProps) {
const { t } = useTranslation(["views/events"]);
const { data: config } = useSWR<FrigateConfig>("config");
const apiHost = useApiHost();
@ -121,7 +123,7 @@ export function AnimatedEventCard({
<Button
className="absolute right-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
size="xs"
aria-label="Mark as Reviewed"
aria-label={t("markAsReviewed")}
onClick={async () => {
await axios.post(`reviews/viewed`, { ids: [event.id] });
updateEvents();
@ -130,7 +132,7 @@ export function AnimatedEventCard({
<FaCircleCheck className="size-3 text-white" />
</Button>
</TooltipTrigger>
<TooltipContent>Mark as Reviewed</TooltipContent>
<TooltipContent>{t("markAsReviewed")}</TooltipContent>
</Tooltip>
)}
{previews != undefined && (

View File

@ -20,6 +20,7 @@ import { MdEditSquare } from "react-icons/md";
import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import { shareOrCopy } from "@/utils/browserUtil";
import { useTranslation } from "react-i18next";
type ExportProps = {
className: string;
@ -36,6 +37,7 @@ export default function ExportCard({
onRename,
onDelete,
}: ExportProps) {
const { t } = useTranslation(["views/exports"]);
const [hovered, setHovered] = useState(false);
const [loading, setLoading] = useState(
exportedRecording.thumb_path.length > 0,
@ -89,10 +91,8 @@ export default function ExportCard({
}
}}
>
<DialogTitle>Rename Export</DialogTitle>
<DialogDescription>
Enter a new name for this export.
</DialogDescription>
<DialogTitle>{t("editExport.title")}</DialogTitle>
<DialogDescription>{t("editExport.desc")}</DialogDescription>
{editName && (
<>
<Input
@ -113,13 +113,13 @@ export default function ExportCard({
/>
<DialogFooter>
<Button
aria-label="Save Export"
aria-label={t("editExport.saveExport")}
size="sm"
variant="select"
disabled={(editName?.update?.length ?? 0) == 0}
onClick={() => submitRename()}
>
Save
{t("button.save", { ns: "common" })}
</Button>
</DialogFooter>
</>
@ -207,7 +207,7 @@ export default function ExportCard({
{!exportedRecording.in_progress && (
<Button
className="absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white"
aria-label="Play"
aria-label={t("button.play", { ns: "common" })}
variant="ghost"
onClick={() => {
onSelect(exportedRecording);

View File

@ -3,6 +3,7 @@ import { Button } from "../ui/button";
import { LuRefreshCcw } from "react-icons/lu";
import { MutableRefObject, useMemo } from "react";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
type NewReviewDataProps = {
className: string;
@ -18,6 +19,7 @@ export default function NewReviewData({
itemsToReview,
pullLatestData,
}: NewReviewDataProps) {
const { t } = useTranslation(["views/events"]);
const hasUpdate = useMemo(() => {
if (!reviewItems || !itemsToReview) {
return false;
@ -36,7 +38,7 @@ export default function NewReviewData({
: "invisible",
"mx-auto bg-gray-400 text-center text-white",
)}
aria-label="View new review items"
aria-label={t("newReviewItems.label")}
onClick={() => {
pullLatestData();
if (contentRef.current) {
@ -48,7 +50,7 @@ export default function NewReviewData({
}}
>
<LuRefreshCcw className="mr-2 h-4 w-4" />
New Items To Review
{t("newReviewItems.button")}
</Button>
</div>
</div>

View File

@ -15,6 +15,7 @@ import { DateRange } from "react-day-picker";
import { useState } from "react";
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import { t } from "i18next";
import { useTranslation } from "react-i18next";
type CalendarFilterButtonProps = {
reviewSummary?: ReviewSummary;
@ -28,16 +29,17 @@ export default function CalendarFilterButton({
day,
updateSelectedDay,
}: CalendarFilterButtonProps) {
const { t } = useTranslation(["components/filter"]);
const [open, setOpen] = useState(false);
const selectedDate = useFormattedTimestamp(
day == undefined ? 0 : day?.getTime() / 1000 + 1,
t("time.formattedTimestampOnlyMonthAndDay"),
t("time.formattedTimestampOnlyMonthAndDay", { ns: "common" }),
);
const trigger = (
<Button
className="flex items-center gap-2"
aria-label="Select a date to filter by"
aria-label={t("date.selectDateBy.label")}
variant={day == undefined ? "default" : "select"}
size="sm"
>
@ -64,7 +66,7 @@ export default function CalendarFilterButton({
<DropdownMenuSeparator />
<div className="flex items-center justify-center p-2">
<Button
aria-label="Reset"
aria-label={t("button.reset", { ns: "common" })}
onClick={() => {
updateSelectedDay(undefined);
}}
@ -107,7 +109,7 @@ export function CalendarRangeFilterButton({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label="Select a date to filter by"
aria-label={t("date.selectDateBy.label")}
variant={range == undefined ? "default" : "select"}
size="sm"
>

View File

@ -154,7 +154,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
: "bg-secondary text-secondary-foreground focus:bg-secondary focus:text-secondary-foreground"
}
aria-label="All Cameras"
aria-label={t("menu.live.allCameras", { ns: "common" })}
size="xs"
onClick={() => (group ? setGroup("default", true) : null)}
onMouseEnter={() => (isDesktop ? showTooltip("default") : null)}
@ -179,7 +179,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
: "bg-secondary text-secondary-foreground"
}
aria-label="Camera Group"
aria-label={t("group.label")}
size="xs"
onClick={() => setGroup(name, group != "default")}
onMouseEnter={() => (isDesktop ? showTooltip(name) : null)}
@ -206,7 +206,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
<Button
className="bg-secondary text-muted-foreground"
aria-label="Add camera group"
aria-label={t("group.add")}
size="xs"
onClick={() => setAddGroup(true)}
>
@ -278,9 +278,15 @@ function NewGroupDialog({
} else {
setOpen(false);
setEditState("none");
toast.error(`Failed to save config changes: ${res.statusText}`, {
position: "top-center",
});
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -290,7 +296,7 @@ function NewGroupDialog({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Failed to save config changes: ${errorMessage}`, {
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
})
@ -305,6 +311,7 @@ function NewGroupDialog({
setOpen,
deleteGroup,
deleteGridLayout,
t,
],
);
@ -373,7 +380,7 @@ function NewGroupDialog({
"size-6 rounded-md bg-secondary-foreground p-1 text-background",
isMobile && "text-secondary-foreground",
)}
aria-label="Add camera group"
aria-label={t("group.add")}
onClick={() => {
setEditState("add");
}}
@ -407,9 +414,7 @@ function NewGroupDialog({
<Title>
{editState == "add" ? t("group.add") : t("group.edit")}
</Title>
<Description className="sr-only">
Edit camera groups
</Description>
<Description className="sr-only">{t("group.edit")}</Description>
</Header>
<CameraGroupEdit
currentGroups={currentGroups}
@ -563,13 +568,13 @@ export function CameraGroupRow({
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuItem
aria-label="Edit group"
aria-label={t("group.edit")}
onClick={onEditGroup}
>
{t("button.edit", { ns: "common" })}
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Delete group"
aria-label={t("group.delete.label")}
onClick={() => setDeleteDialogOpen(true)}
>
{t("button.delete", { ns: "common" })}
@ -855,7 +860,7 @@ export function CameraGroupEdit({
<DialogTrigger asChild>
<Button
className="flex h-auto items-center gap-1"
aria-label="Camera streaming settings"
aria-label={t("group.camera.setting.label")}
size="icon"
variant="ghost"
disabled={
@ -934,7 +939,7 @@ export function CameraGroupEdit({
<Button
type="button"
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
>
{t("button.cancel", { ns: "common" })}
@ -943,7 +948,7 @@ export function CameraGroupEdit({
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
type="submit"
>
{isLoading ? (

View File

@ -59,7 +59,7 @@ export function CamerasFilterButton({
const trigger = (
<Button
className="flex items-center gap-2 capitalize"
aria-label="Cameras Filter"
aria-label={t("cameras.label")}
variant={selectedCameras?.length == undefined ? "default" : "select"}
size="sm"
>
@ -228,7 +228,7 @@ export function CamerasFilterContent({
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
aria-label="Apply"
aria-label={t("button.apply", { ns: "common" })}
variant="select"
disabled={currentCameras?.length === 0}
onClick={() => {
@ -239,7 +239,7 @@ export function CamerasFilterContent({
{t("button.apply", { ns: "common" })}
</Button>
<Button
aria-label="Reset"
aria-label={t("button.reset", { ns: "common" })}
onClick={() => {
setCurrentCameras(undefined);
updateCameraFilter(undefined);

View File

@ -9,6 +9,8 @@ import { Switch } from "../ui/switch";
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
import { cn } from "@/lib/utils";
import FilterSwitch from "./FilterSwitch";
import { useTranslation } from "react-i18next";
import { t } from "i18next";
type LogSettingsButtonProps = {
selectedLabels?: LogSeverity[];
@ -22,23 +24,26 @@ export function LogSettingsButton({
logSettings,
setLogSettings,
}: LogSettingsButtonProps) {
const { t } = useTranslation(["components/filter"]);
const trigger = (
<Button
size="sm"
className="flex items-center gap-2"
aria-label="Filter log level"
aria-label={t("logSettings.label")}
>
<FaCog className="text-secondary-foreground" />
<div className="hidden text-primary md:block">Settings</div>
<div className="hidden text-primary md:block">
{t("menu.settings", { ns: "common" })}
</div>
</Button>
);
const content = (
<div className={cn("my-3 space-y-3 py-3 md:mt-0 md:py-0")}>
<div className="space-y-4">
<div className="space-y-0.5">
<div className="text-md">Filter</div>
<div className="text-md">{t("filter")}</div>
<div className="space-y-1 text-xs text-muted-foreground">
Filter logs by severity.
{t("logSettings.filterBySeverity")}
</div>
</div>
<GeneralFilterContent
@ -49,14 +54,13 @@ export function LogSettingsButton({
<DropdownMenuSeparator />
<div className="space-y-4">
<div className="space-y-0.5">
<div className="text-md">Loading</div>
<div className="text-md">{t("logSettings.loading")}</div>
<div className="mt-2.5 flex flex-col gap-2.5">
<div className="space-y-1 text-xs text-muted-foreground">
When the log pane is scrolled to the bottom, new logs
automatically stream as they are added.
{t("logSettings.loading.desc")}
</div>
<FilterSwitch
label="Disable log streaming"
label={t("logSettings.disableLogStreaming")}
isChecked={logSettings?.disableStreaming ?? false}
onCheckedChange={(isChecked) => {
setLogSettings({
@ -105,7 +109,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
All Logs
{t("logSettings.allLogs")}
</Label>
<Switch
className="ml-1"

View File

@ -16,6 +16,7 @@ import {
AlertDialogTitle,
} from "../ui/alert-dialog";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Trans, useTranslation } from "react-i18next";
type ReviewActionGroupProps = {
selectedReviews: string[];
@ -29,6 +30,7 @@ export default function ReviewActionGroup({
onExport,
pullLatestData,
}: ReviewActionGroupProps) {
const { t } = useTranslation(["components/dialog"]);
const onClearSelected = useCallback(() => {
setSelectedReviews([]);
}, [setSelectedReviews]);
@ -68,22 +70,24 @@ export default function ReviewActionGroup({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
<AlertDialogTitle>
{t("recording.confirmDelete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
the selected review items?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
<Trans ns="components/dialog">
recording.confirmDelete.desc.selected
</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={onDelete}
>
Delete
{t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -97,14 +101,14 @@ export default function ReviewActionGroup({
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
onClick={onClearSelected}
>
Unselect
{t("button.unselect", { ns: "common" })}
</div>
</div>
<div className="flex items-center gap-1 md:gap-2">
{selectedReviews.length == 1 && (
<Button
className="flex items-center gap-2 p-2"
aria-label="Export"
aria-label={t("recording.button.export")}
size="sm"
onClick={() => {
onExport(selectedReviews[0]);
@ -112,28 +116,38 @@ export default function ReviewActionGroup({
}}
>
<FaCompactDisc className="text-secondary-foreground" />
{isDesktop && <div className="text-primary">Export</div>}
{isDesktop && (
<div className="text-primary">
{t("recording.button.export")}
</div>
)}
</Button>
)}
<Button
className="flex items-center gap-2 p-2"
aria-label="Mark as reviewed"
aria-label={t("recording.button.markAsReviewed")}
size="sm"
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
{isDesktop && <div className="text-primary">Mark as reviewed</div>}
{isDesktop && (
<div className="text-primary">
{t("recording.button.markAsReviewed")}
</div>
)}
</Button>
<Button
className="flex items-center gap-2 p-2"
aria-label="Delete"
aria-label={t("button.delete", { ns: "common" })}
size="sm"
onClick={handleDelete}
>
<HiTrash className="text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
{bypassDialog ? "Delete Now" : "Delete"}
{bypassDialog
? t("recording.button.deleteNow")
: t("button.delete", { ns: "common" })}
</div>
)}
</Button>

View File

@ -286,7 +286,7 @@ function ShowReviewFilter({
<Button
className="block duration-0 md:hidden"
aria-label="Show reviewed"
aria-label={t("review.showReviewed")}
variant={showReviewedSwitch ? "select" : "default"}
size="sm"
onClick={() =>
@ -351,7 +351,7 @@ function GeneralFilterButton({
selectedLabels?.length || selectedZones?.length ? "select" : "default"
}
className="flex items-center gap-2 capitalize"
aria-label="Filter"
aria-label={t("filter")}
>
<FaFilter
className={`${
@ -572,7 +572,7 @@ export function GeneralFilterContent({
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
aria-label="Apply"
aria-label={t("button.apply", { ns: "common" })}
variant="select"
onClick={() => {
onApply();
@ -581,7 +581,10 @@ export function GeneralFilterContent({
>
{t("button.apply", { ns: "common" })}
</Button>
<Button aria-label="Reset" onClick={onReset}>
<Button
aria-label={t("button.reset", { ns: "common" })}
onClick={onReset}
>
{t("button.reset", { ns: "common" })}
</Button>
</div>
@ -624,7 +627,7 @@ function ShowMotionOnlyButton({
<Button
size="sm"
className="duration-0"
aria-label="Show Motion Only"
aria-label={t("motion.showMotionOnly", { ns: "components/filter" })}
variant={motionOnlyButton ? "select" : "default"}
onClick={() => setMotionOnlyButton(!motionOnlyButton)}
>

View File

@ -15,6 +15,7 @@ import {
} from "../ui/alert-dialog";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { toast } from "sonner";
import { Trans, useTranslation } from "react-i18next";
type SearchActionGroupProps = {
selectedObjects: string[];
@ -26,6 +27,7 @@ export default function SearchActionGroup({
setSelectedObjects,
pullLatestData,
}: SearchActionGroupProps) {
const { t } = useTranslation(["views/filter"]);
const onClearSelected = useCallback(() => {
setSelectedObjects([]);
}, [setSelectedObjects]);
@ -37,7 +39,7 @@ export default function SearchActionGroup({
})
.then((resp) => {
if (resp.status == 200) {
toast.success("Tracked objects deleted successfully.", {
toast.success(t("trackedObjectDelete.toast.success"), {
position: "top-center",
});
setSelectedObjects([]);
@ -49,11 +51,11 @@ export default function SearchActionGroup({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Failed to delete tracked objects.: ${errorMessage}`, {
toast.error(t("trackedObjectDelete.toast.error", { errorMessage }), {
position: "top-center",
});
});
}, [selectedObjects, setSelectedObjects, pullLatestData]);
}, [selectedObjects, setSelectedObjects, pullLatestData, t]);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [bypassDialog, setBypassDialog] = useState(false);
@ -78,27 +80,27 @@ export default function SearchActionGroup({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
<AlertDialogTitle>
{t("trackedObjectDelete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Deleting these {selectedObjects.length} tracked objects removes the
snapshot, any saved embeddings, and any associated object lifecycle
entries. Recorded footage of these tracked objects in History view
will <em>NOT</em> be deleted.
<br />
<br />
Are you sure you want to proceed?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
<Trans
ns="components/filter"
values={{ objectLength: selectedObjects.length }}
>
trackedObjectDelete.desc
</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={onDelete}
>
Delete
{t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -112,20 +114,22 @@ export default function SearchActionGroup({
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
onClick={onClearSelected}
>
Unselect
{t("button.unselect", { ns: "common" })}
</div>
</div>
<div className="flex items-center gap-1 md:gap-2">
<Button
className="flex items-center gap-2 p-2"
aria-label="Delete"
aria-label={t("button.delete", { ns: "common" })}
size="sm"
onClick={handleDelete}
>
<HiTrash className="text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
{bypassDialog ? "Delete Now" : "Delete"}
{bypassDialog
? t("button.deleteNow", { ns: "common" })
: t("button.delete", { ns: "common" })}
</div>
)}
</Button>

View File

@ -253,7 +253,6 @@ function GeneralFilterButton({
return t("labels.count", {
count: selectedLabels.length,
ns: "components/filter",
});
}, [selectedLabels, t]);
@ -270,7 +269,7 @@ function GeneralFilterButton({
size="sm"
variant={selectedLabels?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize"
aria-label="Labels"
aria-label={t("labels.label")}
>
<MdLabel
className={`${selectedLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
@ -381,7 +380,7 @@ export function GeneralFilterContent({
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
aria-label="Apply"
aria-label={t("button.apply", { ns: "common" })}
variant="select"
onClick={() => {
if (selectedLabels != currentLabels) {
@ -394,7 +393,7 @@ export function GeneralFilterContent({
{t("button.apply", { ns: "common" })}
</Button>
<Button
aria-label="Reset"
aria-label={t("button.reset", { ns: "common" })}
onClick={() => {
setCurrentLabels(undefined);
updateLabelFilter(undefined);
@ -442,7 +441,7 @@ function SortTypeButton({
: "default"
}
className="flex items-center gap-2 capitalize"
aria-label="Labels"
aria-label={t("labels.label")}
>
<MdSort
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-secondary-foreground"}`}
@ -557,7 +556,7 @@ export function SortTypeContent({
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
aria-label="Apply"
aria-label={t("button.apply", { ns: "common" })}
variant="select"
onClick={() => {
if (selectedSortType != currentSortType) {
@ -570,7 +569,7 @@ export function SortTypeContent({
{t("button.apply", { ns: "common" })}
</Button>
<Button
aria-label="Reset"
aria-label={t("button.reset", { ns: "common" })}
onClick={() => {
setCurrentSortType(undefined);
updateSortType(undefined);

View File

@ -23,7 +23,7 @@ export function ZoneMaskFilterButton({
size="sm"
variant={selectedZoneMask?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize"
aria-label="Filter by zone mask"
aria-label={t("zoneMask.filterBy")}
>
<FaFilter
className={`${selectedZoneMask?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
@ -31,7 +31,7 @@ export function ZoneMaskFilterButton({
<div
className={`hidden md:block ${selectedZoneMask?.length ? "text-selected-foreground" : "text-primary"}`}
>
{t("label")}
{t("filter")}
</div>
</Button>
);

View File

@ -205,11 +205,15 @@ export function CombinedStorageGraph({
<PopoverTrigger asChild>
<button
className="focus:outline-none"
aria-label="Unused Storage Information"
aria-label={t(
"storage.cameraStorage.unusedStorageInformation",
)}
>
<CiCircleAlert
className="size-5"
aria-label="Unused Storage Information"
aria-label={t(
"storage.cameraStorage.unusedStorageInformation",
)}
/>
</button>
</PopoverTrigger>

View File

@ -71,7 +71,7 @@ export default function IconPicker({
{!selectedIcon?.name || !selectedIcon?.Icon ? (
<Button
className="mt-2 w-full text-muted-foreground"
aria-label="Select an icon"
aria-label={t("iconPicker.selectIcon")}
>
{t("iconPicker.selectIcon")}
</Button>

View File

@ -79,14 +79,17 @@ export function SaveSearchDialog({
</div>
)}
<DialogFooter>
<Button aria-label="Cancel" onClick={onClose}>
<Button
aria-label={t("button.cancel", { ns: "common" })}
onClick={onClose}
>
{t("button.cancel", { ns: "common" })}
</Button>
<Button
onClick={handleSave}
variant="select"
className="mb-2 md:mb-0"
aria-label="Save this search"
aria-label={t("search.saveSearch.button.save.label")}
>
{t("button.save", { ns: "common" })}
</Button>

View File

@ -117,7 +117,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
}
aria-label="Set Password"
aria-label={t("menu.user.setPassword")}
onClick={() => setPasswordDialogOpen(true)}
>
<LuSquarePen className="mr-2 size-4" />
@ -128,7 +128,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
}
aria-label="Log out"
aria-label={t("menu.user.logout")}
>
<a className="flex" href={logoutUrl}>
<LuLogOut className="mr-2 size-4" />

View File

@ -182,7 +182,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Set Password"
aria-label={t("menu.user.setPassword")}
onClick={() => setPasswordDialogOpen(true)}
>
<LuSquarePen className="mr-2 size-4" />
@ -195,7 +195,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Log out"
aria-label={t("menu.user.logout", { ns: "common" })}
>
<a className="flex" href={logoutUrl}>
<LuLogOut className="mr-2 size-4" />
@ -216,7 +216,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex w-full items-center p-2 text-sm"
}
aria-label="System metrics"
aria-label={t("menu.systemMetrics")}
>
<LuActivity className="mr-2 size-4" />
<span>{t("menu.systemMetrics")}</span>
@ -229,7 +229,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex w-full items-center p-2 text-sm"
}
aria-label="System logs"
aria-label={t("menu.systemLogs")}
>
<LuList className="mr-2 size-4" />
<span>{t("menu.systemLogs")}</span>
@ -252,7 +252,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex w-full items-center p-2 text-sm"
}
aria-label="Settings"
aria-label={t("menu.settings")}
>
<LuSettings className="mr-2 size-4" />
<span>{t("menu.settings")}</span>
@ -267,7 +267,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex w-full items-center p-2 text-sm"
}
aria-label="Configuration editor"
aria-label={t("menu.configurationEditor")}
>
<LuSquarePen className="mr-2 size-4" />
<span>{t("menu.configurationEditor")}</span>
@ -340,7 +340,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Use the system settings for language"
aria-label={t("menu.language.withSystem.label")}
onClick={() => setLanguage(systemLanguage)}
>
{language === systemLanguage ? (
@ -377,7 +377,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Light mode"
aria-label={t("menu.darkMode.light")}
onClick={() => setTheme("light")}
>
{theme === "light" ? (
@ -397,7 +397,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Dark mode"
aria-label={t("menu.darkMode.dark")}
onClick={() => setTheme("dark")}
>
{theme === "dark" ? (
@ -417,7 +417,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Use the system settings for light or dark mode"
aria-label={t("menu.darkMode.withSystem.label")}
onClick={() => setTheme("system")}
>
{theme === "system" ? (
@ -514,7 +514,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Restart Frigate"
aria-label={t("menu.restart")}
onClick={() => setRestartDialogOpen(true)}
>
<LuRotateCw className="mr-2 size-4" />

View File

@ -150,7 +150,7 @@ export default function SearchResultActions({
</MenuItem>
)}
<MenuItem
aria-label="Delete this tracked object"
aria-label={t("itemMenu.deleteTrackedObject.label")}
onClick={() => setDeleteDialogOpen(true)}
>
<LuTrash2 className="mr-2 size-4" />

View File

@ -5,6 +5,7 @@ import { IoMdArrowRoundBack } from "react-icons/io";
import { cn } from "@/lib/utils";
import { isPWA } from "@/utils/isPWA";
import { Button } from "@/components/ui/button";
import { t } from "i18next";
const MobilePageContext = createContext<{
open: boolean;
@ -160,7 +161,7 @@ export function MobilePageHeader({
>
<Button
className="absolute left-0 rounded-lg"
aria-label="Go back"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={handleClose}
>

View File

@ -181,7 +181,7 @@ export default function CameraInfoDialog({
<DialogFooter>
<Button
variant="select"
aria-label="Copy"
aria-label={t("button.copy", { ns: "common" })}
onClick={() => onCopyFfprobe()}
>
{t("button.copy", { ns: "common" })}

View File

@ -256,7 +256,7 @@ export default function CreateUserDialog({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
disabled={isLoading}
onClick={handleCancel}
type="button"
@ -265,7 +265,7 @@ export default function CreateUserDialog({
</Button>
<Button
variant="select"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
disabled={isLoading || !form.formState.isValid}
className="flex flex-1"
type="submit"

View File

@ -44,7 +44,7 @@ export default function DeleteUserDialog({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
type="button"
>
@ -52,7 +52,7 @@ export default function DeleteUserDialog({
</Button>
<Button
variant="destructive"
aria-label="Delete"
aria-label={t("button.delete", { ns: "common" })}
className="flex flex-1"
onClick={onDelete}
>

View File

@ -151,7 +151,7 @@ export default function ExportDialog({
<Trigger asChild>
<Button
className="flex items-center gap-2"
aria-label="Export"
aria-label={t("menu.export", { ns: "common" })}
size="sm"
onClick={() => {
const now = new Date(latestTime * 1000);
@ -327,7 +327,7 @@ export function ExportContent({
</div>
<Button
className={isDesktop ? "" : "w-full"}
aria-label="Select or export"
aria-label={t("export.selectOrExport")}
variant="select"
size="sm"
onClick={() => {
@ -444,7 +444,7 @@ function CustomTimeSelector({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label="Start time"
aria-label={t("export.time.start")}
variant={startOpen ? "select" : "default"}
size="sm"
onClick={() => {
@ -510,7 +510,7 @@ function CustomTimeSelector({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label="End time"
aria-label={t("export.time.end")}
variant={endOpen ? "select" : "default"}
size="sm"
onClick={() => {

View File

@ -11,6 +11,7 @@ import { GpuInfo, Nvinfo, Vainfo } from "@/types/stats";
import { Button } from "../ui/button";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
type GPUInfoDialogProps = {
showGpuInfo: boolean;
@ -22,6 +23,8 @@ export default function GPUInfoDialog({
gpuType,
setShowGpuInfo,
}: GPUInfoDialogProps) {
const { t } = useTranslation(["views/system"]);
const { data: vainfo } = useSWR<Vainfo>(
showGpuInfo && gpuType == "vainfo" ? "vainfo" : null,
);
@ -43,13 +46,23 @@ export default function GPUInfoDialog({
<Dialog open={showGpuInfo} onOpenChange={setShowGpuInfo}>
<DialogContent>
<DialogHeader>
<DialogTitle>Vainfo Output</DialogTitle>
<DialogTitle>
{t("general.hardwareInfo.gpuInfo.vainfoOutput.title")}
</DialogTitle>
</DialogHeader>
{vainfo ? (
<div className="scrollbar-container mb-2 max-h-96 overflow-y-scroll whitespace-pre-line">
<div>Return Code: {vainfo.return_code}</div>
<div>
{t("general.hardwareInfo.gpuInfo.vainfoOutput.returnCode", {
code: vainfo.return_code,
})}
</div>
<br />
<div>Process {vainfo.return_code == 0 ? "Output" : "Error"}:</div>
<div>
{vainfo.return_code == 0
? t("general.hardwareInfo.gpuInfo.vainfoOutput.processOutput")
: t("general.hardwareInfo.gpuInfo.vainfoOutput.processError")}
</div>
<br />
<div>
{vainfo.return_code == 0 ? vainfo.stdout : vainfo.stderr}
@ -60,17 +73,17 @@ export default function GPUInfoDialog({
)}
<DialogFooter>
<Button
aria-label="Close GPU info"
aria-label={t("general.hardwareInfo.gpuInfo.closeInfo.label")}
onClick={() => setShowGpuInfo(false)}
>
Close
{t("button.close", { ns: "common" })}
</Button>
<Button
aria-label="Copy GPU info"
aria-label={t("general.hardwareInfo.gpuInfo.copyInfo.label")}
variant="select"
onClick={() => onCopyInfo()}
>
Copy
{t("button.copy", { ns: "common" })}
</Button>
</DialogFooter>
</DialogContent>
@ -81,34 +94,52 @@ export default function GPUInfoDialog({
<Dialog open={showGpuInfo} onOpenChange={setShowGpuInfo}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nvidia SMI Output</DialogTitle>
<DialogTitle>
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.title")}
</DialogTitle>
</DialogHeader>
{nvinfo ? (
<div className="scrollbar-container mb-2 max-h-96 overflow-y-scroll whitespace-pre-line">
<div>Name: {nvinfo["0"].name}</div>
<div>
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
name: nvinfo["0"].name,
})}
</div>
<br />
<div>Driver: {nvinfo["0"].driver}</div>
<div>
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
name: nvinfo["0"].driver,
})}
</div>
<br />
<div>Cuda Compute Capability: {nvinfo["0"].cuda_compute}</div>
<div>
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
name: nvinfo["0"].cuda_compute,
})}
</div>
<br />
<div>VBios Info: {nvinfo["0"].vbios}</div>
<div>
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
name: nvinfo["0"].vbios,
})}
</div>
</div>
) : (
<ActivityIndicator />
)}
<DialogFooter>
<Button
aria-label="Close GPU info"
aria-label={t("general.hardwareInfo.gpuInfo.closeInfo.label")}
onClick={() => setShowGpuInfo(false)}
>
Close
{t("button.close", { ns: "common" })}
</Button>
<Button
aria-label="Copy GPU info"
aria-label={t("general.hardwareInfo.gpuInfo.copyInfo.label")}
variant="select"
onClick={() => onCopyInfo()}
>
Copy
{t("button.copy", { ns: "common" })}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -3,6 +3,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Button } from "../ui/button";
import { FaVideo } from "react-icons/fa";
import { isMobile } from "react-device-detect";
import { t } from "i18next";
type MobileCameraDrawerProps = {
allCameras: string[];
@ -25,7 +26,7 @@ export default function MobileCameraDrawer({
<DrawerTrigger asChild>
<Button
className="rounded-lg capitalize"
aria-label="Cameras"
aria-label={t("menu.live.cameras")}
size="sm"
>
<FaVideo className="text-secondary-foreground" />

View File

@ -149,7 +149,7 @@ export default function MobileReviewSettingsDrawer({
{features.includes("export") && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label="Export"
aria-label={t("export")}
onClick={() => {
setDrawerMode("export");
setMode("select");
@ -162,7 +162,7 @@ export default function MobileReviewSettingsDrawer({
{features.includes("calendar") && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label="Calendar"
aria-label={t("calendar")}
variant={filter?.after ? "select" : "default"}
onClick={() => setDrawerMode("calendar")}
>
@ -175,7 +175,7 @@ export default function MobileReviewSettingsDrawer({
{features.includes("filter") && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label="Filter"
aria-label={t("filter")}
variant={filter?.labels || filter?.zones ? "select" : "default"}
onClick={() => setDrawerMode("filter")}
>
@ -247,7 +247,7 @@ export default function MobileReviewSettingsDrawer({
<SelectSeparator />
<div className="flex items-center justify-center p-2">
<Button
aria-label="Reset"
aria-label={t("button.reset", { ns: "common" })}
onClick={() => {
onUpdateFilter({
...filter,
@ -272,7 +272,7 @@ export default function MobileReviewSettingsDrawer({
{t("button.back", { ns: "common" })}
</div>
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
Filter
{t("filter")}
</div>
</div>
<GeneralFilterContent
@ -326,7 +326,7 @@ export default function MobileReviewSettingsDrawer({
<DrawerTrigger asChild>
<Button
className="rounded-lg capitalize"
aria-label="Filters"
aria-label={t("filters")}
variant={
filter?.labels || filter?.after || filter?.zones
? "select"

View File

@ -86,7 +86,7 @@ export default function RoleChangeDialog({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
type="button"
>
@ -94,7 +94,7 @@ export default function RoleChangeDialog({
</Button>
<Button
variant="select"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
className="flex flex-1"
onClick={() => onSave(selectedRole)}
disabled={selectedRole === currentRole}

View File

@ -30,7 +30,7 @@ export default function SaveExportOverlay({
>
<Button
className="flex items-center gap-1 text-primary"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
size="sm"
onClick={onCancel}
>
@ -39,7 +39,7 @@ export default function SaveExportOverlay({
</Button>
<Button
className="flex items-center gap-1"
aria-label="Preview export"
aria-label={t("export.fromTimeline.previewExport")}
size="sm"
onClick={onPreview}
>
@ -48,13 +48,13 @@ export default function SaveExportOverlay({
</Button>
<Button
className="flex items-center gap-1"
aria-label="Save export"
aria-label={t("export.fromTimeline.saveExport")}
variant="select"
size="sm"
onClick={onSave}
>
<FaCompactDisc />
{t("export.fromTimeline.saveExport", { ns: "components/dialog" })}
{t("export.fromTimeline.saveExport")}
</Button>
</div>
</div>

View File

@ -201,7 +201,7 @@ export default function SetPasswordDialog({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
type="button"
>
@ -209,7 +209,7 @@ export default function SetPasswordDialog({
</Button>
<Button
variant="select"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
className="flex flex-1"
onClick={handleSave}
disabled={!password || password !== confirmPassword}

View File

@ -26,6 +26,7 @@ import { Button } from "@/components/ui/button";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Trans, useTranslation } from "react-i18next";
type AnnotationSettingsPaneProps = {
event: Event;
@ -41,6 +42,8 @@ export function AnnotationSettingsPane({
annotationOffset,
setAnnotationOffset,
}: AnnotationSettingsPaneProps) {
const { t } = useTranslation(["views/explore"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@ -81,9 +84,15 @@ export function AnnotationSettingsPane({
);
updateConfig();
} else {
toast.error(`Failed to save config changes: ${res.statusText}`, {
position: "top-center",
});
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -91,7 +100,7 @@ export function AnnotationSettingsPane({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Failed to save config changes: ${errorMessage}`, {
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
})
@ -99,7 +108,7 @@ export function AnnotationSettingsPane({
setIsLoading(false);
});
},
[updateConfig, config, event],
[updateConfig, config, event, t],
);
function onSubmit(values: z.infer<typeof formSchema>) {
@ -126,7 +135,7 @@ export function AnnotationSettingsPane({
return (
<div className="mb-3 space-y-3 rounded-lg border border-secondary-foreground bg-background_alt p-2">
<Heading as="h4" className="my-2">
Annotation Settings
{t("objectLifecycle.annotationSettings.title")}
</Heading>
<div className="flex flex-col">
<div className="flex flex-row items-center justify-start gap-2 p-3">
@ -136,11 +145,11 @@ export function AnnotationSettingsPane({
onCheckedChange={setShowZones}
/>
<Label className="cursor-pointer" htmlFor="show-zones">
Show All Zones
{t("objectLifecycle.annotationSettings.showAllZones")}
</Label>
</div>
<div className="text-sm text-muted-foreground">
Always show zones on frames where objects have entered a zone.
{t("objectLifecycle.annotationSettings.showAllZones.desc")}
</div>
</div>
<Separator className="my-2 flex bg-secondary" />
@ -154,17 +163,16 @@ export function AnnotationSettingsPane({
name="annotationOffset"
render={({ field }) => (
<FormItem>
<FormLabel>Annotation Offset</FormLabel>
<FormLabel>
{t("objectLifecycle.annotationSettings.offset.label")}
</FormLabel>
<div className="flex flex-col gap-3 md:flex-row-reverse md:gap-8">
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-0 md:my-5">
<PiWarningCircle className="size-24" />
<div>
This data comes from your camera's detect feed but is
overlayed on images from the the record feed. It is
unlikely that the two streams are perfectly in sync. As a
result, the bounding box and the footage will not line up
perfectly. However, the <code>annotation_offset</code>{" "}
field can be used to adjust this.
<Trans ns="views/explore">
objectLifecycle.annotationSettings.offset.desc
</Trans>
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/reference"
@ -172,7 +180,9 @@ export function AnnotationSettingsPane({
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
{t(
"objectLifecycle.annotationSettings.offset.documentation",
)}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
@ -187,16 +197,11 @@ export function AnnotationSettingsPane({
/>
</FormControl>
<FormDescription>
Milliseconds to offset detect annotations by.{" "}
<em>Default: 0</em>
{t(
"objectLifecycle.annotationSettings.offset.millisecondsToOffset",
)}
<div className="mt-2">
TIP: Imagine there is an event clip with a person
walking from left to right. If the event timeline
bounding box is consistently to the left of the person
then the value should be decreased. Similarly, if a
person is walking from left to right and the bounding
box is consistently ahead of the person then the value
should be increased.
{t("objectLifecycle.annotationSettings.offset.tips")}
</div>
</FormDescription>
</div>
@ -210,14 +215,14 @@ export function AnnotationSettingsPane({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Apply"
aria-label={t("button.apply", { ns: "common" })}
onClick={form.handleSubmit(onApply)}
>
Apply
{t("button.apply", { ns: "common" })}
</Button>
<Button
variant="select"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
disabled={isLoading}
className="flex flex-1"
type="submit"
@ -225,10 +230,10 @@ export function AnnotationSettingsPane({
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>Saving...</span>
<span>{t("button.saving", { ns: "common" })}</span>
</div>
) : (
"Save"
t("button.save", { ns: "common" })
)}
</Button>
</div>

View File

@ -54,6 +54,7 @@ import { useNavigate } from "react-router-dom";
import { ObjectPath } from "./ObjectPath";
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
import { IoPlayCircleOutline } from "react-icons/io5";
import { useTranslation } from "react-i18next";
type ObjectLifecycleProps = {
className?: string;
@ -68,6 +69,8 @@ export default function ObjectLifecycle({
fullscreen = false,
setPane,
}: ObjectLifecycleProps) {
const { t } = useTranslation(["views/explore"]);
const { data: eventSequence } = useSWR<ObjectLifecycleSequence[]>([
"timeline",
{
@ -334,12 +337,16 @@ export default function ObjectLifecycle({
<div className={cn("flex items-center gap-2")}>
<Button
className="mb-2 mt-3 flex items-center gap-2.5 rounded-lg md:mt-0"
aria-label="Go back"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={() => setPane("overview")}
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">Back</div>}
{isDesktop && (
<div className="text-primary">
{t("button.back", { ns: "common" })}
</div>
)}
</Button>
</div>
)}
@ -361,7 +368,7 @@ export default function ObjectLifecycle({
<div className="relative aspect-video">
<div className="flex flex-col items-center justify-center p-20 text-center">
<LuFolderX className="size-16" />
No image found for this timestamp.
{t("objectLifecycle.noImageFound")}
</div>
</div>
)}
@ -468,7 +475,9 @@ export default function ObjectLifecycle({
)
}
>
<div className="text-primary">Create Object Mask</div>
<div className="text-primary">
{t("objectLifecycle.createObjectMask")}
</div>
</div>
</ContextMenuItem>
</ContextMenuContent>
@ -477,7 +486,7 @@ export default function ObjectLifecycle({
</div>
<div className="mt-3 flex flex-row items-center justify-between">
<Heading as="h4">Object Lifecycle</Heading>
<Heading as="h4">{t("objectLifecycle.title")}</Heading>
<div className="flex flex-row gap-2">
<Tooltip>
@ -485,7 +494,7 @@ export default function ObjectLifecycle({
<Button
variant={showControls ? "select" : "default"}
className="size-7 p-1.5"
aria-label="Adjust annotation settings"
aria-label={t("objectLifecycle.adjustAnnotationSettings")}
>
<LuSettings
className="size-5"
@ -494,14 +503,16 @@ export default function ObjectLifecycle({
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>Adjust annotation settings</TooltipContent>
<TooltipContent>
{t("objectLifecycle.adjustAnnotationSettings")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="mb-2 text-sm text-muted-foreground">
Scroll to view the significant moments of this object's lifecycle.
{t("objectLifecycle.scrollViewTips")}
</div>
<div className="min-w-20 text-right text-sm text-muted-foreground">
{current + 1} of {eventSequence.length}
@ -509,7 +520,7 @@ export default function ObjectLifecycle({
</div>
{config?.cameras[event.camera]?.onvif.autotracking.enabled_in_config && (
<div className="-mt-2 mb-2 text-sm text-danger">
Bounding box positions will be inaccurate for autotracking cameras.
{t("objectLifecycle.autoTrackingTips")}
</div>
)}
{showControls && (
@ -559,8 +570,8 @@ export default function ObjectLifecycle({
timezone: config.ui.timezone,
strftime_fmt:
config.ui.time_format == "24hour"
? "%d %b %H:%M:%S"
: "%m/%d %I:%M:%S%P",
? t("time.formattedTimestamp2.24hour")
: t("time.formattedTimestamp2"),
time_style: "medium",
date_style: "medium",
})}

View File

@ -193,7 +193,7 @@ export default function ReviewDetailDialog({
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label="Share this review item"
aria-label={t("details.item.button.share")}
size="sm"
onClick={() =>
shareOrCopy(`${baseUrl}review?id=${review.id}`)

View File

@ -677,7 +677,7 @@ function ObjectDetailsTab({
<div className="flex items-start">
<Button
className="rounded-r-none border-r-0"
aria-label="Regenerate tracked object description"
aria-label={t("details.button.regenerate.label")}
onClick={() => regenerateDescription("thumbnails")}
>
{t("details.button.regenerate")}
@ -687,7 +687,7 @@ function ObjectDetailsTab({
<DropdownMenuTrigger asChild>
<Button
className="rounded-l-none border-l-0 px-2"
aria-label="Expand regeneration menu"
aria-label={t("details.expandRegenerationMenu")}
>
<FaChevronDown className="size-3" />
</Button>
@ -695,14 +695,14 @@ function ObjectDetailsTab({
<DropdownMenuContent>
<DropdownMenuItem
className="cursor-pointer"
aria-label="Regenerate from snapshot"
aria-label={t("details.regenerateFromSnapshot")}
onClick={() => regenerateDescription("snapshot")}
>
{t("details.regenerateFromSnapshot")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
aria-label="Regenerate from thumbnails"
aria-label={t("details.regenerateFromThumbnails")}
onClick={() => regenerateDescription("thumbnails")}
>
{t("details.regenerateFromThumbnails")}
@ -716,7 +716,7 @@ function ObjectDetailsTab({
!config?.cameras[search.camera].genai.enabled) && (
<Button
variant="select"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
onClick={updateDescription}
>
{t("button.save", { ns: "common" })}
@ -867,7 +867,7 @@ export function ObjectSnapshotTab({
<>
<Button
className="bg-success"
aria-label="Confirm this label for Frigate Plus"
aria-label={t("explore.plus.review.true.label")}
onClick={() => {
setState("uploading");
onSubmitToPlus(false);
@ -883,7 +883,7 @@ export function ObjectSnapshotTab({
</Button>
<Button
className="text-white"
aria-label="Do not confirm this label for Frigate Plus"
aria-label={t("explore.plus.review.false.label")}
variant="destructive"
onClick={() => {
setState("uploading");

View File

@ -116,7 +116,7 @@ export default function RestartDialog({
<Button
size="lg"
className="mt-5"
aria-label="Force reload now"
aria-label={t("restart.restarting.button")}
onClick={handleForceReload}
>
{t("restart.restarting.button")}

View File

@ -85,7 +85,7 @@ export default function SearchFilterDialog({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label="More Filters"
aria-label={t("more")}
size="sm"
variant={moreFiltersSelected ? "select" : "default"}
>
@ -167,7 +167,7 @@ export default function SearchFilterDialog({
<div className="flex items-center justify-evenly p-2">
<Button
variant="select"
aria-label="Apply"
aria-label={t("button.apply", { ns: "common" })}
onClick={() => {
if (currentFilter != filter) {
onUpdateFilter(currentFilter);
@ -179,7 +179,7 @@ export default function SearchFilterDialog({
{t("button.apply", { ns: "common" })}
</Button>
<Button
aria-label="Reset filters to default values"
aria-label={t("reset.label")}
onClick={() => {
setCurrentFilter((prevFilter) => ({
...prevFilter,
@ -287,7 +287,9 @@ function TimeRangeFilterContent({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"} `}
aria-label="Select Start Time"
aria-label={t("export.time.start.label", {
ns: "components/dialog",
})}
variant={startOpen ? "select" : "default"}
size="sm"
onClick={() => {
@ -325,7 +327,9 @@ function TimeRangeFilterContent({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label="Select End Time"
aria-label={t("export.time.end.label", {
ns: "components/dialog",
})}
variant={endOpen ? "select" : "default"}
size="sm"
onClick={() => {
@ -688,14 +692,14 @@ export function SnapshotClipFilterContent({
>
<ToggleGroupItem
value="yes"
aria-label="Yes"
aria-label={t("button.yes", { ns: "common" })}
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
{t("button.yes", { ns: "common" })}
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
aria-label={t("button.no", { ns: "common" })}
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
{t("button.no", { ns: "common" })}
@ -766,14 +770,14 @@ export function SnapshotClipFilterContent({
>
<ToggleGroupItem
value="yes"
aria-label="Yes"
aria-label={t("button.yes", { ns: "common" })}
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
{t("button.yes", { ns: "common" })}
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
aria-label={t("button.no", { ns: "common" })}
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
{t("button.no", { ns: "common" })}
@ -821,14 +825,14 @@ export function SnapshotClipFilterContent({
>
<ToggleGroupItem
value="yes"
aria-label="Yes"
aria-label={t("button.yes", { ns: "common" })}
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
{t("button.yes", { ns: "common" })}
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
aria-label={t("button.no", { ns: "common" })}
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
{t("button.no", { ns: "common" })}

View File

@ -33,6 +33,7 @@ import {
} from "../ui/alert-dialog";
import { cn } from "@/lib/utils";
import { FaCompress, FaExpand } from "react-icons/fa";
import { useTranslation } from "react-i18next";
type VideoControls = {
volume?: boolean;
@ -309,6 +310,8 @@ function FrigatePlusUploadButton({
onUploadFrame,
containerRef,
}: FrigatePlusUploadButtonProps) {
const { t } = useTranslation(["components/player"]);
const [videoImg, setVideoImg] = useState<string>();
return (
@ -346,14 +349,16 @@ function FrigatePlusUploadButton({
className="md:max-w-2xl lg:max-w-3xl xl:max-w-4xl"
>
<AlertDialogHeader>
<AlertDialogTitle>Submit this frame to Frigate+?</AlertDialogTitle>
<AlertDialogTitle>{t("submitFrigatePlus.title")}</AlertDialogTitle>
</AlertDialogHeader>
<img className="aspect-video w-full object-contain" src={videoImg} />
<AlertDialogFooter>
<AlertDialogAction className="bg-selected" onClick={onUploadFrame}>
Submit
{t("submitFrigatePlus.submit")}
</AlertDialogAction>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@ -358,14 +358,14 @@ export function CameraStreamingDialog({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={handleCancel}
>
{t("button.cancel", { ns: "common" })}
</Button>
<Button
variant="select"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
disabled={isLoading}
className="flex flex-1"
onClick={handleSave}

View File

@ -176,9 +176,15 @@ export default function MotionMaskEditPane({
);
updateConfig();
} else {
toast.error(`Failed to save config changes: ${res.statusText}`, {
position: "top-center",
});
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -186,7 +192,7 @@ export default function MotionMaskEditPane({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Failed to save config changes: ${errorMessage}`, {
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
})
@ -323,14 +329,14 @@ export default function MotionMaskEditPane({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
>
{t("button.cancel", { ns: "common" })}
</Button>
<Button
variant="select"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
disabled={isLoading}
className="flex flex-1"
type="submit"

View File

@ -364,7 +364,7 @@ export default function ObjectMaskEditPane({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
>
{t("button.cancel", { ns: "common" })}
@ -373,7 +373,7 @@ export default function ObjectMaskEditPane({
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
type="submit"
>
{isLoading ? (

View File

@ -4,6 +4,7 @@ import { MdOutlineRestartAlt, MdUndo } from "react-icons/md";
import { Button } from "../ui/button";
import { TbPolygon, TbPolygonOff } from "react-icons/tb";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
type PolygonEditControlsProps = {
polygons: Polygon[];
@ -20,6 +21,7 @@ export default function PolygonEditControls({
snapPoints,
setSnapPoints,
}: PolygonEditControlsProps) {
const { t } = useTranslation(["views/settings"]);
const undo = () => {
if (activePolygonIndex === undefined || !polygons) {
return;
@ -80,35 +82,37 @@ export default function PolygonEditControls({
<Button
variant="default"
className="size-6 rounded-md p-1"
aria-label="Remove last point"
aria-label={t("masksAndZones.form.polygonDrawing.removeLastPoint")}
disabled={!polygons[activePolygonIndex].points.length}
onClick={undo}
>
<MdUndo className="text-secondary-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent>Remove last point</TooltipContent>
<TooltipContent>
{t("masksAndZones.form.polygonDrawing.removeLastPoint")}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
className="size-6 rounded-md p-1"
aria-label="Clear all points"
aria-label={t("masksAndZones.form.polygonDrawing.reset.label")}
disabled={!polygons[activePolygonIndex].points.length}
onClick={reset}
>
<MdOutlineRestartAlt className="text-secondary-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent>Reset</TooltipContent>
<TooltipContent>{t("button.reset", { ns: "common" })}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={snapPoints ? "select" : "default"}
className={cn("size-6 rounded-md p-1")}
aria-label="Snap points"
aria-label={t("masksAndZones.form.polygonDrawing.snapPoints.true")}
onClick={() => setSnapPoints((prev) => !prev)}
>
{snapPoints ? (
@ -119,7 +123,9 @@ export default function PolygonEditControls({
</Button>
</TooltipTrigger>
<TooltipContent>
{snapPoints ? "Don't snap points" : "Snap points"}
{snapPoints
? t("masksAndZones.form.polygonDrawing.snapPoints.false")
: t("masksAndZones.form.polygonDrawing.snapPoints.true")}
</TooltipContent>
</Tooltip>
</div>

View File

@ -36,7 +36,7 @@ import { reviewQueries } from "@/utils/zoneEdutUtil";
import IconWrapper from "../ui/icon-wrapper";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { buttonVariants } from "../ui/button";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
type PolygonItemProps = {
polygon: Polygon;
@ -177,14 +177,25 @@ export default function PolygonItem({
.put(`config/set?${url}`, { requires_restart: 0 })
.then((res) => {
if (res.status === 200) {
toast.success(`${polygon?.name} has been deleted.`, {
position: "top-center",
});
toast.success(
t("masksAndZones.form.polygonDrawing.delete.success", {
name: polygon?.name,
}),
{
position: "top-center",
},
);
updateConfig();
} else {
toast.error(`Failed to save config changes: ${res.statusText}`, {
position: "top-center",
});
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -192,7 +203,7 @@ export default function PolygonItem({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Failed to save config changes: ${errorMessage}`, {
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
})
@ -200,7 +211,7 @@ export default function PolygonItem({
setIsLoading(false);
});
},
[updateConfig, cameraConfig],
[updateConfig, cameraConfig, t],
);
const handleDelete = () => {
@ -255,19 +266,30 @@ export default function PolygonItem({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
<AlertDialogTitle>
{t("masksAndZones.form.polygonDrawing.delete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete the{" "}
{polygon.type.replace("_", " ")} <em>{polygon.name}</em>?
<Trans
ns="views/settings"
values={{
type: polygon.type.replace("_", " "),
name: polygon.name,
}}
>
masksAndZones.form.polygonDrawing.delete.desc
</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={handleDelete}
>
Delete
{t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -281,26 +303,26 @@ export default function PolygonItem({
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
aria-label="Edit"
aria-label={t("button.edit", { ns: "common" })}
onClick={() => {
setActivePolygonIndex(index);
setEditPane(polygon.type);
}}
>
Edit
{t("button.edit", { ns: "common" })}
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Copy"
aria-label={t("button.copy", { ns: "common" })}
onClick={() => handleCopyCoordinates(index)}
>
Copy
{t("button.copy", { ns: "common" })}
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Delete"
aria-label={t("button.delete", { ns: "common" })}
disabled={isLoading}
onClick={() => setDeleteDialogOpen(true)}
>
Delete
{t("button.delete", { ns: "common" })}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -48,7 +48,7 @@ export default function ExploreSettings({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label="Explore Settings"
aria-label={t("explore.settings.title")}
size="sm"
>
<FaCog className="text-secondary-foreground" />

View File

@ -813,7 +813,7 @@ export default function ZoneEditPane({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
>
{t("button.cancel", { ns: "common" })}
@ -822,7 +822,7 @@ export default function ZoneEditPane({
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
type="submit"
>
{isLoading ? (

View File

@ -420,7 +420,7 @@ export function DateRangePicker({
<div className="mx-auto flex w-64 items-center justify-evenly gap-2 py-2">
<Button
variant="select"
aria-label="Apply"
aria-label={t("button.apply", { ns: "common" })}
onClick={() => {
setIsOpen(false);
if (
@ -440,7 +440,7 @@ export function DateRangePicker({
onReset?.();
}}
variant="ghost"
aria-label="Reset"
aria-label={t("button.reset", { ns: "common" })}
>
{t("button.reset", { ns: "common"})}
</Button>

View File

@ -6,6 +6,7 @@ import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
@ -196,6 +197,7 @@ const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { t } = useTranslation(["views/explore"]);
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
@ -210,13 +212,13 @@ const CarouselPrevious = React.forwardRef<
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
aria-label="Previous slide"
aria-label={t("objectLifecycle.carousel.previous")}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
<span className="sr-only">{t("objectLifecycle.carousel.previous")}</span>
</Button>
);
});
@ -226,6 +228,7 @@ const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { t } = useTranslation(["views/explore"]);
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
@ -240,13 +243,13 @@ const CarouselNext = React.forwardRef<
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
aria-label="Next slide"
aria-label={t("objectLifecycle.carousel.next")}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
<span className="sr-only">{t("objectLifecycle.carousel.next")}</span>
</Button>
);
});

View File

@ -3,11 +3,12 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { t } from "i18next"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
aria-label={t("pagination.label")}
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
@ -64,13 +65,13 @@ const PaginationPrevious = ({
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
aria-label={t("pagination.previous.label")}
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
<span>{t("pagination.previous")}</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
@ -80,12 +81,12 @@ const PaginationNext = ({
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
aria-label={t("pagination.next.label")}
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<span>{t("pagination.next")}</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
@ -101,7 +102,7 @@ const PaginationEllipsis = ({
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
<span className="sr-only">{t("pagination.more")}</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"

View File

@ -198,7 +198,7 @@ function ConfigEditor() {
<Button
size="sm"
className="flex items-center gap-2"
aria-label="Copy config"
aria-label={t("copyConfig")}
onClick={() => handleCopyConfig()}
>
<LuCopy className="text-secondary-foreground" />
@ -207,7 +207,7 @@ function ConfigEditor() {
<Button
size="sm"
className="flex items-center gap-2"
aria-label="Save and restart"
aria-label={t("saveAndRestart")}
onClick={() => setRestartDialogOpen(true)}
>
<div className="relative size-5">
@ -219,7 +219,7 @@ function ConfigEditor() {
<Button
size="sm"
className="flex items-center gap-2"
aria-label="Save only without restarting"
aria-label={t("saveOnly")}
onClick={() => onHandleSaveConfig("saveonly")}
>
<LuSave className="text-secondary-foreground" />

View File

@ -34,8 +34,10 @@ import { debounce } from "lodash";
import { isIOS, isMobile } from "react-device-detect";
import { isPWA } from "@/utils/isPWA";
import { isInIframe } from "@/utils/isIFrame";
import { useTranslation } from "react-i18next";
function Logs() {
const { t } = useTranslation(["views/system"]);
const [logService, setLogService] = useState<LogType>("frigate");
const tabsRef = useRef<HTMLDivElement | null>(null);
const lazyLogWrapperRef = useRef<HTMLDivElement>(null);
@ -285,13 +287,13 @@ function Logs() {
fetchInitialLogs()
.then(() => {
copy(logs.join("\n"));
toast.success("Copied logs to clipboard");
toast.success(t("logs.copy.success"));
})
.catch(() => {
toast.error("Could not copy logs to clipboard");
toast.error(t("logs.copy.error"));
});
}
}, [logs, fetchInitialLogs]);
}, [logs, fetchInitialLogs, t]);
const handleDownloadLogs = useCallback(() => {
axios
@ -496,23 +498,25 @@ function Logs() {
<div className="flex items-center gap-2">
<Button
className="flex items-center justify-between gap-2"
aria-label="Copy logs to clipboard"
aria-label={t("logs.copy.label")}
size="sm"
onClick={handleCopyLogs}
>
<FaCopy className="text-secondary-foreground" />
<div className="hidden text-primary md:block">
Copy to Clipboard
{t("logs.copy.label")}
</div>
</Button>
<Button
className="flex items-center justify-between gap-2"
aria-label="Download logs"
aria-label={t("logs.download.label")}
size="sm"
onClick={handleDownloadLogs}
>
<FaDownload className="text-secondary-foreground" />
<div className="hidden text-primary md:block">Download</div>
<div className="hidden text-primary md:block">
{t("button.download", { ns: "common" })}
</div>
</Button>
<LogSettingsButton
selectedLabels={filterSeverity}
@ -527,8 +531,10 @@ function Logs() {
<div className="grid grid-cols-5 *:px-0 *:py-3 *:text-sm *:text-primary/40 md:grid-cols-12">
<div className="col-span-3 lg:col-span-2">
<div className="flex w-full flex-row items-center">
<div className="ml-1 min-w-16 capitalize lg:min-w-20">Type</div>
<div className="mr-3">Timestamp</div>
<div className="ml-1 min-w-16 capitalize lg:min-w-20">
{t("logs.type.label")}
</div>
<div className="mr-3">{t("logs.type.timestamp")}</div>
</div>
</div>
<div
@ -537,7 +543,7 @@ function Logs() {
logService == "frigate" ? "col-span-2" : "col-span-1",
)}
>
Tag
{t("logs.type.tag")}
</div>
<div
className={cn(
@ -547,7 +553,7 @@ function Logs() {
: "md:col-span-8 lg:col-span-9",
)}
>
<div className="flex flex-1">Message</div>
<div className="flex flex-1">{t("logs.type.message")}</div>
</div>
</div>
@ -566,9 +572,7 @@ function Logs() {
<TooltipTrigger>
<MdCircle className="mr-2 size-2 animate-pulse cursor-default text-selected shadow-selected drop-shadow-md" />
</TooltipTrigger>
<TooltipContent>
Logs are streaming from the server
</TooltipContent>
<TooltipContent>{t("logs.tips")}</TooltipContent>
</Tooltip>
</div>
)}

View File

@ -1,8 +1,10 @@
import { ObjectLifecycleSequence } from "@/types/timeline";
import { t } from "i18next";
export function getLifecycleItemDescription(
lifecycleItem: ObjectLifecycleSequence,
) {
// can't use useTranslation here
const label = (
(Array.isArray(lifecycleItem.data.sub_label)
? lifecycleItem.data.sub_label[0]
@ -11,37 +13,63 @@ export function getLifecycleItemDescription(
switch (lifecycleItem.class_type) {
case "visible":
return `${label} detected`;
return t("objectLifecycle.lifecycleItemDesc.visible", {
label,
ns: "views/explore",
});
case "entered_zone":
return `${label} entered ${lifecycleItem.data.zones
.join(" and ")
.replaceAll("_", " ")}`;
return t("objectLifecycle.lifecycleItemDesc.entered_zone", {
label,
ns: "views/explore",
zones: lifecycleItem.data.zones.join(" and ").replaceAll("_", " "),
});
case "active":
return `${label} became active`;
return t("objectLifecycle.lifecycleItemDesc.active", {
label,
ns: "views/explore",
});
case "stationary":
return `${label} became stationary`;
return t("objectLifecycle.lifecycleItemDesc.stationary", {
label,
ns: "views/explore",
});
case "attribute": {
let title = "";
if (
lifecycleItem.data.attribute == "face" ||
lifecycleItem.data.attribute == "license_plate"
) {
title = `${lifecycleItem.data.attribute.replaceAll(
"_",
" ",
)} detected for ${label}`;
title = t(
"objectLifecycle.lifecycleItemDesc.attribute.faceOrLicense_plate",
{
label,
attribute: lifecycleItem.data.attribute.replaceAll("_", " "),
ns: "views/explore",
},
);
} else {
title = `${
lifecycleItem.data.label
} recognized as ${lifecycleItem.data.attribute.replaceAll("_", " ")}`;
title = t("objectLifecycle.lifecycleItemDesc.attribute.other", {
label: lifecycleItem.data.label,
attribute: lifecycleItem.data.attribute.replaceAll("_", " "),
ns: "views/explore",
});
}
return title;
}
case "gone":
return `${label} left`;
return t("objectLifecycle.lifecycleItemDesc.gone", {
label,
ns: "views/explore",
});
case "heard":
return `${label} heard`;
return t("objectLifecycle.lifecycleItemDesc.heard", {
label,
ns: "views/explore",
});
case "external":
return `${label} detected`;
return t("objectLifecycle.lifecycleItemDesc.external", {
label,
ns: "views/explore",
});
}
}

View File

@ -277,7 +277,7 @@ export default function EventView({
<ToggleGroupItem
className={cn(severityToggle != "alert" && "text-muted-foreground")}
value="alert"
aria-label="Select alerts"
aria-label={t("alerts")}
>
{isMobileOnly ? (
<div
@ -311,7 +311,7 @@ export default function EventView({
severityToggle != "detection" && "text-muted-foreground",
)}
value="detection"
aria-label="Select detections"
aria-label={t("detections")}
>
{isMobileOnly ? (
<div
@ -348,7 +348,7 @@ export default function EventView({
severityToggle != "significant_motion" && "text-muted-foreground",
)}
value="significant_motion"
aria-label="Select motion"
aria-label={t("motion.label")}
>
{isMobileOnly ? (
<GiSoundWaves className="size-6 rotate-90 text-severity_significant_motion" />
@ -792,14 +792,14 @@ function DetectionReview({
<div className="col-span-full flex items-center justify-center">
<Button
className="text-white"
aria-label="Mark these items as reviewed"
aria-label={t("markTheseItemsAsReviewed")}
variant="select"
onClick={() => {
setSelectedReviews([]);
markAllItemsAsReviewed(currentItems ?? []);
}}
>
Mark these items as reviewed
{t("markTheseItemsAsReviewed")}
</Button>
</div>
)}

View File

@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useResizeObserver } from "@/hooks/resize-observer";
import { FrigateConfig } from "@/types/frigateConfig";
import { t } from "i18next";
import { useEffect, useMemo, useRef, useState } from "react";
import {
isDesktop,
@ -144,12 +145,14 @@ export default function LiveBirdseyeView({
{!fullscreen ? (
<Button
className={`flex items-center gap-2 rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
aria-label="Go Back"
aria-label={t("label.back", { ns: "common" })}
size={isMobile ? "icon" : "sm"}
onClick={() => navigate(-1)}
>
<IoMdArrowBack className="size-5" />
{isDesktop && <div className="text-primary">Back</div>}
{isDesktop && (
<div className="text-primary">{t("button.back")}</div>
)}
</Button>
) : (
<div />

View File

@ -118,7 +118,7 @@ import axios from "axios";
import { toast } from "sonner";
import { Toaster } from "@/components/ui/sonner";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
type LiveCameraViewProps = {
config?: FrigateConfig;
@ -430,7 +430,7 @@ export default function LiveCameraView({
>
<Button
className={`flex items-center gap-2.5 rounded-lg`}
aria-label="Go back"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={() => navigate(-1)}
>
@ -443,7 +443,7 @@ export default function LiveCameraView({
</Button>
<Button
className="flex items-center gap-2.5 rounded-lg"
aria-label="Show historical footage"
aria-label={t("history.label")}
size="sm"
onClick={() => {
navigate("review", {
@ -476,7 +476,7 @@ export default function LiveCameraView({
{fullscreen && (
<Button
className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary"
aria-label="Go back"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={() => navigate(-1)}
>
@ -876,14 +876,19 @@ function PtzControlPanel({
<TooltipTrigger asChild>
<Button
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
aria-label="Click in the frame to center the camera"
aria-label={t("ptz.move.clickMove.label")}
onClick={() => setClickOverlay(!clickOverlay)}
>
<TbViewfinder />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{clickOverlay ? "Disable" : "Enable"} click to move</p>
<p>
{clickOverlay
? t("ptz.move.clickMove.disable")
: t("ptz.move.clickMove.enable")}{" "}
click to move
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -894,7 +899,7 @@ function PtzControlPanel({
<TooltipTrigger asChild>
<DropdownMenu modal={!isDesktop}>
<DropdownMenuTrigger asChild>
<Button aria-label="PTZ camera presets">
<Button aria-label={t("ptz.presets")}>
<BsThreeDotsVertical />
</Button>
</DropdownMenuTrigger>
@ -916,7 +921,7 @@ function PtzControlPanel({
</DropdownMenu>
</TooltipTrigger>
<TooltipContent>
<p>PTZ camera presets</p>
<p>{t("ptz.presets")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -926,6 +931,7 @@ function PtzControlPanel({
}
function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
const { t } = useTranslation(["views/live"]);
const rankMap = { all: 0, motion: 1, active_objects: 2 };
const getValidMode = (retain?: { mode?: string }): keyof typeof rankMap => {
const mode = retain?.mode;
@ -940,13 +946,25 @@ function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
? recordRetainMode
: alertsRetainMode;
const source = effectiveRetainMode === recordRetainMode ? "camera" : "alerts";
const source =
effectiveRetainMode === recordRetainMode
? t("camera", { ns: "views/events" })
: t("alerts", { ns: "views/events" });
return effectiveRetainMode !== "all" ? (
<div>
Your {source} recording retention configuration is set to{" "}
<code>mode: {effectiveRetainMode}</code>, so this on-demand recording will
only keep segments with {effectiveRetainMode.replaceAll("_", " ")}.
<Trans
ns="views/live"
values={{
source,
effectiveRetainMode,
effectiveRetainModeName: t(
"effectiveRetainMode.modes." + effectiveRetainMode,
),
}}
>
effectiveRetainMode.notAllTips
</Trans>
</div>
) : null;
}
@ -1034,9 +1052,7 @@ function FrigateCameraFeatures({
setIsRecording(true);
const toastId = toast.success(
<div className="flex flex-col space-y-3">
<div className="font-semibold">
Started manual on-demand recording.
</div>
<div className="font-semibold">{t("manualRecording.started")}</div>
{!camera.record.enabled || camera.record.alerts.retain.days == 0 ? (
<div>{t("manualRecording.recordDisabledTips")}</div>
) : (

View File

@ -395,7 +395,7 @@ export function RecordingView({
<div className={cn("flex items-center gap-2")}>
<Button
className="flex items-center gap-2.5 rounded-lg"
aria-label="Go back"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={() => navigate(-1)}
>
@ -492,14 +492,14 @@ export function RecordingView({
<ToggleGroupItem
className={`${timelineType == "timeline" ? "" : "text-muted-foreground"}`}
value="timeline"
aria-label="Select timeline"
aria-label={t("timeline.aria")}
>
<div className="">{t("timeline")}</div>
</ToggleGroupItem>
<ToggleGroupItem
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
value="events"
aria-label="Select events"
aria-label={t("events.aria")}
>
<div className="">{t("events.label")}</div>
</ToggleGroupItem>

View File

@ -202,7 +202,7 @@ export default function AuthenticationView() {
</div>
<Button
className="flex items-center gap-2 self-start sm:self-auto"
aria-label="Add a new user"
aria-label={t("users.addUser")}
variant="default"
onClick={() => setShowCreate(true)}
>

View File

@ -633,7 +633,7 @@ export default function CameraSettingsView({
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
type="button"
>
@ -643,7 +643,7 @@ export default function CameraSettingsView({
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
type="submit"
>
{isLoading ? (

View File

@ -518,7 +518,7 @@ export default function MasksAndZonesView({
<Button
variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label="Add a new zone"
aria-label={t("masksAndZones.zones.add")}
onClick={() => {
setEditPane("zone");
handleNewPolygon("zone");
@ -586,7 +586,7 @@ export default function MasksAndZonesView({
<Button
variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label="Add a new motion mask"
aria-label={t("masksAndZones.motionMasks.add")}
onClick={() => {
setEditPane("motion_mask");
handleNewPolygon("motion_mask");
@ -654,7 +654,7 @@ export default function MasksAndZonesView({
<Button
variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label="Add a new object mask"
aria-label={t("masksAndZones.objectMasks.add")}
onClick={() => {
setEditPane("object_mask");
handleNewPolygon("object_mask");

View File

@ -295,7 +295,7 @@ export default function MotionTunerView({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Reset"
aria-label={t("button.reset", { ns: "common" })}
onClick={onCancel}
>
{t("button.reset", { ns: "common" })}
@ -304,7 +304,7 @@ export default function MotionTunerView({
variant="select"
disabled={!changedValue || isLoading}
className="flex flex-1"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
onClick={saveToConfig}
>
{isLoading ? (

View File

@ -266,9 +266,15 @@ export default function NotificationView({
});
updateConfig();
} else {
toast.error(`Failed to save config changes: ${res.statusText}`, {
position: "top-center",
});
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
@ -276,7 +282,7 @@ export default function NotificationView({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(`Failed to save config changes: ${errorMessage}`, {
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
})
@ -284,7 +290,7 @@ export default function NotificationView({
setIsLoading(false);
});
},
[updateConfig, setIsLoading, allCameras],
[updateConfig, setIsLoading, allCameras, t],
);
function onSubmit(values: z.infer<typeof formSchema>) {
@ -474,7 +480,7 @@ export default function NotificationView({
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[50%]">
<Button
className="flex flex-1"
aria-label="Cancel"
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
type="button"
>
@ -484,7 +490,7 @@ export default function NotificationView({
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
type="submit"
>
{isLoading ? (

View File

@ -295,14 +295,18 @@ export default function ExploreSettingsView({
<Separator className="my-2 flex bg-secondary" />
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button className="flex flex-1" aria-label="Reset" onClick={onCancel}>
<Button
className="flex flex-1"
aria-label={t("button.reset", { ns: "common" })}
onClick={onCancel}
>
{t("button.reset", { ns: "common" })}
</Button>
<Button
variant="select"
disabled={!changedValue || isLoading}
className="flex flex-1"
aria-label="Save"
aria-label={t("button.save", { ns: "common" })}
onClick={saveToConfig}
>
{isLoading ? (

View File

@ -142,7 +142,7 @@ export default function UiSettingsView() {
</div>
</div>
<Button
aria-label="Clear all saved layouts"
aria-label={t("general.storedLayouts.clearAll")}
onClick={clearStoredLayouts}
>
{t("general.storedLayouts.clearAll")}
@ -159,7 +159,7 @@ export default function UiSettingsView() {
</div>
</div>
<Button
aria-label="Clear all group streaming settings"
aria-label={t("general.cameraGroupStreaming.clearAll")}
onClick={clearStreamingSettings}
>
{t("general.cameraGroupStreaming.clearAll")}

View File

@ -542,7 +542,7 @@ export default function GeneralMetrics({
{canGetGpuInfo && (
<Button
className="cursor-pointer"
aria-label="Hardware information"
aria-label={t("general.hardwareInfo.title")}
size="sm"
onClick={() => setShowVainfo(true)}
>

View File

@ -87,11 +87,15 @@ export default function StorageMetrics({
<PopoverTrigger asChild>
<button
className="focus:outline-none"
aria-label="Unused Storage Information"
aria-label={t(
"storage.cameraStorage.unusedStorageInformation",
)}
>
<CiCircleAlert
className="size-5"
aria-label="Unused Storage Information"
aria-label={t(
"storage.cameraStorage.unusedStorageInformation",
)}
/>
</button>
</PopoverTrigger>
@ -107,7 +111,9 @@ export default function StorageMetrics({
/>
{earliestDate && (
<div className="mt-2 text-xs text-primary-variant">
<span className="font-medium">Earliest recording available:</span>{" "}
<span className="font-medium">
{t("storage.recordings.earliestRecording")}
</span>{" "}
{formatUnixTimestampToDateTime(earliestDate, {
timezone: timezone,
strftime_fmt: