Add more translation

This commit is contained in:
ZhaiSoul 2025-01-02 23:44:48 +08:00
parent 7476fa1300
commit 08d0215673
32 changed files with 622 additions and 269 deletions

View File

@ -109,6 +109,20 @@
"audio.fire_alarm": "Fire alarm", "audio.fire_alarm": "Fire alarm",
"ui.time.justNow": "Just now", "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.title": "Are you sure you want to restart Frigate?",
"ui.dialog.restart.button": "Restart", "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.content": "This page will reload in {{countdown}} seconds.",
"ui.dialog.restart.restarting.button": "Force Reload Now", "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.ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
"ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)", "ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
"ui.stats.healthy": "System is healthy", "ui.stats.healthy": "System is healthy",
@ -209,6 +237,23 @@
"ui.menu.user.anonymous": "anonymous", "ui.menu.user.anonymous": "anonymous",
"ui.menu.user.logout": "Logout", "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.alerts": "Alerts",
"ui.eventView.detections": "Detections", "ui.eventView.detections": "Detections",
"ui.eventView.motion": "Motion", "ui.eventView.motion": "Motion",
@ -216,9 +261,33 @@
"ui.eventView.empty.alert": "There are no alerts to review", "ui.eventView.empty.alert": "There are no alerts to review",
"ui.eventView.empty.detection": "There are no detections to review", "ui.eventView.empty.detection": "There are no detections to review",
"ui.reviewFilter.filter": "Filter", "ui.filter": "Filter",
"ui.reviewFilter.filter.allLabels": "All Labels", "ui.filter.allLabels": "All Labels",
"ui.reviewFilter.filter.allZones": "All Zones", "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", "ui.reviewFilter.showReviewed": "Show Reviewed",
@ -236,8 +305,13 @@
"ui.pictureInPicture": "Picture in Picture", "ui.pictureInPicture": "Picture in Picture",
"ui.on": "ON", "ui.on": "ON",
"ui.off": "OFF", "ui.off": "OFF",
"ui.edit": "Edit",
"ui.delete": "Delete", "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.enable": "Enable Two Way Talk",
"ui.live.twoWayTalk.disable": "Disable Two Way Talk", "ui.live.twoWayTalk.disable": "Disable Two Way Talk",
"ui.live.cameraAudio.enable": "Enable Camera Audio", "ui.live.cameraAudio.enable": "Enable Camera Audio",
@ -261,10 +335,31 @@
"ui.live.autotracking.enable": "Enable Autotracking", "ui.live.autotracking.enable": "Enable Autotracking",
"ui.live.autotracking.disable": "Disable 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.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.uiSettings": "UI Settings",
"ui.settingView.menu.searchSettings": "Search Settings", "ui.settingView.menu.exploreSettings": "Explore Settings",
"ui.settingView.menu.cameraSettings": "Camera Settings", "ui.settingView.menu.cameraSettings": "Camera Settings",
"ui.settingView.menu.masksAndZones": "Masks / Zones", "ui.settingView.menu.masksAndZones": "Masks / Zones",
"ui.settingView.menu.motionTuner": "Motion Tuner", "ui.settingView.menu.motionTuner": "Motion Tuner",
@ -290,18 +385,18 @@
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "Sunday", "ui.settingView.generalSettings.calendar.firstWeekday.sunday": "Sunday",
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "Monday", "ui.settingView.generalSettings.calendar.firstWeekday.monday": "Monday",
"ui.settingView.searchSettings": "Search Settings", "ui.settingView.exploreSettings": "Explore Settings",
"ui.settingView.searchSettings.semanticSearch": "Semantic Search", "ui.settingView.exploreSettings.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.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.searchSettings.semanticSearch.readTheDocumentation": "Read the Documentation", "ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup", "ui.settingView.exploreSettings.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.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.searchSettings.semanticSearch.modelSize": "Model Size", "ui.settingView.exploreSettings.semanticSearch.modelSize": "Model Size",
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.", "ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "small", "ui.settingView.exploreSettings.semanticSearch.modelSize.small": "small",
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "large", "ui.settingView.exploreSettings.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.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.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.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": "Camera Settings",
"ui.settingView.cameraSettings.reviewClassification": "Review Classification", "ui.settingView.cameraSettings.reviewClassification": "Review Classification",
@ -319,6 +414,7 @@
"ui.settingView.masksAndZonesSettings": "Masks / Zones", "ui.settingView.masksAndZonesSettings": "Masks / Zones",
"ui.settingView.masksAndZonesSettings.zone": "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": "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.desc.documentation": "Documentation",
"ui.settingView.masksAndZonesSettings.zone.add": "Add Zone", "ui.settingView.masksAndZonesSettings.zone.add": "Add Zone",
@ -338,6 +434,7 @@
"ui.settingView.masksAndZonesSettings.zone.allObjects": "All Objects", "ui.settingView.masksAndZonesSettings.zone.allObjects": "All Objects",
"ui.settingView.masksAndZonesSettings.motionMasks": "Motion Mask", "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": "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.desc.documentation": "Documentation",
"ui.settingView.masksAndZonesSettings.motionMasks.add": "New Motion Mask", "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.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.motionMasks.polygonAreaTooLarge.documentation": "Read the documentation",
"ui.settingView.masksAndZonesSettings.objectMasks": "Object Masks", "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.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.documentation": "Documentation",
"ui.settingView.masksAndZonesSettings.objectMasks.add": "Add Object Mask", "ui.settingView.masksAndZonesSettings.objectMasks.add": "Add Object Mask",
@ -423,6 +520,11 @@
"ui.configEditorView.configEditor": "Config Editor", "ui.configEditorView.configEditor": "Config Editor",
"ui.configEditorView.copyConfig": "Copy Config", "ui.configEditorView.copyConfig": "Copy Config",
"ui.configEditorView.saveAndRestart": "Save & Restart", "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}}?"
} }

View File

