mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-19 01:17:06 +03:00
Add more translation
This commit is contained in:
parent
7476fa1300
commit
08d0215673
@ -109,6 +109,20 @@
|
||||
"audio.fire_alarm": "Fire alarm",
|
||||
|
||||
"ui.time.justNow": "Just now",
|
||||
"ui.time.today": "Today",
|
||||
"ui.time.yesterday": "Yesterday",
|
||||
"ui.time.last7": "Last 7 days",
|
||||
"ui.time.last14": "Last 14 days",
|
||||
"ui.time.last30": "Last 30 days",
|
||||
"ui.time.thisWeek": "This Week",
|
||||
"ui.time.lastWeek": "Last Week",
|
||||
"ui.time.thisMonth": "This Month",
|
||||
"ui.time.lastMonth": "Last Month",
|
||||
"ui.time.formattedTimestamp": "%b %-d, %I:%M:%S %p",
|
||||
"ui.time.formattedTimestamp.24hour": "%b %-d, %H:%M:%S",
|
||||
|
||||
"ui.iconPicker.selectIcon": "Select an icon",
|
||||
"ui.iconPicker.search.placeholder": "Search for an icon...",
|
||||
|
||||
"ui.dialog.restart.title": "Are you sure you want to restart Frigate?",
|
||||
"ui.dialog.restart.button": "Restart",
|
||||
@ -116,6 +130,20 @@
|
||||
"ui.dialog.restart.restarting.content": "This page will reload in {{countdown}} seconds.",
|
||||
"ui.dialog.restart.restarting.button": "Force Reload Now",
|
||||
|
||||
"ui.dialog.export.time.fromTimeline": "Select from Timeline",
|
||||
"ui.dialog.export.time.lastHour_one": "Last Hour",
|
||||
"ui.dialog.export.time.lastHour_other": "Last {{count}} Hours",
|
||||
"ui.dialog.export.time.custom": "Custom",
|
||||
"ui.dialog.export.name.placeholder": "Name the Export",
|
||||
"ui.dialog.export.select": "Select",
|
||||
"ui.dialog.export.export": "Export",
|
||||
"ui.dialog.export.toast.success": "Successfully started export. View the file in the /exports folder.",
|
||||
"ui.dialog.export.toast.error.failed": "Failed to start export: {{error}}",
|
||||
"ui.dialog.export.toast.error.endTimeMustAfterStartTime": "End time must be after start time",
|
||||
"ui.dialog.export.toast.error.noVaildTimeSelected": "No valid time range selected",
|
||||
"ui.dialog.export.fromTimeline.saveExport": "Save Export",
|
||||
"ui.dialog.export.fromTimeline.previewExport": "Preview Export",
|
||||
|
||||
"ui.stats.ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
|
||||
"ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
|
||||
"ui.stats.healthy": "System is healthy",
|
||||
@ -209,6 +237,23 @@
|
||||
"ui.menu.user.anonymous": "anonymous",
|
||||
"ui.menu.user.logout": "Logout",
|
||||
|
||||
"ui.cameraGroup": "Camera Groups",
|
||||
"ui.cameraGroup.add": "添加摄像头组",
|
||||
"ui.cameraGroup.edit": "Edit camera groups",
|
||||
"ui.cameraGroup.delete.confirm": "Confirm Delete",
|
||||
"ui.cameraGroup.delete.confirm.desc": "Are you sure you want to delete the camera group <em>{{name}}</em>?",
|
||||
"ui.cameraGroup.name": "Name",
|
||||
"ui.cameraGroup.name.placeholder": "Enter a name...",
|
||||
"ui.cameraGroup.name.errorMessage.mustLeastCharacters": "Camera group name must be at least 2 characters.",
|
||||
"ui.cameraGroup.name.errorMessage.exists": "Camera group name already exists.",
|
||||
"ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "Camera group name must not contain a period.",
|
||||
"ui.cameraGroup.name.errorMessage.invalid": "Invalid camera group name.",
|
||||
"ui.cameraGroup.cameras": "Cameras",
|
||||
"ui.cameraGroup.cameras.desc": "Select cameras for this group.",
|
||||
"ui.cameraGroup.icon": "Icon",
|
||||
"ui.cameraGroup.success": "Camera group ({{name}}) has been saved.",
|
||||
"ui.cameraGroup.toast.error": "Failed to save config changes: {{error}}",
|
||||
|
||||
"ui.eventView.alerts": "Alerts",
|
||||
"ui.eventView.detections": "Detections",
|
||||
"ui.eventView.motion": "Motion",
|
||||
@ -216,9 +261,33 @@
|
||||
"ui.eventView.empty.alert": "There are no alerts to review",
|
||||
"ui.eventView.empty.detection": "There are no detections to review",
|
||||
|
||||
"ui.reviewFilter.filter": "Filter",
|
||||
"ui.reviewFilter.filter.allLabels": "All Labels",
|
||||
"ui.reviewFilter.filter.allZones": "All Zones",
|
||||
"ui.filter": "Filter",
|
||||
"ui.filter.allLabels": "All Labels",
|
||||
"ui.filter.allLabels.short": "Labels",
|
||||
"ui.filter.countLabels": "{{count}} Labels",
|
||||
"ui.filter.allZones": "All Zones",
|
||||
"ui.filter.allZones.short": "Zones",
|
||||
"ui.filter.allDates": "All Dates",
|
||||
"ui.filter.allDates.short": "Dates",
|
||||
"ui.filter.more": "More Filters",
|
||||
"ui.filter.timeRange": "Time Range",
|
||||
"ui.filter.zones": "Zones",
|
||||
"ui.filter.subLabels": "Sub Labels",
|
||||
"ui.filter.allSubLabels": "All Sub Labels",
|
||||
"ui.filter.score": "Score",
|
||||
"ui.filter.features": "Features",
|
||||
"ui.filter.features.hasSnapshot": "Has a snapshot",
|
||||
"ui.filter.features.hasVideoClip": "Has a video clip",
|
||||
"ui.filter.features.submittedToFrigatePlus": "Submitted to Frigate+",
|
||||
"ui.filter.features.submittedToFrigatePlus.tips": "You must first filter on tracked objects that have a snapshot.<br /><br />Tracked objects without a snapshot cannot be submitted to Frigate+.",
|
||||
"ui.filter.sort": "Sort",
|
||||
"ui.filter.sort.dateAsc": "Date (Ascending)",
|
||||
"ui.filter.sort.dateDesc": "Date (Descending)",
|
||||
"ui.filter.sort.scoreAsc": "Object Score (Ascending)",
|
||||
"ui.filter.sort.scoreDesc": "Object Score (Descending)",
|
||||
"ui.filter.sort.relevance": "Relevance",
|
||||
"ui.filter.allCameras": "All Cameras",
|
||||
"ui.filter.allCameras.short": "Cameras",
|
||||
|
||||
"ui.reviewFilter.showReviewed": "Show Reviewed",
|
||||
|
||||
@ -236,8 +305,13 @@
|
||||
"ui.pictureInPicture": "Picture in Picture",
|
||||
"ui.on": "ON",
|
||||
"ui.off": "OFF",
|
||||
"ui.edit": "Edit",
|
||||
"ui.delete": "Delete",
|
||||
|
||||
"ui.yes": "Yes",
|
||||
"ui.no": "No",
|
||||
|
||||
"ui.live.documentTitle": "Live - Frigate",
|
||||
"ui.live.documentTitle.withCamera": "{{camera}} - Live - Frigate",
|
||||
"ui.live.twoWayTalk.enable": "Enable Two Way Talk",
|
||||
"ui.live.twoWayTalk.disable": "Disable Two Way Talk",
|
||||
"ui.live.cameraAudio.enable": "Enable Camera Audio",
|
||||
@ -261,10 +335,31 @@
|
||||
"ui.live.autotracking.enable": "Enable Autotracking",
|
||||
"ui.live.autotracking.disable": "Disable Autotracking",
|
||||
|
||||
"ui.review.timeline": "Timeline",
|
||||
"ui.review.events": "Events",
|
||||
"ui.review.events.noFoundForTimePeriod": "No events found for this time period.",
|
||||
"ui.review.documentTitle": "Review - Frigate",
|
||||
"ui.review.recordings.documentTitle": "Recordings - Frigate",
|
||||
|
||||
"ui.player.noRecordingsFoundForThisTime": "No recordings found for this time",
|
||||
"ui.player.noPreviewFound": "No Preview Found",
|
||||
"ui.player.noPreviewFoundFor": "No Preview Found for {{cameraName}}",
|
||||
|
||||
"ui.calendarFilter.last24Hours": "Last 24 Hours",
|
||||
|
||||
"ui.searchView.noTrackedObjects": "No Tracked Objects Found",
|
||||
"ui.searchView.settings": "Settings",
|
||||
"ui.searchView.settings.defaultView": "Default View",
|
||||
"ui.searchView.settings.defaultView.desc": "When no filters are selected, display a summary of the most recent tracked objects per label, or display an unfiltered grid.",
|
||||
"ui.searchView.settings.defaultView.summary": "Summary",
|
||||
"ui.searchView.settings.defaultView.unfilteredGrid": "Unfiltered Grid",
|
||||
"ui.searchView.settings.gridColumns": "Grid Columns",
|
||||
"ui.searchView.settings.gridColumns.desc": "Select the number of columns in the grid view.",
|
||||
"ui.searchView.settings.searchSource": "Search Source",
|
||||
"ui.searchView.settings.searchSource.desc": "Choose whether to search the thumbnails or descriptions of your tracked objects.",
|
||||
|
||||
"ui.settingView.menu.uiSettings": "UI Settings",
|
||||
"ui.settingView.menu.searchSettings": "Search Settings",
|
||||
"ui.settingView.menu.exploreSettings": "Explore Settings",
|
||||
"ui.settingView.menu.cameraSettings": "Camera Settings",
|
||||
"ui.settingView.menu.masksAndZones": "Masks / Zones",
|
||||
"ui.settingView.menu.motionTuner": "Motion Tuner",
|
||||
@ -290,18 +385,18 @@
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "Sunday",
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "Monday",
|
||||
|
||||
"ui.settingView.searchSettings": "Search Settings",
|
||||
"ui.settingView.searchSettings.semanticSearch": "Semantic Search",
|
||||
"ui.settingView.searchSettings.semanticSearch.desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
|
||||
"ui.settingView.searchSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
|
||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup",
|
||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. <em>Don't forget to disable the option after restarting!</em>",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize": "Model Size",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "small",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "large",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small.desc": "Using <em>small</em> employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality.",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large.desc": "Using <em>large</em> employs the full Jina model and will automatically run on the GPU if applicable.",
|
||||
"ui.settingView.exploreSettings": "Explore Settings",
|
||||
"ui.settingView.exploreSettings.semanticSearch": "Semantic Search",
|
||||
"ui.settingView.exploreSettings.semanticSearch.desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
|
||||
"ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
|
||||
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup",
|
||||
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. <em>Don't forget to disable the option after restarting!</em>",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize": "Model Size",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.small": "small",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.large": "large",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "Using <em>small</em> employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality.",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "Using <em>large</em> employs the full Jina model and will automatically run on the GPU if applicable.",
|
||||
|
||||
"ui.settingView.cameraSettings": "Camera Settings",
|
||||
"ui.settingView.cameraSettings.reviewClassification": "Review Classification",
|
||||
@ -319,6 +414,7 @@
|
||||
|
||||
"ui.settingView.masksAndZonesSettings": "Masks / Zones",
|
||||
"ui.settingView.masksAndZonesSettings.zone": "Zones",
|
||||
"ui.settingView.masksAndZonesSettings.zone.documentTitle": "Edit Zone - Frigate",
|
||||
"ui.settingView.masksAndZonesSettings.zone.desc": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
|
||||
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "Documentation",
|
||||
"ui.settingView.masksAndZonesSettings.zone.add": "Add Zone",
|
||||
@ -338,6 +434,7 @@
|
||||
"ui.settingView.masksAndZonesSettings.zone.allObjects": "All Objects",
|
||||
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks": "Motion Mask",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "Edit Motion Mask - Frigate",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "Motion masks are used to prevent unwanted types of motion from triggering detection. Over masking will make it more difficult for objects to be tracked.",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "Documentation",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.add": "New Motion Mask",
|
||||
@ -351,8 +448,8 @@
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead.",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.documentation": "Read the documentation",
|
||||
|
||||
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks": "Object Masks",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle": "Edit Object Mask - Frigate",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "Object filter masks are used to filter out false positives for a given object type based on location.",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "Documentation",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.add": "Add Object Mask",
|
||||
@ -423,6 +520,11 @@
|
||||
"ui.configEditorView.configEditor": "Config Editor",
|
||||
"ui.configEditorView.copyConfig": "Copy Config",
|
||||
"ui.configEditorView.saveAndRestart": "Save & Restart",
|
||||
"ui.configEditorView.saveOnly": "Save Only"
|
||||
"ui.configEditorView.saveOnly": "Save Only",
|
||||
|
||||
"ui.exportView.documentTitle": "Export - Frigate",
|
||||
"ui.exportView.search": "Search",
|
||||
"ui.exportView.noExports": "No exports found",
|
||||
"ui.exportView.deleteExport": "Delete Export",
|
||||
"ui.exportView.deleteExport.desc": "Are you sure you want to delete {{exportName}}?"
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
{
|
||||
|
||||
"object.person": "人",
|
||||
"object.bicycle": "自行车",
|
||||
"object.car": "汽车",
|
||||
@ -110,6 +109,22 @@
|
||||
"audio.fire_alarm": "火灾警报器",
|
||||
|
||||
"ui.time.justNow": "刚才",
|
||||
"ui.time.today": "今天",
|
||||
"ui.time.yesterday": "昨天",
|
||||
"ui.time.last7": "最后 7 天",
|
||||
"ui.time.last14": "最后 14 天",
|
||||
"ui.time.last30": "最后 30 天",
|
||||
"ui.time.thisWeek": "本周",
|
||||
"ui.time.lastWeek": "上个周",
|
||||
"ui.time.thisMonth": "本月",
|
||||
"ui.time.lastMonth": "上个月",
|
||||
"ui.time.pm": "上午",
|
||||
"ui.time.am": "下午",
|
||||
"ui.time.formattedTimestamp": "%m月%-d日 %I:%M:%S %p",
|
||||
"ui.time.formattedTimestamp.24hour": "%m月%-d日 %H:%M:%S",
|
||||
|
||||
"ui.iconPicker.selectIcon": "选择图标",
|
||||
"ui.iconPicker.search.placeholder": "搜索图标...",
|
||||
|
||||
"ui.dialog.restart.title": "你确定要重启 Frigate?",
|
||||
"ui.dialog.restart.button": "重启",
|
||||
@ -117,6 +132,21 @@
|
||||
"ui.dialog.restart.restarting.content": "该页面将会在 {{countdown}} 秒后自动刷新。",
|
||||
"ui.dialog.restart.restarting.button": "强制刷新",
|
||||
|
||||
"ui.dialog.export.time.fromTimeline": "从时间线选择",
|
||||
"ui.dialog.export.time.lastHour_one": "最后1小时",
|
||||
"ui.dialog.export.time.lastHour_other": "最后 {{count}} 小时",
|
||||
"ui.dialog.export.time.custom": "自定义",
|
||||
"ui.dialog.export.name.placeholder": "导出项目的名字",
|
||||
"ui.dialog.export.select": "选择",
|
||||
"ui.dialog.export.export": "导出",
|
||||
"ui.dialog.export.toast.success": "导出成功。进入 /exports 目录查看文件。",
|
||||
"ui.dialog.export.toast.error.failed": "导出失败:{{error}}",
|
||||
"ui.dialog.export.toast.error.endTimeMustAfterStartTime": "结束时间必须在开始时间之后",
|
||||
"ui.dialog.export.toast.error.noVaildTimeSelected": "未选择有效的时间范围",
|
||||
"ui.dialog.export.fromTimeline.saveExport": "保存导出",
|
||||
"ui.dialog.export.fromTimeline.previewExport": "预览导出",
|
||||
|
||||
|
||||
"ui.stats.ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%)",
|
||||
"ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%)",
|
||||
"ui.stats.healthy": "系统运行正常",
|
||||
@ -203,13 +233,31 @@
|
||||
"ui.menu.live": "实时监控",
|
||||
"ui.menu.live.allCameras": "所有摄像头",
|
||||
"ui.menu.review": "回放",
|
||||
"ui.menu.explore": "Explore",
|
||||
"ui.menu.explore": "探测",
|
||||
"ui.menu.export": "导出",
|
||||
"ui.menu.uiPlayground": "UI Playground",
|
||||
"ui.menu.user.current": "当前用户:{{user}}",
|
||||
"ui.menu.user.anonymous": "匿名",
|
||||
"ui.menu.user.logout": "登出",
|
||||
|
||||
"ui.cameraGroup": "摄像头组",
|
||||
"ui.cameraGroup.add": "添加摄像头组",
|
||||
"ui.cameraGroup.edit": "编辑摄像头组",
|
||||
"ui.cameraGroup.edit.desc": "编辑摄像头组",
|
||||
"ui.cameraGroup.delete.confirm": "确认删除",
|
||||
"ui.cameraGroup.delete.confirm.desc": "你确定要删除摄像头组 <strong>{{name}}</strong> 吗?",
|
||||
"ui.cameraGroup.name": "名称",
|
||||
"ui.cameraGroup.name.placeholder": "请输入名称",
|
||||
"ui.cameraGroup.name.errorMessage.mustLeastCharacters": "摄像头组的名称必须至少有 2 个字符。",
|
||||
"ui.cameraGroup.name.errorMessage.exists": "摄像头组名称已存在。",
|
||||
"ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "摄像头组名称不能包含英文句号(.)。",
|
||||
"ui.cameraGroup.name.errorMessage.invalid": "无效的摄像头组名称。",
|
||||
"ui.cameraGroup.cameras": "摄像头",
|
||||
"ui.cameraGroup.cameras.desc": "选择添加至该组的摄像头。",
|
||||
"ui.cameraGroup.icon": "图标",
|
||||
"ui.cameraGroup.toast.success": "摄像头组({{name}})保存成功。",
|
||||
"ui.cameraGroup.toast.error": "保存设置失败: {{error}}",
|
||||
|
||||
"ui.eventView.alerts": "警告",
|
||||
"ui.eventView.detections": "检测",
|
||||
"ui.eventView.motion": "运动",
|
||||
@ -217,9 +265,33 @@
|
||||
"ui.eventView.empty.alert": "还没有“警告”类回放",
|
||||
"ui.eventView.empty.detection": "还没有“探测”类回放",
|
||||
|
||||
"ui.reviewFilter.filter": "过滤器",
|
||||
"ui.reviewFilter.filter.allLabels": "所有标签",
|
||||
"ui.reviewFilter.filter.allZones": "所有区域",
|
||||
"ui.filter": "过滤器",
|
||||
"ui.filter.allLabels": "所有标签",
|
||||
"ui.filter.allLabels.short": "标签",
|
||||
"ui.filter.countLabels": "{{count}} 个标签",
|
||||
"ui.filter.allZones": "所有区域",
|
||||
"ui.filter.allZones.short": "区域",
|
||||
"ui.filter.allDates": "所有日期",
|
||||
"ui.filter.allDates.short": "日期",
|
||||
"ui.filter.more": "更多筛选项",
|
||||
"ui.filter.timeRange": "时间范围",
|
||||
"ui.filter.zones": "区域",
|
||||
"ui.filter.subLabels": "子标签",
|
||||
"ui.filter.allSubLabels": "所有子标签",
|
||||
"ui.filter.score": "分值",
|
||||
"ui.filter.features": "特性",
|
||||
"ui.filter.features.hasSnapshot": "包含快照",
|
||||
"ui.filter.features.hasVideoClip": "包含视频片段",
|
||||
"ui.filter.features.submittedToFrigatePlus": "提交至 Frigate+",
|
||||
"ui.filter.features.submittedToFrigatePlus.tips": "你必须要先筛选具有快照的探测对象。<br /><br />没有快照的跟踪对象无法提交至 Frigate+.",
|
||||
"ui.filter.sort": "排序",
|
||||
"ui.filter.sort.dateAsc": "日期 (正序)",
|
||||
"ui.filter.sort.dateDesc": "日期 (倒序)",
|
||||
"ui.filter.sort.scoreAsc": "对象分值 (正序)",
|
||||
"ui.filter.sort.scoreDesc": "对象分值 (倒序)",
|
||||
"ui.filter.sort.relevance": "关联性",
|
||||
"ui.filter.allCameras": "所有摄像头",
|
||||
"ui.filter.allCameras.short": "摄像头",
|
||||
|
||||
"ui.reviewFilter.showReviewed": "显示已查看的项目",
|
||||
|
||||
@ -238,8 +310,13 @@
|
||||
"ui.pictureInPicture": "画中画",
|
||||
"ui.on": "开",
|
||||
"ui.off": "关",
|
||||
"ui.edit": "编辑",
|
||||
"ui.delete": "删除",
|
||||
"ui.yes": "是",
|
||||
"ui.no": "否",
|
||||
|
||||
"ui.live.documentTitle": "实时监控 - Frigate",
|
||||
"ui.live.documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate",
|
||||
"ui.live.twoWayTalk.enable": "开启双向对话",
|
||||
"ui.live.twoWayTalk.disable": "关闭双向对话",
|
||||
"ui.live.cameraAudio.enable": "开启摄像头音频",
|
||||
@ -263,10 +340,31 @@
|
||||
"ui.live.autotracking.enable": "启用自动追踪",
|
||||
"ui.live.autotracking.disable": "关闭自动追踪",
|
||||
|
||||
"ui.review.timeline": "时间线",
|
||||
"ui.review.events": "事件",
|
||||
"ui.review.events.noFoundForTimePeriod": "未找到该时间段的事件。",
|
||||
"ui.review.documentTitle": "预览 - Frigate",
|
||||
"ui.review.recordings.documentTitle": "回放 - Frigate",
|
||||
|
||||
"ui.player.noRecordingsFoundForThisTime": "找不到此次录制",
|
||||
"ui.player.noPreviewFound": "没有找到预览",
|
||||
"ui.player.noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览",
|
||||
|
||||
"ui.calendarFilter.last24Hours": "过去24小时",
|
||||
|
||||
"ui.searchView.noTrackedObjects": "找不到探测的对象",
|
||||
"ui.searchView.settings": "设置",
|
||||
"ui.searchView.settings.defaultView": "默认视图",
|
||||
"ui.searchView.settings.defaultView.desc": "当未选择任何过滤器时,显示每个标签最近跟踪对象的摘要,或显示未过滤的网格。",
|
||||
"ui.searchView.settings.defaultView.summary": "摘要",
|
||||
"ui.searchView.settings.defaultView.unfilteredGrid": "未过滤网格",
|
||||
"ui.searchView.settings.gridColumns": "网格列数",
|
||||
"ui.searchView.settings.gridColumns.desc": "选择网格视图中的列数。",
|
||||
"ui.searchView.settings.searchSource": "搜索源",
|
||||
"ui.searchView.settings.searchSource.desc": "选择是搜索缩略图还是跟踪对象的描述。",
|
||||
|
||||
"ui.settingView.menu.uiSettings": "界面设置",
|
||||
"ui.settingView.menu.searchSettings": "搜索设置",
|
||||
"ui.settingView.menu.exploreSettings": "搜索设置",
|
||||
"ui.settingView.menu.cameraSettings": "摄像头设置",
|
||||
"ui.settingView.menu.masksAndZones": "屏罩 / 区域",
|
||||
"ui.settingView.menu.motionTuner": "运动调整器",
|
||||
@ -292,18 +390,18 @@
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "星期天",
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "星期一",
|
||||
|
||||
"ui.settingView.searchSettings": "搜索设置",
|
||||
"ui.settingView.searchSettings.semanticSearch": "语义搜索",
|
||||
"ui.settingView.searchSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
|
||||
"ui.settingView.searchSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
|
||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
|
||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize": "模型大小",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "小",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "大",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存,在CPU上也能较快的运行。质量较好。",
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型,并在适用的情况下使用GPU。",
|
||||
"ui.settingView.exploreSettings": "探测设置",
|
||||
"ui.settingView.exploreSettings.semanticSearch": "语义搜索",
|
||||
"ui.settingView.exploreSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
|
||||
"ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
|
||||
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
|
||||
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize": "模型大小",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.small": "小",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.large": "大",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存,在CPU上也能较快的运行。质量较好。",
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型,并在适用的情况下使用GPU。",
|
||||
|
||||
"ui.settingView.cameraSettings": "摄像头设置",
|
||||
"ui.settingView.cameraSettings.reviewClassification": "预览分级",
|
||||
@ -321,6 +419,7 @@
|
||||
|
||||
"ui.settingView.masksAndZonesSettings": "屏罩 / 区域",
|
||||
"ui.settingView.masksAndZonesSettings.zone": "区域",
|
||||
"ui.settingView.masksAndZonesSettings.zone.documentTitle": "编辑区域 - Frigate",
|
||||
"ui.settingView.masksAndZonesSettings.zone.desc": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。",
|
||||
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "文档(英文)",
|
||||
"ui.settingView.masksAndZonesSettings.zone.add": "添加区域",
|
||||
@ -340,6 +439,7 @@
|
||||
"ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象",
|
||||
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "编辑运动遮罩 - Frigate",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩",
|
||||
@ -355,6 +455,7 @@
|
||||
|
||||
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks": "对象遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle": "编辑对象遮罩 - Frigate",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "对象过滤器用于防止特定位置的指定对象被误报。",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "文档(英文)",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.add": "添加对象遮罩",
|
||||
@ -425,5 +526,12 @@
|
||||
"ui.configEditorView.configEditor": "配置编辑器",
|
||||
"ui.configEditorView.copyConfig": "复制配置",
|
||||
"ui.configEditorView.saveAndRestart": "保存并重启",
|
||||
"ui.configEditorView.saveOnly": "只保存"
|
||||
"ui.configEditorView.saveOnly": "只保存",
|
||||
|
||||
"ui.exportView.documentTitle": "导出 - Frigate",
|
||||
"ui.exportView.search": "搜索",
|
||||
"ui.exportView.noExports": "没有找到导出的项目",
|
||||
"ui.exportView.deleteExport": "删除导出的项目",
|
||||
"ui.exportView.deleteExport.desc": "你确定要删除 {{exportName}} 吗?"
|
||||
|
||||
}
|
||||
@ -82,10 +82,9 @@ export default function ReviewCard({
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
"Successfully started export. View the file in the /exports folder.",
|
||||
{ position: "top-center" },
|
||||
);
|
||||
toast.success(t("ui.dialog.export.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@ -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 { Trans } from "react-i18next";
|
||||
|
||||
type CalendarFilterButtonProps = {
|
||||
reviewSummary?: ReviewSummary;
|
||||
@ -64,7 +65,7 @@ export default function CalendarFilterButton({
|
||||
updateSelectedDay(undefined);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -67,6 +67,7 @@ import {
|
||||
MobilePageTitle,
|
||||
} from "../mobile/MobilePage";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type CameraGroupSelectorProps = {
|
||||
className?: string;
|
||||
@ -341,9 +342,11 @@ function NewGroupDialog({
|
||||
className={cn(isDesktop && "mt-5", "justify-center")}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Title>Camera Groups</Title>
|
||||
<Title>
|
||||
<Trans>ui.cameraGroup</Trans>
|
||||
</Title>
|
||||
<Description className="sr-only">
|
||||
Edit camera groups
|
||||
<Trans>ui.cameraGroup.edit</Trans>
|
||||
</Description>
|
||||
<div
|
||||
className={cn(
|
||||
@ -391,7 +394,11 @@ function NewGroupDialog({
|
||||
}}
|
||||
>
|
||||
<Title>
|
||||
{editState == "add" ? "Add" : "Edit"} Camera Group
|
||||
{editState == "add" ? (
|
||||
<Trans>ui.cameraGroup.add</Trans>
|
||||
) : (
|
||||
<Trans>ui.cameraGroup.edit</Trans>
|
||||
)}
|
||||
</Title>
|
||||
<Description className="sr-only">
|
||||
Edit camera groups
|
||||
@ -464,8 +471,12 @@ export function EditGroupDialog({
|
||||
>
|
||||
<div className="scrollbar-container flex flex-col overflow-y-auto md:my-4">
|
||||
<Header className="mt-2" onClose={() => setOpen(false)}>
|
||||
<Title>Edit Camera Group</Title>
|
||||
<Description className="sr-only">Edit camera group</Description>
|
||||
<Title>
|
||||
<Trans>ui.cameraGroup.edit</Trans>
|
||||
</Title>
|
||||
<Description className="sr-only">
|
||||
<Trans>ui.cameraGroup.edit.desc</Trans>
|
||||
</Description>
|
||||
</Header>
|
||||
|
||||
<CameraGroupEdit
|
||||
@ -515,19 +526,24 @@ export function CameraGroupRow({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
<Trans>ui.cameraGroup.delete.confirm</Trans>
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the camera group{" "}
|
||||
<em>{group[0]}</em>?
|
||||
<Trans values={{ name: group[0] }}>
|
||||
ui.cameraGroup.delete.confirm.desc
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
<Trans>ui.cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={onDeleteGroup}
|
||||
>
|
||||
Delete
|
||||
<Trans>ui.delete</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -568,7 +584,9 @@ export function CameraGroupRow({
|
||||
onClick={onEditGroup}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Edit</TooltipContent>
|
||||
<TooltipContent>
|
||||
<Trans>ui.edit</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
@ -579,7 +597,9 @@ export function CameraGroupRow({
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
<TooltipContent>
|
||||
<Trans>ui.delete</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
@ -614,7 +634,7 @@ export function CameraGroupEdit({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: "Camera group name must be at least 2 characters.",
|
||||
message: t("ui.cameraGroup.name.errorMessage.mustLeastCharacters"),
|
||||
})
|
||||
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
||||
.refine(
|
||||
@ -625,7 +645,7 @@ export function CameraGroupEdit({
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Camera group name already exists.",
|
||||
message: t("ui.cameraGroup.name.errorMessage.exists"),
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
@ -633,11 +653,11 @@ export function CameraGroupEdit({
|
||||
return !value.includes(".");
|
||||
},
|
||||
{
|
||||
message: "Camera group name must not contain a period.",
|
||||
message: t("ui.cameraGroup.name.errorMessage.nameMustNotPeriod"),
|
||||
},
|
||||
)
|
||||
.refine((value: string) => value.toLowerCase() !== "default", {
|
||||
message: "Invalid camera group name.",
|
||||
message: t("ui.cameraGroup.name.errorMessage.invalid"),
|
||||
}),
|
||||
|
||||
cameras: z.array(z.string()),
|
||||
@ -682,22 +702,30 @@ export function CameraGroupEdit({
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
toast.success(`Camera group (${values.name}) has been saved.`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.success(
|
||||
t("ui.cameraGroup.toast.success", { name: values.name }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
updateConfig();
|
||||
if (onSave) {
|
||||
onSave();
|
||||
}
|
||||
} else {
|
||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("ui.cameraGroup.toast.error", { error: res.statusText }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(
|
||||
`Failed to save config changes: ${error.response.data.message}`,
|
||||
t("ui.cameraGroup.toast.error", {
|
||||
error: error.response.data.message,
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
})
|
||||
@ -729,11 +757,13 @@ export function CameraGroupEdit({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>
|
||||
<Trans>ui.cameraGroup.name</Trans>
|
||||
</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]"
|
||||
placeholder="Enter a name..."
|
||||
placeholder={t("ui.cameraGroup.name.placeholder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -749,9 +779,11 @@ export function CameraGroupEdit({
|
||||
name="cameras"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cameras</FormLabel>
|
||||
<FormLabel>
|
||||
<Trans>ui.cameraGroup.cameras</Trans>
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Select cameras for this group.
|
||||
<Trans>ui.cameraGroup.cameras.desc</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
{[
|
||||
@ -782,7 +814,9 @@ export function CameraGroupEdit({
|
||||
name="icon"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col space-y-2">
|
||||
<FormLabel>Icon</FormLabel>
|
||||
<FormLabel>
|
||||
<Trans>ui.cameraGroup.icon</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<IconPicker
|
||||
selectedIcon={{
|
||||
@ -810,7 +844,7 @@ export function CameraGroupEdit({
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
<Trans>ui.cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -822,10 +856,12 @@ export function CameraGroupEdit({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>Saving...</span>
|
||||
<span>
|
||||
<Trans>ui.saving</Trans>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
"Save"
|
||||
<Trans>ui.save</Trans>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -13,6 +13,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
import { FaVideo } from "react-icons/fa";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type CameraFilterButtonProps = {
|
||||
allCameras: string[];
|
||||
@ -139,7 +140,7 @@ export function CamerasFilterContent({
|
||||
{isMobile && (
|
||||
<>
|
||||
<DropdownMenuLabel className="flex justify-center">
|
||||
Cameras
|
||||
<Trans>ui.filter.allCameras.short</Trans>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
@ -147,7 +148,7 @@ export function CamerasFilterContent({
|
||||
<div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
|
||||
<FilterSwitch
|
||||
isChecked={currentCameras == undefined}
|
||||
label="All Cameras"
|
||||
label={t("ui.filter.allCameras")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setCurrentCameras(undefined);
|
||||
@ -212,7 +213,7 @@ export function CamerasFilterContent({
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
<Trans>ui.apply</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
@ -221,7 +222,7 @@ export function CamerasFilterContent({
|
||||
updateCameraFilter(undefined);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -354,7 +354,7 @@ function GeneralFilterButton({
|
||||
: "text-primary"
|
||||
}`}
|
||||
>
|
||||
<Trans>ui.reviewFilter.filter</Trans>
|
||||
<Trans>ui.filter</Trans>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -462,7 +462,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
<Trans>ui.reviewFilter.filter.allLabels</Trans>
|
||||
<Trans>ui.filter.allLabels</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -509,7 +509,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allZones"
|
||||
>
|
||||
<Trans>ui.reviewFilter.filter.allZones</Trans>
|
||||
<Trans>ui.filter.allZones</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
|
||||
@ -24,6 +24,8 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
||||
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type SearchFilterGroupProps = {
|
||||
className: string;
|
||||
@ -190,7 +192,9 @@ export default function SearchFilterGroup({
|
||||
to: new Date(filter.before * 1000),
|
||||
}
|
||||
}
|
||||
defaultText={isMobile ? "Dates" : "All Dates"}
|
||||
defaultText={
|
||||
isMobile ? t("ui.filter.allDates.short") : t("ui.filter.allDates")
|
||||
}
|
||||
updateSelectedRange={onUpdateSelectedRange}
|
||||
/>
|
||||
)}
|
||||
@ -231,18 +235,18 @@ function GeneralFilterButton({
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
if (isMobile) {
|
||||
return "Labels";
|
||||
return t("ui.filter.allLabels.short");
|
||||
}
|
||||
|
||||
if (!selectedLabels || selectedLabels.length == 0) {
|
||||
return "All Labels";
|
||||
return t("ui.filter.allLabels");
|
||||
}
|
||||
|
||||
if (selectedLabels.length == 1) {
|
||||
return selectedLabels[0];
|
||||
return t("object." + selectedLabels[0]);
|
||||
}
|
||||
|
||||
return `${selectedLabels.length} Labels`;
|
||||
return t("ui.filter.countLabels", { count: selectedLabels.length });
|
||||
}, [selectedLabels]);
|
||||
|
||||
// ui
|
||||
@ -326,7 +330,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
All Labels
|
||||
<Trans>ui.filter.allLabels</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -343,7 +347,7 @@ export function GeneralFilterContent({
|
||||
{allLabels.map((item) => (
|
||||
<FilterSwitch
|
||||
key={item}
|
||||
label={item.replaceAll("_", " ")}
|
||||
label={t("object." + item)}
|
||||
isChecked={currentLabels?.includes(item) ?? false}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
@ -378,7 +382,7 @@ export function GeneralFilterContent({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
<Trans>ui.apply</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
@ -387,7 +391,7 @@ export function GeneralFilterContent({
|
||||
updateLabelFilter(undefined);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@ -436,7 +440,7 @@ function SortTypeButton({
|
||||
<div
|
||||
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`}
|
||||
>
|
||||
Sort
|
||||
<Trans>ui.filter.sort</Trans>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -492,11 +496,11 @@ export function SortTypeContent({
|
||||
onClose,
|
||||
}: SortTypeContentProps) {
|
||||
const sortLabels = {
|
||||
date_asc: "Date (Ascending)",
|
||||
date_desc: "Date (Descending)",
|
||||
score_asc: "Object Score (Ascending)",
|
||||
score_desc: "Object Score (Descending)",
|
||||
relevance: "Relevance",
|
||||
date_asc: t("ui.filter.sort.dateAsc"),
|
||||
date_desc: t("ui.filter.sort.dateDesc"),
|
||||
score_asc: t("ui.filter.sort.scoreAsc"),
|
||||
score_desc: t("ui.filter.sort.scoreDesc"),
|
||||
relevance: t("ui.filter.sort.relevance"),
|
||||
};
|
||||
|
||||
return (
|
||||
@ -551,7 +555,7 @@ export function SortTypeContent({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
<Trans>ui.apply</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
@ -560,7 +564,7 @@ export function SortTypeContent({
|
||||
updateSortType(undefined);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -11,6 +11,8 @@ import { IoClose } from "react-icons/io5";
|
||||
import Heading from "../ui/heading";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
export type IconName = keyof typeof LuIcons;
|
||||
|
||||
@ -70,7 +72,7 @@ export default function IconPicker({
|
||||
className="mt-2 w-full text-muted-foreground"
|
||||
aria-label="Select an icon"
|
||||
>
|
||||
Select an icon
|
||||
<Trans>ui.iconPicker.selectIcon</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="hover:cursor-pointer">
|
||||
@ -101,7 +103,9 @@ export default function IconPicker({
|
||||
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
|
||||
>
|
||||
<div className="mb-3 flex flex-row items-center justify-between">
|
||||
<Heading as="h4">Select an icon</Heading>
|
||||
<Heading as="h4">
|
||||
<Trans>ui.iconPicker.selectIcon</Trans>
|
||||
</Heading>
|
||||
<span tabIndex={0} className="sr-only" />
|
||||
<IoClose
|
||||
size={15}
|
||||
@ -113,7 +117,7 @@ export default function IconPicker({
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search for an icon..."
|
||||
placeholder={t("ui.iconPicker.search.placeholder")}
|
||||
className="text-md mb-3 md:text-sm"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
|
||||
@ -30,6 +30,8 @@ import { getUTCOffset } from "@/utils/dateUtil";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
const EXPORT_OPTIONS = [
|
||||
"1",
|
||||
@ -68,12 +70,14 @@ export default function ExportDialog({
|
||||
|
||||
const onStartExport = useCallback(() => {
|
||||
if (!range) {
|
||||
toast.error("No valid time range selected", { position: "top-center" });
|
||||
toast.error(t("ui.dialog.export.toast.error.noVaildTimeSelected"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (range.before < range.after) {
|
||||
toast.error("End time must be after start time", {
|
||||
toast.error(t("ui.dialog.export.toast.error.endTimeMustAfterStartTime"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
@ -89,10 +93,9 @@ export default function ExportDialog({
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
"Successfully started export. View the file in the /exports folder.",
|
||||
{ position: "top-center" },
|
||||
);
|
||||
toast.success(t("ui.dialog.export.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
setName("");
|
||||
setRange(undefined);
|
||||
setMode("none");
|
||||
@ -100,14 +103,18 @@ export default function ExportDialog({
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response?.data?.message) {
|
||||
// api error message need to be translated
|
||||
toast.error(
|
||||
`Failed to start export: ${error.response.data.message}`,
|
||||
`${t("ui.dialog.export.toast.error.failed", { error: error.response.data.message })}`,
|
||||
{ position: "top-center" },
|
||||
);
|
||||
} else {
|
||||
toast.error(`Failed to start export: ${error.message}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
`${t("ui.dialog.export.toast.error.failed", { error: error.message })}`,
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [camera, name, range, setRange, setName, setMode]);
|
||||
@ -157,7 +164,11 @@ export default function ExportDialog({
|
||||
}}
|
||||
>
|
||||
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
|
||||
{isDesktop && <div className="text-primary">Export</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>ui.menu.export</Trans>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</Trigger>
|
||||
<Content
|
||||
@ -250,7 +261,9 @@ export function ExportContent({
|
||||
{isDesktop && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Trans>ui.menu.export</Trans>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SelectSeparator className="my-4 bg-secondary" />
|
||||
</>
|
||||
@ -274,9 +287,11 @@ export function ExportContent({
|
||||
<Label className="cursor-pointer capitalize" htmlFor={opt}>
|
||||
{isNaN(parseInt(opt))
|
||||
? opt == "timeline"
|
||||
? "Select from Timeline"
|
||||
: `${opt}`
|
||||
: `Last ${opt > "1" ? `${opt} Hours` : "Hour"}`}
|
||||
? t("ui.dialog.export.time.fromTimeline")
|
||||
: t("ui.dialog.export.time." + opt)
|
||||
: t("ui.dialog.export.time.lastHour", {
|
||||
count: parseInt(opt),
|
||||
})}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
@ -292,7 +307,7 @@ export function ExportContent({
|
||||
<Input
|
||||
className="text-md my-6"
|
||||
type="search"
|
||||
placeholder="Name the Export"
|
||||
placeholder={t("ui.dialog.export.name.placeholder")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
@ -304,7 +319,7 @@ export function ExportContent({
|
||||
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
<Trans>ui.cancel</Trans>
|
||||
</div>
|
||||
<Button
|
||||
className={isDesktop ? "" : "w-full"}
|
||||
@ -322,7 +337,9 @@ export function ExportContent({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedOption == "timeline" ? "Select" : "Export"}
|
||||
{selectedOption == "timeline"
|
||||
? t("ui.dialog.export.select")
|
||||
: t("ui.dialog.export.export")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
@ -382,14 +399,14 @@ function CustomTimeSelector({
|
||||
const formattedStart = useFormattedTimestamp(
|
||||
startTime,
|
||||
config?.ui.time_format == "24hour"
|
||||
? "%b %-d, %H:%M:%S"
|
||||
: "%b %-d, %I:%M:%S %p",
|
||||
? t("ui.time.formattedTimestamp.24hour")
|
||||
: t("ui.time.formattedTimestamp"),
|
||||
);
|
||||
const formattedEnd = useFormattedTimestamp(
|
||||
endTime,
|
||||
config?.ui.time_format == "24hour"
|
||||
? "%b %-d, %H:%M:%S"
|
||||
: "%b %-d, %I:%M:%S %p",
|
||||
? t("ui.time.formattedTimestamp.24hour")
|
||||
: t("ui.time.formattedTimestamp"),
|
||||
);
|
||||
|
||||
const startClock = useMemo(() => {
|
||||
@ -576,9 +593,11 @@ export function ExportPreviewDialog({
|
||||
)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Preview Export</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Preview Export
|
||||
<Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<GenericVideoPlayer source={source} />
|
||||
|
||||
@ -14,6 +14,7 @@ import { toast } from "sonner";
|
||||
import axios from "axios";
|
||||
import SaveExportOverlay from "./SaveExportOverlay";
|
||||
import { isIOS, isMobile } from "react-device-detect";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
||||
|
||||
@ -89,10 +90,9 @@ export default function MobileReviewSettingsDrawer({
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
"Successfully started export. View the file in the /exports folder.",
|
||||
{ position: "top-center" },
|
||||
);
|
||||
toast.success(t("ui.dialog.export.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
setName("");
|
||||
setRange(undefined);
|
||||
setMode("none");
|
||||
@ -238,7 +238,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@ import { LuVideo, LuX } from "react-icons/lu";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaCompactDisc } from "react-icons/fa";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type SaveExportOverlayProps = {
|
||||
className: string;
|
||||
@ -33,7 +34,7 @@ export default function SaveExportOverlay({
|
||||
onClick={onCancel}
|
||||
>
|
||||
<LuX />
|
||||
Cancel
|
||||
<Trans>ui.cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
@ -42,7 +43,7 @@ export default function SaveExportOverlay({
|
||||
onClick={onPreview}
|
||||
>
|
||||
<LuVideo />
|
||||
Preview Export
|
||||
<Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
@ -52,7 +53,7 @@ export default function SaveExportOverlay({
|
||||
onClick={onSave}
|
||||
>
|
||||
<FaCompactDisc />
|
||||
Save Export
|
||||
<Trans>ui.dialog.export.fromTimeline.saveExport</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type SearchFilterDialogProps = {
|
||||
config?: FrigateConfig;
|
||||
@ -91,7 +92,7 @@ export default function SearchFilterDialog({
|
||||
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
|
||||
)}
|
||||
/>
|
||||
More Filters
|
||||
<Trans>ui.filter.more</Trans>
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
@ -165,7 +166,7 @@ export default function SearchFilterDialog({
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
<Trans>ui.apply</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset filters to default values"
|
||||
@ -183,7 +184,7 @@ export default function SearchFilterDialog({
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -260,7 +261,9 @@ function TimeRangeFilterContent({
|
||||
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<div className="text-lg">Time Range</div>
|
||||
<div className="text-lg">
|
||||
<Trans>ui.filter.timeRange</Trans>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-row items-center justify-center gap-2">
|
||||
<Popover
|
||||
open={startOpen}
|
||||
@ -358,7 +361,9 @@ export function ZoneFilterContent({
|
||||
<>
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="text-lg">Zones</div>
|
||||
<div className="text-lg">
|
||||
<Trans>ui.filter.zones</Trans>
|
||||
</div>
|
||||
{allZones && (
|
||||
<>
|
||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||
@ -366,7 +371,7 @@ export function ZoneFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allZones"
|
||||
>
|
||||
All Zones
|
||||
<Trans>ui.filter.allZones</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -424,10 +429,12 @@ export function SubFilterContent({
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="text-lg">Sub Labels</div>
|
||||
<div className="text-lg">
|
||||
<Trans>ui.filter.subLabels</Trans>
|
||||
</div>
|
||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||
<Label className="mx-2 cursor-pointer text-primary" htmlFor="allLabels">
|
||||
All Sub Labels
|
||||
<Trans>ui.filter.allSubLabels</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -482,7 +489,9 @@ export function ScoreFilterContent({
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="mb-3 text-lg">Score</div>
|
||||
<div className="mb-3 text-lg">
|
||||
<Trans>ui.filter.score</Trans>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
className="w-14 text-center"
|
||||
@ -568,7 +577,9 @@ export function SnapshotClipFilterContent({
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="mb-3 text-lg">Features</div>
|
||||
<div className="mb-3 text-lg">
|
||||
<Trans>ui.filter.features</Trans>
|
||||
</div>
|
||||
|
||||
<div className="my-2.5 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -590,7 +601,7 @@ export function SnapshotClipFilterContent({
|
||||
htmlFor="snapshot-filter"
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
>
|
||||
Has a snapshot
|
||||
<Trans>ui.filter.features.hasSnapshot</Trans>
|
||||
</Label>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
@ -611,14 +622,14 @@ export function SnapshotClipFilterContent({
|
||||
aria-label="Yes"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
Yes
|
||||
<Trans>ui.yes</Trans>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
No
|
||||
<Trans>ui.no</Trans>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
@ -652,12 +663,9 @@ export function SnapshotClipFilterContent({
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
>
|
||||
You must first filter on tracked objects that have a
|
||||
snapshot.
|
||||
<br />
|
||||
<br />
|
||||
Tracked objects without a snapshot cannot be submitted to
|
||||
Frigate+.
|
||||
<Trans>
|
||||
ui.filter.features.submittedToFrigatePlus.tips
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
@ -666,7 +674,7 @@ export function SnapshotClipFilterContent({
|
||||
htmlFor="plus-filter"
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
>
|
||||
Submitted to Frigate+
|
||||
<Trans>ui.filter.features.submittedToFrigatePlus</Trans>
|
||||
</Label>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
@ -692,14 +700,14 @@ export function SnapshotClipFilterContent({
|
||||
aria-label="Yes"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
Yes
|
||||
<Trans>ui.yes</Trans>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
No
|
||||
<Trans>ui.no</Trans>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
@ -728,7 +736,7 @@ export function SnapshotClipFilterContent({
|
||||
htmlFor="clip-filter"
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
>
|
||||
Has a video clip
|
||||
<Trans>ui.filter.features.hasVideoClip</Trans>
|
||||
</Label>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
@ -747,14 +755,14 @@ export function SnapshotClipFilterContent({
|
||||
aria-label="Yes"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
Yes
|
||||
<Trans>ui.yes</Trans>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
No
|
||||
<Trans>ui.no</Trans>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
getPreviewForTimeRange,
|
||||
usePreviewForTimeRange,
|
||||
} from "@/hooks/use-camera-previews";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type PreviewPlayerProps = {
|
||||
className?: string;
|
||||
@ -88,7 +89,7 @@ export default function PreviewPlayer({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
No Preview Found
|
||||
<Trans>ui.player.noPreviewFound</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -324,7 +325,9 @@ function PreviewVideoPlayer({
|
||||
</video>
|
||||
{cameraPreviews && !currentPreview && (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
|
||||
No Preview Found for {camera.replaceAll("_", " ")}
|
||||
<Trans value={{ camera: camera.replaceAll("_", " ") }}>
|
||||
ui.player.noPreviewFoundFor
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
||||
@ -544,7 +547,9 @@ function PreviewFramesPlayer({
|
||||
/>
|
||||
{previewFrames?.length === 0 && (
|
||||
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
|
||||
No Preview Found for {camera.replaceAll("_", " ")}
|
||||
<Trans values={{ cameraName: camera.replaceAll("_", " ") }}>
|
||||
ui.player.noPreviewFoundFor
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
||||
|
||||
@ -12,6 +12,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { VideoResolutionType } from "@/types/live";
|
||||
import axios from "axios";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Dynamically switches between video playback and scrubbing preview player.
|
||||
@ -247,7 +248,7 @@ export default function DynamicVideoPlayer({
|
||||
)}
|
||||
{!isScrubbing && !isLoading && noRecording && (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
No recordings found for this time
|
||||
<Trans>ui.player.noRecordingsFoundForThisTime</Trans>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -204,7 +204,9 @@ export default function MotionMaskEditPane({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Edit Motion Mask - Frigate";
|
||||
document.title = t(
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle",
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!polygon) {
|
||||
|
||||
@ -238,7 +238,9 @@ export default function ObjectMaskEditPane({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Edit Object Mask - Frigate";
|
||||
document.title = t(
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle",
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!polygon) {
|
||||
|
||||
@ -17,8 +17,10 @@ import FilterSwitch from "../filter/FilterSwitch";
|
||||
import { SearchFilter, SearchSource } from "@/types/search";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type SearchSettingsProps = {
|
||||
type ExploreSettingsProps = {
|
||||
className?: string;
|
||||
columns: number;
|
||||
defaultView: string;
|
||||
@ -27,7 +29,7 @@ type SearchSettingsProps = {
|
||||
setDefaultView: (view: string) => void;
|
||||
onUpdateFilter: (filter: SearchFilter) => void;
|
||||
};
|
||||
export default function SearchSettings({
|
||||
export default function ExploreSettings({
|
||||
className,
|
||||
columns,
|
||||
setColumns,
|
||||
@ -35,7 +37,7 @@ export default function SearchSettings({
|
||||
filter,
|
||||
setDefaultView,
|
||||
onUpdateFilter,
|
||||
}: SearchSettingsProps) {
|
||||
}: ExploreSettingsProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@ -46,21 +48,22 @@ export default function SearchSettings({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Search Settings"
|
||||
aria-label="Explore Settings"
|
||||
size="sm"
|
||||
>
|
||||
<FaCog className="text-secondary-foreground" />
|
||||
Settings
|
||||
<Trans>ui.searchView.settings</Trans>
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
<div className={cn(className, "my-3 space-y-5 py-3 md:mt-0 md:py-0")}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Default View</div>
|
||||
<div className="text-md">
|
||||
<Trans>ui.searchView.settings.defaultView</Trans>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
When no filters are selected, display a summary of the most recent
|
||||
tracked objects per label, or display an unfiltered grid.
|
||||
<Trans>ui.searchView.settings.defaultView.desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
@ -68,7 +71,9 @@ export default function SearchSettings({
|
||||
onValueChange={(value) => setDefaultView(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{defaultView == "summary" ? "Summary" : "Unfiltered Grid"}
|
||||
{defaultView == "summary"
|
||||
? t("ui.searchView.settings.defaultView.summary")
|
||||
: t("ui.searchView.settings.defaultView.unfilteredGrid")}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@ -78,7 +83,9 @@ export default function SearchSettings({
|
||||
className="cursor-pointer"
|
||||
value={value}
|
||||
>
|
||||
{value == "summary" ? "Summary" : "Unfiltered Grid"}
|
||||
{value == "summary"
|
||||
? t("ui.searchView.settings.defaultView.summary")
|
||||
: t("ui.searchView.settings.defaultView.unfilteredGrid")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@ -90,9 +97,11 @@ export default function SearchSettings({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Grid Columns</div>
|
||||
<div className="text-md">
|
||||
<Trans>ui.searchView.settings.gridColumns</Trans>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
Select the number of columns in the grid view.
|
||||
<Trans>ui.searchView.settings.gridColumns.desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
@ -153,10 +162,11 @@ export function SearchTypeContent({
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Search Source</div>
|
||||
<div className="text-md">
|
||||
<Trans>ui.searchView.settings.searchSource</Trans>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
Choose whether to search the thumbnails or descriptions of your
|
||||
tracked objects.
|
||||
<Trans>ui.searchView.settings.searchSource.desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2.5 flex flex-col gap-2.5">
|
||||
|
||||
@ -321,7 +321,9 @@ export default function ZoneEditPane({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Edit Zone - Frigate";
|
||||
document.title = t(
|
||||
"ui.settingView.masksAndZonesSettings.zone.documentTitle",
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!polygon) {
|
||||
|
||||
@ -12,6 +12,8 @@ import {
|
||||
import { Switch } from "./switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LuCheck } from "react-icons/lu";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
export interface DateRangePickerProps {
|
||||
/** Click handler for applying the updates from DateRangePicker. */
|
||||
@ -59,15 +61,15 @@ interface Preset {
|
||||
|
||||
// Define presets
|
||||
const PRESETS: Preset[] = [
|
||||
{ name: "today", label: "Today" },
|
||||
{ name: "yesterday", label: "Yesterday" },
|
||||
{ name: "last7", label: "Last 7 days" },
|
||||
{ name: "last14", label: "Last 14 days" },
|
||||
{ name: "last30", label: "Last 30 days" },
|
||||
{ name: "thisWeek", label: "This Week" },
|
||||
{ name: "lastWeek", label: "Last Week" },
|
||||
{ name: "thisMonth", label: "This Month" },
|
||||
{ name: "lastMonth", label: "Last Month" },
|
||||
{ name: "today", label: t("ui.time.today") },
|
||||
{ name: "yesterday", label: t("ui.time.yesterday") },
|
||||
{ name: "last7", label: t("ui.time.last7") },
|
||||
{ name: "last14", label: t("ui.time.last14") },
|
||||
{ name: "last30", label: t("ui.time.last30") },
|
||||
{ name: "thisWeek", label: t("ui.time.thisWeek") },
|
||||
{ name: "lastWeek", label: t("ui.time.lastWeek") },
|
||||
{ name: "thisMonth", label: t("ui.time.thisMonth") },
|
||||
{ name: "lastMonth", label: t("ui.time.lastMonth") },
|
||||
];
|
||||
|
||||
/** The DateRangePicker component allows a user to select a range of dates */
|
||||
@ -429,7 +431,7 @@ export function DateRangePicker({
|
||||
}
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
<Trans>ui.apply</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -440,7 +442,7 @@ export function DateRangePicker({
|
||||
variant="ghost"
|
||||
aria-label="Reset"
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,24 @@
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { enUS, Locale, zhCN } from "date-fns/locale";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import i18n from "@/utils/i18n";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
|
||||
let locale: Locale;
|
||||
switch(i18n.language) {
|
||||
case "zh-CN":
|
||||
locale = zhCN;
|
||||
break;
|
||||
default:
|
||||
locale = enUS;
|
||||
break;
|
||||
}
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
@ -15,6 +27,7 @@ function Calendar({
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
locale={locale}
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
import EventView from "@/views/events/EventView";
|
||||
import { RecordingView } from "@/views/recording/RecordingView";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -76,9 +77,9 @@ export default function Events() {
|
||||
|
||||
useEffect(() => {
|
||||
if (recording) {
|
||||
document.title = "Recordings - Frigate";
|
||||
document.title = t("ui.review.recordings.documentTitle");
|
||||
} else {
|
||||
document.title = `Review - Frigate`;
|
||||
document.title = t("ui.review.documentTitle");
|
||||
}
|
||||
}, [recording, severity]);
|
||||
|
||||
|
||||
@ -17,8 +17,10 @@ import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DeleteClipType, Export } from "@/types/export";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { Trans } from "react-i18next";
|
||||
import { LuFolderX } from "react-icons/lu";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
@ -27,7 +29,7 @@ function Exports() {
|
||||
const { data: exports, mutate } = useSWR<Export[]>("exports");
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Export - Frigate";
|
||||
document.title = t("ui.exportView.documentTitle");
|
||||
}, []);
|
||||
|
||||
// Search
|
||||
@ -116,20 +118,26 @@ function Exports() {
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Export</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
<Trans>ui.exportView.deleteExport</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete {deleteClip?.exportName}?
|
||||
<Trans values={{ exportName: deleteClip?.exportName }}>
|
||||
ui.exportView.deleteExport.desc
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
<Trans>ui.cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Delete Export"
|
||||
variant="destructive"
|
||||
onClick={() => onHandleDelete()}
|
||||
>
|
||||
Delete
|
||||
<Trans>ui.delete</Trans>
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -177,7 +185,7 @@ function Exports() {
|
||||
<div className="flex w-full items-center justify-center p-2">
|
||||
<Input
|
||||
className="text-md w-full bg-muted md:w-1/3"
|
||||
placeholder="Search"
|
||||
placeholder={t("ui.exportView.search")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
@ -205,7 +213,7 @@ function Exports() {
|
||||
) : (
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||
<LuFolderX className="size-16" />
|
||||
No exports found
|
||||
<Trans>ui.exportView.noExports</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -9,6 +9,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
|
||||
import LiveCameraView from "@/views/live/LiveCameraView";
|
||||
import LiveDashboardView from "@/views/live/LiveDashboardView";
|
||||
import { t } from "i18next";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -64,11 +65,15 @@ function Live() {
|
||||
.split("_")
|
||||
.filter((text) => text)
|
||||
.map((text) => text[0].toUpperCase() + text.substring(1));
|
||||
document.title = `${capitalized.join(" ")} - Live - Frigate`;
|
||||
document.title = t("ui.live.documentTitle.withCamera", {
|
||||
camera: capitalized.join(" "),
|
||||
});
|
||||
} else if (cameraGroup && cameraGroup != "default") {
|
||||
document.title = `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)} - Live - Frigate`;
|
||||
document.title = t("ui.live.documentTitle.withCamera", {
|
||||
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
|
||||
});
|
||||
} else {
|
||||
document.title = "Live - Frigate";
|
||||
document.title = t("ui.live.documentTitle");
|
||||
}
|
||||
}, [cameraGroup, selectedCameraName]);
|
||||
|
||||
|
||||
@ -35,13 +35,13 @@ import MotionTunerView from "@/views/settings/MotionTunerView";
|
||||
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
|
||||
import AuthenticationView from "@/views/settings/AuthenticationView";
|
||||
import NotificationView from "@/views/settings/NotificationsSettingsView";
|
||||
import SearchSettingsView from "@/views/settings/SearchSettingsView";
|
||||
import ExploreSettingsView from "@/views/settings/SearchSettingsView";
|
||||
import UiSettingsView from "@/views/settings/UiSettingsView";
|
||||
import { t } from "i18next";
|
||||
|
||||
const allSettingsViews = [
|
||||
"uiSettings",
|
||||
"searchSettings",
|
||||
"exploreSettings",
|
||||
"cameraSettings",
|
||||
"masksAndZones",
|
||||
"motionTuner",
|
||||
@ -178,8 +178,8 @@ export default function Settings() {
|
||||
</div>
|
||||
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
|
||||
{page == "uiSettings" && <UiSettingsView />}
|
||||
{page == "searchSettings" && (
|
||||
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} />
|
||||
{page == "exploreSettings" && (
|
||||
<ExploreSettingsView setUnsavedChanges={setUnsavedChanges} />
|
||||
)}
|
||||
{page == "debug" && (
|
||||
<ObjectSettingsView selectedCamera={selectedCamera} />
|
||||
|
||||
@ -32,16 +32,18 @@ i18n
|
||||
},
|
||||
keySeparator: false,
|
||||
parseMissingKeyHandler: (key: string) => {
|
||||
console.debug("Missing key: " + key);
|
||||
const parts = key.split('.');
|
||||
const parts = key.split(".");
|
||||
if (parts.length > 1) {
|
||||
if (parts[0] === 'object' || parts[0] === 'audio') {
|
||||
return parts[1].replaceAll("_", " ").charAt(0).toUpperCase() + parts[1].slice(1);
|
||||
if (parts[0] === "object" || parts[0] === "audio") {
|
||||
return (
|
||||
parts[1].replaceAll("_", " ").charAt(0).toUpperCase() +
|
||||
parts[1].slice(1)
|
||||
);
|
||||
}
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@ -54,6 +54,7 @@ import { GiSoundWaves } from "react-icons/gi";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type EventViewProps = {
|
||||
reviewItems?: SegmentedReviewData;
|
||||
@ -194,10 +195,9 @@ export default function EventView({
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
"Successfully started export. View the file in the /exports folder.",
|
||||
{ position: "top-center" },
|
||||
);
|
||||
toast.success(t("ui.dialog.export.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@ -412,7 +412,9 @@ export default function LiveCameraView({
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-secondary-foreground">Back</div>
|
||||
<div className="text-secondary-foreground">
|
||||
<Trans>ui.back</Trans>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -46,6 +46,7 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFullscreen } from "@/hooks/use-fullscreen";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
const SEGMENT_DURATION = 30;
|
||||
|
||||
@ -385,7 +386,11 @@ export function RecordingView({
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">Back</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>ui.back</Trans>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
@ -396,7 +401,11 @@ export function RecordingView({
|
||||
}}
|
||||
>
|
||||
<FaVideo className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">Live</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>ui.menu.live</Trans>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@ -455,14 +464,18 @@ export function RecordingView({
|
||||
value="timeline"
|
||||
aria-label="Select timeline"
|
||||
>
|
||||
<div className="">Timeline</div>
|
||||
<div className="">
|
||||
<Trans>ui.review.timeline</Trans>
|
||||
</div>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
|
||||
value="events"
|
||||
aria-label="Select events"
|
||||
>
|
||||
<div className="">Events</div>
|
||||
<div className="">
|
||||
<Trans>ui.review.events</Trans>
|
||||
</div>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
) : (
|
||||
@ -728,7 +741,7 @@ function Timeline({
|
||||
>
|
||||
{mainCameraReviewItems.length === 0 ? (
|
||||
<div className="mt-5 text-center text-primary">
|
||||
No events found for this time period.
|
||||
<Trans>ui.review.events.noFoundForTimePeriod</Trans>
|
||||
</div>
|
||||
) : (
|
||||
mainCameraReviewItems.map((review) => {
|
||||
|
||||
@ -22,7 +22,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { isEqual } from "lodash";
|
||||
import { formatDateToLocaleString } from "@/utils/dateUtil";
|
||||
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
|
||||
import SearchSettings from "@/components/settings/SearchSettings";
|
||||
import ExploreSettings from "@/components/settings/SearchSettings";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -31,6 +31,7 @@ import {
|
||||
import Chip from "@/components/indicators/Chip";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import SearchActionGroup from "@/components/filter/SearchActionGroup";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type SearchViewProps = {
|
||||
search: string;
|
||||
@ -481,7 +482,7 @@ export default function SearchView({
|
||||
filter={searchFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
/>
|
||||
<SearchSettings
|
||||
<ExploreSettings
|
||||
columns={columns}
|
||||
setColumns={setColumns}
|
||||
defaultView={defaultView}
|
||||
@ -517,7 +518,7 @@ export default function SearchView({
|
||||
{uniqueResults?.length == 0 && !isLoading && (
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||
<LuSearchX className="size-16" />
|
||||
No Tracked Objects Found
|
||||
<Trans>ui.searchView.noTrackedObjects</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -475,8 +475,8 @@ export default function CameraSettingsView({
|
||||
|
||||
<div className="text-sm">
|
||||
{watchedDetectionsZones &&
|
||||
watchedDetectionsZones.length > 0
|
||||
? !selectDetections ? (
|
||||
watchedDetectionsZones.length > 0 ? (
|
||||
!selectDetections ? (
|
||||
<Trans
|
||||
i18nKey="ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips"
|
||||
values={{
|
||||
@ -513,7 +513,7 @@ export default function CameraSettingsView({
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips"
|
||||
values={{
|
||||
|
||||
@ -23,19 +23,19 @@ import {
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type SearchSettingsViewProps = {
|
||||
type ExploreSettingsViewProps = {
|
||||
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
type SearchSettings = {
|
||||
type ExploreSettings = {
|
||||
enabled?: boolean;
|
||||
reindex?: boolean;
|
||||
model_size?: SearchModelSize;
|
||||
};
|
||||
|
||||
export default function SearchSettingsView({
|
||||
export default function ExploreSettingsView({
|
||||
setUnsavedChanges,
|
||||
}: SearchSettingsViewProps) {
|
||||
}: ExploreSettingsViewProps) {
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
const [changedValue, setChangedValue] = useState(false);
|
||||
@ -43,29 +43,30 @@ export default function SearchSettingsView({
|
||||
|
||||
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
|
||||
|
||||
const [searchSettings, setSearchSettings] = useState<SearchSettings>({
|
||||
const [ExploreSettings, setExploreSettings] = useState<ExploreSettings>({
|
||||
enabled: undefined,
|
||||
reindex: undefined,
|
||||
model_size: undefined,
|
||||
});
|
||||
|
||||
const [origSearchSettings, setOrigSearchSettings] = useState<SearchSettings>({
|
||||
enabled: undefined,
|
||||
reindex: undefined,
|
||||
model_size: undefined,
|
||||
});
|
||||
const [origExploreSettings, setOrigExploreSettings] =
|
||||
useState<ExploreSettings>({
|
||||
enabled: undefined,
|
||||
reindex: undefined,
|
||||
model_size: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
if (searchSettings?.enabled == undefined) {
|
||||
setSearchSettings({
|
||||
if (ExploreSettings?.enabled == undefined) {
|
||||
setExploreSettings({
|
||||
enabled: config.semantic_search.enabled,
|
||||
reindex: config.semantic_search.reindex,
|
||||
model_size: config.semantic_search.model_size,
|
||||
});
|
||||
}
|
||||
|
||||
setOrigSearchSettings({
|
||||
setOrigExploreSettings({
|
||||
enabled: config.semantic_search.enabled,
|
||||
reindex: config.semantic_search.reindex,
|
||||
model_size: config.semantic_search.model_size,
|
||||
@ -75,8 +76,8 @@ export default function SearchSettingsView({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config]);
|
||||
|
||||
const handleSearchConfigChange = (newConfig: Partial<SearchSettings>) => {
|
||||
setSearchSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
|
||||
const handleSearchConfigChange = (newConfig: Partial<ExploreSettings>) => {
|
||||
setExploreSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
|
||||
setUnsavedChanges(true);
|
||||
setChangedValue(true);
|
||||
};
|
||||
@ -86,7 +87,7 @@ export default function SearchSettingsView({
|
||||
|
||||
axios
|
||||
.put(
|
||||
`config/set?semantic_search.enabled=${searchSettings.enabled ? "True" : "False"}&semantic_search.reindex=${searchSettings.reindex ? "True" : "False"}&semantic_search.model_size=${searchSettings.model_size}`,
|
||||
`config/set?semantic_search.enabled=${ExploreSettings.enabled ? "True" : "False"}&semantic_search.reindex=${ExploreSettings.reindex ? "True" : "False"}&semantic_search.model_size=${ExploreSettings.model_size}`,
|
||||
{
|
||||
requires_restart: 0,
|
||||
},
|
||||
@ -115,16 +116,16 @@ export default function SearchSettingsView({
|
||||
});
|
||||
}, [
|
||||
updateConfig,
|
||||
searchSettings.enabled,
|
||||
searchSettings.reindex,
|
||||
searchSettings.model_size,
|
||||
ExploreSettings.enabled,
|
||||
ExploreSettings.reindex,
|
||||
ExploreSettings.model_size,
|
||||
]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setSearchSettings(origSearchSettings);
|
||||
setExploreSettings(origExploreSettings);
|
||||
setChangedValue(false);
|
||||
removeMessage("search_settings", "search_settings");
|
||||
}, [origSearchSettings, removeMessage]);
|
||||
}, [origExploreSettings, removeMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (changedValue) {
|
||||
@ -142,7 +143,7 @@ export default function SearchSettingsView({
|
||||
}, [changedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Search Settings - Frigate";
|
||||
document.title = "Explore Settings - Frigate";
|
||||
}, []);
|
||||
|
||||
if (!config) {
|
||||
@ -154,16 +155,16 @@ export default function SearchSettingsView({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans>ui.settingView.searchSettings</Trans>
|
||||
<Trans>ui.settingView.exploreSettings</Trans>
|
||||
</Heading>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch</Trans>
|
||||
<Trans>ui.settingView.exploreSettings.semanticSearch</Trans>
|
||||
</Heading>
|
||||
<div className="max-w-6xl">
|
||||
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch.desc</Trans>
|
||||
<Trans>ui.settingView.exploreSettings.semanticSearch.desc</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center text-primary">
|
||||
@ -174,7 +175,7 @@ export default function SearchSettingsView({
|
||||
className="inline"
|
||||
>
|
||||
<Trans>
|
||||
ui.settingView.searchSettings.semanticSearch.readTheDocumentation
|
||||
ui.settingView.exploreSettings.semanticSearch.readTheDocumentation
|
||||
</Trans>
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
@ -187,8 +188,8 @@ export default function SearchSettingsView({
|
||||
<Switch
|
||||
id="enabled"
|
||||
className="mr-3"
|
||||
disabled={searchSettings.enabled === undefined}
|
||||
checked={searchSettings.enabled === true}
|
||||
disabled={ExploreSettings.enabled === undefined}
|
||||
checked={ExploreSettings.enabled === true}
|
||||
onCheckedChange={(isChecked) => {
|
||||
handleSearchConfigChange({ enabled: isChecked });
|
||||
}}
|
||||
@ -204,8 +205,8 @@ export default function SearchSettingsView({
|
||||
<Switch
|
||||
id="reindex"
|
||||
className="mr-3"
|
||||
disabled={searchSettings.reindex === undefined}
|
||||
checked={searchSettings.reindex === true}
|
||||
disabled={ExploreSettings.reindex === undefined}
|
||||
checked={ExploreSettings.reindex === true}
|
||||
onCheckedChange={(isChecked) => {
|
||||
handleSearchConfigChange({ reindex: isChecked });
|
||||
}}
|
||||
@ -213,14 +214,14 @@ export default function SearchSettingsView({
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="reindex">
|
||||
<Trans>
|
||||
ui.settingView.searchSettings.semanticSearch.reindexOnStartup
|
||||
ui.settingView.exploreSettings.semanticSearch.reindexOnStartup
|
||||
</Trans>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
<Trans>
|
||||
ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc
|
||||
ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
@ -228,31 +229,31 @@ export default function SearchSettingsView({
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans>
|
||||
ui.settingView.searchSettings.semanticSearch.modelSize
|
||||
ui.settingView.exploreSettings.semanticSearch.modelSize
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
ui.settingView.searchSettings.semanticSearch.modelSize.desc
|
||||
ui.settingView.exploreSettings.semanticSearch.modelSize.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<ul className="list-disc pl-5 text-sm">
|
||||
<li>
|
||||
<Trans>
|
||||
ui.settingView.searchSettings.semanticSearch.modelSize.small.desc
|
||||
ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
ui.settingView.searchSettings.semanticSearch.modelSize.large.desc
|
||||
ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={searchSettings.model_size}
|
||||
value={ExploreSettings.model_size}
|
||||
onValueChange={(value) =>
|
||||
handleSearchConfigChange({
|
||||
model_size: value as SearchModelSize,
|
||||
@ -261,8 +262,8 @@ export default function SearchSettingsView({
|
||||
>
|
||||
<SelectTrigger className="w-20">
|
||||
{t(
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize." +
|
||||
searchSettings.model_size,
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize." +
|
||||
ExploreSettings.model_size,
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -274,7 +275,7 @@ export default function SearchSettingsView({
|
||||
value={size}
|
||||
>
|
||||
{t(
|
||||
"ui.settingView.searchSettings.semanticSearch.modelSize." +
|
||||
"ui.settingView.exploreSettings.semanticSearch.modelSize." +
|
||||
size,
|
||||
)}
|
||||
</SelectItem>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user