mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-18 17:14:26 +03:00
feat: add i18n
This commit is contained in:
parent
f84713487f
commit
f45242d22c
82
web/package-lock.json
generated
82
web/package-lock.json
generated
@ -40,6 +40,7 @@
|
||||
"embla-carousel-react": "^8.2.0",
|
||||
"framer-motion": "^11.5.4",
|
||||
"hls.js": "^1.5.17",
|
||||
"i18next": "^24.2.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.1.1",
|
||||
"konva": "^9.3.16",
|
||||
@ -55,6 +56,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-grid-layout": "^1.4.4",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-router-dom": "^6.26.0",
|
||||
@ -188,9 +190,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
|
||||
"integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@ -5530,6 +5533,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
@ -5567,6 +5579,37 @@
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz",
|
||||
"integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
|
||||
@ -7275,6 +7318,28 @@
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz",
|
||||
"integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
|
||||
@ -8571,7 +8636,7 @@
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -8922,6 +8987,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
"embla-carousel-react": "^8.2.0",
|
||||
"framer-motion": "^11.5.4",
|
||||
"hls.js": "^1.5.17",
|
||||
"i18next": "^24.2.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.1.1",
|
||||
"konva": "^9.3.16",
|
||||
@ -61,6 +62,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-grid-layout": "^1.4.4",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-router-dom": "^6.26.0",
|
||||
|
||||
264
web/public/locales/en/translation.json
Normal file
264
web/public/locales/en/translation.json
Normal file
@ -0,0 +1,264 @@
|
||||
{
|
||||
"object.person": "Person",
|
||||
"object.cat": "Cat",
|
||||
"object.car": "Car",
|
||||
|
||||
"ui.time.justNow": "Just now",
|
||||
|
||||
"ui.stats.ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
|
||||
"ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
|
||||
|
||||
"ui.system.general": "General",
|
||||
"ui.system.storage": "Storage",
|
||||
"ui.system.cameras": "Cameras",
|
||||
"ui.system.lastRefreshed": "Last refreshed: ",
|
||||
"ui.system.general.detector": "Detectors",
|
||||
"ui.system.general.detectorInferenceSpeed": "Detector Inference Speed",
|
||||
"ui.system.general.detectorCpuUsage": "Detector CPU Usage",
|
||||
"ui.system.general.detectorMemoryUsage": "Detector Memory Usage",
|
||||
"ui.system.general.hardwareInfo": "Hardware Info",
|
||||
"ui.system.general.gpuUsage": "GPU Usage",
|
||||
"ui.system.general.gpuMemory": "GPU Memory",
|
||||
"ui.system.general.gpuEncoder": "GPU Encoder",
|
||||
"ui.system.general.gpuDecoder": "GPU Decoder",
|
||||
"ui.system.general.otherProcesses": "Other Processes",
|
||||
"ui.system.general.processCpuUsage": "Process CPU Usage",
|
||||
"ui.system.general.processMemoryUsage": "Process Memory Usage",
|
||||
|
||||
"ui.system.storage.overview": "Overview",
|
||||
"ui.system.storage.recordings": "Recordings",
|
||||
"ui.system.storage.recordings.tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.",
|
||||
"ui.system.storage.cameraStorage": "Camera Storage",
|
||||
"ui.system.storage.cameraStorage.camera": "Camera",
|
||||
"ui.system.storage.cameraStorage.unused": "Unused",
|
||||
"ui.system.storage.cameraStorage.storageUsed": "Storage Used",
|
||||
"ui.system.storage.cameraStorage.percentageOfTotalUsed": "Percentage of Total Used",
|
||||
"ui.system.storage.cameraStorage.bandwidth": "Bandwidth",
|
||||
"ui.system.storage.cameraStorage.unused.tips": "This value may not accurately represent the free space available to Frigate if you have other files stored on your drive beyond Frigate's recordings. Frigate does not track storage usage outside of its recordings.",
|
||||
|
||||
"ui.system.cameras.overview": "Overview",
|
||||
"ui.system.cameras.info.cameraProbeInfo": "{{camera}} Camera Probe Info",
|
||||
"ui.system.cameras.info.streamDataFromFFPROBE": "Stream data is obtained with <code>ffprobe</code>.",
|
||||
"ui.system.cameras.info.fetching": "Fetching Camera Data",
|
||||
"ui.system.cameras.info.stream": "Stream {{idx}}",
|
||||
"ui.system.cameras.info.video": "Video:",
|
||||
"ui.system.cameras.info.codec": "Codec:",
|
||||
"ui.system.cameras.info.resolution": "Resolution:",
|
||||
"ui.system.cameras.info.fps": "FPS:",
|
||||
"ui.system.cameras.info.unknown": "Unknown",
|
||||
"ui.system.cameras.info.audio": "Audio:",
|
||||
"ui.system.cameras.info.error": "Error: {{error}}",
|
||||
"ui.system.cameras.framesAndDetections": "Frames / Detections",
|
||||
"ui.system.cameras.label.camera": "camera",
|
||||
"ui.system.cameras.label.detect": "detect",
|
||||
"ui.system.cameras.label.skipped": "skipped",
|
||||
"ui.system.cameras.label.ffmpeg": "ffmpeg",
|
||||
"ui.system.cameras.label.capture": "capture",
|
||||
|
||||
|
||||
"ui.system": "System",
|
||||
"ui.systemMetrics": "System metrics",
|
||||
"ui.systemLogs": "System logs",
|
||||
|
||||
"ui.configuration": "Configuration",
|
||||
"ui.settings": "Settings",
|
||||
"ui.configurationEditor": "Configuration Editor",
|
||||
"ui.withSystem": "System",
|
||||
"ui.language.en": "English",
|
||||
"ui.language.zhCN": "简体中文(Simplified Chinese)",
|
||||
"ui.languages" : "Languages",
|
||||
|
||||
|
||||
"ui.appearance": "Appearance",
|
||||
"ui.darkMode": "Dark Mode",
|
||||
"ui.darkMode.light": "Light",
|
||||
"ui.darkMode.dark": "Dark",
|
||||
|
||||
"ui.theme": "Theme",
|
||||
"ui.theme.blue": "Blue",
|
||||
"ui.theme.green": "Green",
|
||||
"ui.theme.nord": "Nord",
|
||||
"ui.theme.red": "Red",
|
||||
"ui.theme.high.contrast": "High Contrast",
|
||||
"ui.theme.default": "Default",
|
||||
|
||||
"ui.help": "Help",
|
||||
"ui.documentation": "Documentation",
|
||||
"ui.documentation.label": "Frigate documentation",
|
||||
"ui.restart": "Restart Frigate",
|
||||
|
||||
"ui.menu.live": "Live",
|
||||
"ui.menu.live.allCameras": "All Cameras",
|
||||
"ui.menu.review": "Review",
|
||||
"ui.menu.explore": "Explore",
|
||||
"ui.menu.export": "Export",
|
||||
"ui.menu.uiPlayground": "UI Playground",
|
||||
"ui.menu.user.current": "Current User: {{user}}",
|
||||
"ui.menu.user.anonymous": "anonymous",
|
||||
"ui.menu.user.logout": "Logout",
|
||||
|
||||
"ui.eventView.alerts": "Alerts",
|
||||
"ui.eventView.detections": "Detections",
|
||||
"ui.eventView.motion": "Motion",
|
||||
"ui.eventView.allCameras": "All Cameras",
|
||||
"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.reviewFilter.showReviewed": "Show Reviewed",
|
||||
|
||||
"ui.apply": "Apply",
|
||||
"ui.reset": "Reset",
|
||||
"ui.enabled": "Enabled",
|
||||
"ui.save": "Save",
|
||||
"ui.saving": "Saving...",
|
||||
"ui.cancel": "Cancel",
|
||||
"ui.copy": "Copy",
|
||||
|
||||
"ui.calendarFilter.last24Hours": "Last 24 Hours",
|
||||
|
||||
"ui.settingView.menu.uiSettings": "UI Settings",
|
||||
"ui.settingView.menu.searchSettings": "Search Settings",
|
||||
"ui.settingView.menu.cameraSettings": "Camera Settings",
|
||||
"ui.settingView.menu.masksAndZones": "Masks / Zones",
|
||||
"ui.settingView.menu.motionTuner": "Motion Tuner",
|
||||
"ui.settingView.menu.debug": "Debug",
|
||||
"ui.settingView.menu.users": "Users",
|
||||
"ui.settingView.menu.notifications": "Notifications",
|
||||
|
||||
"ui.settingView.generalSettings": "General Settings",
|
||||
"ui.settingView.generalSettings.liveDashboard": "Live Dashboard",
|
||||
"ui.settingView.generalSettings.automaticLiveView": "Automatic Live View",
|
||||
"ui.settingView.generalSettings.automaticLiveView.desc": "Automatically switch to a camera's live view when activity is detected. Disabling this option causes static camera images on the Live dashboard to only update once per minute.",
|
||||
"ui.settingView.generalSettings.playAlertVideos": "Play Alert Videos",
|
||||
"ui.settingView.generalSettings.playAlertVideos.desc": "By default, recent alerts on the Live dashboard play as small looping videos. Disable this option to only show a static image of recent alerts on this device/browser.",
|
||||
"ui.settingView.generalSettings.storedLayouts": "Stored Layouts",
|
||||
"ui.settingView.generalSettings.storedLayouts.desc": "The layout of cameras in a camera group can be dragged/resized. The positions are stored in your browser's local storage.",
|
||||
"ui.settingView.generalSettings.storedLayouts.clearAll": "Clear All Layouts",
|
||||
"ui.settingView.generalSettings.recordingsViewer": "Recordings Viewer",
|
||||
"ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate": "Default Playback Rate",
|
||||
"ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate.desc": "Default playback rate for recordings playback.",
|
||||
"ui.settingView.generalSettings.calendar": "Calendar",
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday": "First Weekday",
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday.desc": "The day that the weeks of the review calendar begin on.",
|
||||
"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.cameraSettings": "Camera Settings",
|
||||
"ui.settingView.cameraSettings.reviewClassification": "Review Classification",
|
||||
"ui.settingView.cameraSettings.reviewClassification.desc": "Frigate categorizes review items as Alerts and Detections. By default, all <em>person</em> and <em>car</em> objects are considered Alerts. You can refine categorization of your review items by configuring required zones for them.",
|
||||
"ui.settingView.cameraSettings.reviewClassification.readTheDocumentation": "Read the Documentation",
|
||||
"ui.settingView.cameraSettings.reviewClassification.noDefinedZones": "No zones are defined for this camera.",
|
||||
"ui.settingView.cameraSettings.reviewClassification.objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.",
|
||||
"ui.settingView.cameraSettings.reviewClassification.selectAlertsZones": "Select zones for Alerts",
|
||||
|
||||
"ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips": "All {{detectionsLabels}} objects <em>not classified as Alerts</em> on {{cameraName}} will be shown as Detections.",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips": "All {{detectionsLabels}} objects <em>not classified as Alerts</em> that are detected in {{zone}} on {{cameraName}} will be shown as Detections.",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips.notSelectDetections": "All {{detectionsLabels}} objects <em>not classified as Alerts</em> that are detected in {{zone}} on {{cameraName}} will be shown as Detections, regardless of zone",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips.regardlessOfZoneObjectDetectionsTips": "All {{detectionsLabels}} objects <em>not classified as Alerts</em> on {{cameraName}} will be shown as Detections, regardless of zone.",
|
||||
|
||||
"ui.settingView.masksAndZonesSettings": "Masks / Zones",
|
||||
"ui.settingView.masksAndZonesSettings.zone": "Zones",
|
||||
"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",
|
||||
"ui.settingView.masksAndZonesSettings.zone.edit": "New Zone",
|
||||
"ui.settingView.masksAndZonesSettings.zone.point_one": "{{count}} point",
|
||||
"ui.settingView.masksAndZonesSettings.zone.point_other": "{{count}} points",
|
||||
"ui.settingView.masksAndZonesSettings.zone.clickDrawPolygon": "Click to draw a polygon on the image.",
|
||||
"ui.settingView.masksAndZonesSettings.zone.name": "Name",
|
||||
"ui.settingView.masksAndZonesSettings.zone.name.inputPlaceHolder": "Enter a name...",
|
||||
"ui.settingView.masksAndZonesSettings.zone.name.tips": "Name must be at least 2 characters and must not be the name of a camera or another zone.",
|
||||
"ui.settingView.masksAndZonesSettings.zone.inertia": "Inertia",
|
||||
"ui.settingView.masksAndZonesSettings.zone.inertia.desc": "Specifies how many frames that an object must be in a zone before they are considered in the zone. <em>Default: 3</em>",
|
||||
"ui.settingView.masksAndZonesSettings.zone.loiteringTime": "Loitering Time",
|
||||
"ui.settingView.masksAndZonesSettings.zone.loiteringTime.desc": "Sets a minimum amount of time in seconds that the object must be in the zone for it to activate. <em>Default: 0</em>",
|
||||
"ui.settingView.masksAndZonesSettings.zone.objects": "Objects",
|
||||
"ui.settingView.masksAndZonesSettings.zone.objects.desc": "List of objects that apply to this zone.",
|
||||
"ui.settingView.masksAndZonesSettings.zone.allObjects": "All Objects",
|
||||
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks": "Motion Mask",
|
||||
"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",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.edit": "Edit Motion Mask",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.context": "Motion masks are used to prevent unwanted types of motion from triggering detection (example: tree branches, camera timestamps). Motion masks should be used <em>very sparingly</em>, over-masking will make it more difficult for objects to be tracked.",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.context.documentation": "Read the documentation",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.point_one": "{{count}} point",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.point_other": "{{count}} points",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.clickDrawPolygon": "Click to draw a polygon on the image.",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge": "The motion mask is covering {{polygonArea}}% of the camera frame. Large motion masks are not recommended.",
|
||||
"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.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",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.edit": "Edit Object Mask",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.context": "Object filter masks are used to filter out false positives for a given object type based on location.",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.point_one": "{{count}} point",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.point_other": "{{count}} points",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.clickDrawPolygon": "Click to draw a polygon on the image.",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.objects": "Objects",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.objects.desc": "The object type that that applies to this object mask.",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.objects.allObjectTypes": "All object types",
|
||||
|
||||
|
||||
"ui.settingView.motionDetectionTuner": "Motion Detection Tuner",
|
||||
"ui.settingView.motionDetectionTuner.desc": "Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.",
|
||||
"ui.settingView.motionDetectionTuner.desc.documentation": "Read the Motion Tuning Guide",
|
||||
"ui.settingView.motionDetectionTuner.Threshold": "Threshold",
|
||||
"ui.settingView.motionDetectionTuner.Threshold.desc": "The threshold value dictates how much of a change in a pixel's luminance is required to be considered motion. <em>Default: 30</em>",
|
||||
"ui.settingView.motionDetectionTuner.contourArea": "Contour Area",
|
||||
"ui.settingView.motionDetectionTuner.contourArea.desc": "The contour area value is used to decide which groups of changed pixels qualify as motion. <em>Default: 10</em>",
|
||||
"ui.settingView.motionDetectionTuner.improveContrast": "Improve Contrast",
|
||||
"ui.settingView.motionDetectionTuner.improveContrast.desc": "Improve contrast for darker scenes. <em>Default: ON</em>",
|
||||
|
||||
"ui.settingView.debug": "Debug",
|
||||
"ui.settingView.debug.detectorDesc": "Frigate uses your detectors ({{detectors}}) to detect objects in your camera's video stream.",
|
||||
"ui.settingView.debug.desc": "Debugging view shows a real-time view of tracked objects and their statistics. The object list shows a time-delayed summary of detected objects.",
|
||||
"ui.settingView.debug.debugging": "Debugging",
|
||||
"ui.settingView.debug.objectList": "Object List",
|
||||
"ui.settingView.debug.noObjects": "No objects",
|
||||
"ui.settingView.debug.boundingBoxes": "Bounding boxes",
|
||||
"ui.settingView.debug.boundingBoxes.desc": "Show bounding boxes around tracked objects",
|
||||
"ui.settingView.debug.boundingBoxes.colors": "Object Bounding Box Colors",
|
||||
"ui.settingView.debug.boundingBoxes.colors.info": "<li>At startup, different colors will be assigned to each object label</li><li>A dark blue thin line indicates that object is not detected at this current point in time</li><li>A gray thin line indicates that object is detected as being stationary</li><li>A thick line indicates that object is the subject of autotracking (when enabled)</li>",
|
||||
"ui.settingView.debug.timestamp": "Timestamp",
|
||||
"ui.settingView.debug.timestamp.desc": "Overlay a timestamp on the image",
|
||||
"ui.settingView.debug.zone": "Zones",
|
||||
"ui.settingView.debug.zone.desc": "Show an outline of any defined zones",
|
||||
"ui.settingView.debug.mask": "Motion masks",
|
||||
"ui.settingView.debug.mask.desc": "Show motion mask polygons",
|
||||
"ui.settingView.debug.motion": "Motion boxes",
|
||||
"ui.settingView.debug.motion.desc": "Show boxes around areas where motion is detected",
|
||||
"ui.settingView.debug.motion.tips": "<p className=\"mb-2\"><strong>Motion Boxes</strong></p><br><p>Red boxes will be overlaid on areas of the frame where motion is currently being detected</p>",
|
||||
"ui.settingView.debug.regions": "Regions",
|
||||
"ui.settingView.debug.regions.desc": "Show a box of the region of interest sent to the object detector",
|
||||
"ui.settingView.debug.regions.tips": "<p className=\"mb-2\"><strong>Region Boxes</strong></p><br><p>Bright green boxes will be overlaid on areas of interest in the frame that are being sent to the object detector.</p>",
|
||||
|
||||
"ui.configEditorView.configEditor": "Config Editor",
|
||||
"ui.configEditorView.copyConfig": "Copy Config",
|
||||
"ui.configEditorView.saveAndRestart": "Save & Restart",
|
||||
"ui.configEditorView.saveOnly": "Save Only"
|
||||
|
||||
}
|
||||
264
web/public/locales/zh-CN/translation.json
Normal file
264
web/public/locales/zh-CN/translation.json
Normal file
@ -0,0 +1,264 @@
|
||||
{
|
||||
|
||||
"object.person": "人",
|
||||
"object.cat": "猫",
|
||||
"object.car": "车",
|
||||
|
||||
"ui.time.justNow": "刚才",
|
||||
|
||||
"ui.stats.ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%)",
|
||||
"ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%)",
|
||||
|
||||
"ui.system.general": "常规",
|
||||
"ui.system.storage": "存储",
|
||||
"ui.system.cameras": "摄像头",
|
||||
"ui.system.lastRefreshed": "最后刷新时间:",
|
||||
|
||||
"ui.system.general.detector": "探测器",
|
||||
"ui.system.general.detectorInferenceSpeed": "探测器推理速度",
|
||||
"ui.system.general.detectorCpuUsage": "探测器CPU使用率",
|
||||
"ui.system.general.detectorMemoryUsage": "探测器内存使用率",
|
||||
"ui.system.general.hardwareInfo": "硬件信息",
|
||||
"ui.system.general.gpuUsage": "GPU使用率",
|
||||
"ui.system.general.gpuMemory": "GPU显存",
|
||||
"ui.system.general.gpuEncoder": "GPU编码",
|
||||
"ui.system.general.gpuDecoder": "GPU解码",
|
||||
"ui.system.general.otherProcesses": "其他进程",
|
||||
"ui.system.general.processCpuUsage": "主进程CPU使用率",
|
||||
"ui.system.general.processMemoryUsage": "主进程CPU使用率",
|
||||
|
||||
"ui.system.storage.overview": "概览",
|
||||
"ui.system.storage.recordings": "录制内容",
|
||||
"ui.system.storage.recordings.tips": "该值表示 Frigate 数据库中录制内容所使用的总存储空间。Frigate 不会追踪磁盘上所有文件的存储使用情况。",
|
||||
"ui.system.storage.cameraStorage": "摄像头存储",
|
||||
"ui.system.storage.cameraStorage.camera": "摄像头",
|
||||
"ui.system.storage.cameraStorage.unused": "未使用",
|
||||
"ui.system.storage.cameraStorage.storageUsed": "存储使用",
|
||||
"ui.system.storage.cameraStorage.percentageOfTotalUsed": "总使用率",
|
||||
"ui.system.storage.cameraStorage.bandwidth": "带宽",
|
||||
"ui.system.storage.cameraStorage.unused.tips": "如果您的驱动器上存储了除 Frigate 录制内容之外的其他文件,该值可能无法准确反映 Frigate 可用的剩余空间。Frigate 不会追踪录制内容以外的存储使用情况。",
|
||||
|
||||
"ui.system.cameras.overview": "概览",
|
||||
"ui.system.cameras.info.cameraProbeInfo": "{{camera}} 的摄像头信息",
|
||||
"ui.system.cameras.info.streamDataFromFFPROBE": "流数据信息通过<code>ffprobe</code>获取。",
|
||||
"ui.system.cameras.info.fetching": "正在获取摄像头数据",
|
||||
"ui.system.cameras.info.stream": "视频流{{idx}}",
|
||||
"ui.system.cameras.info.video": "视频:",
|
||||
"ui.system.cameras.info.codec": "编解码器:",
|
||||
"ui.system.cameras.info.resolution": "分辨率:",
|
||||
"ui.system.cameras.info.fps": "帧率:",
|
||||
"ui.system.cameras.info.unknown": "未知",
|
||||
"ui.system.cameras.info.audio": "音频:",
|
||||
"ui.system.cameras.info.error": "错误:{{error}}",
|
||||
"ui.system.cameras.framesAndDetections": "帧数/检测次数",
|
||||
"ui.system.cameras.label.camera": "摄像头",
|
||||
"ui.system.cameras.label.detect": "探测",
|
||||
"ui.system.cameras.label.skipped": "跳过",
|
||||
"ui.system.cameras.label.ffmpeg": "ffmpeg编码器",
|
||||
"ui.system.cameras.label.capture": "捕获",
|
||||
|
||||
"ui.system": "系统",
|
||||
"ui.systemMetrics": "系统指标",
|
||||
"ui.systemLogs": "系统日志",
|
||||
|
||||
"ui.configuration": "配置",
|
||||
"ui.settings": "设置",
|
||||
"ui.configurationEditor": "配置编辑器",
|
||||
"ui.withSystem": "跟随系统",
|
||||
"ui.language.en": "English",
|
||||
"ui.language.zhCN": "简体中文",
|
||||
"ui.languages" : "languages / 语言",
|
||||
|
||||
|
||||
"ui.appearance": "外观",
|
||||
"ui.darkMode": "深色模式",
|
||||
"ui.darkMode.light": "浅色",
|
||||
"ui.darkMode.dark": "深色",
|
||||
|
||||
"ui.theme": "主题",
|
||||
"ui.theme.blue": "蓝色",
|
||||
"ui.theme.green": "绿色",
|
||||
"ui.theme.nord": "Nord",
|
||||
"ui.theme.red": "红色",
|
||||
"ui.theme.high.contrast": "高对比度",
|
||||
"ui.theme.default": "默认",
|
||||
|
||||
"ui.help": "帮助",
|
||||
"ui.documentation": "文档(英文)",
|
||||
"ui.documentation.label": "Frigate 的官方文档",
|
||||
"ui.restart": "重启 Frigate",
|
||||
|
||||
"ui.menu.live": "实时监控",
|
||||
"ui.menu.live.allCameras": "所有摄像头",
|
||||
"ui.menu.review": "回放",
|
||||
"ui.menu.explore": "Explore",
|
||||
"ui.menu.export": "导出",
|
||||
"ui.menu.uiPlayground": "UI Playground",
|
||||
"ui.menu.user.current": "当前用户:{{user}}",
|
||||
"ui.menu.user.anonymous": "匿名",
|
||||
"ui.menu.user.logout": "登出",
|
||||
|
||||
"ui.eventView.alerts": "警告",
|
||||
"ui.eventView.detections": "检测",
|
||||
"ui.eventView.motion": "运动",
|
||||
"ui.eventView.allCameras": "所有摄像头",
|
||||
"ui.eventView.empty.alert": "还没有“警告”类回放",
|
||||
"ui.eventView.empty.detection": "还没有“探测”类回放",
|
||||
|
||||
"ui.reviewFilter.filter": "过滤器",
|
||||
"ui.reviewFilter.filter.allLabels": "所有标签",
|
||||
"ui.reviewFilter.filter.allZones": "所有区域",
|
||||
|
||||
"ui.reviewFilter.showReviewed": "显示已查看的项目",
|
||||
|
||||
"ui.apply": "应用",
|
||||
"ui.reset": "重置",
|
||||
"ui.enabled": "启用",
|
||||
"ui.save": "保存",
|
||||
"ui.saving": "保存中……",
|
||||
"ui.cancel": "取消",
|
||||
"ui.copy": "复制",
|
||||
|
||||
"ui.calendarFilter.last24Hours": "过去24小时",
|
||||
|
||||
"ui.settingView.menu.uiSettings": "界面设置",
|
||||
"ui.settingView.menu.searchSettings": "搜索设置",
|
||||
"ui.settingView.menu.cameraSettings": "摄像头设置",
|
||||
"ui.settingView.menu.masksAndZones": "屏罩 / 区域",
|
||||
"ui.settingView.menu.motionTuner": "运动调整器",
|
||||
"ui.settingView.menu.debug": "调试",
|
||||
"ui.settingView.menu.users": "用户",
|
||||
"ui.settingView.menu.notifications": "通知",
|
||||
|
||||
"ui.settingView.generalSettings": "常规设置",
|
||||
"ui.settingView.generalSettings.liveDashboard": "实时监控面板",
|
||||
"ui.settingView.generalSettings.automaticLiveView": "自动实时预览",
|
||||
"ui.settingView.generalSettings.automaticLiveView.desc": "检测到画面活动时将自动切换至该摄像头实时画面。禁用此选项会导致实时监控页面的摄像头图像每分钟只更新一次。",
|
||||
"ui.settingView.generalSettings.playAlertVideos": "播放警告视频",
|
||||
"ui.settingView.generalSettings.playAlertVideos.desc": "默认情况下,实时监控页面上的最新警告会以一小段循环的形式进行播放。禁用此选项将仅显示浏览器本地缓存的静态图片。",
|
||||
"ui.settingView.generalSettings.storedLayouts": "存储监控面板布局",
|
||||
"ui.settingView.generalSettings.storedLayouts.desc": "可以在监控面板调整或拖动摄像头的布局。这些设置将保存在浏览器的本地存储中。",
|
||||
"ui.settingView.generalSettings.storedLayouts.clearAll": "清除所有布局",
|
||||
"ui.settingView.generalSettings.recordingsViewer": "回放查看",
|
||||
"ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate": "默认播放速率",
|
||||
"ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate.desc": "调整播放录像时默认的速率。",
|
||||
"ui.settingView.generalSettings.calendar": "日历",
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday": "每周第一天",
|
||||
"ui.settingView.generalSettings.calendar.firstWeekday.desc": "设置每周第一天是星期几。",
|
||||
"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.cameraSettings": "摄像头设置",
|
||||
"ui.settingView.cameraSettings.reviewClassification": "预览分级",
|
||||
"ui.settingView.cameraSettings.reviewClassification.desc": "Frigate 将回放项目分为“警告”和“检测”。默认情况下,所有的 <em>人</em>、<em>汽车</em> 的对象都视为警告。你可以通过修改配置文件配置区域来细分。",
|
||||
"ui.settingView.cameraSettings.reviewClassification.readTheDocumentation": "阅读文档(英文)",
|
||||
"ui.settingView.cameraSettings.reviewClassification.noDefinedZones": "该摄像头没有设置区域。",
|
||||
"ui.settingView.cameraSettings.reviewClassification.objectAlertsTips": "所有的 {{alertsLabels}} 对象在 {{cameraName}} 都将显示为警告。",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectAlertsTips": "所有的 {{alertsLabels}} 对象在 {{cameraName}} 的 {{zone}} 区域都将显示为警告。",
|
||||
"ui.settingView.cameraSettings.reviewClassification.selectAlertsZones": "选择要显示为警告的区域",
|
||||
|
||||
"ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 对象,无论它位于哪个区域,都将显示为检测。",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips": "所有未在 {{cameraName}} 上归类为 {{detectionsLabels}} 的对象在 {{zone}} 区域都将显示为检测。",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips.notSelectDetections": "所有在 {{cameraName}} 的 {{zone}} 上检测到的未归类为警告的 {{detectionsLabels}} 对象,无论它位于哪个区域,都将显示为检测。",
|
||||
"ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips.regardlessOfZoneObjectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 对象,无论它位于哪个区域,都将显示为检测。",
|
||||
|
||||
"ui.settingView.masksAndZonesSettings": "屏罩 / 区域",
|
||||
"ui.settingView.masksAndZonesSettings.zone": "区域",
|
||||
"ui.settingView.masksAndZonesSettings.zone.desc": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。",
|
||||
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "文档(英文)",
|
||||
"ui.settingView.masksAndZonesSettings.zone.add": "添加区域",
|
||||
"ui.settingView.masksAndZonesSettings.zone.edit": "编辑区域",
|
||||
"ui.settingView.masksAndZonesSettings.zone.point_one": "{{count}} 点",
|
||||
"ui.settingView.masksAndZonesSettings.zone.point_other": "{{count}} 点",
|
||||
"ui.settingView.masksAndZonesSettings.zone.clickDrawPolygon": "在图像上点击添加点绘制多边形区域。",
|
||||
"ui.settingView.masksAndZonesSettings.zone.name": "区域名称",
|
||||
"ui.settingView.masksAndZonesSettings.zone.name.inputPlaceHolder": "请输入名称",
|
||||
"ui.settingView.masksAndZonesSettings.zone.name.tips": "名称至少包含两个字符,且不能和摄像头或其他区域同名。",
|
||||
"ui.settingView.masksAndZonesSettings.zone.inertia": "区域名称",
|
||||
"ui.settingView.masksAndZonesSettings.zone.inertia.desc": "识别指定对象前该对象必须在这个区域内出现了多少帧。<em>默认值:3</em>",
|
||||
"ui.settingView.masksAndZonesSettings.zone.loiteringTime": "停留时间",
|
||||
"ui.settingView.masksAndZonesSettings.zone.loiteringTime.desc": "设置对象必须在区域中活动的最小时间(单位为秒)。<em>默认值:0</em>",
|
||||
"ui.settingView.masksAndZonesSettings.zone.objects": "对象",
|
||||
"ui.settingView.masksAndZonesSettings.zone.objects.desc": "将在此区域应用的对象列表。",
|
||||
"ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象",
|
||||
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.edit": "编辑运动遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.context": "运动遮罩用于防止不需要的运动类型触发检测(例如:树枝、摄像头显示的时间等)。运动遮罩需要<strong>谨慎使用</strong>,过度的遮罩会导致追踪对象变得更加困难。",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.context.documentation": "阅读文档(英文)",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.point_one": "{{count}} 点",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.point_other": "{{count}} 点",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.clickDrawPolygon": "在图像上点击添加点绘制多边形区域。",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge": "运动遮罩的大小达到了摄像头画面的{{polygonArea}}%。不建议设置太大的运动遮罩。",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.tips": "运动遮罩不会阻止检测到对象,你应该使用区域来限制检测对象。",
|
||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.documentation": "阅读文档(英文)",
|
||||
|
||||
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks": "对象遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "对象过滤器用于防止特定位置的指定对象被误报。",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "文档(英文)",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.add": "添加对象遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.edit": "编辑对象遮罩",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.context": "对象过滤器用于防止特定位置的指定对象被误报。",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.point_one": "{{count}} 点",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.point_other": "{{count}} 点",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.clickDrawPolygon": "在图像上点击添加点绘制多边形区域。",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.objects": "对象",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.objects.desc": "将应用于此对象遮罩的对象列表。",
|
||||
"ui.settingView.masksAndZonesSettings.objectMasks.objects.allObjectTypes": "所有对象类型",
|
||||
|
||||
|
||||
"ui.settingView.motionDetectionTuner": "运动检测调整器",
|
||||
"ui.settingView.motionDetectionTuner.desc": "Frigate 将使用运动检测作为首个步骤,以确认一帧画面中是否有对象需要使用对象检测。",
|
||||
"ui.settingView.motionDetectionTuner.desc.documentation": "阅读有关运动检测的文档(英文)",
|
||||
"ui.settingView.motionDetectionTuner.Threshold": "阈值",
|
||||
"ui.settingView.motionDetectionTuner.Threshold.desc": "阈值决定像素亮度高于多少时会被认为是运动。<em>默认值:30</em>",
|
||||
"ui.settingView.motionDetectionTuner.contourArea": "轮廓面积",
|
||||
"ui.settingView.motionDetectionTuner.contourArea.desc": "轮廓面积决定哪些变化的像素组符合运动条件。<em>默认值:10</em>",
|
||||
"ui.settingView.motionDetectionTuner.improveContrast": "提高对比度",
|
||||
"ui.settingView.motionDetectionTuner.improveContrast.desc": "提高较暗场景的对比度。默认值:开启",
|
||||
|
||||
"ui.settingView.debug": "调试",
|
||||
"ui.settingView.debug.detectorDesc": "Frigate 将使用探测器({{detectors}})来检测摄像头视频流中的对象。",
|
||||
"ui.settingView.debug.desc": "调试界面将实时显示被追踪的对象以及统计信息,对象列表将显示检测到的对象和延迟显示的概览。",
|
||||
"ui.settingView.debug.debugging": "调试选项",
|
||||
"ui.settingView.debug.objectList": "对象列表",
|
||||
"ui.settingView.debug.noObjects": "没有对象",
|
||||
"ui.settingView.debug.boundingBoxes": "边界框",
|
||||
"ui.settingView.debug.boundingBoxes.desc": "将在被追踪的对象周围显示边界框",
|
||||
"ui.settingView.debug.boundingBoxes.colors": "对象边界框颜色定义",
|
||||
"ui.settingView.debug.boundingBoxes.colors.info": "<li>启用后,将会为每个对象标签分配不同的颜色</li><li>深蓝色细线代表该对象在当前时间点未被检测到</li><li>灰色细线代表检测到的物体静止不动</li><li>粗线表示该对象为自动跟踪的主体(在启动时)</li>",
|
||||
"ui.settingView.debug.timestamp": "时间戳",
|
||||
"ui.settingView.debug.timestamp.desc": "在图像上显示时间戳",
|
||||
"ui.settingView.debug.zone": "区域",
|
||||
"ui.settingView.debug.zone.desc": "显示已定义的区域图层",
|
||||
"ui.settingView.debug.mask": "运动遮罩",
|
||||
"ui.settingView.debug.mask.desc": "显示运动遮罩图层",
|
||||
"ui.settingView.debug.motion": "运动区域框",
|
||||
"ui.settingView.debug.motion.desc": "在检测到运动的区域显示区域框",
|
||||
"ui.settingView.debug.motion.tips": "<p className=\"mb-2\"><strong>运动区域框</strong></p><br><p>将在当前检测到运动的区域内显示红色区域框。</p>",
|
||||
"ui.settingView.debug.regions": "范围",
|
||||
"ui.settingView.debug.regions.desc": "显示发送到运动检测器感兴趣范围的框。",
|
||||
"ui.settingView.debug.regions.tips": "<p className=\"mb-2\"><strong>范围框</strong></p><br><p>将在帧中发送到目标检测器的感兴趣范围上叠加绿色框。</p>",
|
||||
|
||||
"ui.configEditorView.configEditor": "配置编辑器",
|
||||
"ui.configEditorView.copyConfig": "复制配置",
|
||||
"ui.configEditorView.saveAndRestart": "保存并重启",
|
||||
"ui.configEditorView.saveOnly": "只保存"
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "i18next";
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from "react";
|
||||
|
||||
interface IProp {
|
||||
@ -40,7 +41,7 @@ const timeAgo = ({
|
||||
|
||||
const elapsed: number = elapsedTime / 1000;
|
||||
if (elapsed < 10) {
|
||||
return "just now";
|
||||
return t("ui.time.justNow");
|
||||
}
|
||||
|
||||
for (let i = 0; i < timeUnits.length; i++) {
|
||||
|
||||
@ -14,6 +14,7 @@ import { DateRangePicker } from "../ui/calendar-range";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { useState } from "react";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { t } from "i18next";
|
||||
|
||||
type CalendarFilterButtonProps = {
|
||||
reviewSummary?: ReviewSummary;
|
||||
@ -44,7 +45,7 @@ export default function CalendarFilterButton({
|
||||
<div
|
||||
className={`hidden md:block ${day == undefined ? "text-primary" : "text-selected-foreground"}`}
|
||||
>
|
||||
{day == undefined ? "Last 24 Hours" : selectedDate}
|
||||
{day == undefined ? t("ui.calendarFilter.last24Hours") : selectedDate}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -66,6 +66,7 @@ import {
|
||||
MobilePageHeader,
|
||||
MobilePageTitle,
|
||||
} from "../mobile/MobilePage";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type CameraGroupSelectorProps = {
|
||||
className?: string;
|
||||
@ -151,8 +152,8 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="capitalize" side="right">
|
||||
All Cameras
|
||||
<TooltipContent className="" side="right">
|
||||
<Trans>ui.menu.live.allCameras</Trans>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
|
||||
@ -12,6 +12,7 @@ import { isMobile } from "react-device-detect";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
import { FaVideo } from "react-icons/fa";
|
||||
import { t } from "i18next";
|
||||
|
||||
type CameraFilterButtonProps = {
|
||||
allCameras: string[];
|
||||
@ -38,7 +39,7 @@ export function CamerasFilterButton({
|
||||
}
|
||||
|
||||
if (!selectedCameras || selectedCameras.length == 0) {
|
||||
return "All Cameras";
|
||||
return t("ui.menu.live.allCameras");
|
||||
}
|
||||
|
||||
return `${selectedCameras.includes("birdseye") ? selectedCameras.length - 1 : selectedCameras.length} Camera${selectedCameras.length !== 1 ? "s" : ""}`;
|
||||
|
||||
@ -18,6 +18,8 @@ import { FilterList, GeneralFilter } from "@/types/filter";
|
||||
import CalendarFilterButton from "./CalendarFilterButton";
|
||||
import { CamerasFilterButton } from "./CamerasFilterButton";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
const REVIEW_FILTERS = [
|
||||
"cameras",
|
||||
@ -266,7 +268,7 @@ function ShowReviewFilter({
|
||||
}
|
||||
/>
|
||||
<Label className="ml-2 cursor-pointer text-primary" htmlFor="reviewed">
|
||||
Show Reviewed
|
||||
<Trans>ui.reviewFilter.showReviewed</Trans>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@ -352,7 +354,7 @@ function GeneralFilterButton({
|
||||
: "text-primary"
|
||||
}`}
|
||||
>
|
||||
Filter
|
||||
<Trans>ui.reviewFilter.filter</Trans>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -433,7 +435,7 @@ export function GeneralFilterContent({
|
||||
{currentSeverity && (
|
||||
<div className="my-2.5 flex flex-col gap-2.5">
|
||||
<FilterSwitch
|
||||
label="Alerts"
|
||||
label={t("ui.eventView.alerts")}
|
||||
disabled={currentSeverity == "alert"}
|
||||
isChecked={
|
||||
currentSeverity == "alert" ? true : filter.showAll === true
|
||||
@ -443,7 +445,7 @@ export function GeneralFilterContent({
|
||||
}
|
||||
/>
|
||||
<FilterSwitch
|
||||
label="Detections"
|
||||
label={t("ui.eventView.detections")}
|
||||
disabled={currentSeverity == "detection"}
|
||||
isChecked={
|
||||
currentSeverity == "detection" ? true : filter.showAll === true
|
||||
@ -460,7 +462,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
All Labels
|
||||
<Trans>ui.reviewFilter.filter.allLabels</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -507,7 +509,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allZones"
|
||||
>
|
||||
All Zones
|
||||
<Trans>ui.reviewFilter.filter.allZones</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -563,10 +565,10 @@ export function GeneralFilterContent({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
<Trans>ui.apply</Trans>
|
||||
</Button>
|
||||
<Button aria-label="Reset" onClick={onReset}>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useTheme } from "@/context/theme-provider";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
@ -126,7 +127,7 @@ export function CameraLineGraph({
|
||||
className="size-2"
|
||||
style={{ color: GRAPH_COLORS[labelIdx] }}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="text-xs text-muted-foreground">{t("ui.system.cameras.label." + label)}</div>
|
||||
<div className="text-xs text-primary">
|
||||
{lastValues[labelIdx]}
|
||||
{unit}
|
||||
|
||||
@ -17,6 +17,8 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { getUnitSize } from "@/utils/storageUtil";
|
||||
import { LuAlertCircle } from "react-icons/lu";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type CameraStorage = {
|
||||
[key: string]: {
|
||||
@ -176,10 +178,10 @@ export function CombinedStorageGraph({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Camera</TableHead>
|
||||
<TableHead>Storage Used</TableHead>
|
||||
<TableHead>Percentage of Total Used</TableHead>
|
||||
<TableHead>Bandwidth</TableHead>
|
||||
<TableHead><Trans>ui.system.storage.cameraStorage.camera</Trans></TableHead>
|
||||
<TableHead><Trans>ui.system.storage.cameraStorage.storageUsed</Trans></TableHead>
|
||||
<TableHead><Trans>ui.system.storage.cameraStorage.percentageOfTotalUsed</Trans></TableHead>
|
||||
<TableHead><Trans>ui.system.storage.cameraStorage.bandwidth</Trans></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@ -191,7 +193,7 @@ export function CombinedStorageGraph({
|
||||
className="size-3 rounded-md"
|
||||
style={{ backgroundColor: item.color }}
|
||||
></div>
|
||||
{item.name.replaceAll("_", " ")}
|
||||
{item.name === "Unused" ? t("ui.system.storage.cameraStorage.unused"): item.name.replaceAll("_", " ")}
|
||||
{item.name === "Unused" && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@ -207,10 +209,7 @@ export function CombinedStorageGraph({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-2">
|
||||
This value may not accurately represent the free space
|
||||
available to Frigate if you have other files stored on
|
||||
your drive beyond Frigate's recordings. Frigate does
|
||||
not track storage usage outside of its recordings.
|
||||
<Trans>ui.system.storage.cameraStorage.unused.tips</Trans>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@ -20,6 +20,8 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { DialogClose } from "../ui/dialog";
|
||||
import { LuLogOut } from "react-icons/lu";
|
||||
import useSWR from "swr";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type AccountSettingsProps = {
|
||||
className?: string;
|
||||
@ -65,7 +67,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
>
|
||||
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
|
||||
<DropdownMenuLabel>
|
||||
Current User: {profile?.username || "anonymous"}
|
||||
{t("ui.menu.user.current", {user: profile?.username || t("ui.menu.user.anonymous")})}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} />
|
||||
<MenuItem
|
||||
@ -76,7 +78,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
<span>Logout</span>
|
||||
<span><Trans>ui.menu.user.logout</Trans></span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
LuActivity,
|
||||
LuGithub,
|
||||
LuLanguages,
|
||||
LuLifeBuoy,
|
||||
LuList,
|
||||
LuLogOut,
|
||||
@ -55,6 +56,9 @@ import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import useSWR from "swr";
|
||||
import RestartDialog from "../overlay/dialog/RestartDialog";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useLanguage } from "@/context/language-provider";
|
||||
|
||||
type GeneralSettingsProps = {
|
||||
className?: string;
|
||||
@ -66,6 +70,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
|
||||
// settings
|
||||
|
||||
const { language, setLanguage, systemLanguage } = useLanguage();
|
||||
const { theme, colorScheme, setTheme, setColorScheme } = useTheme();
|
||||
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
||||
const { send: sendRestart } = useRestart();
|
||||
@ -99,7 +104,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>Settings</p>
|
||||
<p><Trans>ui.settings</Trans></p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
@ -143,7 +148,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuLabel>System</DropdownMenuLabel>
|
||||
<DropdownMenuLabel><Trans>ui.system</Trans></DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
|
||||
<Link to="/system#general">
|
||||
@ -156,7 +161,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="System metrics"
|
||||
>
|
||||
<LuActivity className="mr-2 size-4" />
|
||||
<span>System metrics</span>
|
||||
<span><Trans>ui.systemMetrics</Trans></span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link to="/logs">
|
||||
@ -169,12 +174,12 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="System logs"
|
||||
>
|
||||
<LuList className="mr-2 size-4" />
|
||||
<span>System logs</span>
|
||||
<span><Trans>ui.systemLogs</Trans></span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
|
||||
Configuration
|
||||
<Trans>ui.configuration</Trans>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
@ -188,7 +193,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="Settings"
|
||||
>
|
||||
<LuSettings className="mr-2 size-4" />
|
||||
<span>Settings</span>
|
||||
<span><Trans>ui.settings</Trans></span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link to="/config">
|
||||
@ -201,11 +206,86 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="Configuration editor"
|
||||
>
|
||||
<LuPenSquare className="mr-2 size-4" />
|
||||
<span>Configuration editor</span>
|
||||
<span><Trans>ui.configurationEditor</Trans></span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<SubItem>
|
||||
<SubItemTrigger
|
||||
className={
|
||||
isDesktop
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
>
|
||||
<LuLanguages className="mr-2 size-4" />
|
||||
<span><Trans>ui.languages</Trans></span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
className={
|
||||
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
|
||||
}
|
||||
>
|
||||
<span tabIndex={0} className="sr-only" />
|
||||
<MenuItem
|
||||
className={
|
||||
isDesktop
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Light mode"
|
||||
onClick={() => setLanguage("en")}
|
||||
>
|
||||
{language === "en" ? (
|
||||
<>
|
||||
<LuSun className="mr-2 size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Trans>ui.language.en</Trans>
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2"><Trans>ui.language.en</Trans></span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={
|
||||
isDesktop
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Dark mode"
|
||||
onClick={() => setLanguage("zh-CN")}
|
||||
>
|
||||
{language === "zh-CN" ? (
|
||||
<>
|
||||
<LuMoon className="mr-2 size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<Trans>ui.language.zhCN</Trans>
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2"><Trans>ui.language.zhCN</Trans></span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={
|
||||
isDesktop
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Use the system settings for light or dark mode"
|
||||
onClick={() => setLanguage(systemLanguage)}
|
||||
>
|
||||
{language === systemLanguage ? (
|
||||
<>
|
||||
<CgDarkMode className="mr-2 size-4 scale-100 transition-all" />
|
||||
<Trans>ui.withSystem</Trans>
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2"><Trans>ui.withSystem</Trans></span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</SubItemContent>
|
||||
</Portal>
|
||||
</SubItem>
|
||||
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
|
||||
Appearance
|
||||
<Trans>ui.appearance</Trans>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<SubItem>
|
||||
@ -217,7 +297,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
}
|
||||
>
|
||||
<LuSunMoon className="mr-2 size-4" />
|
||||
<span>Dark Mode</span>
|
||||
<span><Trans>ui.darkMode</Trans></span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
@ -238,10 +318,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{theme === "light" ? (
|
||||
<>
|
||||
<LuSun className="mr-2 size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
Light
|
||||
<Trans>ui.darkMode.light</Trans>
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">Light</span>
|
||||
<span className="ml-6 mr-2"><Trans>ui.darkMode.light</Trans></span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
@ -256,10 +336,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{theme === "dark" ? (
|
||||
<>
|
||||
<LuMoon className="mr-2 size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
Dark
|
||||
<Trans>ui.darkMode.dark</Trans>
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">Dark</span>
|
||||
<span className="ml-6 mr-2"><Trans>ui.darkMode.dark</Trans></span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
@ -274,10 +354,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{theme === "system" ? (
|
||||
<>
|
||||
<CgDarkMode className="mr-2 size-4 scale-100 transition-all" />
|
||||
System
|
||||
<Trans>ui.withSystem</Trans>
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">System</span>
|
||||
<span className="ml-6 mr-2"><Trans>ui.withSystem</Trans></span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</SubItemContent>
|
||||
@ -292,7 +372,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
}
|
||||
>
|
||||
<LuSunMoon className="mr-2 size-4" />
|
||||
<span>Theme</span>
|
||||
<span><Trans>ui.theme</Trans></span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
@ -315,11 +395,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{scheme === colorScheme ? (
|
||||
<>
|
||||
<IoColorPalette className="mr-2 size-4 rotate-0 scale-100 transition-all" />
|
||||
{friendlyColorSchemeName(scheme)}
|
||||
<Trans>{friendlyColorSchemeName(scheme)}</Trans>
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
{friendlyColorSchemeName(scheme)}
|
||||
<Trans>{friendlyColorSchemeName(scheme)}</Trans>
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
@ -329,7 +409,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
</SubItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
|
||||
Help
|
||||
<Trans>ui.help</Trans>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<a href="https://docs.frigate.video" target="_blank">
|
||||
@ -337,10 +417,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Frigate documentation"
|
||||
aria-label={t("ui.documentation.label")}
|
||||
>
|
||||
<LuLifeBuoy className="mr-2 size-4" />
|
||||
<span>Documentation</span>
|
||||
<span><Trans>ui.documentation</Trans></span>
|
||||
</MenuItem>
|
||||
</a>
|
||||
<a
|
||||
@ -362,11 +442,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Restart Frigate"
|
||||
aria-label={t("ui.restart")}
|
||||
onClick={() => setRestartDialogOpen(true)}
|
||||
>
|
||||
<LuRotateCw className="mr-2 size-4" />
|
||||
<span>Restart Frigate</span>
|
||||
<span><Trans>ui.restart</Trans></span>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
@ -9,6 +9,7 @@ import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { NavData } from "@/types/navigation";
|
||||
import { IconType } from "react-icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
const variants = {
|
||||
primary: {
|
||||
@ -60,7 +61,7 @@ export default function NavItem({
|
||||
<TooltipTrigger>{content}</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>{item.title}</p>
|
||||
<p><Trans>{item.title}</Trans></p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
|
||||
@ -15,6 +15,8 @@ import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "../ui/sonner";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type CameraInfoDialogProps = {
|
||||
camera: CameraConfig;
|
||||
@ -72,11 +74,11 @@ export default function CameraInfoDialog({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="capitalize">
|
||||
{camera.name.replaceAll("_", " ")} Camera Probe Info
|
||||
{t("ui.system.cameras.info.cameraProbeInfo", {camera: camera.name.replaceAll("_", " ")})}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
Stream data is obtained with <code>ffprobe</code>.
|
||||
<Trans>ui.system.cameras.info.streamDataFromFFPROBE</Trans>
|
||||
</DialogDescription>
|
||||
|
||||
<div className="mb-2 p-4">
|
||||
@ -85,7 +87,7 @@ export default function CameraInfoDialog({
|
||||
{ffprobeInfo.map((stream, idx) => (
|
||||
<div key={idx} className="mb-5">
|
||||
<div className="mb-1 rounded-md bg-secondary p-2 text-lg text-primary">
|
||||
Stream {idx + 1}
|
||||
{t("ui.system.cameras.info.stream", {idx: idx + 1})}
|
||||
</div>
|
||||
{stream.return_code == 0 ? (
|
||||
<div>
|
||||
@ -93,10 +95,10 @@ export default function CameraInfoDialog({
|
||||
<div className="" key={idx}>
|
||||
{codec.width ? (
|
||||
<div className="text-muted-foreground">
|
||||
<div className="ml-2">Video:</div>
|
||||
<div className="ml-2"><Trans>ui.system.cameras.info.video</Trans></div>
|
||||
<div className="ml-5">
|
||||
<div>
|
||||
Codec:
|
||||
<Trans>ui.system.cameras.info.codec</Trans>
|
||||
<span className="text-primary">
|
||||
{" "}
|
||||
{codec.codec_long_name}
|
||||
@ -105,7 +107,7 @@ export default function CameraInfoDialog({
|
||||
<div>
|
||||
{codec.width && codec.height ? (
|
||||
<>
|
||||
Resolution:{" "}
|
||||
<Trans>ui.system.cameras.info.resolution</Trans>{" "}
|
||||
<span className="text-primary">
|
||||
{" "}
|
||||
{codec.width}x{codec.height} (
|
||||
@ -119,7 +121,7 @@ export default function CameraInfoDialog({
|
||||
</>
|
||||
) : (
|
||||
<span>
|
||||
Resolution:{" "}
|
||||
<Trans>ui.system.cameras.info.resolution</Trans>{" "}
|
||||
<span className="text-primary">
|
||||
Unknown
|
||||
</span>
|
||||
@ -127,10 +129,10 @@ export default function CameraInfoDialog({
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
FPS:{" "}
|
||||
<Trans>ui.system.cameras.info.fps</Trans>{" "}
|
||||
<span className="text-primary">
|
||||
{codec.avg_frame_rate == "0/0"
|
||||
? "Unknown"
|
||||
? t("ui.system.cameras.info.unknown")
|
||||
: codec.avg_frame_rate}
|
||||
</span>
|
||||
</div>
|
||||
@ -140,7 +142,7 @@ export default function CameraInfoDialog({
|
||||
<div className="text-muted-foreground">
|
||||
<div className="ml-2 mt-1">Audio:</div>
|
||||
<div className="ml-4">
|
||||
Codec:{" "}
|
||||
<Trans>ui.system.cameras.info.codec</Trans>{" "}
|
||||
<span className="text-primary">
|
||||
{codec.codec_long_name}
|
||||
</span>
|
||||
@ -152,7 +154,7 @@ export default function CameraInfoDialog({
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-2">
|
||||
<div>Error: {stream.stderr}</div>
|
||||
<div>{t("ui.system.cameras.info.error", {error: stream.stderr})}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -161,7 +163,7 @@ export default function CameraInfoDialog({
|
||||
) : (
|
||||
<div className="flex flex-col items-center">
|
||||
<ActivityIndicator />
|
||||
<div className="mt-2">Fetching Camera Data</div>
|
||||
<div className="mt-2"><Trans>ui.system.cameras.info.fetching</Trans></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -172,7 +174,7 @@ export default function CameraInfoDialog({
|
||||
aria-label="Copy"
|
||||
onClick={() => onCopyFfprobe()}
|
||||
>
|
||||
Copy
|
||||
<Trans>ui.copy</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -22,6 +22,8 @@ import { Toaster } from "../ui/sonner";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type MotionMaskEditPaneProps = {
|
||||
polygons?: Polygon[];
|
||||
@ -213,14 +215,11 @@ export default function MotionMaskEditPane({
|
||||
<>
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<Heading as="h3" className="my-2">
|
||||
{polygon.name.length ? "Edit" : "New"} Motion Mask
|
||||
{polygon.name.length ? t("ui.settingView.masksAndZonesSettings.motionMasks.edit") : t("ui.settingView.masksAndZonesSettings.motionMasks.add")}
|
||||
</Heading>
|
||||
<div className="my-3 space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Motion masks are used to prevent unwanted types of motion from
|
||||
triggering detection (example: tree branches, camera timestamps).
|
||||
Motion masks should be used <em>very sparingly</em>, over-masking will
|
||||
make it more difficult for objects to be tracked.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks.context</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center text-primary">
|
||||
@ -230,7 +229,7 @@ export default function MotionMaskEditPane({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks.context.documentation</Trans>{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -239,11 +238,9 @@ export default function MotionMaskEditPane({
|
||||
{polygons && activePolygonIndex !== undefined && (
|
||||
<div className="my-2 flex w-full flex-row justify-between text-sm">
|
||||
<div className="my-1 inline-flex">
|
||||
{polygons[activePolygonIndex].points.length}{" "}
|
||||
{polygons[activePolygonIndex].points.length > 1 ||
|
||||
polygons[activePolygonIndex].points.length == 0
|
||||
? "points"
|
||||
: "point"}
|
||||
{t("ui.settingView.masksAndZonesSettings.motionMasks.point", {
|
||||
count: polygons[activePolygonIndex].points.length
|
||||
})}
|
||||
{polygons[activePolygonIndex].isFinished && (
|
||||
<FaCheckCircle className="ml-2 size-5" />
|
||||
)}
|
||||
@ -256,7 +253,7 @@ export default function MotionMaskEditPane({
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
Click to draw a polygon on the image.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks.clickDrawPolygon</Trans>Click to draw a polygon on the image.
|
||||
</div>
|
||||
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
@ -264,19 +261,19 @@ export default function MotionMaskEditPane({
|
||||
{polygonArea && polygonArea >= 0.35 && (
|
||||
<>
|
||||
<div className="mb-3 text-sm text-danger">
|
||||
The motion mask is covering {Math.round(polygonArea * 100)}% of the
|
||||
camera frame. Large motion masks are not recommended.
|
||||
{t("ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge", {
|
||||
polygonArea: Math.round(polygonArea * 100)
|
||||
})}
|
||||
</div>
|
||||
<div className="mb-3 text-sm text-primary">
|
||||
Motion masks do not prevent objects from being detected. You should
|
||||
use a required zone instead.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.tips</Trans>
|
||||
<Link
|
||||
to="https://github.com/blakeblackshear/frigate/discussions/13040"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="my-3 block"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.documentation</Trans>{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -313,7 +310,7 @@ export default function MotionMaskEditPane({
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
<Trans>ui.cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -325,10 +322,10 @@ export default function MotionMaskEditPane({
|
||||
{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>
|
||||
|
||||
@ -38,6 +38,8 @@ import { toast } from "sonner";
|
||||
import { Toaster } from "../ui/sonner";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { getAttributeLabels } from "@/utils/iconUtil";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type ObjectMaskEditPaneProps = {
|
||||
polygons?: Polygon[];
|
||||
@ -247,23 +249,20 @@ export default function ObjectMaskEditPane({
|
||||
<>
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<Heading as="h3" className="my-2">
|
||||
{polygon.name.length ? "Edit" : "New"} Object Mask
|
||||
{polygon.name.length ? t("ui.settingView.masksAndZonesSettings.objectMasks.edit") : t("ui.settingView.masksAndZonesSettings.objectMasks.add")}
|
||||
</Heading>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Object filter masks are used to filter out false positives for a given
|
||||
object type based on location.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.objectMasks.context</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
{polygons && activePolygonIndex !== undefined && (
|
||||
<div className="my-2 flex w-full flex-row justify-between text-sm">
|
||||
<div className="my-1 inline-flex">
|
||||
{polygons[activePolygonIndex].points.length}{" "}
|
||||
{polygons[activePolygonIndex].points.length > 1 ||
|
||||
polygons[activePolygonIndex].points.length == 0
|
||||
? "points"
|
||||
: "point"}
|
||||
{t("ui.settingView.masksAndZonesSettings.objectMasks.point", {
|
||||
count: polygons[activePolygonIndex].points.length
|
||||
})}
|
||||
{polygons[activePolygonIndex].isFinished && (
|
||||
<FaCheckCircle className="ml-2 size-5" />
|
||||
)}
|
||||
@ -276,6 +275,7 @@ export default function ObjectMaskEditPane({
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
<Trans>ui.settingView.masksAndZonesSettings.objectMasks.clickDrawPolygon</Trans>
|
||||
Click to draw a polygon on the image.
|
||||
</div>
|
||||
|
||||
@ -301,7 +301,7 @@ export default function ObjectMaskEditPane({
|
||||
name="objects"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Objects</FormLabel>
|
||||
<FormLabel><Trans>ui.settingView.masksAndZonesSettings.objectMasks.objects</Trans></FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
@ -317,7 +317,7 @@ export default function ObjectMaskEditPane({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
The object type that that applies to this object mask.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.objectMasks.objects.desc</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -414,11 +414,11 @@ export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
|
||||
return (
|
||||
<>
|
||||
<SelectGroup>
|
||||
<SelectItem value="all_labels">All object types</SelectItem>
|
||||
<SelectItem value="all_labels"><Trans>ui.settingView.masksAndZonesSettings.objectMasks.objects.allObjectTypes</Trans></SelectItem>
|
||||
<SelectSeparator className="bg-secondary" />
|
||||
{allLabels.map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item.replaceAll("_", " ").charAt(0).toUpperCase() + item.slice(1)}
|
||||
{t("object." + item)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
|
||||
@ -29,6 +29,8 @@ import { toast } from "sonner";
|
||||
import { flattenPoints, interpolatePoints } from "@/utils/canvasUtil";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { getAttributeLabels } from "@/utils/iconUtil";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type ZoneEditPaneProps = {
|
||||
polygons?: Polygon[];
|
||||
@ -330,23 +332,19 @@ export default function ZoneEditPane({
|
||||
<>
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<Heading as="h3" className="my-2">
|
||||
{polygon.name.length ? "Edit" : "New"} Zone
|
||||
{polygon.name.length ? t("ui.settingView.masksAndZonesSettings.zone.edit") : t("ui.settingView.masksAndZonesSettings.zone.add")}
|
||||
</Heading>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
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.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
{polygons && activePolygonIndex !== undefined && (
|
||||
<div className="my-2 flex w-full flex-row justify-between text-sm">
|
||||
<div className="my-1 inline-flex">
|
||||
{polygons[activePolygonIndex].points.length}{" "}
|
||||
{polygons[activePolygonIndex].points.length > 1 ||
|
||||
polygons[activePolygonIndex].points.length == 0
|
||||
? "points"
|
||||
: "point"}
|
||||
{t("ui.settingView.masksAndZonesSettings.zone.point", { count: polygons[activePolygonIndex].points.length })}
|
||||
|
||||
{polygons[activePolygonIndex].isFinished && (
|
||||
<FaCheckCircle className="ml-2 size-5" />
|
||||
)}
|
||||
@ -359,7 +357,7 @@ export default function ZoneEditPane({
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
Click to draw a polygon on the image.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.clickDrawPolygon</Trans>
|
||||
</div>
|
||||
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
@ -371,17 +369,16 @@ export default function ZoneEditPane({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel><Trans>ui.settingView.masksAndZonesSettings.zone.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.settingView.masksAndZonesSettings.zone.name.inputPlaceHolder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Name must be at least 2 characters and must not be the name of
|
||||
a camera or another zone.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.name.tips</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -393,7 +390,7 @@ export default function ZoneEditPane({
|
||||
name="inertia"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Inertia</FormLabel>
|
||||
<FormLabel><Trans>ui.settingView.masksAndZonesSettings.zone.inertia</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]"
|
||||
@ -402,8 +399,7 @@ export default function ZoneEditPane({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Specifies how many frames that an object must be in a zone
|
||||
before they are considered in the zone. <em>Default: 3</em>
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.inertia.desc</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -415,7 +411,7 @@ export default function ZoneEditPane({
|
||||
name="loitering_time"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Loitering Time</FormLabel>
|
||||
<FormLabel><Trans>ui.settingView.masksAndZonesSettings.zone.loiteringTime</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]"
|
||||
@ -424,8 +420,7 @@ export default function ZoneEditPane({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Sets a minimum amount of time in seconds that the object must
|
||||
be in the zone for it to activate. <em>Default: 0</em>
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.loiteringTime.desc</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -433,9 +428,9 @@ export default function ZoneEditPane({
|
||||
/>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<FormItem>
|
||||
<FormLabel>Objects</FormLabel>
|
||||
<FormLabel><Trans>ui.settingView.masksAndZonesSettings.zone.objects</Trans></FormLabel>
|
||||
<FormDescription>
|
||||
List of objects that apply to this zone.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.objects.desc</Trans>
|
||||
</FormDescription>
|
||||
<ZoneObjectSelector
|
||||
camera={polygon.camera}
|
||||
@ -564,7 +559,7 @@ export function ZoneObjectSelector({
|
||||
<div className="scrollbar-container h-auto overflow-y-auto overflow-x-hidden">
|
||||
<div className="my-2.5 flex items-center justify-between">
|
||||
<Label className="cursor-pointer text-primary" htmlFor="allLabels">
|
||||
All Objects
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.allObjects</Trans>
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -585,7 +580,7 @@ export function ZoneObjectSelector({
|
||||
className="w-full cursor-pointer capitalize text-primary"
|
||||
htmlFor={item}
|
||||
>
|
||||
{item.replaceAll("_", " ")}
|
||||
{t("object." + item)}
|
||||
</Label>
|
||||
<Switch
|
||||
key={item}
|
||||
|
||||
77
web/src/context/language-provider.tsx
Normal file
77
web/src/context/language-provider.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
||||
import i18next from 'i18next';
|
||||
|
||||
type LanguageProviderState = {
|
||||
language: string;
|
||||
systemLanguage?: string;
|
||||
setLanguage: (language: string) => void;
|
||||
};
|
||||
|
||||
const initialState: LanguageProviderState = {
|
||||
language: i18next.language || 'en',
|
||||
systemLanguage: 'en',
|
||||
setLanguage: () => null,
|
||||
};
|
||||
|
||||
const LanguageProviderContext = createContext<LanguageProviderState>(initialState);
|
||||
|
||||
export function LanguageProvider({
|
||||
children,
|
||||
defaultLanguage = 'en',
|
||||
storageKey = 'frigate-ui-language',
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
defaultLanguage?: string;
|
||||
storageKey?: string;
|
||||
}) {
|
||||
const [language, setLanguage] = useState<string>(() => {
|
||||
try {
|
||||
|
||||
const storedData = localStorage.getItem(storageKey);
|
||||
const newLanguage = storedData || defaultLanguage;
|
||||
i18next.changeLanguage(newLanguage);
|
||||
return newLanguage;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error retrieving language data from storage:', error);
|
||||
return defaultLanguage;
|
||||
}
|
||||
});
|
||||
|
||||
const systemLanguage = useMemo<string | undefined>(() => {
|
||||
if (typeof window === 'undefined') return undefined;
|
||||
return window.navigator.language;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (language === systemLanguage) return;
|
||||
i18next.changeLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
const value = {
|
||||
language,
|
||||
systemLanguage,
|
||||
setLanguage: (language: string) => {
|
||||
localStorage.setItem(storageKey, language);
|
||||
setLanguage(language);
|
||||
window.location.reload();
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<LanguageProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</LanguageProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useLanguage = () => {
|
||||
const context = useContext(LanguageProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useLanguage must be used within a LanguageProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
@ -5,6 +5,7 @@ import { ApiProvider } from "@/api";
|
||||
import { IconContext } from "react-icons";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { StatusBarMessagesProvider } from "@/context/statusbar-provider";
|
||||
import { LanguageProvider } from "./language-provider";
|
||||
|
||||
type TProvidersProps = {
|
||||
children: ReactNode;
|
||||
@ -21,6 +22,13 @@ function providers({ children }: TProvidersProps) {
|
||||
</IconContext.Provider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<TooltipProvider>
|
||||
<IconContext.Provider value={{ size: "20" }}>
|
||||
<StatusBarMessagesProvider>{children}</StatusBarMessagesProvider>
|
||||
</IconContext.Provider>
|
||||
</TooltipProvider>
|
||||
</LanguageProvider>
|
||||
</ApiProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
@ -23,9 +23,7 @@ export const colorSchemes: ColorScheme[] = [
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const friendlyColorSchemeName = (className: string): string => {
|
||||
const words = className.split("-").slice(1); // Exclude the first word (e.g., 'theme')
|
||||
return words
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
return "ui.theme."+words.join(".");
|
||||
};
|
||||
|
||||
type ThemeProviderProps = {
|
||||
@ -127,7 +125,7 @@ export function ThemeProvider({
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -31,35 +31,35 @@ export default function useNavigation(
|
||||
id: ID_LIVE,
|
||||
variant,
|
||||
icon: FaVideo,
|
||||
title: "Live",
|
||||
title: "ui.menu.live",
|
||||
url: "/",
|
||||
},
|
||||
{
|
||||
id: ID_REVIEW,
|
||||
variant,
|
||||
icon: MdVideoLibrary,
|
||||
title: "Review",
|
||||
title: "ui.menu.review",
|
||||
url: "/review",
|
||||
},
|
||||
{
|
||||
id: ID_EXPLORE,
|
||||
variant,
|
||||
icon: IoSearch,
|
||||
title: "Explore",
|
||||
title: "ui.menu.explore",
|
||||
url: "/explore",
|
||||
},
|
||||
{
|
||||
id: ID_EXPORT,
|
||||
variant,
|
||||
icon: FaCompactDisc,
|
||||
title: "Export",
|
||||
title: "ui.menu.export",
|
||||
url: "/export",
|
||||
},
|
||||
{
|
||||
id: ID_PLAYGROUND,
|
||||
variant,
|
||||
icon: LuConstruction,
|
||||
title: "UI Playground",
|
||||
title: "ui.menu.uiPlayground",
|
||||
url: "/playground",
|
||||
enabled: ENV !== "production",
|
||||
},
|
||||
|
||||
@ -10,6 +10,7 @@ import useSWR from "swr";
|
||||
import useDeepMemo from "./use-deep-memo";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { useFrigateStats } from "@/api/ws";
|
||||
import { t } from "i18next";
|
||||
|
||||
export default function useStats(stats: FrigateStats | undefined) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
@ -72,7 +73,7 @@ export default function useStats(stats: FrigateStats | undefined) {
|
||||
|
||||
if (!isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error) {
|
||||
problems.push({
|
||||
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
|
||||
text: t("ui.stats.ffmpegHighCpuUsage", {camera: capitalizeFirstLetter(name.replaceAll("_", " ")), ffmpegAvg}),//`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
|
||||
color: "text-danger",
|
||||
relevantLink: "/system#cameras",
|
||||
});
|
||||
@ -80,7 +81,7 @@ export default function useStats(stats: FrigateStats | undefined) {
|
||||
|
||||
if (!isNaN(detectAvg) && detectAvg >= CameraDetectThreshold.error) {
|
||||
problems.push({
|
||||
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
|
||||
text: t("ui.stats.detectHighCpuUsage", {camera: capitalizeFirstLetter(name.replaceAll("_", " ")), detectAvg}),//`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
|
||||
color: "text-danger",
|
||||
relevantLink: "/system#cameras",
|
||||
});
|
||||
|
||||
@ -2,6 +2,8 @@ import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import "@/utils/i18n";
|
||||
import "react-i18next";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
|
||||
@ -14,6 +14,7 @@ import { toast } from "sonner";
|
||||
import { LuCopy, LuSave } from "react-icons/lu";
|
||||
import { MdOutlineRestartAlt } from "react-icons/md";
|
||||
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type SaveOptions = "saveonly" | "restart";
|
||||
|
||||
@ -189,7 +190,7 @@ function ConfigEditor() {
|
||||
<div className="relative h-full overflow-hidden">
|
||||
<div className="mr-1 flex items-center justify-between">
|
||||
<Heading as="h2" className="mb-0 ml-1 md:ml-0">
|
||||
Config Editor
|
||||
<Trans>ui.configEditorView.configEditor</Trans>
|
||||
</Heading>
|
||||
<div className="flex flex-row gap-1">
|
||||
<Button
|
||||
@ -199,7 +200,7 @@ function ConfigEditor() {
|
||||
onClick={() => handleCopyConfig()}
|
||||
>
|
||||
<LuCopy className="text-secondary-foreground" />
|
||||
<span className="hidden md:block">Copy Config</span>
|
||||
<span className="hidden md:block"><Trans>ui.configEditorView.copyConfig</Trans></span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -211,7 +212,7 @@ function ConfigEditor() {
|
||||
<LuSave className="absolute left-0 top-0 size-3 text-secondary-foreground" />
|
||||
<MdOutlineRestartAlt className="absolute size-4 translate-x-1 translate-y-1/2 text-secondary-foreground" />
|
||||
</div>
|
||||
<span className="hidden md:block">Save & Restart</span>
|
||||
<span className="hidden md:block"><Trans>ui.configEditorView.saveAndRestart</Trans></span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -220,7 +221,7 @@ function ConfigEditor() {
|
||||
onClick={() => onHandleSaveConfig("saveonly")}
|
||||
>
|
||||
<LuSave className="text-secondary-foreground" />
|
||||
<span className="hidden md:block">Save Only</span>
|
||||
<span className="hidden md:block"><Trans>ui.configEditorView.saveOnly</Trans></span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -37,13 +37,14 @@ import AuthenticationView from "@/views/settings/AuthenticationView";
|
||||
import NotificationView from "@/views/settings/NotificationsSettingsView";
|
||||
import SearchSettingsView from "@/views/settings/SearchSettingsView";
|
||||
import UiSettingsView from "@/views/settings/UiSettingsView";
|
||||
import { t } from "i18next";
|
||||
|
||||
const allSettingsViews = [
|
||||
"UI settings",
|
||||
"search settings",
|
||||
"camera settings",
|
||||
"masks / zones",
|
||||
"motion tuner",
|
||||
"uiSettings",
|
||||
"searchSettings",
|
||||
"cameraSettings",
|
||||
"masksAndZones",
|
||||
"motionTuner",
|
||||
"debug",
|
||||
"users",
|
||||
"notifications",
|
||||
@ -51,7 +52,7 @@ const allSettingsViews = [
|
||||
type SettingsType = (typeof allSettingsViews)[number];
|
||||
|
||||
export default function Settings() {
|
||||
const [page, setPage] = useState<SettingsType>("UI settings");
|
||||
const [page, setPage] = useState<SettingsType>("uiSettings");
|
||||
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
||||
const tabsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@ -142,12 +143,12 @@ export default function Settings() {
|
||||
{Object.values(settingsViews).map((item) => (
|
||||
<ToggleGroupItem
|
||||
key={item}
|
||||
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "UI settings" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
||||
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "uiSettings" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
||||
value={item}
|
||||
data-nav-item={item}
|
||||
aria-label={`Select ${item}`}
|
||||
>
|
||||
<div className="capitalize">{item}</div>
|
||||
<div className="capitalize">{t("ui.settingView.menu." + item)}</div>
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
@ -155,11 +156,11 @@ export default function Settings() {
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{(page == "debug" ||
|
||||
page == "camera settings" ||
|
||||
page == "masks / zones" ||
|
||||
page == "motion tuner") && (
|
||||
page == "cameraSettings" ||
|
||||
page == "masksAndZones" ||
|
||||
page == "motionTuner") && (
|
||||
<div className="ml-2 flex flex-shrink-0 items-center gap-2">
|
||||
{page == "masks / zones" && (
|
||||
{page == "masksAndZones" && (
|
||||
<ZoneMaskFilterButton
|
||||
selectedZoneMask={filterZoneMask}
|
||||
updateZoneMaskFilter={setFilterZoneMask}
|
||||
@ -174,27 +175,27 @@ 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 == "UI settings" && <UiSettingsView />}
|
||||
{page == "search settings" && (
|
||||
{page == "uiSettings" && <UiSettingsView />}
|
||||
{page == "searchSettings" && (
|
||||
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} />
|
||||
)}
|
||||
{page == "debug" && (
|
||||
<ObjectSettingsView selectedCamera={selectedCamera} />
|
||||
)}
|
||||
{page == "camera settings" && (
|
||||
{page == "cameraSettings" && (
|
||||
<CameraSettingsView
|
||||
selectedCamera={selectedCamera}
|
||||
setUnsavedChanges={setUnsavedChanges}
|
||||
/>
|
||||
)}
|
||||
{page == "masks / zones" && (
|
||||
{page == "masksAndZones" && (
|
||||
<MasksAndZonesView
|
||||
selectedCamera={selectedCamera}
|
||||
selectedZoneMask={filterZoneMask}
|
||||
setUnsavedChanges={setUnsavedChanges}
|
||||
/>
|
||||
)}
|
||||
{page == "motion tuner" && (
|
||||
{page == "motionTuner" && (
|
||||
<MotionTunerView
|
||||
selectedCamera={selectedCamera}
|
||||
setUnsavedChanges={setUnsavedChanges}
|
||||
|
||||
@ -14,6 +14,8 @@ import CameraMetrics from "@/views/system/CameraMetrics";
|
||||
import { useHashState } from "@/hooks/use-overlay-state";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
const metrics = ["general", "storage", "cameras"] as const;
|
||||
type SystemMetric = (typeof metrics)[number];
|
||||
@ -69,7 +71,7 @@ function System() {
|
||||
{item == "general" && <LuActivity className="size-4" />}
|
||||
{item == "storage" && <LuHardDrive className="size-4" />}
|
||||
{item == "cameras" && <FaVideo className="size-4" />}
|
||||
{isDesktop && <div className="capitalize">{item}</div>}
|
||||
{isDesktop && <div className="capitalize">{t("ui.system." + item)}</div>}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
@ -77,7 +79,7 @@ function System() {
|
||||
<div className="flex h-full items-center">
|
||||
{lastUpdated && (
|
||||
<div className="h-full content-center text-sm text-muted-foreground">
|
||||
Last refreshed: <TimeAgo time={lastUpdated * 1000} dense />
|
||||
<Trans>ui.system.lastRefreshed</Trans><TimeAgo time={lastUpdated * 1000} dense />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
27
web/src/utils/i18n.ts
Normal file
27
web/src/utils/i18n.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import HttpBackend from "i18next-http-backend";
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.use(HttpBackend)
|
||||
.init({
|
||||
fallbackLng: "en", // use en if detected lng is not available
|
||||
//lng: "zh-Hans", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
||||
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
||||
// if you're using a language detector, do not define the lng option
|
||||
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json"
|
||||
},
|
||||
|
||||
react: {
|
||||
transSupportBasicHtmlNodes: true,
|
||||
transKeepBasicHtmlNodesFor: ["br", "strong", "i", "em", "li", "p", "code"],
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false // react already safes from xss
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@ -53,6 +53,7 @@ import { FilterList, LAST_24_HOURS_KEY } from "@/types/filter";
|
||||
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";
|
||||
|
||||
type EventViewProps = {
|
||||
reviewItems?: SegmentedReviewData;
|
||||
@ -284,7 +285,7 @@ export default function EventView({
|
||||
<>
|
||||
<MdCircle className="size-2 text-severity_alert md:mr-[10px]" />
|
||||
<div className="hidden md:flex md:flex-row md:items-center">
|
||||
Alerts
|
||||
<Trans>ui.eventView.alerts</Trans>
|
||||
{reviewCounts.alert > -1 ? (
|
||||
` ∙ ${reviewCounts.alert}`
|
||||
) : (
|
||||
@ -320,7 +321,7 @@ export default function EventView({
|
||||
<>
|
||||
<MdCircle className="size-2 text-severity_detection md:mr-[10px]" />
|
||||
<div className="hidden md:flex md:flex-row md:items-center">
|
||||
Detections
|
||||
<Trans>ui.eventView.detections</Trans>
|
||||
{reviewCounts.detection > -1 ? (
|
||||
` ∙ ${reviewCounts.detection}`
|
||||
) : (
|
||||
@ -673,7 +674,7 @@ function DetectionReview({
|
||||
{!loading && currentItems?.length === 0 && (
|
||||
<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">
|
||||
<LuFolderCheck className="size-16" />
|
||||
There are no {severity.replace(/_/g, " ")}s to review
|
||||
<Trans>ui.eventView.empty.{severity.replace(/_/g, " ")}</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -27,6 +27,8 @@ import { LuExternalLink } from "react-icons/lu";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { MdCircle } from "react-icons/md";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type CameraSettingsViewProps = {
|
||||
selectedCamera: string;
|
||||
@ -73,7 +75,7 @@ export default function CameraSettingsView({
|
||||
const alertsLabels = useMemo(() => {
|
||||
return cameraConfig?.review.alerts.labels
|
||||
? cameraConfig.review.alerts.labels
|
||||
.map((label) => label.replaceAll("_", " "))
|
||||
.map((label) => t("object." + label))
|
||||
.join(", ")
|
||||
: "";
|
||||
}, [cameraConfig]);
|
||||
@ -81,7 +83,7 @@ export default function CameraSettingsView({
|
||||
const detectionsLabels = useMemo(() => {
|
||||
return cameraConfig?.review.detections.labels
|
||||
? cameraConfig.review.detections.labels
|
||||
.map((label) => label.replaceAll("_", " "))
|
||||
.map((label) => t("object." + label))
|
||||
.join(", ")
|
||||
: "";
|
||||
}, [cameraConfig]);
|
||||
@ -239,22 +241,19 @@ export default function CameraSettingsView({
|
||||
<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">
|
||||
Camera Settings
|
||||
<Trans>ui.settingView.cameraSettings</Trans>
|
||||
</Heading>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
Review Classification
|
||||
<Trans>ui.settingView.cameraSettings.reviewClassification</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>
|
||||
Frigate categorizes review items as Alerts and Detections. By
|
||||
default, all <em>person</em> and <em>car</em> objects are
|
||||
considered Alerts. You can refine categorization of your review
|
||||
items by configuring required zones for them.
|
||||
<Trans>ui.settingView.cameraSettings.reviewClassification.desc</Trans>
|
||||
</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
@ -263,7 +262,7 @@ export default function CameraSettingsView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the Documentation{" "}
|
||||
<Trans>ui.settingView.cameraSettings.reviewClassification.readTheDocumentation</Trans>{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -296,7 +295,7 @@ export default function CameraSettingsView({
|
||||
<MdCircle className="ml-3 size-2 text-severity_alert" />
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Select zones for Alerts
|
||||
<Trans>ui.settingView.cameraSettings.reviewClassification.selectAlertsZones</Trans>
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
|
||||
@ -345,20 +344,17 @@ export default function CameraSettingsView({
|
||||
</>
|
||||
) : (
|
||||
<div className="font-normal text-destructive">
|
||||
No zones are defined for this camera.
|
||||
<Trans>ui.settingView.cameraSettings.reviewClassification.noDefinedZones</Trans>
|
||||
</div>
|
||||
)}
|
||||
<FormMessage />
|
||||
<div className="text-sm">
|
||||
All {alertsLabels} objects
|
||||
{watchedAlertsZones && watchedAlertsZones.length > 0
|
||||
? ` detected in ${watchedAlertsZones.map((zone) => capitalizeFirstLetter(zone).replaceAll("_", " ")).join(", ")}`
|
||||
: ""}{" "}
|
||||
on{" "}
|
||||
{capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " ")}{" "}
|
||||
will be shown as Alerts.
|
||||
{watchedAlertsZones && watchedAlertsZones.length > 0
|
||||
?
|
||||
t("ui.settingView.cameraSettings.reviewClassification.zoneObjectAlertsTips", { alertsLabels, zone: watchedAlertsZones.map((zone) => capitalizeFirstLetter(zone).replaceAll("_", " ")).join(", "), cameraName: capitalizeFirstLetter(cameraConfig?.name ?? "",).replaceAll("_", " ") })
|
||||
:
|
||||
t("ui.settingView.cameraSettings.reviewClassification.objectAlertsTips", { alertsLabels, cameraName: capitalizeFirstLetter(cameraConfig?.name ?? "",).replaceAll("_", " ") })
|
||||
}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -449,22 +445,37 @@ export default function CameraSettingsView({
|
||||
)}
|
||||
|
||||
<div className="text-sm">
|
||||
All {detectionsLabels} objects{" "}
|
||||
<em>not classified as Alerts</em>{" "}
|
||||
{watchedDetectionsZones &&
|
||||
{watchedDetectionsZones &&
|
||||
watchedDetectionsZones.length > 0
|
||||
? ` that are detected in ${watchedDetectionsZones.map((zone) => capitalizeFirstLetter(zone).replaceAll("_", " ")).join(", ")}`
|
||||
: ""}{" "}
|
||||
on{" "}
|
||||
{capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " ")}{" "}
|
||||
will be shown as Detections
|
||||
{(!selectDetections ||
|
||||
(watchedDetectionsZones &&
|
||||
watchedDetectionsZones.length === 0)) &&
|
||||
", regardless of zone"}
|
||||
.
|
||||
?
|
||||
!selectDetections ?
|
||||
t("ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips", {
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll("_", " "),
|
||||
).join(", "),
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
})
|
||||
:
|
||||
t("ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips.notSelectDetections",{
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll("_", " "),
|
||||
).join(", "),
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
})
|
||||
:
|
||||
t("ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips", {
|
||||
detectionsLabels,
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -479,7 +490,7 @@ export default function CameraSettingsView({
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
<Trans>ui.cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -491,10 +502,10 @@ export default function CameraSettingsView({
|
||||
{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>
|
||||
|
||||
@ -37,6 +37,7 @@ import PolygonItem from "@/components/settings/PolygonItem";
|
||||
import { Link } from "react-router-dom";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type MasksAndZoneViewProps = {
|
||||
selectedCamera: string;
|
||||
@ -422,7 +423,7 @@ export default function MasksAndZonesView({
|
||||
{editPane === undefined && (
|
||||
<>
|
||||
<Heading as="h3" className="my-2">
|
||||
Masks / Zones
|
||||
<Trans>ui.settingView.masksAndZonesSettings</Trans>
|
||||
</Heading>
|
||||
<div className="flex w-full flex-col">
|
||||
{(selectedZoneMask === undefined ||
|
||||
@ -431,14 +432,12 @@ export default function MasksAndZonesView({
|
||||
<div className="my-3 flex flex-row items-center justify-between">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="text-md cursor-default">Zones</div>
|
||||
<div className="text-md cursor-default"><Trans>ui.settingView.masksAndZonesSettings.zone</Trans></div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
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.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.desc</Trans>
|
||||
</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
@ -447,7 +446,7 @@ export default function MasksAndZonesView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Documentation{" "}
|
||||
<Trans>ui.settingView.masksAndZonesSettings.zone.desc.documentation</Trans>{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -468,7 +467,7 @@ export default function MasksAndZonesView({
|
||||
<LuPlus />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Zone</TooltipContent>
|
||||
<TooltipContent><Trans>ui.settingView.masksAndZonesSettings.zone.add</Trans></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{allPolygons
|
||||
@ -498,16 +497,13 @@ export default function MasksAndZonesView({
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="text-md cursor-default">
|
||||
Motion Masks
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks</Trans>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
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.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks.desc</Trans>
|
||||
</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
@ -516,7 +512,7 @@ export default function MasksAndZonesView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Documentation{" "}
|
||||
<Trans>ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation</Trans>{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -537,7 +533,7 @@ export default function MasksAndZonesView({
|
||||
<LuPlus />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Motion Mask</TooltipContent>
|
||||
<TooltipContent><Trans>ui.settingView.masksAndZonesSettings.motionMasks.add</Trans></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{allPolygons
|
||||
@ -569,15 +565,13 @@ export default function MasksAndZonesView({
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="text-md cursor-default">
|
||||
Object Masks
|
||||
<Trans>ui.settingView.masksAndZonesSettings.objectMasks</Trans>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
Object filter masks are used to filter out false
|
||||
positives for a given object type based on
|
||||
location.
|
||||
<Trans>ui.settingView.masksAndZonesSettings.objectMasks.desc</Trans>
|
||||
</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
@ -586,7 +580,7 @@ export default function MasksAndZonesView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Documentation{" "}
|
||||
<Trans>ui.settingView.masksAndZonesSettings.objectMasks.documentation</Trans>{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -607,7 +601,7 @@ export default function MasksAndZonesView({
|
||||
<LuPlus />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Object Mask</TooltipContent>
|
||||
<TooltipContent><Trans>ui.settingView.masksAndZonesSettings.objectMasks.add</Trans></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{allPolygons
|
||||
|
||||
@ -21,6 +21,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type MotionTunerViewProps = {
|
||||
selectedCamera: string;
|
||||
@ -179,13 +180,11 @@ export default function MotionTunerView({
|
||||
<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 md:w-3/12">
|
||||
<Heading as="h3" className="my-2">
|
||||
Motion Detection Tuner
|
||||
<Trans>ui.settingView.motionDetectionTuner</Trans>
|
||||
</Heading>
|
||||
<div className="my-3 space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Frigate uses motion detection as a first line check to see if there
|
||||
is anything happening in the frame worth checking with object
|
||||
detection.
|
||||
<Trans>ui.settingView.motionDetectionTuner.desc</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center text-primary">
|
||||
@ -195,7 +194,7 @@ export default function MotionTunerView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the Motion Tuning Guide{" "}
|
||||
<Trans>ui.settingView.motionDetectionTuner.desc.documentation</Trans>{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -205,13 +204,11 @@ export default function MotionTunerView({
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="motion-threshold" className="text-md">
|
||||
Threshold
|
||||
<Trans>ui.settingView.motionDetectionTuner.Threshold</Trans>
|
||||
</Label>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
The threshold value dictates how much of a change in a pixel's
|
||||
luminance is required to be considered motion.{" "}
|
||||
<em>Default: 30</em>
|
||||
<Trans>ui.settingView.motionDetectionTuner.Threshold.desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -236,12 +233,11 @@ export default function MotionTunerView({
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="motion-threshold" className="text-md">
|
||||
Contour Area
|
||||
<Trans>ui.settingView.motionDetectionTuner.contourArea</Trans>
|
||||
</Label>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
The contour area value is used to decide which groups of
|
||||
changed pixels qualify as motion. <em>Default: 10</em>
|
||||
<Trans>ui.settingView.motionDetectionTuner.contourArea.desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -266,9 +262,9 @@ export default function MotionTunerView({
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="improve-contrast">Improve Contrast</Label>
|
||||
<Label htmlFor="improve-contrast"><Trans>ui.settingView.motionDetectionTuner.improveContrast</Trans></Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Improve contrast for darker scenes. <em>Default: ON</em>
|
||||
<Trans>ui.settingView.motionDetectionTuner.improveContrast.desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -289,7 +285,7 @@ export default function MotionTunerView({
|
||||
aria-label="Reset"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -301,10 +297,10 @@ export default function MotionTunerView({
|
||||
{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>
|
||||
|
||||
@ -23,6 +23,8 @@ import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { LuExternalLink, LuInfo } from "react-icons/lu";
|
||||
import { Link } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type ObjectSettingsViewProps = {
|
||||
selectedCamera?: string;
|
||||
@ -40,78 +42,51 @@ export default function ObjectSettingsView({
|
||||
const DEBUG_OPTIONS = [
|
||||
{
|
||||
param: "bbox",
|
||||
title: "Bounding boxes",
|
||||
description: "Show bounding boxes around tracked objects",
|
||||
title: t("ui.settingView.debug.boundingBoxes"),
|
||||
description: t("ui.settingView.debug.boundingBoxes.desc"),
|
||||
info: (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
<strong>Object Bounding Box Colors</strong>
|
||||
<strong><Trans>ui.settingView.debug.boundingBoxes.colors</Trans></strong>
|
||||
</p>
|
||||
<ul className="list-disc space-y-1 pl-5">
|
||||
<li>
|
||||
At startup, different colors will be assigned to each object label
|
||||
</li>
|
||||
<li>
|
||||
A dark blue thin line indicates that object is not detected at
|
||||
this current point in time
|
||||
</li>
|
||||
<li>
|
||||
A gray thin line indicates that object is detected as being
|
||||
stationary
|
||||
</li>
|
||||
<li>
|
||||
A thick line indicates that object is the subject of autotracking
|
||||
(when enabled)
|
||||
</li>
|
||||
<Trans>ui.settingView.debug.boundingBoxes.colors.info</Trans>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
param: "timestamp",
|
||||
title: "Timestamp",
|
||||
description: "Overlay a timestamp on the image",
|
||||
title: t("ui.settingView.debug.timestamp"),
|
||||
description: t("ui.settingView.debug.timestamp.desc"),
|
||||
},
|
||||
{
|
||||
param: "zones",
|
||||
title: "Zones",
|
||||
description: "Show an outline of any defined zones",
|
||||
title: t("ui.settingView.debug.zone"),
|
||||
description: t("ui.settingView.debug.zone.desc"),
|
||||
},
|
||||
{
|
||||
param: "mask",
|
||||
title: "Motion masks",
|
||||
description: "Show motion mask polygons",
|
||||
title: t("ui.settingView.debug.mask"),
|
||||
description: t("ui.settingView.debug.mask.desc"),
|
||||
},
|
||||
{
|
||||
param: "motion",
|
||||
title: "Motion boxes",
|
||||
description: "Show boxes around areas where motion is detected",
|
||||
title: t("ui.settingView.debug.motion"),
|
||||
description: t("ui.settingView.debug.motion.desc"),
|
||||
info: (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
<strong>Motion Boxes</strong>
|
||||
</p>
|
||||
<p>
|
||||
Red boxes will be overlaid on areas of the frame where motion is
|
||||
currently being detected
|
||||
</p>
|
||||
<Trans>ui.settingView.debug.motion.tips</Trans>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
param: "regions",
|
||||
title: "Regions",
|
||||
description:
|
||||
"Show a box of the region of interest sent to the object detector",
|
||||
title: t("ui.settingView.debug.regions"),
|
||||
description: t("ui.settingView.debug.regions.desc"),
|
||||
info: (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
<strong>Region Boxes</strong>
|
||||
</p>
|
||||
<p>
|
||||
Bright green boxes will be overlaid on areas of interest in the
|
||||
frame that are being sent to the object detector.
|
||||
</p>
|
||||
<Trans>ui.settingView.debug.regions.tips</Trans>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -168,24 +143,18 @@ export default function ObjectSettingsView({
|
||||
<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 md:w-3/12">
|
||||
<Heading as="h3" className="my-2">
|
||||
Debug
|
||||
<Trans>ui.settingView.debug</Trans>
|
||||
</Heading>
|
||||
<div className="mb-5 space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Frigate uses your detectors{" "}
|
||||
{config
|
||||
? "(" +
|
||||
Object.keys(config?.detectors)
|
||||
.map((detector) => capitalizeFirstLetter(detector))
|
||||
.join(",") +
|
||||
")"
|
||||
: ""}{" "}
|
||||
to detect objects in your camera's video stream.
|
||||
{t("ui.settingView.debug.detectorDesc", {
|
||||
detectors: config ? Object.keys(config?.detectors)
|
||||
.map((detector) => capitalizeFirstLetter(detector))
|
||||
.join(",") : ""
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
Debugging view shows a real-time view of tracked objects and their
|
||||
statistics. The object list shows a time-delayed summary of detected
|
||||
objects.
|
||||
<Trans>ui.settingView.debug.desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
{config?.cameras[cameraConfig.name]?.webui_url && (
|
||||
@ -206,8 +175,8 @@ export default function ObjectSettingsView({
|
||||
|
||||
<Tabs defaultValue="debug" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="debug">Debugging</TabsTrigger>
|
||||
<TabsTrigger value="objectlist">Object List</TabsTrigger>
|
||||
<TabsTrigger value="debug"><Trans>ui.settingView.debug.debugging</Trans></TabsTrigger>
|
||||
<TabsTrigger value="objectlist"><Trans>ui.settingView.debug.objectList</Trans></TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="debug">
|
||||
<div className="flex w-full flex-col space-y-6">
|
||||
@ -360,7 +329,7 @@ function ObjectList(objects?: ObjectType[]) {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="p-3 text-center">No objects</div>
|
||||
<div className="p-3 text-center"><Trans>ui.settingView.debug.noObjects</Trans></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -20,6 +20,8 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type SearchSettingsViewProps = {
|
||||
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -152,18 +154,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">
|
||||
Search Settings
|
||||
<Trans>ui.settingView.searchSettings</Trans>
|
||||
</Heading>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<Heading as="h4" className="my-2">
|
||||
Semantic Search
|
||||
<Trans>ui.settingView.searchSettings.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>
|
||||
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.
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch.desc</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center text-primary">
|
||||
@ -173,7 +173,7 @@ export default function SearchSettingsView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the Documentation
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch.readTheDocumentation</Trans>
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -192,7 +192,7 @@ export default function SearchSettingsView({
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="enabled">Enabled</Label>
|
||||
<Label htmlFor="enabled"><Trans>ui.enabled</Trans></Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@ -207,31 +207,26 @@ export default function SearchSettingsView({
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="reindex">Re-Index On Startup</Label>
|
||||
<Label htmlFor="reindex"><Trans>ui.settingView.searchSettings.semanticSearch.reindexOnStartup</Trans></Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
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>
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Model Size</div>
|
||||
<div className="text-md"><Trans>ui.settingView.searchSettings.semanticSearch.modelSize</Trans></div>
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p>
|
||||
The size of the model used for semantic search embeddings.
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch.modelSize.desc</Trans>
|
||||
</p>
|
||||
<ul className="list-disc pl-5 text-sm">
|
||||
<li>
|
||||
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.
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch.modelSize.small.desc</Trans>
|
||||
</li>
|
||||
<li>
|
||||
Using <em>large</em> employs the full Jina model and will
|
||||
automatically run on the GPU if applicable.
|
||||
<Trans>ui.settingView.searchSettings.semanticSearch.modelSize.large.desc</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -245,7 +240,7 @@ export default function SearchSettingsView({
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-20">
|
||||
{searchSettings.model_size}
|
||||
{t("ui.settingView.searchSettings.semanticSearch.modelSize." + searchSettings.model_size)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@ -255,7 +250,7 @@ export default function SearchSettingsView({
|
||||
className="cursor-pointer"
|
||||
value={size}
|
||||
>
|
||||
{size}
|
||||
{t("ui.settingView.searchSettings.semanticSearch.modelSize." + size)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@ -267,7 +262,7 @@ export default function SearchSettingsView({
|
||||
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
|
||||
<Button className="flex flex-1" aria-label="Reset" onClick={onCancel}>
|
||||
Reset
|
||||
<Trans>ui.reset</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -279,10 +274,10 @@ export default function SearchSettingsView({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>Saving...</span>
|
||||
<span><Trans>ui.saving</Trans></span>
|
||||
</div>
|
||||
) : (
|
||||
"Save"
|
||||
t("ui.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -18,6 +18,8 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "../../components/ui/select";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
||||
const WEEK_STARTS_ON = ["Sunday", "Monday"];
|
||||
@ -63,13 +65,13 @@ export default function UiSettingsView() {
|
||||
<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">
|
||||
General Settings
|
||||
<Trans>ui.settingView.generalSettings</Trans>
|
||||
</Heading>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
Live Dashboard
|
||||
<Trans>ui.settingView.generalSettings.liveDashboard</Trans>
|
||||
</Heading>
|
||||
|
||||
<div className="mt-2 space-y-6">
|
||||
@ -81,14 +83,12 @@ export default function UiSettingsView() {
|
||||
onCheckedChange={setAutoLive}
|
||||
/>
|
||||
<Label className="cursor-pointer" htmlFor="auto-live">
|
||||
Automatic Live View
|
||||
<Trans>ui.settingView.generalSettings.automaticLiveView</Trans>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Automatically switch to a camera's live view when activity is
|
||||
detected. Disabling this option causes static camera images on
|
||||
the Live dashboard to only update once per minute.
|
||||
<Trans>ui.settingView.generalSettings.automaticLiveView.desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,14 +100,12 @@ export default function UiSettingsView() {
|
||||
onCheckedChange={setAlertVideos}
|
||||
/>
|
||||
<Label className="cursor-pointer" htmlFor="images-only">
|
||||
Play Alert Videos
|
||||
<Trans>ui.settingView.generalSettings.playAlertVideos</Trans>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
By default, recent alerts on the Live dashboard play as small
|
||||
looping videos. Disable this option to only show a static
|
||||
image of recent alerts on this device/browser.
|
||||
<Trans>ui.settingView.generalSettings.playAlertVideos.desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -116,12 +114,10 @@ export default function UiSettingsView() {
|
||||
<div className="my-3 flex w-full flex-col space-y-6">
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Stored Layouts</div>
|
||||
<div className="text-md"><Trans>ui.settingView.generalSettings.storedLayouts</Trans></div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
The layout of cameras in a camera group can be
|
||||
dragged/resized. The positions are stored in your browser's
|
||||
local storage.
|
||||
<Trans>ui.settingView.generalSettings.storedLayouts.desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -129,21 +125,21 @@ export default function UiSettingsView() {
|
||||
aria-label="Clear all saved layouts"
|
||||
onClick={clearStoredLayouts}
|
||||
>
|
||||
Clear All Layouts
|
||||
<Trans>ui.settingView.generalSettings.storedLayouts.clearAll</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
Recordings Viewer
|
||||
<Trans>ui.settingView.generalSettings.recordingsViewer</Trans>
|
||||
</Heading>
|
||||
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Default Playback Rate</div>
|
||||
<div className="text-md"><Trans>ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate</Trans></div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>Default playback rate for recordings playback.</p>
|
||||
<p><Trans>ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate.desc</Trans></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -171,14 +167,14 @@ export default function UiSettingsView() {
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
Calendar
|
||||
<Trans>ui.settingView.generalSettings.calendar</Trans>
|
||||
</Heading>
|
||||
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">First Weekday</div>
|
||||
<div className="text-md"><Trans>ui.settingView.generalSettings.calendar.firstWeekday</Trans></div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>The day that the weeks of the review calendar begin on.</p>
|
||||
<p><Trans>ui.settingView.generalSettings.calendar.firstWeekday.desc</Trans></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -187,7 +183,7 @@ export default function UiSettingsView() {
|
||||
onValueChange={(value) => setWeekStartsOn(parseInt(value))}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
{WEEK_STARTS_ON[weekStartsOn ?? 0]}
|
||||
{t("ui.settingView.generalSettings.calendar.firstWeekday." + WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase())}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@ -197,7 +193,7 @@ export default function UiSettingsView() {
|
||||
className="cursor-pointer"
|
||||
value={index.toString()}
|
||||
>
|
||||
{day}
|
||||
{t("ui.settingView.generalSettings.calendar.firstWeekday."+day.toLowerCase())}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import useSWR from "swr";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type CameraMetricsProps = {
|
||||
lastUpdated: number;
|
||||
@ -223,11 +224,11 @@ export default function CameraMetrics({
|
||||
|
||||
return (
|
||||
<div className="scrollbar-container mt-4 flex size-full flex-col gap-3 overflow-y-auto">
|
||||
<div className="text-sm font-medium text-muted-foreground">Overview</div>
|
||||
<div className="text-sm font-medium text-muted-foreground"><Trans>ui.system.cameras.overview</Trans></div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3">
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">Frames / Detections</div>
|
||||
<div className="mb-5"><Trans>ui.system.cameras.framesAndDetections</Trans></div>
|
||||
<CameraLineGraph
|
||||
graphId="overall-stats"
|
||||
unit=""
|
||||
@ -294,7 +295,7 @@ export default function CameraMetrics({
|
||||
)}
|
||||
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">Frames / Detections</div>
|
||||
<div className="mb-5"><Trans>ui.system.cameras.framesAndDetections</Trans></div>
|
||||
<CameraLineGraph
|
||||
graphId={`${camera.name}-dps`}
|
||||
unit=""
|
||||
|
||||
@ -15,6 +15,7 @@ import GPUInfoDialog from "@/components/overlay/GPUInfoDialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ThresholdBarGraph } from "@/components/graph/SystemGraph";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type GeneralMetricsProps = {
|
||||
lastUpdated: number;
|
||||
@ -448,7 +449,7 @@ export default function GeneralMetrics({
|
||||
|
||||
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
Detectors
|
||||
<Trans>ui.system.general.detector</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@ -458,7 +459,7 @@ export default function GeneralMetrics({
|
||||
>
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">Detector Inference Speed</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.detectorInferenceSpeed</Trans></div>
|
||||
{detInferenceTimeSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -496,7 +497,7 @@ export default function GeneralMetrics({
|
||||
)}
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">Detector CPU Usage</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.detectorCpuUsage</Trans></div>
|
||||
{detCpuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -514,7 +515,7 @@ export default function GeneralMetrics({
|
||||
)}
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">Detector Memory Usage</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.detectorMemoryUsage</Trans></div>
|
||||
{detMemSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -545,7 +546,7 @@ export default function GeneralMetrics({
|
||||
size="sm"
|
||||
onClick={() => setShowVainfo(true)}
|
||||
>
|
||||
Hardware Info
|
||||
<Trans>ui.system.general.hardwareInfo</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -557,7 +558,7 @@ export default function GeneralMetrics({
|
||||
>
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">GPU Usage</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.gpuUsage</Trans></div>
|
||||
{gpuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -577,7 +578,7 @@ export default function GeneralMetrics({
|
||||
<>
|
||||
{gpuMemSeries && (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">GPU Memory</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.gpuMemroy</Trans></div>
|
||||
{gpuMemSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -599,7 +600,7 @@ export default function GeneralMetrics({
|
||||
<>
|
||||
{gpuEncSeries && gpuEncSeries?.length != 0 && (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">GPU Encoder</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.gpuEncoder</Trans></div>
|
||||
{gpuEncSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -621,7 +622,7 @@ export default function GeneralMetrics({
|
||||
<>
|
||||
{gpuDecSeries && gpuDecSeries?.length != 0 && (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">GPU Decoder</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.gpuDecoder</Trans></div>
|
||||
{gpuDecSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -644,12 +645,14 @@ export default function GeneralMetrics({
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
||||
Other Processes
|
||||
<Trans>ui.system.general.otherProcesses</Trans>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">Process CPU Usage</div>
|
||||
<div className="mb-5">
|
||||
<Trans>ui.system.general.processCpuUsage</Trans>
|
||||
</div>
|
||||
{otherProcessCpuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -667,7 +670,7 @@ export default function GeneralMetrics({
|
||||
)}
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">Process Memory Usage</div>
|
||||
<div className="mb-5"><Trans>ui.system.general.processMemoryUsage</Trans></div>
|
||||
{otherProcessMemSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import useSWR from "swr";
|
||||
import { LuAlertCircle } from "react-icons/lu";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type CameraStorage = {
|
||||
[key: string]: {
|
||||
@ -50,11 +51,11 @@ export default function StorageMetrics({
|
||||
|
||||
return (
|
||||
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
|
||||
<div className="text-sm font-medium text-muted-foreground">Overview</div>
|
||||
<div className="text-sm font-medium text-muted-foreground"><Trans>ui.system.storage.overview</Trans></div>
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
<div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5 flex flex-row items-center justify-between">
|
||||
Recordings
|
||||
<Trans>ui.system.storage.recordings</Trans>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
@ -69,9 +70,7 @@ export default function StorageMetrics({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-2">
|
||||
This value represents the total storage used by the recordings
|
||||
in Frigate's database. Frigate does not track storage usage
|
||||
for all files on your disk.
|
||||
<Trans>ui.system.storage.recordings.tips</Trans>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@ -100,7 +99,7 @@ export default function StorageMetrics({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
||||
Camera Storage
|
||||
<Trans>ui.system.storage.cameraStorage</Trans>
|
||||
</div>
|
||||
<div className="mt-4 bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<CombinedStorageGraph
|
||||
|
||||
Loading…
Reference in New Issue
Block a user