@ -1,5 +1,4 @@
{ {
"object.person": "人", "object.person": "人",
"object.bicycle": "自行车", "object.bicycle": "自行车",
"object.car": "汽车", "object.car": "汽车",
@ -110,6 +109,22 @@
"audio.fire_alarm": "火灾警报器", "audio.fire_alarm": "火灾警报器",
"ui.time.justNow": "刚才", "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.title": "你确定要重启 Frigate?",
"ui.dialog.restart.button": "重启", "ui.dialog.restart.button": "重启",
@ -117,6 +132,21 @@
"ui.dialog.restart.restarting.content": "该页面将会在 {{countdown}} 秒后自动刷新。", "ui.dialog.restart.restarting.content": "该页面将会在 {{countdown}} 秒后自动刷新。",
"ui.dialog.restart.restarting.button": "强制刷新", "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.ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%",
"ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%", "ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%",
"ui.stats.healthy": "系统运行正常", "ui.stats.healthy": "系统运行正常",
@ -203,13 +233,31 @@
"ui.menu.live": "实时监控", "ui.menu.live": "实时监控",
"ui.menu.live.allCameras": "所有摄像头", "ui.menu.live.allCameras": "所有摄像头",
"ui.menu.review": "回放", "ui.menu.review": "回放",
"ui.menu.explore": "Explore", "ui.menu.explore": "探测",
"ui.menu.export": "导出", "ui.menu.export": "导出",
"ui.menu.uiPlayground": "UI Playground", "ui.menu.uiPlayground": "UI Playground",
"ui.menu.user.current": "当前用户:{{user}}", "ui.menu.user.current": "当前用户:{{user}}",
"ui.menu.user.anonymous": "匿名", "ui.menu.user.anonymous": "匿名",
"ui.menu.user.logout": "登出", "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.alerts": "警告",
"ui.eventView.detections": "检测", "ui.eventView.detections": "检测",
"ui.eventView.motion": "运动", "ui.eventView.motion": "运动",
@ -217,9 +265,33 @@
"ui.eventView.empty.alert": "还没有“警告”类回放", "ui.eventView.empty.alert": "还没有“警告”类回放",
"ui.eventView.empty.detection": "还没有“探测”类回放", "ui.eventView.empty.detection": "还没有“探测”类回放",
"ui.reviewFilter.filter": "过滤器", "ui.filter": "过滤器",
"ui.reviewFilter.filter.allLabels": "所有标签", "ui.filter.allLabels": "所有标签",
"ui.reviewFilter.filter.allZones": "所有区域", "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": "显示已查看的项目", "ui.reviewFilter.showReviewed": "显示已查看的项目",
@ -238,8 +310,13 @@
"ui.pictureInPicture": "画中画", "ui.pictureInPicture": "画中画",
"ui.on": "开", "ui.on": "开",
"ui.off": "关", "ui.off": "关",
"ui.edit": "编辑",
"ui.delete": "删除", "ui.delete": "删除",
"ui.yes": "是",
"ui.no": "否",
"ui.live.documentTitle": "实时监控 - Frigate",
"ui.live.documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate",
"ui.live.twoWayTalk.enable": "开启双向对话", "ui.live.twoWayTalk.enable": "开启双向对话",
"ui.live.twoWayTalk.disable": "关闭双向对话", "ui.live.twoWayTalk.disable": "关闭双向对话",
"ui.live.cameraAudio.enable": "开启摄像头音频", "ui.live.cameraAudio.enable": "开启摄像头音频",
@ -263,10 +340,31 @@
"ui.live.autotracking.enable": "启用自动追踪", "ui.live.autotracking.enable": "启用自动追踪",
"ui.live.autotracking.disable": "关闭自动追踪", "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.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.uiSettings": "界面设置",
"ui.settingView.menu.searchSettings": "搜索设置", "ui.settingView.menu.exploreSettings": "搜索设置",
"ui.settingView.menu.cameraSettings": "摄像头设置", "ui.settingView.menu.cameraSettings": "摄像头设置",
"ui.settingView.menu.masksAndZones": "屏罩 / 区域", "ui.settingView.menu.masksAndZones": "屏罩 / 区域",
"ui.settingView.menu.motionTuner": "运动调整器", "ui.settingView.menu.motionTuner": "运动调整器",
@ -292,18 +390,18 @@
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "星期天", "ui.settingView.generalSettings.calendar.firstWeekday.sunday": "星期天",
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "星期一", "ui.settingView.generalSettings.calendar.firstWeekday.monday": "星期一",
"ui.settingView.searchSettings": "搜索设置", "ui.settingView.exploreSettings": "探测设置",
"ui.settingView.searchSettings.semanticSearch": "语义搜索", "ui.settingView.exploreSettings.semanticSearch": "语义搜索",
"ui.settingView.searchSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。", "ui.settingView.exploreSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
"ui.settingView.searchSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)", "ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "启动时重新索引", "ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>", "ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>",
"ui.settingView.searchSettings.semanticSearch.modelSize": "模型大小", "ui.settingView.exploreSettings.semanticSearch.modelSize": "模型大小",
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小", "ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "小", "ui.settingView.exploreSettings.semanticSearch.modelSize.small": "小",
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "大", "ui.settingView.exploreSettings.semanticSearch.modelSize.large": "大",
"ui.settingView.searchSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存在CPU上也能较快的运行。质量较好。", "ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存在CPU上也能较快的运行。质量较好。",
"ui.settingView.searchSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型并在适用的情况下使用GPU。", "ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型并在适用的情况下使用GPU。",
"ui.settingView.cameraSettings": "摄像头设置", "ui.settingView.cameraSettings": "摄像头设置",
"ui.settingView.cameraSettings.reviewClassification": "预览分级", "ui.settingView.cameraSettings.reviewClassification": "预览分级",
@ -321,6 +419,7 @@
"ui.settingView.masksAndZonesSettings": "屏罩 / 区域", "ui.settingView.masksAndZonesSettings": "屏罩 / 区域",
"ui.settingView.masksAndZonesSettings.zone": "区域", "ui.settingView.masksAndZonesSettings.zone": "区域",
"ui.settingView.masksAndZonesSettings.zone.documentTitle": "编辑区域 - Frigate",
"ui.settingView.masksAndZonesSettings.zone.desc": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。", "ui.settingView.masksAndZonesSettings.zone.desc": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。",
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "文档(英文)", "ui.settingView.masksAndZonesSettings.zone.desc.documentation": "文档(英文)",
"ui.settingView.masksAndZonesSettings.zone.add": "添加区域", "ui.settingView.masksAndZonesSettings.zone.add": "添加区域",
@ -340,6 +439,7 @@
"ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象", "ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象",
"ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩", "ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩",
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "编辑运动遮罩 - Frigate",
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪", "ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)", "ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)",
"ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩", "ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩",
@ -355,6 +455,7 @@
"ui.settingView.masksAndZonesSettings.objectMasks": "对象遮罩", "ui.settingView.masksAndZonesSettings.objectMasks": "对象遮罩",
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle": "编辑对象遮罩 - Frigate",
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "对象过滤器用于防止特定位置的指定对象被误报。", "ui.settingView.masksAndZonesSettings.objectMasks.desc": "对象过滤器用于防止特定位置的指定对象被误报。",
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "文档(英文)", "ui.settingView.masksAndZonesSettings.objectMasks.documentation": "文档(英文)",
"ui.settingView.masksAndZonesSettings.objectMasks.add": "添加对象遮罩", "ui.settingView.masksAndZonesSettings.objectMasks.add": "添加对象遮罩",
@ -425,5 +526,12 @@
"ui.configEditorView.configEditor": "配置编辑器", "ui.configEditorView.configEditor": "配置编辑器",
"ui.configEditorView.copyConfig": "复制配置", "ui.configEditorView.copyConfig": "复制配置",
"ui.configEditorView.saveAndRestart": "保存并重启", "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}} 吗?"
} }

View File

