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",
"ui.time.justNow": "Just now",
"ui.time.today": "Today",
"ui.time.yesterday": "Yesterday",
"ui.time.last7": "Last 7 days",
"ui.time.last14": "Last 14 days",
"ui.time.last30": "Last 30 days",
"ui.time.thisWeek": "This Week",
"ui.time.lastWeek": "Last Week",
"ui.time.thisMonth": "This Month",
"ui.time.lastMonth": "Last Month",
"ui.time.formattedTimestamp": "%b %-d, %I:%M:%S %p",
"ui.time.formattedTimestamp.24hour": "%b %-d, %H:%M:%S",
"ui.iconPicker.selectIcon": "Select an icon",
"ui.iconPicker.search.placeholder": "Search for an icon...",
"ui.dialog.restart.title": "Are you sure you want to restart Frigate?",
"ui.dialog.restart.button": "Restart",
@ -116,6 +130,20 @@
"ui.dialog.restart.restarting.content": "This page will reload in {{countdown}} seconds.",
"ui.dialog.restart.restarting.button": "Force Reload Now",
"ui.dialog.export.time.fromTimeline": "Select from Timeline",
"ui.dialog.export.time.lastHour_one": "Last Hour",
"ui.dialog.export.time.lastHour_other": "Last {{count}} Hours",
"ui.dialog.export.time.custom": "Custom",
"ui.dialog.export.name.placeholder": "Name the Export",
"ui.dialog.export.select": "Select",
"ui.dialog.export.export": "Export",
"ui.dialog.export.toast.success": "Successfully started export. View the file in the /exports folder.",
"ui.dialog.export.toast.error.failed": "Failed to start export: {{error}}",
"ui.dialog.export.toast.error.endTimeMustAfterStartTime": "End time must be after start time",
"ui.dialog.export.toast.error.noVaildTimeSelected": "No valid time range selected",
"ui.dialog.export.fromTimeline.saveExport": "Save Export",
"ui.dialog.export.fromTimeline.previewExport": "Preview Export",
"ui.stats.ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
"ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
"ui.stats.healthy": "System is healthy",
@ -209,6 +237,23 @@
"ui.menu.user.anonymous": "anonymous",
"ui.menu.user.logout": "Logout",
"ui.cameraGroup": "Camera Groups",
"ui.cameraGroup.add": "添加摄像头组",
"ui.cameraGroup.edit": "Edit camera groups",
"ui.cameraGroup.delete.confirm": "Confirm Delete",
"ui.cameraGroup.delete.confirm.desc": "Are you sure you want to delete the camera group <em>{{name}}</em>?",
"ui.cameraGroup.name": "Name",
"ui.cameraGroup.name.placeholder": "Enter a name...",
"ui.cameraGroup.name.errorMessage.mustLeastCharacters": "Camera group name must be at least 2 characters.",
"ui.cameraGroup.name.errorMessage.exists": "Camera group name already exists.",
"ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "Camera group name must not contain a period.",
"ui.cameraGroup.name.errorMessage.invalid": "Invalid camera group name.",
"ui.cameraGroup.cameras": "Cameras",
"ui.cameraGroup.cameras.desc": "Select cameras for this group.",
"ui.cameraGroup.icon": "Icon",
"ui.cameraGroup.success": "Camera group ({{name}}) has been saved.",
"ui.cameraGroup.toast.error": "Failed to save config changes: {{error}}",
"ui.eventView.alerts": "Alerts",
"ui.eventView.detections": "Detections",
"ui.eventView.motion": "Motion",
@ -216,9 +261,33 @@
"ui.eventView.empty.alert": "There are no alerts to review",
"ui.eventView.empty.detection": "There are no detections to review",
"ui.reviewFilter.filter": "Filter",
"ui.reviewFilter.filter.allLabels": "All Labels",
"ui.reviewFilter.filter.allZones": "All Zones",
"ui.filter": "Filter",
"ui.filter.allLabels": "All Labels",
"ui.filter.allLabels.short": "Labels",
"ui.filter.countLabels": "{{count}} Labels",
"ui.filter.allZones": "All Zones",
"ui.filter.allZones.short": "Zones",
"ui.filter.allDates": "All Dates",
"ui.filter.allDates.short": "Dates",
"ui.filter.more": "More Filters",
"ui.filter.timeRange": "Time Range",
"ui.filter.zones": "Zones",
"ui.filter.subLabels": "Sub Labels",
"ui.filter.allSubLabels": "All Sub Labels",
"ui.filter.score": "Score",
"ui.filter.features": "Features",
"ui.filter.features.hasSnapshot": "Has a snapshot",
"ui.filter.features.hasVideoClip": "Has a video clip",
"ui.filter.features.submittedToFrigatePlus": "Submitted to Frigate+",
"ui.filter.features.submittedToFrigatePlus.tips": "You must first filter on tracked objects that have a snapshot.<br /><br />Tracked objects without a snapshot cannot be submitted to Frigate+.",
"ui.filter.sort": "Sort",
"ui.filter.sort.dateAsc": "Date (Ascending)",
"ui.filter.sort.dateDesc": "Date (Descending)",
"ui.filter.sort.scoreAsc": "Object Score (Ascending)",
"ui.filter.sort.scoreDesc": "Object Score (Descending)",
"ui.filter.sort.relevance": "Relevance",
"ui.filter.allCameras": "All Cameras",
"ui.filter.allCameras.short": "Cameras",
"ui.reviewFilter.showReviewed": "Show Reviewed",
@ -236,8 +305,13 @@
"ui.pictureInPicture": "Picture in Picture",
"ui.on": "ON",
"ui.off": "OFF",
"ui.edit": "Edit",
"ui.delete": "Delete",
"ui.yes": "Yes",
"ui.no": "No",
"ui.live.documentTitle": "Live - Frigate",
"ui.live.documentTitle.withCamera": "{{camera}} - Live - Frigate",
"ui.live.twoWayTalk.enable": "Enable Two Way Talk",
"ui.live.twoWayTalk.disable": "Disable Two Way Talk",
"ui.live.cameraAudio.enable": "Enable Camera Audio",
@ -261,10 +335,31 @@
"ui.live.autotracking.enable": "Enable Autotracking",
"ui.live.autotracking.disable": "Disable Autotracking",
"ui.review.timeline": "Timeline",
"ui.review.events": "Events",
"ui.review.events.noFoundForTimePeriod": "No events found for this time period.",
"ui.review.documentTitle": "Review - Frigate",
"ui.review.recordings.documentTitle": "Recordings - Frigate",
"ui.player.noRecordingsFoundForThisTime": "No recordings found for this time",
"ui.player.noPreviewFound": "No Preview Found",
"ui.player.noPreviewFoundFor": "No Preview Found for {{cameraName}}",
"ui.calendarFilter.last24Hours": "Last 24 Hours",
"ui.searchView.noTrackedObjects": "No Tracked Objects Found",
"ui.searchView.settings": "Settings",
"ui.searchView.settings.defaultView": "Default View",
"ui.searchView.settings.defaultView.desc": "When no filters are selected, display a summary of the most recent tracked objects per label, or display an unfiltered grid.",
"ui.searchView.settings.defaultView.summary": "Summary",
"ui.searchView.settings.defaultView.unfilteredGrid": "Unfiltered Grid",
"ui.searchView.settings.gridColumns": "Grid Columns",
"ui.searchView.settings.gridColumns.desc": "Select the number of columns in the grid view.",
"ui.searchView.settings.searchSource": "Search Source",
"ui.searchView.settings.searchSource.desc": "Choose whether to search the thumbnails or descriptions of your tracked objects.",
"ui.settingView.menu.uiSettings": "UI Settings",
"ui.settingView.menu.searchSettings": "Search Settings",
"ui.settingView.menu.exploreSettings": "Explore Settings",
"ui.settingView.menu.cameraSettings": "Camera Settings",
"ui.settingView.menu.masksAndZones": "Masks / Zones",
"ui.settingView.menu.motionTuner": "Motion Tuner",
@ -290,18 +385,18 @@
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "Sunday",
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "Monday",
"ui.settingView.searchSettings": "Search Settings",
"ui.settingView.searchSettings.semanticSearch": "Semantic Search",
"ui.settingView.searchSettings.semanticSearch.desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
"ui.settingView.searchSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup",
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. <em>Don't forget to disable the option after restarting!</em>",
"ui.settingView.searchSettings.semanticSearch.modelSize": "Model Size",
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "small",
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "large",
"ui.settingView.searchSettings.semanticSearch.modelSize.small.desc": "Using <em>small</em> employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality.",
"ui.settingView.searchSettings.semanticSearch.modelSize.large.desc": "Using <em>large</em> employs the full Jina model and will automatically run on the GPU if applicable.",
"ui.settingView.exploreSettings": "Explore Settings",
"ui.settingView.exploreSettings.semanticSearch": "Semantic Search",
"ui.settingView.exploreSettings.semanticSearch.desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
"ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup",
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. <em>Don't forget to disable the option after restarting!</em>",
"ui.settingView.exploreSettings.semanticSearch.modelSize": "Model Size",
"ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
"ui.settingView.exploreSettings.semanticSearch.modelSize.small": "small",
"ui.settingView.exploreSettings.semanticSearch.modelSize.large": "large",
"ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "Using <em>small</em> employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality.",
"ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "Using <em>large</em> employs the full Jina model and will automatically run on the GPU if applicable.",
"ui.settingView.cameraSettings": "Camera Settings",
"ui.settingView.cameraSettings.reviewClassification": "Review Classification",
@ -319,6 +414,7 @@
"ui.settingView.masksAndZonesSettings": "Masks / Zones",
"ui.settingView.masksAndZonesSettings.zone": "Zones",
"ui.settingView.masksAndZonesSettings.zone.documentTitle": "Edit Zone - Frigate",
"ui.settingView.masksAndZonesSettings.zone.desc": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "Documentation",
"ui.settingView.masksAndZonesSettings.zone.add": "Add Zone",
@ -338,6 +434,7 @@
"ui.settingView.masksAndZonesSettings.zone.allObjects": "All Objects",
"ui.settingView.masksAndZonesSettings.motionMasks": "Motion Mask",
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "Edit Motion Mask - Frigate",
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "Motion masks are used to prevent unwanted types of motion from triggering detection. Over masking will make it more difficult for objects to be tracked.",
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "Documentation",
"ui.settingView.masksAndZonesSettings.motionMasks.add": "New Motion Mask",
@ -351,8 +448,8 @@
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead.",
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.documentation": "Read the documentation",
"ui.settingView.masksAndZonesSettings.objectMasks": "Object Masks",
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle": "Edit Object Mask - Frigate",
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "Object filter masks are used to filter out false positives for a given object type based on location.",
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "Documentation",
"ui.settingView.masksAndZonesSettings.objectMasks.add": "Add Object Mask",
@ -423,6 +520,11 @@
"ui.configEditorView.configEditor": "Config Editor",
"ui.configEditorView.copyConfig": "Copy Config",
"ui.configEditorView.saveAndRestart": "Save & Restart",
"ui.configEditorView.saveOnly": "Save Only"
"ui.configEditorView.saveOnly": "Save Only",
"ui.exportView.documentTitle": "Export - Frigate",
"ui.exportView.search": "Search",
"ui.exportView.noExports": "No exports found",
"ui.exportView.deleteExport": "Delete Export",
"ui.exportView.deleteExport.desc": "Are you sure you want to delete {{exportName}}?"
}

View File

@ -1,5 +1,4 @@
{
"object.person": "人",
"object.bicycle": "自行车",
"object.car": "汽车",
@ -110,6 +109,22 @@
"audio.fire_alarm": "火灾警报器",
"ui.time.justNow": "刚才",
"ui.time.today": "今天",
"ui.time.yesterday": "昨天",
"ui.time.last7": "最后 7 天",
"ui.time.last14": "最后 14 天",
"ui.time.last30": "最后 30 天",
"ui.time.thisWeek": "本周",
"ui.time.lastWeek": "上个周",
"ui.time.thisMonth": "本月",
"ui.time.lastMonth": "上个月",
"ui.time.pm": "上午",
"ui.time.am": "下午",
"ui.time.formattedTimestamp": "%m月%-d日 %I:%M:%S %p",
"ui.time.formattedTimestamp.24hour": "%m月%-d日 %H:%M:%S",
"ui.iconPicker.selectIcon": "选择图标",
"ui.iconPicker.search.placeholder": "搜索图标...",
"ui.dialog.restart.title": "你确定要重启 Frigate?",
"ui.dialog.restart.button": "重启",
@ -117,6 +132,21 @@
"ui.dialog.restart.restarting.content": "该页面将会在 {{countdown}} 秒后自动刷新。",
"ui.dialog.restart.restarting.button": "强制刷新",
"ui.dialog.export.time.fromTimeline": "从时间线选择",
"ui.dialog.export.time.lastHour_one": "最后1小时",
"ui.dialog.export.time.lastHour_other": "最后 {{count}} 小时",
"ui.dialog.export.time.custom": "自定义",
"ui.dialog.export.name.placeholder": "导出项目的名字",
"ui.dialog.export.select": "选择",
"ui.dialog.export.export": "导出",
"ui.dialog.export.toast.success": "导出成功。进入 /exports 目录查看文件。",
"ui.dialog.export.toast.error.failed": "导出失败:{{error}}",
"ui.dialog.export.toast.error.endTimeMustAfterStartTime": "结束时间必须在开始时间之后",
"ui.dialog.export.toast.error.noVaildTimeSelected": "未选择有效的时间范围",
"ui.dialog.export.fromTimeline.saveExport": "保存导出",
"ui.dialog.export.fromTimeline.previewExport": "预览导出",
"ui.stats.ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%",
"ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%",
"ui.stats.healthy": "系统运行正常",
@ -203,13 +233,31 @@
"ui.menu.live": "实时监控",
"ui.menu.live.allCameras": "所有摄像头",
"ui.menu.review": "回放",
"ui.menu.explore": "Explore",
"ui.menu.explore": "探测",
"ui.menu.export": "导出",
"ui.menu.uiPlayground": "UI Playground",
"ui.menu.user.current": "当前用户:{{user}}",
"ui.menu.user.anonymous": "匿名",
"ui.menu.user.logout": "登出",
"ui.cameraGroup": "摄像头组",
"ui.cameraGroup.add": "添加摄像头组",
"ui.cameraGroup.edit": "编辑摄像头组",
"ui.cameraGroup.edit.desc": "编辑摄像头组",
"ui.cameraGroup.delete.confirm": "确认删除",
"ui.cameraGroup.delete.confirm.desc": "你确定要删除摄像头组 <strong>{{name}}</strong> 吗?",
"ui.cameraGroup.name": "名称",
"ui.cameraGroup.name.placeholder": "请输入名称",
"ui.cameraGroup.name.errorMessage.mustLeastCharacters": "摄像头组的名称必须至少有 2 个字符。",
"ui.cameraGroup.name.errorMessage.exists": "摄像头组名称已存在。",
"ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "摄像头组名称不能包含英文句号(.)。",
"ui.cameraGroup.name.errorMessage.invalid": "无效的摄像头组名称。",
"ui.cameraGroup.cameras": "摄像头",
"ui.cameraGroup.cameras.desc": "选择添加至该组的摄像头。",
"ui.cameraGroup.icon": "图标",
"ui.cameraGroup.toast.success": "摄像头组({{name}})保存成功。",
"ui.cameraGroup.toast.error": "保存设置失败: {{error}}",
"ui.eventView.alerts": "警告",
"ui.eventView.detections": "检测",
"ui.eventView.motion": "运动",
@ -217,9 +265,33 @@
"ui.eventView.empty.alert": "还没有“警告”类回放",
"ui.eventView.empty.detection": "还没有“探测”类回放",
"ui.reviewFilter.filter": "过滤器",
"ui.reviewFilter.filter.allLabels": "所有标签",
"ui.reviewFilter.filter.allZones": "所有区域",
"ui.filter": "过滤器",
"ui.filter.allLabels": "所有标签",
"ui.filter.allLabels.short": "标签",
"ui.filter.countLabels": "{{count}} 个标签",
"ui.filter.allZones": "所有区域",
"ui.filter.allZones.short": "区域",
"ui.filter.allDates": "所有日期",
"ui.filter.allDates.short": "日期",
"ui.filter.more": "更多筛选项",
"ui.filter.timeRange": "时间范围",
"ui.filter.zones": "区域",
"ui.filter.subLabels": "子标签",
"ui.filter.allSubLabels": "所有子标签",
"ui.filter.score": "分值",
"ui.filter.features": "特性",
"ui.filter.features.hasSnapshot": "包含快照",
"ui.filter.features.hasVideoClip": "包含视频片段",
"ui.filter.features.submittedToFrigatePlus": "提交至 Frigate+",
"ui.filter.features.submittedToFrigatePlus.tips": "你必须要先筛选具有快照的探测对象。<br /><br />没有快照的跟踪对象无法提交至 Frigate+.",
"ui.filter.sort": "排序",
"ui.filter.sort.dateAsc": "日期 (正序)",
"ui.filter.sort.dateDesc": "日期 (倒序)",
"ui.filter.sort.scoreAsc": "对象分值 (正序)",
"ui.filter.sort.scoreDesc": "对象分值 (倒序)",
"ui.filter.sort.relevance": "关联性",
"ui.filter.allCameras": "所有摄像头",
"ui.filter.allCameras.short": "摄像头",
"ui.reviewFilter.showReviewed": "显示已查看的项目",
@ -238,8 +310,13 @@
"ui.pictureInPicture": "画中画",
"ui.on": "开",
"ui.off": "关",
"ui.edit": "编辑",
"ui.delete": "删除",
"ui.yes": "是",
"ui.no": "否",
"ui.live.documentTitle": "实时监控 - Frigate",
"ui.live.documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate",
"ui.live.twoWayTalk.enable": "开启双向对话",
"ui.live.twoWayTalk.disable": "关闭双向对话",
"ui.live.cameraAudio.enable": "开启摄像头音频",
@ -263,10 +340,31 @@
"ui.live.autotracking.enable": "启用自动追踪",
"ui.live.autotracking.disable": "关闭自动追踪",
"ui.review.timeline": "时间线",
"ui.review.events": "事件",
"ui.review.events.noFoundForTimePeriod": "未找到该时间段的事件。",
"ui.review.documentTitle": "预览 - Frigate",
"ui.review.recordings.documentTitle": "回放 - Frigate",
"ui.player.noRecordingsFoundForThisTime": "找不到此次录制",
"ui.player.noPreviewFound": "没有找到预览",
"ui.player.noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览",
"ui.calendarFilter.last24Hours": "过去24小时",
"ui.searchView.noTrackedObjects": "找不到探测的对象",
"ui.searchView.settings": "设置",
"ui.searchView.settings.defaultView": "默认视图",
"ui.searchView.settings.defaultView.desc": "当未选择任何过滤器时,显示每个标签最近跟踪对象的摘要,或显示未过滤的网格。",
"ui.searchView.settings.defaultView.summary": "摘要",
"ui.searchView.settings.defaultView.unfilteredGrid": "未过滤网格",
"ui.searchView.settings.gridColumns": "网格列数",
"ui.searchView.settings.gridColumns.desc": "选择网格视图中的列数。",
"ui.searchView.settings.searchSource": "搜索源",
"ui.searchView.settings.searchSource.desc": "选择是搜索缩略图还是跟踪对象的描述。",
"ui.settingView.menu.uiSettings": "界面设置",
"ui.settingView.menu.searchSettings": "搜索设置",
"ui.settingView.menu.exploreSettings": "搜索设置",
"ui.settingView.menu.cameraSettings": "摄像头设置",
"ui.settingView.menu.masksAndZones": "屏罩 / 区域",
"ui.settingView.menu.motionTuner": "运动调整器",
@ -292,18 +390,18 @@
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "星期天",
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "星期一",
"ui.settingView.searchSettings": "搜索设置",
"ui.settingView.searchSettings.semanticSearch": "语义搜索",
"ui.settingView.searchSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
"ui.settingView.searchSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>",
"ui.settingView.searchSettings.semanticSearch.modelSize": "模型大小",
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "小",
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "大",
"ui.settingView.searchSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存在CPU上也能较快的运行。质量较好。",
"ui.settingView.searchSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型并在适用的情况下使用GPU。",
"ui.settingView.exploreSettings": "探测设置",
"ui.settingView.exploreSettings.semanticSearch": "语义搜索",
"ui.settingView.exploreSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
"ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>",
"ui.settingView.exploreSettings.semanticSearch.modelSize": "模型大小",
"ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
"ui.settingView.exploreSettings.semanticSearch.modelSize.small": "小",
"ui.settingView.exploreSettings.semanticSearch.modelSize.large": "大",
"ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存在CPU上也能较快的运行。质量较好。",
"ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型并在适用的情况下使用GPU。",
"ui.settingView.cameraSettings": "摄像头设置",
"ui.settingView.cameraSettings.reviewClassification": "预览分级",
@ -321,6 +419,7 @@
"ui.settingView.masksAndZonesSettings": "屏罩 / 区域",
"ui.settingView.masksAndZonesSettings.zone": "区域",
"ui.settingView.masksAndZonesSettings.zone.documentTitle": "编辑区域 - Frigate",
"ui.settingView.masksAndZonesSettings.zone.desc": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。",
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "文档(英文)",
"ui.settingView.masksAndZonesSettings.zone.add": "添加区域",
@ -340,6 +439,7 @@
"ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象",
"ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩",
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "编辑运动遮罩 - Frigate",
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)",
"ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩",
@ -355,6 +455,7 @@
"ui.settingView.masksAndZonesSettings.objectMasks": "对象遮罩",
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle": "编辑对象遮罩 - Frigate",
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "对象过滤器用于防止特定位置的指定对象被误报。",
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "文档(英文)",
"ui.settingView.masksAndZonesSettings.objectMasks.add": "添加对象遮罩",
@ -425,5 +526,12 @@
"ui.configEditorView.configEditor": "配置编辑器",
"ui.configEditorView.copyConfig": "复制配置",
"ui.configEditorView.saveAndRestart": "保存并重启",
"ui.configEditorView.saveOnly": "只保存"
"ui.configEditorView.saveOnly": "只保存",
"ui.exportView.documentTitle": "导出 - Frigate",
"ui.exportView.search": "搜索",
"ui.exportView.noExports": "没有找到导出的项目",
"ui.exportView.deleteExport": "删除导出的项目",
"ui.exportView.deleteExport.desc": "你确定要删除 {{exportName}} 吗?"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ import { IoClose } from "react-icons/io5";
import Heading from "../ui/heading";
import { cn } from "@/lib/utils";
import { Button } from "../ui/button";
import { Trans } from "react-i18next";
import { t } from "i18next";
export type IconName = keyof typeof LuIcons;
@ -70,7 +72,7 @@ export default function IconPicker({
className="mt-2 w-full text-muted-foreground"
aria-label="Select an icon"
>
Select an icon
<Trans>ui.iconPicker.selectIcon</Trans>
</Button>
) : (
<div className="hover:cursor-pointer">
@ -101,7 +103,9 @@ export default function IconPicker({
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
>
<div className="mb-3 flex flex-row items-center justify-between">
<Heading as="h4">Select an icon</Heading>
<Heading as="h4">
<Trans>ui.iconPicker.selectIcon</Trans>
</Heading>
<span tabIndex={0} className="sr-only" />
<IoClose
size={15}
@ -113,7 +117,7 @@ export default function IconPicker({
</div>
<Input
type="text"
placeholder="Search for an icon..."
placeholder={t("ui.iconPicker.search.placeholder")}
className="text-md mb-3 md:text-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import {
getPreviewForTimeRange,
usePreviewForTimeRange,
} from "@/hooks/use-camera-previews";
import { Trans } from "react-i18next";
type PreviewPlayerProps = {
className?: string;
@ -88,7 +89,7 @@ export default function PreviewPlayer({
className,
)}
>
No Preview Found
<Trans>ui.player.noPreviewFound</Trans>
</div>
);
}
@ -324,7 +325,9 @@ function PreviewVideoPlayer({
</video>
{cameraPreviews && !currentPreview && (
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
No Preview Found for {camera.replaceAll("_", " ")}
<Trans value={{ camera: camera.replaceAll("_", " ") }}>
ui.player.noPreviewFoundFor
</Trans>
</div>
)}
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
@ -544,7 +547,9 @@ function PreviewFramesPlayer({
/>
{previewFrames?.length === 0 && (
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
No Preview Found for {camera.replaceAll("_", " ")}
<Trans values={{ cameraName: camera.replaceAll("_", " ") }}>
ui.player.noPreviewFoundFor
</Trans>
</div>
)}
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
import LiveCameraView from "@/views/live/LiveCameraView";
import LiveDashboardView from "@/views/live/LiveDashboardView";
import { t } from "i18next";
import { useEffect, useMemo, useRef } from "react";
import useSWR from "swr";
@ -64,11 +65,15 @@ function Live() {
.split("_")
.filter((text) => text)
.map((text) => text[0].toUpperCase() + text.substring(1));
document.title = `${capitalized.join(" ")} - Live - Frigate`;
document.title = t("ui.live.documentTitle.withCamera", {
camera: capitalized.join(" "),
});
} else if (cameraGroup && cameraGroup != "default") {
document.title = `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)} - Live - Frigate`;
document.title = t("ui.live.documentTitle.withCamera", {
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
});
} else {
document.title = "Live - Frigate";
document.title = t("ui.live.documentTitle");
}
}, [cameraGroup, selectedCameraName]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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