@ -82,10 +82,9 @@ export default function ReviewCard({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success( toast.success(t("ui.dialog.export.toast.success"), {
"Successfully started export. View the file in the /exports folder.", position: "top-center",
{ position: "top-center" }, });
);
} }
}) })
.catch((error) => { .catch((error) => {

View File

@ -15,6 +15,7 @@ import { DateRange } from "react-day-picker";
import { useState } from "react"; import { useState } from "react";
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import { t } from "i18next"; import { t } from "i18next";
import { Trans } from "react-i18next";
type CalendarFilterButtonProps = { type CalendarFilterButtonProps = {
reviewSummary?: ReviewSummary; reviewSummary?: ReviewSummary;
@ -64,7 +65,7 @@ export default function CalendarFilterButton({
updateSelectedDay(undefined); updateSelectedDay(undefined);
}} }}
> >
Reset <Trans>ui.reset</Trans>
</Button> </Button>
</div> </div>
</> </>

View File

@ -67,6 +67,7 @@ import {
MobilePageTitle, MobilePageTitle,
} from "../mobile/MobilePage"; } from "../mobile/MobilePage";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { t } from "i18next";
type CameraGroupSelectorProps = { type CameraGroupSelectorProps = {
className?: string; className?: string;
@ -341,9 +342,11 @@ function NewGroupDialog({
className={cn(isDesktop && "mt-5", "justify-center")} className={cn(isDesktop && "mt-5", "justify-center")}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
> >
<Title>Camera Groups</Title> <Title>
<Trans>ui.cameraGroup</Trans>
</Title>
<Description className="sr-only"> <Description className="sr-only">
Edit camera groups <Trans>ui.cameraGroup.edit</Trans>
</Description> </Description>
<div <div
className={cn( className={cn(
@ -391,7 +394,11 @@ function NewGroupDialog({
}} }}
> >
<Title> <Title>
{editState == "add" ? "Add" : "Edit"} Camera Group {editState == "add" ? (
<Trans>ui.cameraGroup.add</Trans>
) : (
<Trans>ui.cameraGroup.edit</Trans>
)}
</Title> </Title>
<Description className="sr-only"> <Description className="sr-only">
Edit camera groups Edit camera groups
@ -464,8 +471,12 @@ export function EditGroupDialog({
> >
<div className="scrollbar-container flex flex-col overflow-y-auto md:my-4"> <div className="scrollbar-container flex flex-col overflow-y-auto md:my-4">
<Header className="mt-2" onClose={() => setOpen(false)}> <Header className="mt-2" onClose={() => setOpen(false)}>
<Title>Edit Camera Group</Title> <Title>
<Description className="sr-only">Edit camera group</Description> <Trans>ui.cameraGroup.edit</Trans>
</Title>
<Description className="sr-only">
<Trans>ui.cameraGroup.edit.desc</Trans>
</Description>
</Header> </Header>
<CameraGroupEdit <CameraGroupEdit
@ -515,19 +526,24 @@ export function CameraGroupRow({
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle> <AlertDialogTitle>
<Trans>ui.cameraGroup.delete.confirm</Trans>
</AlertDialogTitle>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete the camera group{" "} <Trans values={{ name: group[0] }}>
<em>{group[0]}</em>? ui.cameraGroup.delete.confirm.desc
</Trans>
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>
<Trans>ui.cancel</Trans>
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({ variant: "destructive" })}
onClick={onDeleteGroup} onClick={onDeleteGroup}
> >
Delete <Trans>ui.delete</Trans>
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -568,7 +584,9 @@ export function CameraGroupRow({
onClick={onEditGroup} onClick={onEditGroup}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Edit</TooltipContent> <TooltipContent>
<Trans>ui.edit</Trans>
</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
@ -579,7 +597,9 @@ export function CameraGroupRow({
onClick={() => setDeleteDialogOpen(true)} onClick={() => setDeleteDialogOpen(true)}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Delete</TooltipContent> <TooltipContent>
<Trans>ui.delete</Trans>
</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
)} )}
@ -614,7 +634,7 @@ export function CameraGroupEdit({
name: z name: z
.string() .string()
.min(2, { .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, "_")) .transform((val: string) => val.trim().replace(/\s+/g, "_"))
.refine( .refine(
@ -625,7 +645,7 @@ export function CameraGroupEdit({
); );
}, },
{ {
message: "Camera group name already exists.", message: t("ui.cameraGroup.name.errorMessage.exists"),
}, },
) )
.refine( .refine(
@ -633,11 +653,11 @@ export function CameraGroupEdit({
return !value.includes("."); 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", { .refine((value: string) => value.toLowerCase() !== "default", {
message: "Invalid camera group name.", message: t("ui.cameraGroup.name.errorMessage.invalid"),
}), }),
cameras: z.array(z.string()), cameras: z.array(z.string()),
@ -682,22 +702,30 @@ export function CameraGroupEdit({
) )
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
toast.success(`Camera group (${values.name}) has been saved.`, { toast.success(
position: "top-center", t("ui.cameraGroup.toast.success", { name: values.name }),
}); {
position: "top-center",
},
);
updateConfig(); updateConfig();
if (onSave) { if (onSave) {
onSave(); onSave();
} }
} else { } else {
toast.error(`Failed to save config changes: ${res.statusText}`, { toast.error(
position: "top-center", t("ui.cameraGroup.toast.error", { error: res.statusText }),
}); {
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {
toast.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" }, { position: "top-center" },
); );
}) })
@ -729,11 +757,13 @@ export function CameraGroupEdit({
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>
<Trans>ui.cameraGroup.name</Trans>
</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" 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} {...field}
/> />
</FormControl> </FormControl>
@ -749,9 +779,11 @@ export function CameraGroupEdit({
name="cameras" name="cameras"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Cameras</FormLabel> <FormLabel>
<Trans>ui.cameraGroup.cameras</Trans>
</FormLabel>
<FormDescription> <FormDescription>
Select cameras for this group. <Trans>ui.cameraGroup.cameras.desc</Trans>
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
{[ {[
@ -782,7 +814,9 @@ export function CameraGroupEdit({
name="icon" name="icon"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-col space-y-2"> <FormItem className="flex flex-col space-y-2">
<FormLabel>Icon</FormLabel> <FormLabel>
<Trans>ui.cameraGroup.icon</Trans>
</FormLabel>
<FormControl> <FormControl>
<IconPicker <IconPicker
selectedIcon={{ selectedIcon={{
@ -810,7 +844,7 @@ export function CameraGroupEdit({
aria-label="Cancel" aria-label="Cancel"
onClick={onCancel} onClick={onCancel}
> >
Cancel <Trans>ui.cancel</Trans>
</Button> </Button>
<Button <Button
variant="select" variant="select"
@ -822,10 +856,12 @@ export function CameraGroupEdit({
{isLoading ? ( {isLoading ? (
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<ActivityIndicator /> <ActivityIndicator />
<span>Saving...</span> <span>
<Trans>ui.saving</Trans>
</span>
</div> </div>
) : ( ) : (
"Save" <Trans>ui.save</Trans>
)} )}
</Button> </Button>
</div> </div>

View File

@ -13,6 +13,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import FilterSwitch from "./FilterSwitch"; import FilterSwitch from "./FilterSwitch";
import { FaVideo } from "react-icons/fa"; import { FaVideo } from "react-icons/fa";
import { t } from "i18next"; import { t } from "i18next";
import { Trans } from "react-i18next";
type CameraFilterButtonProps = { type CameraFilterButtonProps = {
allCameras: string[]; allCameras: string[];
@ -139,7 +140,7 @@ export function CamerasFilterContent({
{isMobile && ( {isMobile && (
<> <>
<DropdownMenuLabel className="flex justify-center"> <DropdownMenuLabel className="flex justify-center">
Cameras <Trans>ui.filter.allCameras.short</Trans>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <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"> <div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
<FilterSwitch <FilterSwitch
isChecked={currentCameras == undefined} isChecked={currentCameras == undefined}
label="All Cameras" label={t("ui.filter.allCameras")}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {
setCurrentCameras(undefined); setCurrentCameras(undefined);
@ -212,7 +213,7 @@ export function CamerasFilterContent({
setOpen(false); setOpen(false);
}} }}
> >
Apply <Trans>ui.apply</Trans>
</Button> </Button>
<Button <Button
aria-label="Reset" aria-label="Reset"
@ -221,7 +222,7 @@ export function CamerasFilterContent({
updateCameraFilter(undefined); updateCameraFilter(undefined);
}} }}
> >
Reset <Trans>ui.reset</Trans>
</Button> </Button>
</div> </div>
</> </>

View File

@ -354,7 +354,7 @@ function GeneralFilterButton({
: "text-primary" : "text-primary"
}`} }`}
> >
<Trans>ui.reviewFilter.filter</Trans> <Trans>ui.filter</Trans>
</div> </div>
</Button> </Button>
); );
@ -462,7 +462,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary" className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels" htmlFor="allLabels"
> >
<Trans>ui.reviewFilter.filter.allLabels</Trans> <Trans>ui.filter.allLabels</Trans>
</Label> </Label>
<Switch <Switch
className="ml-1" className="ml-1"
@ -509,7 +509,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary" className="mx-2 cursor-pointer text-primary"
htmlFor="allZones" htmlFor="allZones"
> >
<Trans>ui.reviewFilter.filter.allZones</Trans> <Trans>ui.filter.allZones</Trans>
</Label> </Label>
<Switch <Switch
className="ml-1" className="ml-1"

View File

@ -24,6 +24,8 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog"; import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
import { CalendarRangeFilterButton } from "./CalendarFilterButton"; import { CalendarRangeFilterButton } from "./CalendarFilterButton";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Trans } from "react-i18next";
import { t } from "i18next";
type SearchFilterGroupProps = { type SearchFilterGroupProps = {
className: string; className: string;
@ -190,7 +192,9 @@ export default function SearchFilterGroup({
to: new Date(filter.before * 1000), to: new Date(filter.before * 1000),
} }
} }
defaultText={isMobile ? "Dates" : "All Dates"} defaultText={
isMobile ? t("ui.filter.allDates.short") : t("ui.filter.allDates")
}
updateSelectedRange={onUpdateSelectedRange} updateSelectedRange={onUpdateSelectedRange}
/> />
)} )}
@ -231,18 +235,18 @@ function GeneralFilterButton({
const buttonText = useMemo(() => { const buttonText = useMemo(() => {
if (isMobile) { if (isMobile) {
return "Labels"; return t("ui.filter.allLabels.short");
} }
if (!selectedLabels || selectedLabels.length == 0) { if (!selectedLabels || selectedLabels.length == 0) {
return "All Labels"; return t("ui.filter.allLabels");
} }
if (selectedLabels.length == 1) { 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]); }, [selectedLabels]);
// ui // ui
@ -326,7 +330,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary" className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels" htmlFor="allLabels"
> >
All Labels <Trans>ui.filter.allLabels</Trans>
</Label> </Label>
<Switch <Switch
className="ml-1" className="ml-1"
@ -343,7 +347,7 @@ export function GeneralFilterContent({
{allLabels.map((item) => ( {allLabels.map((item) => (
<FilterSwitch <FilterSwitch
key={item} key={item}
label={item.replaceAll("_", " ")} label={t("object." + item)}
isChecked={currentLabels?.includes(item) ?? false} isChecked={currentLabels?.includes(item) ?? false}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {
@ -378,7 +382,7 @@ export function GeneralFilterContent({
onClose(); onClose();
}} }}
> >
Apply <Trans>ui.apply</Trans>
</Button> </Button>
<Button <Button
aria-label="Reset" aria-label="Reset"
@ -387,7 +391,7 @@ export function GeneralFilterContent({
updateLabelFilter(undefined); updateLabelFilter(undefined);
}} }}
> >
Reset <Trans>ui.reset</Trans>
</Button> </Button>
</div> </div>
</> </>
@ -436,7 +440,7 @@ function SortTypeButton({
<div <div
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`} className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`}
> >
Sort <Trans>ui.filter.sort</Trans>
</div> </div>
</Button> </Button>
); );
@ -492,11 +496,11 @@ export function SortTypeContent({
onClose, onClose,
}: SortTypeContentProps) { }: SortTypeContentProps) {
const sortLabels = { const sortLabels = {
date_asc: "Date (Ascending)", date_asc: t("ui.filter.sort.dateAsc"),
date_desc: "Date (Descending)", date_desc: t("ui.filter.sort.dateDesc"),
score_asc: "Object Score (Ascending)", score_asc: t("ui.filter.sort.scoreAsc"),
score_desc: "Object Score (Descending)", score_desc: t("ui.filter.sort.scoreDesc"),
relevance: "Relevance", relevance: t("ui.filter.sort.relevance"),
}; };
return ( return (
@ -551,7 +555,7 @@ export function SortTypeContent({
onClose(); onClose();
}} }}
> >
Apply <Trans>ui.apply</Trans>
</Button> </Button>
<Button <Button
aria-label="Reset" aria-label="Reset"
@ -560,7 +564,7 @@ export function SortTypeContent({
updateSortType(undefined); updateSortType(undefined);
}} }}
> >
Reset <Trans>ui.reset</Trans>
</Button> </Button>
</div> </div>
</> </>

View File

@ -11,6 +11,8 @@ import { IoClose } from "react-icons/io5";
import Heading from "../ui/heading"; import Heading from "../ui/heading";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Trans } from "react-i18next";
import { t } from "i18next";
export type IconName = keyof typeof LuIcons; export type IconName = keyof typeof LuIcons;
@ -70,7 +72,7 @@ export default function IconPicker({
className="mt-2 w-full text-muted-foreground" className="mt-2 w-full text-muted-foreground"
aria-label="Select an icon" aria-label="Select an icon"
> >
Select an icon <Trans>ui.iconPicker.selectIcon</Trans>
</Button> </Button>
) : ( ) : (
<div className="hover:cursor-pointer"> <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]" 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"> <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" /> <span tabIndex={0} className="sr-only" />
<IoClose <IoClose
size={15} size={15}
@ -113,7 +117,7 @@ export default function IconPicker({
</div> </div>
<Input <Input
type="text" type="text"
placeholder="Search for an icon..." placeholder={t("ui.iconPicker.search.placeholder")}
className="text-md mb-3 md:text-sm" className="text-md mb-3 md:text-sm"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}

View File

@ -30,6 +30,8 @@ import { getUTCOffset } from "@/utils/dateUtil";
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { GenericVideoPlayer } from "../player/GenericVideoPlayer"; import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
import { Trans } from "react-i18next";
import { t } from "i18next";
const EXPORT_OPTIONS = [ const EXPORT_OPTIONS = [
"1", "1",
@ -68,12 +70,14 @@ export default function ExportDialog({
const onStartExport = useCallback(() => { const onStartExport = useCallback(() => {
if (!range) { 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; return;
} }
if (range.before < range.after) { 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", position: "top-center",
}); });
return; return;
@ -89,10 +93,9 @@ export default function ExportDialog({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success( toast.success(t("ui.dialog.export.toast.success"), {
"Successfully started export. View the file in the /exports folder.", position: "top-center",
{ position: "top-center" }, });
);
setName(""); setName("");
setRange(undefined); setRange(undefined);
setMode("none"); setMode("none");
@ -100,14 +103,18 @@ export default function ExportDialog({
}) })
.catch((error) => { .catch((error) => {
if (error.response?.data?.message) { if (error.response?.data?.message) {
// api error message need to be translated
toast.error( 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" }, { position: "top-center" },
); );
} else { } else {
toast.error(`Failed to start export: ${error.message}`, { toast.error(
position: "top-center", `${t("ui.dialog.export.toast.error.failed", { error: error.message })}`,
}); {
position: "top-center",
},
);
} }
}); });
}, [camera, name, range, setRange, setName, setMode]); }, [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" /> <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> </Button>
</Trigger> </Trigger>
<Content <Content
@ -250,7 +261,9 @@ export function ExportContent({
{isDesktop && ( {isDesktop && (
<> <>
<DialogHeader> <DialogHeader>
<DialogTitle>Export</DialogTitle> <DialogTitle>
<Trans>ui.menu.export</Trans>
</DialogTitle>
</DialogHeader> </DialogHeader>
<SelectSeparator className="my-4 bg-secondary" /> <SelectSeparator className="my-4 bg-secondary" />
</> </>
@ -274,9 +287,11 @@ export function ExportContent({
<Label className="cursor-pointer capitalize" htmlFor={opt}> <Label className="cursor-pointer capitalize" htmlFor={opt}>
{isNaN(parseInt(opt)) {isNaN(parseInt(opt))
? opt == "timeline" ? opt == "timeline"
? "Select from Timeline" ? t("ui.dialog.export.time.fromTimeline")
: `${opt}` : t("ui.dialog.export.time." + opt)
: `Last ${opt > "1" ? `${opt} Hours` : "Hour"}`} : t("ui.dialog.export.time.lastHour", {
count: parseInt(opt),
})}
</Label> </Label>
</div> </div>
); );
@ -292,7 +307,7 @@ export function ExportContent({
<Input <Input
className="text-md my-6" className="text-md my-6"
type="search" type="search"
placeholder="Name the Export" placeholder={t("ui.dialog.export.name.placeholder")}
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
/> />
@ -304,7 +319,7 @@ export function ExportContent({
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`} className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
onClick={onCancel} onClick={onCancel}
> >
Cancel <Trans>ui.cancel</Trans>
</div> </div>
<Button <Button
className={isDesktop ? "" : "w-full"} 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> </Button>
</DialogFooter> </DialogFooter>
</div> </div>
@ -382,14 +399,14 @@ function CustomTimeSelector({
const formattedStart = useFormattedTimestamp( const formattedStart = useFormattedTimestamp(
startTime, startTime,
config?.ui.time_format == "24hour" config?.ui.time_format == "24hour"
? "%b %-d, %H:%M:%S" ? t("ui.time.formattedTimestamp.24hour")
: "%b %-d, %I:%M:%S %p", : t("ui.time.formattedTimestamp"),
); );
const formattedEnd = useFormattedTimestamp( const formattedEnd = useFormattedTimestamp(
endTime, endTime,
config?.ui.time_format == "24hour" config?.ui.time_format == "24hour"
? "%b %-d, %H:%M:%S" ? t("ui.time.formattedTimestamp.24hour")
: "%b %-d, %I:%M:%S %p", : t("ui.time.formattedTimestamp"),
); );
const startClock = useMemo(() => { const startClock = useMemo(() => {
@ -576,9 +593,11 @@ export function ExportPreviewDialog({
)} )}
> >
<DialogHeader> <DialogHeader>
<DialogTitle>Preview Export</DialogTitle> <DialogTitle>
<Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
</DialogTitle>
<DialogDescription className="sr-only"> <DialogDescription className="sr-only">
Preview Export <Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<GenericVideoPlayer source={source} /> <GenericVideoPlayer source={source} />

View File

@ -14,6 +14,7 @@ import { toast } from "sonner";
import axios from "axios"; import axios from "axios";
import SaveExportOverlay from "./SaveExportOverlay"; import SaveExportOverlay from "./SaveExportOverlay";
import { isIOS, isMobile } from "react-device-detect"; import { isIOS, isMobile } from "react-device-detect";
import { Trans } from "react-i18next";
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter"; type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
@ -89,10 +90,9 @@ export default function MobileReviewSettingsDrawer({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success( toast.success(t("ui.dialog.export.toast.success"), {
"Successfully started export. View the file in the /exports folder.", position: "top-center",
{ position: "top-center" }, });
);
setName(""); setName("");
setRange(undefined); setRange(undefined);
setMode("none"); setMode("none");
@ -238,7 +238,7 @@ export default function MobileReviewSettingsDrawer({
}); });
}} }}
> >
Reset <Trans>ui.reset</Trans>
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import { LuVideo, LuX } from "react-icons/lu";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { FaCompactDisc } from "react-icons/fa"; import { FaCompactDisc } from "react-icons/fa";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Trans } from "react-i18next";
type SaveExportOverlayProps = { type SaveExportOverlayProps = {
className: string; className: string;
@ -33,7 +34,7 @@ export default function SaveExportOverlay({
onClick={onCancel} onClick={onCancel}
> >
<LuX /> <LuX />
Cancel <Trans>ui.cancel</Trans>
</Button> </Button>
<Button <Button
className="flex items-center gap-1" className="flex items-center gap-1"
@ -42,7 +43,7 @@ export default function SaveExportOverlay({
onClick={onPreview} onClick={onPreview}
> >
<LuVideo /> <LuVideo />
Preview Export <Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
</Button> </Button>
<Button <Button
className="flex items-center gap-1" className="flex items-center gap-1"
@ -52,7 +53,7 @@ export default function SaveExportOverlay({
onClick={onSave} onClick={onSave}
> >
<FaCompactDisc /> <FaCompactDisc />
Save Export <Trans>ui.dialog.export.fromTimeline.saveExport</Trans>
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -33,6 +33,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Trans } from "react-i18next";
type SearchFilterDialogProps = { type SearchFilterDialogProps = {
config?: FrigateConfig; config?: FrigateConfig;
@ -91,7 +92,7 @@ export default function SearchFilterDialog({
moreFiltersSelected ? "text-white" : "text-secondary-foreground", moreFiltersSelected ? "text-white" : "text-secondary-foreground",
)} )}
/> />
More Filters <Trans>ui.filter.more</Trans>
</Button> </Button>
); );
const content = ( const content = (
@ -165,7 +166,7 @@ export default function SearchFilterDialog({
setOpen(false); setOpen(false);
}} }}
> >
Apply <Trans>ui.apply</Trans>
</Button> </Button>
<Button <Button
aria-label="Reset filters to default values" aria-label="Reset filters to default values"
@ -183,7 +184,7 @@ export default function SearchFilterDialog({
})); }));
}} }}
> >
Reset <Trans>ui.reset</Trans>
</Button> </Button>
</div> </div>
</div> </div>
@ -260,7 +261,9 @@ function TimeRangeFilterContent({
return ( return (
<div className="overflow-x-hidden"> <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"> <div className="mt-3 flex flex-row items-center justify-center gap-2">
<Popover <Popover
open={startOpen} open={startOpen}
@ -358,7 +361,9 @@ export function ZoneFilterContent({
<> <>
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" /> <DropdownMenuSeparator className="mb-3" />
<div className="text-lg">Zones</div> <div className="text-lg">
<Trans>ui.filter.zones</Trans>
</div>
{allZones && ( {allZones && (
<> <>
<div className="mb-5 mt-2.5 flex items-center justify-between"> <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" className="mx-2 cursor-pointer text-primary"
htmlFor="allZones" htmlFor="allZones"
> >
All Zones <Trans>ui.filter.allZones</Trans>
</Label> </Label>
<Switch <Switch
className="ml-1" className="ml-1"
@ -424,10 +429,12 @@ export function SubFilterContent({
return ( return (
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" /> <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"> <div className="mb-5 mt-2.5 flex items-center justify-between">
<Label className="mx-2 cursor-pointer text-primary" htmlFor="allLabels"> <Label className="mx-2 cursor-pointer text-primary" htmlFor="allLabels">
All Sub Labels <Trans>ui.filter.allSubLabels</Trans>
</Label> </Label>
<Switch <Switch
className="ml-1" className="ml-1"
@ -482,7 +489,9 @@ export function ScoreFilterContent({
return ( return (
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" /> <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"> <div className="flex items-center gap-1">
<Input <Input
className="w-14 text-center" className="w-14 text-center"
@ -568,7 +577,9 @@ export function SnapshotClipFilterContent({
return ( return (
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" /> <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="my-2.5 space-y-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -590,7 +601,7 @@ export function SnapshotClipFilterContent({
htmlFor="snapshot-filter" htmlFor="snapshot-filter"
className="cursor-pointer text-sm font-medium leading-none" className="cursor-pointer text-sm font-medium leading-none"
> >
Has a snapshot <Trans>ui.filter.features.hasSnapshot</Trans>
</Label> </Label>
</div> </div>
<ToggleGroup <ToggleGroup
@ -611,14 +622,14 @@ export function SnapshotClipFilterContent({
aria-label="Yes" 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" 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>
<ToggleGroupItem <ToggleGroupItem
value="no" value="no"
aria-label="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" 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> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
</div> </div>
@ -652,12 +663,9 @@ export function SnapshotClipFilterContent({
side="left" side="left"
sideOffset={5} sideOffset={5}
> >
You must first filter on tracked objects that have a <Trans>
snapshot. ui.filter.features.submittedToFrigatePlus.tips
<br /> </Trans>
<br />
Tracked objects without a snapshot cannot be submitted to
Frigate+.
</TooltipContent> </TooltipContent>
)} )}
</Tooltip> </Tooltip>
@ -666,7 +674,7 @@ export function SnapshotClipFilterContent({
htmlFor="plus-filter" htmlFor="plus-filter"
className="cursor-pointer text-sm font-medium leading-none" className="cursor-pointer text-sm font-medium leading-none"
> >
Submitted to Frigate+ <Trans>ui.filter.features.submittedToFrigatePlus</Trans>
</Label> </Label>
</div> </div>
<ToggleGroup <ToggleGroup
@ -692,14 +700,14 @@ export function SnapshotClipFilterContent({
aria-label="Yes" 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" 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>
<ToggleGroupItem <ToggleGroupItem
value="no" value="no"
aria-label="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" 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> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
</div> </div>
@ -728,7 +736,7 @@ export function SnapshotClipFilterContent({
htmlFor="clip-filter" htmlFor="clip-filter"
className="cursor-pointer text-sm font-medium leading-none" className="cursor-pointer text-sm font-medium leading-none"
> >
Has a video clip <Trans>ui.filter.features.hasVideoClip</Trans>
</Label> </Label>
</div> </div>
<ToggleGroup <ToggleGroup
@ -747,14 +755,14 @@ export function SnapshotClipFilterContent({
aria-label="Yes" 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" 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>
<ToggleGroupItem <ToggleGroupItem
value="no" value="no"
aria-label="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" 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> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
</div> </div>

View File

@ -20,6 +20,7 @@ import {
getPreviewForTimeRange, getPreviewForTimeRange,
usePreviewForTimeRange, usePreviewForTimeRange,
} from "@/hooks/use-camera-previews"; } from "@/hooks/use-camera-previews";
import { Trans } from "react-i18next";
type PreviewPlayerProps = { type PreviewPlayerProps = {
className?: string; className?: string;
@ -88,7 +89,7 @@ export default function PreviewPlayer({
className, className,
)} )}
> >
No Preview Found <Trans>ui.player.noPreviewFound</Trans>
</div> </div>
); );
} }
@ -324,7 +325,9 @@ function PreviewVideoPlayer({
</video> </video>
{cameraPreviews && !currentPreview && ( {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"> <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> </div>
)} )}
{firstLoad && <Skeleton className="absolute aspect-video size-full" />} {firstLoad && <Skeleton className="absolute aspect-video size-full" />}
@ -544,7 +547,9 @@ function PreviewFramesPlayer({
/> />
{previewFrames?.length === 0 && ( {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"> <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> </div>
)} )}
{firstLoad && <Skeleton className="absolute aspect-video size-full" />} {firstLoad && <Skeleton className="absolute aspect-video size-full" />}

View File

@ -12,6 +12,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
import { VideoResolutionType } from "@/types/live"; import { VideoResolutionType } from "@/types/live";
import axios from "axios"; import axios from "axios";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Trans } from "react-i18next";
/** /**
* Dynamically switches between video playback and scrubbing preview player. * Dynamically switches between video playback and scrubbing preview player.
@ -247,7 +248,7 @@ export default function DynamicVideoPlayer({
)} )}
{!isScrubbing && !isLoading && noRecording && ( {!isScrubbing && !isLoading && noRecording && (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"> <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> </div>
)} )}
</> </>

View File

@ -204,7 +204,9 @@ export default function MotionMaskEditPane({
} }
useEffect(() => { useEffect(() => {
document.title = "Edit Motion Mask - Frigate"; document.title = t(
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle",
);
}, []); }, []);
if (!polygon) { if (!polygon) {

View File

@ -238,7 +238,9 @@ export default function ObjectMaskEditPane({
} }
useEffect(() => { useEffect(() => {
document.title = "Edit Object Mask - Frigate"; document.title = t(
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle",
);
}, []); }, []);
if (!polygon) { if (!polygon) {

View File

@ -17,8 +17,10 @@ import FilterSwitch from "../filter/FilterSwitch";
import { SearchFilter, SearchSource } from "@/types/search"; import { SearchFilter, SearchSource } from "@/types/search";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { Trans } from "react-i18next";
import { t } from "i18next";
type SearchSettingsProps = { type ExploreSettingsProps = {
className?: string; className?: string;
columns: number; columns: number;
defaultView: string; defaultView: string;
@ -27,7 +29,7 @@ type SearchSettingsProps = {
setDefaultView: (view: string) => void; setDefaultView: (view: string) => void;
onUpdateFilter: (filter: SearchFilter) => void; onUpdateFilter: (filter: SearchFilter) => void;
}; };
export default function SearchSettings({ export default function ExploreSettings({
className, className,
columns, columns,
setColumns, setColumns,
@ -35,7 +37,7 @@ export default function SearchSettings({
filter, filter,
setDefaultView, setDefaultView,
onUpdateFilter, onUpdateFilter,
}: SearchSettingsProps) { }: ExploreSettingsProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -46,21 +48,22 @@ export default function SearchSettings({
const trigger = ( const trigger = (
<Button <Button
className="flex items-center gap-2" className="flex items-center gap-2"
aria-label="Search Settings" aria-label="Explore Settings"
size="sm" size="sm"
> >
<FaCog className="text-secondary-foreground" /> <FaCog className="text-secondary-foreground" />
Settings <Trans>ui.searchView.settings</Trans>
</Button> </Button>
); );
const content = ( const content = (
<div className={cn(className, "my-3 space-y-5 py-3 md:mt-0 md:py-0")}> <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-4">
<div className="space-y-0.5"> <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"> <div className="space-y-1 text-xs text-muted-foreground">
When no filters are selected, display a summary of the most recent <Trans>ui.searchView.settings.defaultView.desc</Trans>
tracked objects per label, or display an unfiltered grid.
</div> </div>
</div> </div>
<Select <Select
@ -68,7 +71,9 @@ export default function SearchSettings({
onValueChange={(value) => setDefaultView(value)} onValueChange={(value) => setDefaultView(value)}
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
{defaultView == "summary" ? "Summary" : "Unfiltered Grid"} {defaultView == "summary"
? t("ui.searchView.settings.defaultView.summary")
: t("ui.searchView.settings.defaultView.unfilteredGrid")}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@ -78,7 +83,9 @@ export default function SearchSettings({
className="cursor-pointer" className="cursor-pointer"
value={value} value={value}
> >
{value == "summary" ? "Summary" : "Unfiltered Grid"} {value == "summary"
? t("ui.searchView.settings.defaultView.summary")
: t("ui.searchView.settings.defaultView.unfilteredGrid")}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@ -90,9 +97,11 @@ export default function SearchSettings({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="flex w-full flex-col space-y-4"> <div className="flex w-full flex-col space-y-4">
<div className="space-y-0.5"> <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"> <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> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
@ -153,10 +162,11 @@ export function SearchTypeContent({
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" /> <DropdownMenuSeparator className="mb-3" />
<div className="space-y-0.5"> <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"> <div className="space-y-1 text-xs text-muted-foreground">
Choose whether to search the thumbnails or descriptions of your <Trans>ui.searchView.settings.searchSource.desc</Trans>
tracked objects.
</div> </div>
</div> </div>
<div className="mt-2.5 flex flex-col gap-2.5"> <div className="mt-2.5 flex flex-col gap-2.5">

View File

@ -321,7 +321,9 @@ export default function ZoneEditPane({
} }
useEffect(() => { useEffect(() => {
document.title = "Edit Zone - Frigate"; document.title = t(
"ui.settingView.masksAndZonesSettings.zone.documentTitle",
);
}, []); }, []);
if (!polygon) { if (!polygon) {

View File

@ -12,6 +12,8 @@ import {
import { Switch } from "./switch"; import { Switch } from "./switch";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { LuCheck } from "react-icons/lu"; import { LuCheck } from "react-icons/lu";
import { t } from "i18next";
import { Trans } from "react-i18next";
export interface DateRangePickerProps { export interface DateRangePickerProps {
/** Click handler for applying the updates from DateRangePicker. */ /** Click handler for applying the updates from DateRangePicker. */
@ -59,15 +61,15 @@ interface Preset {
// Define presets // Define presets
const PRESETS: Preset[] = [ const PRESETS: Preset[] = [
{ name: "today", label: "Today" }, { name: "today", label: t("ui.time.today") },
{ name: "yesterday", label: "Yesterday" }, { name: "yesterday", label: t("ui.time.yesterday") },
{ name: "last7", label: "Last 7 days" }, { name: "last7", label: t("ui.time.last7") },
{ name: "last14", label: "Last 14 days" }, { name: "last14", label: t("ui.time.last14") },
{ name: "last30", label: "Last 30 days" }, { name: "last30", label: t("ui.time.last30") },
{ name: "thisWeek", label: "This Week" }, { name: "thisWeek", label: t("ui.time.thisWeek") },
{ name: "lastWeek", label: "Last Week" }, { name: "lastWeek", label: t("ui.time.lastWeek") },
{ name: "thisMonth", label: "This Month" }, { name: "thisMonth", label: t("ui.time.thisMonth") },
{ name: "lastMonth", label: "Last Month" }, { name: "lastMonth", label: t("ui.time.lastMonth") },
]; ];
/** The DateRangePicker component allows a user to select a range of dates */ /** 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>
<Button <Button
onClick={() => { onClick={() => {
@ -440,7 +442,7 @@ export function DateRangePicker({
variant="ghost" variant="ghost"
aria-label="Reset" aria-label="Reset"
> >
Reset <Trans>ui.reset</Trans>
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,12 +1,24 @@
import * as React from "react"; import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker"; import { DayPicker } from "react-day-picker";
import { enUS, Locale, zhCN } from "date-fns/locale";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import i18n from "@/utils/i18n";
export type CalendarProps = React.ComponentProps<typeof DayPicker>; 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({ function Calendar({
className, className,
classNames, classNames,
@ -15,6 +27,7 @@ function Calendar({
}: CalendarProps) { }: CalendarProps) {
return ( return (
<DayPicker <DayPicker
locale={locale}
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn("p-3", className)} className={cn("p-3", className)}
classNames={{ classNames={{

View File

@ -21,6 +21,7 @@ import {
import EventView from "@/views/events/EventView"; import EventView from "@/views/events/EventView";
import { RecordingView } from "@/views/recording/RecordingView"; import { RecordingView } from "@/views/recording/RecordingView";
import axios from "axios"; import axios from "axios";
import { t } from "i18next";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
@ -76,9 +77,9 @@ export default function Events() {
useEffect(() => { useEffect(() => {
if (recording) { if (recording) {
document.title = "Recordings - Frigate"; document.title = t("ui.review.recordings.documentTitle");
} else { } else {
document.title = `Review - Frigate`; document.title = t("ui.review.documentTitle");
} }
}, [recording, severity]); }, [recording, severity]);

View File

@ -17,8 +17,10 @@ import { useSearchEffect } from "@/hooks/use-overlay-state";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DeleteClipType, Export } from "@/types/export"; import { DeleteClipType, Export } from "@/types/export";
import axios from "axios"; import axios from "axios";
import { t } from "i18next";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { Trans } from "react-i18next";
import { LuFolderX } from "react-icons/lu"; import { LuFolderX } from "react-icons/lu";
import { toast } from "sonner"; import { toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
@ -27,7 +29,7 @@ function Exports() {
const { data: exports, mutate } = useSWR<Export[]>("exports"); const { data: exports, mutate } = useSWR<Export[]>("exports");
useEffect(() => { useEffect(() => {
document.title = "Export - Frigate"; document.title = t("ui.exportView.documentTitle");
}, []); }, []);
// Search // Search
@ -116,20 +118,26 @@ function Exports() {
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Export</AlertDialogTitle> <AlertDialogTitle>
<Trans>ui.exportView.deleteExport</Trans>
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete {deleteClip?.exportName}? <Trans values={{ exportName: deleteClip?.exportName }}>
ui.exportView.deleteExport.desc
</Trans>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>
<Trans>ui.cancel</Trans>
</AlertDialogCancel>
<Button <Button
className="text-white" className="text-white"
aria-label="Delete Export" aria-label="Delete Export"
variant="destructive" variant="destructive"
onClick={() => onHandleDelete()} onClick={() => onHandleDelete()}
> >
Delete <Trans>ui.delete</Trans>
</Button> </Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -177,7 +185,7 @@ function Exports() {
<div className="flex w-full items-center justify-center p-2"> <div className="flex w-full items-center justify-center p-2">
<Input <Input
className="text-md w-full bg-muted md:w-1/3" className="text-md w-full bg-muted md:w-1/3"
placeholder="Search" placeholder={t("ui.exportView.search")}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} 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"> <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" /> <LuFolderX className="size-16" />
No exports found <Trans>ui.exportView.noExports</Trans>
</div> </div>
)} )}
</div> </div>

View File

@ -9,6 +9,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView"; import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
import LiveCameraView from "@/views/live/LiveCameraView"; import LiveCameraView from "@/views/live/LiveCameraView";
import LiveDashboardView from "@/views/live/LiveDashboardView"; import LiveDashboardView from "@/views/live/LiveDashboardView";
import { t } from "i18next";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import useSWR from "swr"; import useSWR from "swr";
@ -64,11 +65,15 @@ function Live() {
.split("_") .split("_")
.filter((text) => text) .filter((text) => text)
.map((text) => text[0].toUpperCase() + text.substring(1)); .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") { } 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 { } else {
document.title = "Live - Frigate"; document.title = t("ui.live.documentTitle");
} }
}, [cameraGroup, selectedCameraName]); }, [cameraGroup, selectedCameraName]);

View File

@ -35,13 +35,13 @@ import MotionTunerView from "@/views/settings/MotionTunerView";
import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
import AuthenticationView from "@/views/settings/AuthenticationView"; import AuthenticationView from "@/views/settings/AuthenticationView";
import NotificationView from "@/views/settings/NotificationsSettingsView"; 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 UiSettingsView from "@/views/settings/UiSettingsView";
import { t } from "i18next"; import { t } from "i18next";
const allSettingsViews = [ const allSettingsViews = [
"uiSettings", "uiSettings",
"searchSettings", "exploreSettings",
"cameraSettings", "cameraSettings",
"masksAndZones", "masksAndZones",
"motionTuner", "motionTuner",
@ -178,8 +178,8 @@ export default function Settings() {
</div> </div>
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24"> <div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
{page == "uiSettings" && <UiSettingsView />} {page == "uiSettings" && <UiSettingsView />}
{page == "searchSettings" && ( {page == "exploreSettings" && (
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} /> <ExploreSettingsView setUnsavedChanges={setUnsavedChanges} />
)} )}
{page == "debug" && ( {page == "debug" && (
<ObjectSettingsView selectedCamera={selectedCamera} /> <ObjectSettingsView selectedCamera={selectedCamera} />

View File

@ -32,16 +32,18 @@ i18n
}, },
keySeparator: false, keySeparator: false,
parseMissingKeyHandler: (key: string) => { parseMissingKeyHandler: (key: string) => {
console.debug("Missing key: " + key); const parts = key.split(".");
const parts = key.split('.');
if (parts.length > 1) { if (parts.length > 1) {
if (parts[0] === 'object' || parts[0] === 'audio') { if (parts[0] === "object" || parts[0] === "audio") {
return parts[1].replaceAll("_", " ").charAt(0).toUpperCase() + parts[1].slice(1); return (
parts[1].replaceAll("_", " ").charAt(0).toUpperCase() +
parts[1].slice(1)
);
} }
return parts[parts.length - 1]; return parts[parts.length - 1];
} }
return key; return key;
} },
}); });
export default i18n; export default i18n;

View File

@ -54,6 +54,7 @@ import { GiSoundWaves } from "react-icons/gi";
import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useKeyboardListener from "@/hooks/use-keyboard-listener";
import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog"; import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { t } from "i18next";
type EventViewProps = { type EventViewProps = {
reviewItems?: SegmentedReviewData; reviewItems?: SegmentedReviewData;
@ -194,10 +195,9 @@ export default function EventView({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success( toast.success(t("ui.dialog.export.toast.success"), {
"Successfully started export. View the file in the /exports folder.", position: "top-center",
{ position: "top-center" }, });
);
} }
}) })
.catch((error) => { .catch((error) => {

View File

@ -412,7 +412,9 @@ export default function LiveCameraView({
> >
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" /> <IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && ( {isDesktop && (
<div className="text-secondary-foreground">Back</div> <div className="text-secondary-foreground">
<Trans>ui.back</Trans>
</div>
)} )}
</Button> </Button>
)} )}

View File

@ -46,6 +46,7 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
import { useResizeObserver } from "@/hooks/resize-observer"; import { useResizeObserver } from "@/hooks/resize-observer";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useFullscreen } from "@/hooks/use-fullscreen"; import { useFullscreen } from "@/hooks/use-fullscreen";
import { Trans } from "react-i18next";
const SEGMENT_DURATION = 30; const SEGMENT_DURATION = 30;
@ -385,7 +386,11 @@ export function RecordingView({
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" /> <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>
<Button <Button
className="flex items-center gap-2.5 rounded-lg" className="flex items-center gap-2.5 rounded-lg"
@ -396,7 +401,11 @@ export function RecordingView({
}} }}
> >
<FaVideo className="size-5 text-secondary-foreground" /> <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> </Button>
</div> </div>
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
@ -455,14 +464,18 @@ export function RecordingView({
value="timeline" value="timeline"
aria-label="Select timeline" aria-label="Select timeline"
> >
<div className="">Timeline</div> <div className="">
<Trans>ui.review.timeline</Trans>
</div>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem <ToggleGroupItem
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`} className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
value="events" value="events"
aria-label="Select events" aria-label="Select events"
> >
<div className="">Events</div> <div className="">
<Trans>ui.review.events</Trans>
</div>
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
) : ( ) : (
@ -728,7 +741,7 @@ function Timeline({
> >
{mainCameraReviewItems.length === 0 ? ( {mainCameraReviewItems.length === 0 ? (
<div className="mt-5 text-center text-primary"> <div className="mt-5 text-center text-primary">
No events found for this time period. <Trans>ui.review.events.noFoundForTimePeriod</Trans>
</div> </div>
) : ( ) : (
mainCameraReviewItems.map((review) => { mainCameraReviewItems.map((review) => {

View File

@ -22,7 +22,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { formatDateToLocaleString } from "@/utils/dateUtil"; import { formatDateToLocaleString } from "@/utils/dateUtil";
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter"; import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
import SearchSettings from "@/components/settings/SearchSettings"; import ExploreSettings from "@/components/settings/SearchSettings";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -31,6 +31,7 @@ import {
import Chip from "@/components/indicators/Chip"; import Chip from "@/components/indicators/Chip";
import { TooltipPortal } from "@radix-ui/react-tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip";
import SearchActionGroup from "@/components/filter/SearchActionGroup"; import SearchActionGroup from "@/components/filter/SearchActionGroup";
import { Trans } from "react-i18next";
type SearchViewProps = { type SearchViewProps = {
search: string; search: string;
@ -481,7 +482,7 @@ export default function SearchView({
filter={searchFilter} filter={searchFilter}
onUpdateFilter={onUpdateFilter} onUpdateFilter={onUpdateFilter}
/> />
<SearchSettings <ExploreSettings
columns={columns} columns={columns}
setColumns={setColumns} setColumns={setColumns}
defaultView={defaultView} defaultView={defaultView}
@ -517,7 +518,7 @@ export default function SearchView({
{uniqueResults?.length == 0 && !isLoading && ( {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"> <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" /> <LuSearchX className="size-16" />
No Tracked Objects Found <Trans>ui.searchView.noTrackedObjects</Trans>
</div> </div>
)} )}

View File

@ -475,8 +475,8 @@ export default function CameraSettingsView({
<div className="text-sm"> <div className="text-sm">
{watchedDetectionsZones && {watchedDetectionsZones &&
watchedDetectionsZones.length > 0 watchedDetectionsZones.length > 0 ? (
? !selectDetections ? ( !selectDetections ? (
<Trans <Trans
i18nKey="ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips" i18nKey="ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips"
values={{ values={{
@ -513,7 +513,7 @@ export default function CameraSettingsView({
}} }}
/> />
) )
: ( ) : (
<Trans <Trans
i18nKey="ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips" i18nKey="ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips"
values={{ values={{

View File

@ -23,19 +23,19 @@ import {
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { t } from "i18next"; import { t } from "i18next";
type SearchSettingsViewProps = { type ExploreSettingsViewProps = {
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>; setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
}; };
type SearchSettings = { type ExploreSettings = {
enabled?: boolean; enabled?: boolean;
reindex?: boolean; reindex?: boolean;
model_size?: SearchModelSize; model_size?: SearchModelSize;
}; };
export default function SearchSettingsView({ export default function ExploreSettingsView({
setUnsavedChanges, setUnsavedChanges,
}: SearchSettingsViewProps) { }: ExploreSettingsViewProps) {
const { data: config, mutate: updateConfig } = const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config"); useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false); const [changedValue, setChangedValue] = useState(false);
@ -43,29 +43,30 @@ export default function SearchSettingsView({
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
const [searchSettings, setSearchSettings] = useState<SearchSettings>({ const [ExploreSettings, setExploreSettings] = useState<ExploreSettings>({
enabled: undefined, enabled: undefined,
reindex: undefined, reindex: undefined,
model_size: undefined, model_size: undefined,
}); });
const [origSearchSettings, setOrigSearchSettings] = useState<SearchSettings>({ const [origExploreSettings, setOrigExploreSettings] =
enabled: undefined, useState<ExploreSettings>({
reindex: undefined, enabled: undefined,
model_size: undefined, reindex: undefined,
}); model_size: undefined,
});
useEffect(() => { useEffect(() => {
if (config) { if (config) {
if (searchSettings?.enabled == undefined) { if (ExploreSettings?.enabled == undefined) {
setSearchSettings({ setExploreSettings({
enabled: config.semantic_search.enabled, enabled: config.semantic_search.enabled,
reindex: config.semantic_search.reindex, reindex: config.semantic_search.reindex,
model_size: config.semantic_search.model_size, model_size: config.semantic_search.model_size,
}); });
} }
setOrigSearchSettings({ setOrigExploreSettings({
enabled: config.semantic_search.enabled, enabled: config.semantic_search.enabled,
reindex: config.semantic_search.reindex, reindex: config.semantic_search.reindex,
model_size: config.semantic_search.model_size, model_size: config.semantic_search.model_size,
@ -75,8 +76,8 @@ export default function SearchSettingsView({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]); }, [config]);
const handleSearchConfigChange = (newConfig: Partial<SearchSettings>) => { const handleSearchConfigChange = (newConfig: Partial<ExploreSettings>) => {
setSearchSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); setExploreSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
setUnsavedChanges(true); setUnsavedChanges(true);
setChangedValue(true); setChangedValue(true);
}; };
@ -86,7 +87,7 @@ export default function SearchSettingsView({
axios axios
.put( .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, requires_restart: 0,
}, },
@ -115,16 +116,16 @@ export default function SearchSettingsView({
}); });
}, [ }, [
updateConfig, updateConfig,
searchSettings.enabled, ExploreSettings.enabled,
searchSettings.reindex, ExploreSettings.reindex,
searchSettings.model_size, ExploreSettings.model_size,
]); ]);
const onCancel = useCallback(() => { const onCancel = useCallback(() => {
setSearchSettings(origSearchSettings); setExploreSettings(origExploreSettings);
setChangedValue(false); setChangedValue(false);
removeMessage("search_settings", "search_settings"); removeMessage("search_settings", "search_settings");
}, [origSearchSettings, removeMessage]); }, [origExploreSettings, removeMessage]);
useEffect(() => { useEffect(() => {
if (changedValue) { if (changedValue) {
@ -142,7 +143,7 @@ export default function SearchSettingsView({
}, [changedValue]); }, [changedValue]);
useEffect(() => { useEffect(() => {
document.title = "Search Settings - Frigate"; document.title = "Explore Settings - Frigate";
}, []); }, []);
if (!config) { if (!config) {
@ -154,16 +155,16 @@ export default function SearchSettingsView({
<Toaster position="top-center" closeButton={true} /> <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"> <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"> <Heading as="h3" className="my-2">
<Trans>ui.settingView.searchSettings</Trans> <Trans>ui.settingView.exploreSettings</Trans>
</Heading> </Heading>
<Separator className="my-2 flex bg-secondary" /> <Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2"> <Heading as="h4" className="my-2">
<Trans>ui.settingView.searchSettings.semanticSearch</Trans> <Trans>ui.settingView.exploreSettings.semanticSearch</Trans>
</Heading> </Heading>
<div className="max-w-6xl"> <div className="max-w-6xl">
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant"> <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
<p> <p>
<Trans>ui.settingView.searchSettings.semanticSearch.desc</Trans> <Trans>ui.settingView.exploreSettings.semanticSearch.desc</Trans>
</p> </p>
<div className="flex items-center text-primary"> <div className="flex items-center text-primary">
@ -174,7 +175,7 @@ export default function SearchSettingsView({
className="inline" className="inline"
> >
<Trans> <Trans>
ui.settingView.searchSettings.semanticSearch.readTheDocumentation ui.settingView.exploreSettings.semanticSearch.readTheDocumentation
</Trans> </Trans>
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
@ -187,8 +188,8 @@ export default function SearchSettingsView({
<Switch <Switch
id="enabled" id="enabled"
className="mr-3" className="mr-3"
disabled={searchSettings.enabled === undefined} disabled={ExploreSettings.enabled === undefined}
checked={searchSettings.enabled === true} checked={ExploreSettings.enabled === true}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
handleSearchConfigChange({ enabled: isChecked }); handleSearchConfigChange({ enabled: isChecked });
}} }}
@ -204,8 +205,8 @@ export default function SearchSettingsView({
<Switch <Switch
id="reindex" id="reindex"
className="mr-3" className="mr-3"
disabled={searchSettings.reindex === undefined} disabled={ExploreSettings.reindex === undefined}
checked={searchSettings.reindex === true} checked={ExploreSettings.reindex === true}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
handleSearchConfigChange({ reindex: isChecked }); handleSearchConfigChange({ reindex: isChecked });
}} }}
@ -213,14 +214,14 @@ export default function SearchSettingsView({
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="reindex"> <Label htmlFor="reindex">
<Trans> <Trans>
ui.settingView.searchSettings.semanticSearch.reindexOnStartup ui.settingView.exploreSettings.semanticSearch.reindexOnStartup
</Trans> </Trans>
</Label> </Label>
</div> </div>
</div> </div>
<div className="mt-3 text-sm text-muted-foreground"> <div className="mt-3 text-sm text-muted-foreground">
<Trans> <Trans>
ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc
</Trans> </Trans>
</div> </div>
</div> </div>
@ -228,31 +229,31 @@ export default function SearchSettingsView({
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="text-md"> <div className="text-md">
<Trans> <Trans>
ui.settingView.searchSettings.semanticSearch.modelSize ui.settingView.exploreSettings.semanticSearch.modelSize
</Trans> </Trans>
</div> </div>
<div className="space-y-1 text-sm text-muted-foreground"> <div className="space-y-1 text-sm text-muted-foreground">
<p> <p>
<Trans> <Trans>
ui.settingView.searchSettings.semanticSearch.modelSize.desc ui.settingView.exploreSettings.semanticSearch.modelSize.desc
</Trans> </Trans>
</p> </p>
<ul className="list-disc pl-5 text-sm"> <ul className="list-disc pl-5 text-sm">
<li> <li>
<Trans> <Trans>
ui.settingView.searchSettings.semanticSearch.modelSize.small.desc ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc
</Trans> </Trans>
</li> </li>
<li> <li>
<Trans> <Trans>
ui.settingView.searchSettings.semanticSearch.modelSize.large.desc ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc
</Trans> </Trans>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<Select <Select
value={searchSettings.model_size} value={ExploreSettings.model_size}
onValueChange={(value) => onValueChange={(value) =>
handleSearchConfigChange({ handleSearchConfigChange({
model_size: value as SearchModelSize, model_size: value as SearchModelSize,
@ -261,8 +262,8 @@ export default function SearchSettingsView({
> >
<SelectTrigger className="w-20"> <SelectTrigger className="w-20">
{t( {t(
"ui.settingView.searchSettings.semanticSearch.modelSize." + "ui.settingView.exploreSettings.semanticSearch.modelSize." +
searchSettings.model_size, ExploreSettings.model_size,
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -274,7 +275,7 @@ export default function SearchSettingsView({
value={size} value={size}
> >
{t( {t(
"ui.settingView.searchSettings.semanticSearch.modelSize." + "ui.settingView.exploreSettings.semanticSearch.modelSize." +
size, size,
)} )}
</SelectItem> </SelectItem>