From f73281c5541de7f6bfe7237b94a8047fdaf1b172 Mon Sep 17 00:00:00 2001
From: ZhaiSoul <842607283@qq.com>
Date: Sun, 2 Mar 2025 00:12:40 +0800
Subject: [PATCH] Translation module init
---
web/package-lock.json | 143 +++-
web/package.json | 3 +
web/public/locales/en/translation.json | 620 +++++++++++++++++
web/public/locales/zh-CN/translation.json | 622 ++++++++++++++++++
web/src/components/Statusbar.tsx | 3 +-
web/src/components/card/ReviewCard.tsx | 8 +-
web/src/components/card/SearchThumbnail.tsx | 4 +-
.../components/card/SearchThumbnailFooter.tsx | 5 +-
web/src/components/dynamic/TimeAgo.tsx | 18 +-
.../filter/CalendarFilterButton.tsx | 6 +-
.../components/filter/CameraGroupSelector.tsx | 103 ++-
.../components/filter/CamerasFilterButton.tsx | 12 +-
.../components/filter/ReviewFilterGroup.tsx | 18 +-
.../components/filter/SearchFilterGroup.tsx | 39 +-
web/src/components/graph/CameraGraph.tsx | 5 +-
.../components/graph/CombinedStorageGraph.tsx | 31 +-
web/src/components/icons/IconPicker.tsx | 10 +-
web/src/components/menu/AccountSettings.tsx | 10 +-
web/src/components/menu/GeneralSettings.tsx | 160 ++++-
web/src/components/menu/LiveContextMenu.tsx | 2 +-
.../components/menu/SearchResultActions.tsx | 59 +-
web/src/components/navigation/NavItem.tsx | 5 +-
.../components/overlay/CameraInfoDialog.tsx | 42 +-
.../components/overlay/CreateUserDialog.tsx | 17 +-
.../components/overlay/DeleteUserDialog.tsx | 11 +-
web/src/components/overlay/ExportDialog.tsx | 67 +-
.../overlay/MobileReviewSettingsDrawer.tsx | 11 +-
.../components/overlay/SaveExportOverlay.tsx | 7 +-
.../components/overlay/SetPasswordDialog.tsx | 7 +-
.../overlay/detail/ReviewDetailDialog.tsx | 5 +-
.../overlay/detail/SearchDetailDialog.tsx | 83 ++-
.../overlay/dialog/RestartDialog.tsx | 18 +-
.../overlay/dialog/SearchFilterDialog.tsx | 58 +-
web/src/components/player/PreviewPlayer.tsx | 11 +-
.../player/PreviewThumbnailPlayer.tsx | 5 +-
.../player/dynamic/DynamicVideoPlayer.tsx | 3 +-
.../settings/CameraStreamingDialog.tsx | 14 +-
.../settings/MotionMaskEditPane.tsx | 58 +-
.../settings/ObjectMaskEditPane.tsx | 44 +-
.../components/settings/SearchSettings.tsx | 38 +-
web/src/components/settings/ZoneEditPane.tsx | 90 ++-
web/src/components/ui/calendar-range.tsx | 24 +-
web/src/components/ui/calendar.tsx | 15 +-
web/src/context/language-provider.tsx | 77 +++
web/src/context/providers.tsx | 21 +-
web/src/context/theme-provider.tsx | 4 +-
web/src/hooks/use-camera-activity.ts | 2 +-
web/src/hooks/use-navigation.ts | 12 +-
web/src/hooks/use-stats.ts | 11 +-
web/src/main.tsx | 2 +
web/src/pages/ConfigEditor.tsx | 15 +-
web/src/pages/Events.tsx | 5 +-
web/src/pages/Exports.tsx | 22 +-
web/src/pages/Live.tsx | 11 +-
web/src/pages/Settings.tsx | 37 +-
web/src/pages/System.tsx | 9 +-
web/src/utils/i18n.ts | 49 ++
web/src/views/events/EventView.tsx | 15 +-
web/src/views/explore/ExploreView.tsx | 2 +-
web/src/views/live/DraggableGridLayout.tsx | 3 +-
web/src/views/live/LiveCameraView.tsx | 120 ++--
web/src/views/live/LiveDashboardView.tsx | 7 +-
web/src/views/recording/RecordingView.tsx | 23 +-
web/src/views/search/SearchView.tsx | 9 +-
web/src/views/settings/AuthenticationView.tsx | 13 +-
web/src/views/settings/CameraSettingsView.tsx | 150 +++--
web/src/views/settings/MasksAndZonesView.tsx | 66 +-
web/src/views/settings/MotionTunerView.tsx | 44 +-
.../settings/NotificationsSettingsView.tsx | 48 +-
web/src/views/settings/ObjectSettingsView.tsx | 97 ++-
web/src/views/settings/SearchSettingsView.tsx | 122 ++--
web/src/views/settings/UiSettingsView.tsx | 97 ++-
web/src/views/system/CameraMetrics.tsx | 13 +-
web/src/views/system/GeneralMetrics.tsx | 43 +-
web/src/views/system/StorageMetrics.tsx | 13 +-
...tailwind.config.js => tailwind.config.cjs} | 0
76 files changed, 2942 insertions(+), 734 deletions(-)
create mode 100644 web/public/locales/en/translation.json
create mode 100644 web/public/locales/zh-CN/translation.json
create mode 100644 web/src/context/language-provider.tsx
create mode 100644 web/src/utils/i18n.ts
rename web/{tailwind.config.js => tailwind.config.cjs} (100%)
diff --git a/web/package-lock.json b/web/package-lock.json
index f2b186312..92a829592 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -41,6 +41,8 @@
"embla-carousel-react": "^8.2.0",
"framer-motion": "^11.5.4",
"hls.js": "^1.5.17",
+ "i18next": "^24.2.0",
+ "i18next-http-backend": "^3.0.1",
"idb-keyval": "^6.2.1",
"immer": "^10.1.1",
"konva": "^9.3.16",
@@ -56,6 +58,7 @@
"react-dom": "^18.3.1",
"react-grid-layout": "^1.5.0",
"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",
@@ -189,9 +192,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"
},
@@ -4544,6 +4548,15 @@
"toggle-selection": "^1.0.6"
}
},
+ "node_modules/cross-fetch": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
+ "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.6.12"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -5716,6 +5729,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",
@@ -5753,6 +5775,46 @@
"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/i18next-http-backend": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.1.tgz",
+ "integrity": "sha512-XT2lYSkbAtDE55c6m7CtKxxrsfuRQO3rUfHzj8ZyRtY9CkIX3aRGwXGTkUhpGWce+J8n7sfu3J0f2wTzo7Lw0A==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-fetch": "4.0.0"
+ }
+ },
"node_modules/idb-keyval": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
@@ -6677,6 +6739,48 @@
"react-dom": "^16.8 || ^17 || ^18"
}
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/node-fetch/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@@ -7468,6 +7572,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",
@@ -8774,7 +8900,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",
@@ -9155,6 +9281,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",
diff --git a/web/package.json b/web/package.json
index 700fd12d7..32c2f1d01 100644
--- a/web/package.json
+++ b/web/package.json
@@ -47,6 +47,8 @@
"embla-carousel-react": "^8.2.0",
"framer-motion": "^11.5.4",
"hls.js": "^1.5.17",
+ "i18next": "^24.2.0",
+ "i18next-http-backend": "^3.0.1",
"idb-keyval": "^6.2.1",
"immer": "^10.1.1",
"konva": "^9.3.16",
@@ -62,6 +64,7 @@
"react-dom": "^18.3.1",
"react-grid-layout": "^1.5.0",
"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",
diff --git a/web/public/locales/en/translation.json b/web/public/locales/en/translation.json
new file mode 100644
index 000000000..de23ffd7e
--- /dev/null
+++ b/web/public/locales/en/translation.json
@@ -0,0 +1,620 @@
+{
+ "object.person": "Person",
+ "object.bicycle": "Bicycle",
+ "object.car": "Car",
+ "object.motorcycle": "Motorcycle",
+ "object.airplane": "Airplane",
+ "object.bus": "Bus",
+ "object.train": "Train",
+ "object.boat": "Boat",
+ "object.traffic_light": "Traffic Light",
+ "object.fire_hydrant": "Fire Hydrant",
+ "object.street_sign": "Street Sign",
+ "object.stop_sign": "Stop Sign",
+ "object.parking_meter": "Parking Meter",
+ "object.bench": "Bench",
+ "object.bird": "Bird",
+ "object.cat": "Cat",
+ "object.dog": "Dog",
+ "object.horse": "Horse",
+ "object.sheep": "Sheep",
+ "object.cow": "Cow",
+ "object.elephant": "Elephant",
+ "object.bear": "Bear",
+ "object.zebra": "Zebra",
+ "object.giraffe": "Giraffe",
+ "object.hat": "Hat",
+ "object.backpack": "Backpack",
+ "object.umbrella": "Umbrella",
+ "object.shoe": "Shoe",
+ "object.eye_glasses": "Eye Glasses",
+ "object.handbag": "Handbag",
+ "object.tie": "Tie",
+ "object.suitcase": "Suitcase",
+ "object.frisbee": "Frisbee",
+ "object.skis": "Skis",
+ "object.snowboard": "Snowboard",
+ "object.sports_ball": "Sports Ball",
+ "object.kite": "Kite",
+ "object.baseball_bat": "Baseball Bat",
+ "object.baseball_glove": "Baseball Glove",
+ "object.skateboard": "Skateboard",
+ "object.surfboard": "Surfboard",
+ "object.tennis_racket": "Tennis Racket",
+ "object.bottle": "Bottle",
+ "object.plate": "Plate",
+ "object.wine_glass": "Wine Glass",
+ "object.cup": "Cup",
+ "object.fork": "Fork",
+ "object.knife": "Knife",
+ "object.spoon": "Spoon",
+ "object.bowl": "Bowl",
+ "object.banana": "Banana",
+ "object.apple": "Apple",
+ "object.sandwich": "Sandwich",
+ "object.orange": "Orange",
+ "object.broccoli": "Broccoli",
+ "object.carrot": "Carrot",
+ "object.hot_dog": "Hot Dog",
+ "object.pizza": "Pizza",
+ "object.donut": "Donut",
+ "object.cake": "Cake",
+ "object.chair": "Chair",
+ "object.couch": "Couch",
+ "object.potted_plant": "Potted Plant",
+ "object.bed": "Bed",
+ "object.mirror": "Mirror",
+ "object.dining_table": "Dining Table",
+ "object.window": "Window",
+ "object.desk": "Desk",
+ "object.toilet": "Toilet",
+ "object.door": "Door",
+ "object.tv": "TV",
+ "object.laptop": "Laptop",
+ "object.mouse": "Mouse",
+ "object.remote": "Remote",
+ "object.keyboard": "Keyboard",
+ "object.cell_phone": "Cell Phone",
+ "object.microwave": "Microwave",
+ "object.oven": "Oven",
+ "object.toaster": "Toaster",
+ "object.sink": "Sink",
+ "object.refrigerator": "Refrigerator",
+ "object.blender": "Blender",
+ "object.book": "Book",
+ "object.clock": "Clock",
+ "object.vase": "Vase",
+ "object.scissors": "Scissors",
+ "object.teddy_bear": "Teddy Bear",
+ "object.hair_dryer": "Hair Dryer",
+ "object.toothbrush": "Toothbrush",
+ "object.hair_brush": "Hair Brush",
+ "object.vehicle": "Vehicle",
+ "object.squirrel": "Squirrel",
+ "object.deer": "Deer",
+ "object.animal": "Animal",
+ "object.bark": "Bark",
+ "object.fox": "Fox",
+ "object.goat": "Goat",
+ "object.rabbit": "Rabbit",
+ "object.raccoon": "Raccoon",
+ "object.robot_lawnmower": "Robot Lawnmower",
+ "object.waste_bin": "Waste bin",
+ "object.on_demand": "On_demand",
+
+ "audio.crying": "Crying",
+ "audio.laughter": "Laughter",
+ "audio.scream": "Scream",
+ "audio.speech": "Speech",
+ "audio.yell": "Yell",
+ "audio.fire_alarm": "Fire alarm",
+
+ "ui.time.ago": "{{timeAgo}} ago",
+ "ui.time.justNow": "Just now",
+ "ui.time.today": "Today",
+ "ui.time.yesterday": "Yesterday",
+ "ui.time.last7": "Last 7 days",
+ "ui.time.last14": "Last 14 days",
+ "ui.time.last30": "Last 30 days",
+ "ui.time.thisWeek": "This Week",
+ "ui.time.lastWeek": "Last Week",
+ "ui.time.thisMonth": "This Month",
+ "ui.time.lastMonth": "Last Month",
+
+ "ui.time.pm": "pm",
+ "ui.time.am": "am",
+
+ "ui.time.yr": "{{time}}yr",
+ "ui.time.year": "{{time}} years",
+ "ui.time.mo": "{{time}}mo",
+ "ui.time.month": "{{time}} months",
+ "ui.time.d": "{{time}}d",
+ "ui.time.day": "{{time}} days",
+ "ui.time.h": "{{time}}h",
+ "ui.time.hour": "{{time}} hours",
+ "ui.time.m": "{{time}}m",
+ "ui.time.minute": "{{time}} minutes",
+ "ui.time.s": "s",
+ "ui.time.second": "{{time}} seconds",
+
+ "ui.time.formattedTimestamp": "%b %-d, %I:%M:%S %p",
+ "ui.time.formattedTimestamp.24hour": "%b %-d, %H:%M:%S",
+ "ui.time.formattedTimestampExcludeSeconds": "%b %-d, %I:%M %p",
+ "ui.time.formattedTimestampExcludeSeconds.24hour": "%b %-d, %H:%M",
+ "ui.time.formattedTimestampWithYear": "%b %-d %Y, %I:%M %p",
+ "ui.time.formattedTimestampWithYear.24hour": "%b %-d %Y, %H:%M",
+
+ "ui.iconPicker.selectIcon": "Select an icon",
+ "ui.iconPicker.search.placeholder": "Search for an icon...",
+
+ "ui.dialog.restart.title": "Are you sure you want to restart Frigate?",
+ "ui.dialog.restart.button": "Restart",
+ "ui.dialog.restart.restarting.title": "Frigate is Restarting",
+ "ui.dialog.restart.restarting.content": "This page will reload in {{countdown}} seconds.",
+ "ui.dialog.restart.restarting.button": "Force Reload Now",
+
+ "ui.dialog.export.time.fromTimeline": "Select from Timeline",
+ "ui.dialog.export.time.lastHour_one": "Last Hour",
+ "ui.dialog.export.time.lastHour_other": "Last {{count}} Hours",
+ "ui.dialog.export.time.custom": "Custom",
+ "ui.dialog.export.name.placeholder": "Name the Export",
+ "ui.dialog.export.select": "Select",
+ "ui.dialog.export.export": "Export",
+ "ui.dialog.export.toast.success": "Successfully started export. View the file in the /exports folder.",
+ "ui.dialog.export.toast.error.failed": "Failed to start export: {{error}}",
+ "ui.dialog.export.toast.error.endTimeMustAfterStartTime": "End time must be after start time",
+ "ui.dialog.export.toast.error.noVaildTimeSelected": "No valid time range selected",
+ "ui.dialog.export.fromTimeline.saveExport": "Save Export",
+ "ui.dialog.export.fromTimeline.previewExport": "Preview Export",
+
+ "ui.dialog.streaming": "Stream",
+ "ui.dialog.streaming.restreaming.NotEnabled": "Restreaming is not enabled for this camera.",
+ "ui.dialog.streaming.restreaming.desc": "Set up go2rtc for additional live view options and audio for this camera.",
+ "ui.dialog.streaming.restreaming.desc.readTheDocumentation": "Read the documentation ",
+
+ "ui.dialog.streaming.showStats": "Show stream stats",
+ "ui.dialog.streaming.showStats.desc": "Enable this option to show stream statistics as an overlay on the camera feed.",
+
+ "ui.dialog.streaming.debugView": "Debug View",
+
+ "ui.stats.ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
+ "ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
+ "ui.stats.healthy": "System is healthy",
+
+ "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 ffprobe.",
+ "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.cameraGroup": "Camera Groups",
+ "ui.cameraGroup.add": "Add camera groups",
+ "ui.cameraGroup.edit": "Edit camera groups",
+ "ui.cameraGroup.delete.confirm": "Confirm Delete",
+ "ui.cameraGroup.delete.confirm.desc": "Are you sure you want to delete the camera group {{name}} ?",
+ "ui.cameraGroup.name": "Name",
+ "ui.cameraGroup.name.placeholder": "Enter a name...",
+ "ui.cameraGroup.name.errorMessage.mustLeastCharacters": "Camera group name must be at least 2 characters.",
+ "ui.cameraGroup.name.errorMessage.exists": "Camera group name already exists.",
+ "ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "Camera group name must not contain a period.",
+ "ui.cameraGroup.name.errorMessage.invalid": "Invalid camera group name.",
+ "ui.cameraGroup.cameras": "Cameras",
+ "ui.cameraGroup.cameras.desc": "Select cameras for this group.",
+ "ui.cameraGroup.icon": "Icon",
+ "ui.cameraGroup.success": "Camera group ({{name}}) has been saved.",
+ "ui.cameraGroup.toast.error": "Failed to save config changes: {{error}}",
+
+ "ui.eventView.alerts": "Alerts",
+ "ui.eventView.detections": "Detections",
+ "ui.eventView.motion": "Motion",
+ "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.exploreView.trackedObjectDetails": "Tracked Object Details",
+ "ui.exploreView.type.details": "details",
+ "ui.exploreView.type.snapshot": "snapshot",
+ "ui.exploreView.type.video": "video",
+ "ui.exploreView.type.object_lifecycle": "object lifecycle",
+ "ui.exploreView.details.label": "Label",
+ "ui.exploreView.details.editSubLable": "Edit sub label",
+ "ui.exploreView.details.topScore": "Top Score",
+ "ui.exploreView.details.topScore.info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail.",
+ "ui.exploreView.details.estimatedSpeed": "Estimated Speed",
+ "ui.exploreView.details.camera": "Camera",
+ "ui.exploreView.details.timestamp": "Timestamp",
+ "ui.exploreView.details.button.findSimilar": "Find Similar",
+ "ui.exploreView.details.description": "Description",
+ "ui.exploreView.details.description.placeholder": "Description of the tracked object",
+ "ui.exploreView.details.description.aiTips": "Frigate will not request a description from your Generative AI provider until the tracked object's lifecycle has ended.",
+ "ui.exploreView.details.button.regenerate": "Regenerate",
+ "ui.exploreView.details.regenerateFromSnapshot": "Regenerate from Snapshot",
+ "ui.exploreView.details.regenerateFromThumbnails": "Regenerate from Thumbnails",
+ "ui.exploreView.details.tips.descriptionSaved": "Successfully saved description",
+ "ui.exploreView.details.tips.saveDescriptionFailed": "Failed to update the description",
+ "ui.exploreView.itemMenu.downloadVideo": "Download video",
+ "ui.exploreView.itemMenu.downloadVideo.aria": "Download video",
+ "ui.exploreView.itemMenu.downloadSnapshot": "Download snapshot",
+ "ui.exploreView.itemMenu.downloadSnapshot.aria": "Download snapshot",
+ "ui.exploreView.itemMenu.viewObjectLifecycle": "View object lifecycle",
+ "ui.exploreView.itemMenu.viewObjectLifecycle.aria": "Show the object lifecycle",
+ "ui.exploreView.itemMenu.findSimilar": "Find similar",
+ "ui.exploreView.itemMenu.findSimilar.aria": "Find similar tracked objects",
+ "ui.exploreView.itemMenu.submitToPlus": "Submit to Frigate+",
+ "ui.exploreView.itemMenu.submitToPlus.aria": "Submit to Frigate Plus",
+ "ui.exploreView.dialog.confirmDelete": "Confirm Delete",
+ "ui.exploreView.dialog.confirmDelete.desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of this tracked object in History view will NOT be deleted. Are you sure you want to proceed?",
+
+ "ui.filter": "Filter",
+ "ui.filter.allLabels": "All Labels",
+ "ui.filter.allLabels.short": "Labels",
+ "ui.filter.countLabels": "{{count}} Labels",
+ "ui.filter.allZones": "All Zones",
+ "ui.filter.allZones.short": "Zones",
+ "ui.filter.allDates": "All Dates",
+ "ui.filter.allDates.short": "Dates",
+ "ui.filter.more": "More Filters",
+ "ui.filter.timeRange": "Time Range",
+ "ui.filter.zones": "Zones",
+ "ui.filter.subLabels": "Sub Labels",
+ "ui.filter.allSubLabels": "All Sub Labels",
+ "ui.filter.score": "Score",
+ "ui.filter.features": "Features",
+ "ui.filter.features.hasSnapshot": "Has a snapshot",
+ "ui.filter.features.hasVideoClip": "Has a video clip",
+ "ui.filter.features.submittedToFrigatePlus": "Submitted to Frigate+",
+ "ui.filter.features.submittedToFrigatePlus.tips": "You must first filter on tracked objects that have a snapshot. Tracked objects without a snapshot cannot be submitted to Frigate+.",
+ "ui.filter.sort": "Sort",
+ "ui.filter.sort.dateAsc": "Date (Ascending)",
+ "ui.filter.sort.dateDesc": "Date (Descending)",
+ "ui.filter.sort.scoreAsc": "Object Score (Ascending)",
+ "ui.filter.sort.scoreDesc": "Object Score (Descending)",
+ "ui.filter.sort.relevance": "Relevance",
+ "ui.filter.allCameras": "All Cameras",
+ "ui.filter.allCameras.short": "Cameras",
+
+ "ui.reviewFilter.showReviewed": "Show Reviewed",
+
+ "ui.apply": "Apply",
+ "ui.reset": "Reset",
+ "ui.enabled": "Enabled",
+ "ui.save": "Save",
+ "ui.saving": "Saving...",
+ "ui.cancel": "Cancel",
+ "ui.close": "Close",
+ "ui.copy": "Copy",
+ "ui.back": "Back",
+ "ui.history": "History",
+ "ui.fullscreen": "Fullscreen",
+ "ui.pictureInPicture": "Picture in Picture",
+ "ui.on": "ON",
+ "ui.off": "OFF",
+ "ui.edit": "Edit",
+ "ui.delete": "Delete",
+ "ui.yes": "Yes",
+ "ui.no": "No",
+ "ui.download": "Download",
+
+ "ui.live.documentTitle": "Live - Frigate",
+ "ui.live.documentTitle.withCamera": "{{camera}} - Live - Frigate",
+ "ui.live.twoWayTalk.enable": "Enable Two Way Talk",
+ "ui.live.twoWayTalk.disable": "Disable Two Way Talk",
+ "ui.live.cameraAudio.enable": "Enable Camera Audio",
+ "ui.live.cameraAudio.disable": "Disable Camera Audio",
+ "ui.live.ptz.move.left.label": "Move PTZ camera to the left",
+ "ui.live.ptz.move.up.label": "Move PTZ camera up",
+ "ui.live.ptz.move.down.label": "Move PTZ camera down",
+ "ui.live.ptz.move.right.label": "Move PTZ camera to the right",
+ "ui.live.ptz.zoom.in.label": "Zoom PTZ camera in",
+ "ui.live.ptz.zoom.out.label": "Zoom PTZ camera out",
+ "ui.live.ptz.frame.center.label": "Click in the frame to center the PTZ camera",
+
+ "ui.live.detect.enable": "Enable Detect",
+ "ui.live.detect.disable": "Disable Detect",
+ "ui.live.recording.enable": "Enable Recording",
+ "ui.live.recording.disable": "Disable Recording",
+ "ui.live.snapshots.enable": "Enable Snapshots",
+ "ui.live.snapshots.disable": "Disable Snapshots",
+ "ui.live.audioDetect.enable": "Enable Audio Detect",
+ "ui.live.audioDetect.disable": "Disable Audio Detect",
+ "ui.live.autotracking.enable": "Enable Autotracking",
+ "ui.live.autotracking.disable": "Disable Autotracking",
+ "ui.live.manualRecording.start": "Start on-demand recording",
+ "ui.live.manualRecording.started": "Started manual on-demand recording.",
+ "ui.live.manualRecording.failedToStart": "Failed to start manual on-demand recording.",
+ "ui.live.manualRecording.recordDisabledTips": "Since recording is disabled or restricted in the config for this camera, only a snapshot will be saved.",
+ "ui.live.manualRecording.end": "End on-demand recording",
+ "ui.live.manualRecording.ended": "Ended manual on-demand recording.",
+ "ui.live.manualRecording.failedToEnd": "Failed to end manual on-demand recording.",
+
+ "ui.review.timeline": "Timeline",
+ "ui.review.events": "Events",
+ "ui.review.events.noFoundForTimePeriod": "No events found for this time period.",
+ "ui.review.documentTitle": "Review - Frigate",
+ "ui.review.recordings.documentTitle": "Recordings - Frigate",
+
+ "ui.player.noRecordingsFoundForThisTime": "No recordings found for this time",
+ "ui.player.noPreviewFound": "No Preview Found",
+ "ui.player.noPreviewFoundFor": "No Preview Found for {{cameraName}}",
+
+ "ui.calendarFilter.last24Hours": "Last 24 Hours",
+
+ "ui.searchView.noTrackedObjects": "No Tracked Objects Found",
+ "ui.searchView.settings": "Settings",
+ "ui.searchView.settings.defaultView": "Default View",
+ "ui.searchView.settings.defaultView.desc": "When no filters are selected, display a summary of the most recent tracked objects per label, or display an unfiltered grid.",
+ "ui.searchView.settings.defaultView.summary": "Summary",
+ "ui.searchView.settings.defaultView.unfilteredGrid": "Unfiltered Grid",
+ "ui.searchView.settings.gridColumns": "Grid Columns",
+ "ui.searchView.settings.gridColumns.desc": "Select the number of columns in the grid view.",
+ "ui.searchView.settings.searchSource": "Search Source",
+ "ui.searchView.settings.searchSource.desc": "Choose whether to search the thumbnails or descriptions of your tracked objects.",
+
+ "ui.settingView.menu.uiSettings": "UI Settings",
+ "ui.settingView.menu.exploreSettings": "Explore 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.cameraGroupStreaming": "Camera Group Streaming Settings",
+ "ui.settingView.generalSettings.cameraGroupStreaming.desc": "Streaming settings for each camera group are stored in your browser's local storage.",
+ "ui.settingView.generalSettings.cameraGroupStreaming.clearAll": "Clear All Streaming Settings",
+ "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.exploreSettings": "Explore Settings",
+ "ui.settingView.exploreSettings.semanticSearch": "Semantic Search",
+ "ui.settingView.exploreSettings.semanticSearch.desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
+ "ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
+ "ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup",
+ "ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. Don't forget to disable the option after restarting! ",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize": "Model Size",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.small": "small",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.large": "large",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "Using small employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality.",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "Using large employs the full Jina model and will automatically run on the GPU if applicable.",
+
+ "ui.settingView.cameraSettings": "Camera Settings",
+ "ui.settingView.cameraSettings.review": "Review",
+ "ui.settingView.cameraSettings.review.desc": "Enable/disable alerts and detections for this camera. When disabled, no new review items will be generated.",
+ "ui.settingView.cameraSettings.review.alerts": "Alerts",
+ "ui.settingView.cameraSettings.review.detections": "Detections",
+ "ui.settingView.cameraSettings.reviewClassification": "Review Classification",
+ "ui.settingView.cameraSettings.reviewClassification.desc": "Frigate categorizes review items as Alerts and Detections. By default, all person and car 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 not classified as Alerts on {{cameraName}} will be shown as Detections.",
+ "ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips": "All {{detectionsLabels}} objects not classified as Alerts that are detected in {{zone}} on {{cameraName}} will be shown as Detections.",
+ "ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips.notSelectDetections": "All {{detectionsLabels}} objects not classified as Alerts that are detected in {{zone}} on {{cameraName}} will be shown as Detections, regardless of zone",
+ "ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips.regardlessOfZoneObjectDetectionsTips": "All {{detectionsLabels}} objects not classified as Alerts on {{cameraName}} will be shown as Detections, regardless of zone.",
+
+ "ui.settingView.masksAndZonesSettings": "Masks / Zones",
+ "ui.settingView.masksAndZonesSettings.zone": "Zones",
+ "ui.settingView.masksAndZonesSettings.zone.documentTitle": "Edit Zone - Frigate",
+ "ui.settingView.masksAndZonesSettings.zone.desc": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
+ "ui.settingView.masksAndZonesSettings.zone.desc.documentation": "Documentation",
+ "ui.settingView.masksAndZonesSettings.zone.add": "Add Zone",
+ "ui.settingView.masksAndZonesSettings.zone.edit": "Edit 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. Default: 3 ",
+ "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. Default: 0 ",
+ "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.zone.speedEstimation": "Speed Estimation",
+ "ui.settingView.masksAndZonesSettings.zone.speedEstimation.desc": "Enable speed estimation for objects in this zone. The zone must have exactly 4 points.",
+ "ui.settingView.masksAndZonesSettings.zone.speedEstimation.pointLengthError": "Zones with speed estimation must have exactly 4 points.",
+ "ui.settingView.masksAndZonesSettings.zone.speedEstimation.loiteringTimeError": "Zones with loitering times greater than 0 should not be used with speed estimation.",
+
+ "ui.settingView.masksAndZonesSettings.motionMasks": "Motion Mask",
+ "ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "Edit Motion Mask - Frigate",
+ "ui.settingView.masksAndZonesSettings.motionMasks.desc": "Motion masks are used to prevent unwanted types of motion from triggering detection. Over masking will make it more difficult for objects to be tracked.",
+ "ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "Documentation",
+ "ui.settingView.masksAndZonesSettings.motionMasks.add": "New Motion Mask",
+ "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 very sparingly , 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.documentTitle": "Edit Object Mask - Frigate",
+ "ui.settingView.masksAndZonesSettings.objectMasks.desc": "Object filter masks are used to filter out false positives for a given object type based on location.",
+ "ui.settingView.masksAndZonesSettings.objectMasks.documentation": "Documentation",
+ "ui.settingView.masksAndZonesSettings.objectMasks.add": "Add Object Mask",
+ "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. Default: 30 ",
+ "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. Default: 10 ",
+ "ui.settingView.motionDetectionTuner.improveContrast": "Improve Contrast",
+ "ui.settingView.motionDetectionTuner.improveContrast.desc": "Improve contrast for darker scenes. Default: ON ",
+
+ "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": "
At startup, different colors will be assigned to each object label A dark blue thin line indicates that object is not detected at this current point in time A gray thin line indicates that object is detected as being stationary A thick line indicates that object is the subject of autotracking (when enabled) ",
+ "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": "Motion Boxes
Red boxes will be overlaid on areas of the frame where motion is currently being detected
",
+ "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": "Region Boxes
Bright green boxes will be overlaid on areas of interest in the frame that are being sent to the object detector.
",
+
+ "ui.settingView.users": "Users",
+ "ui.settingView.users.addUser": "Add User",
+ "ui.settingView.users.updatePassword": "Update Password",
+ "ui.settingView.users.dialog.createUser": "Create User",
+ "ui.settingView.users.dialog.createUser.user": "User",
+ "ui.settingView.users.dialog.createUser.password": "Password",
+ "ui.settingView.users.dialog.deleteUser": "Delete User",
+ "ui.settingView.users.dialog.deleteUser.warn": "Are you sure?",
+ "ui.settingView.users.dialog.setPassword": "Set Password",
+
+ "ui.settingView.notification": "Notifications",
+ "ui.settingView.notification.notificationSettings": "Notification Settings",
+ "ui.settingView.notification.desc": "Frigate can natively send push notifications to your device when it is running in the browser or installed as a PWA.",
+ "ui.settingView.notification.documentation": "Read the Documentation",
+ "ui.settingView.notification.email": "Email",
+ "ui.settingView.notification.email.placeholder": "example@email.com",
+ "ui.settingView.notification.email.desc": "Entering a valid email is required, as this is used by the push server in case problems occur.",
+ "ui.settingView.notification.cameras": "Cameras",
+ "ui.settingView.notification.cameras.noCameras": "No cameras available.",
+ "ui.settingView.notification.cameras.desc": "Select the cameras to enable notifications for.",
+ "ui.settingView.notification.deviceSpecific": "Device-Specific Settings",
+ "ui.settingView.notification.registerDevice": "Register this device",
+ "ui.settingView.notification.unregisterDevice": "Unregister this device",
+
+
+ "ui.configEditorView.configEditor": "Config Editor",
+ "ui.configEditorView.copyConfig": "Copy Config",
+ "ui.configEditorView.saveAndRestart": "Save & Restart",
+ "ui.configEditorView.saveOnly": "Save Only",
+
+ "ui.exportView.documentTitle": "Export - Frigate",
+ "ui.exportView.search": "Search",
+ "ui.exportView.noExports": "No exports found",
+ "ui.exportView.deleteExport": "Delete Export",
+ "ui.exportView.deleteExport.desc": "Are you sure you want to delete {{exportName}}?"
+}
\ No newline at end of file
diff --git a/web/public/locales/zh-CN/translation.json b/web/public/locales/zh-CN/translation.json
new file mode 100644
index 000000000..82658d045
--- /dev/null
+++ b/web/public/locales/zh-CN/translation.json
@@ -0,0 +1,622 @@
+{
+ "object.person": "人",
+ "object.bicycle": "自行车",
+ "object.car": "汽车",
+ "object.motorcycle": "摩托车",
+ "object.airplane": "飞机",
+ "object.bus": "公交车",
+ "object.train": "火车",
+ "object.boat": "船",
+ "object.traffic_light": "交通灯",
+ "object.fire_hydrant": "消防栓",
+ "object.street_sign": "路标",
+ "object.stop_sign": "停车标志",
+ "object.parking_meter": "停车计时器",
+ "object.bench": "长椅",
+ "object.bird": "鸟",
+ "object.cat": "猫",
+ "object.dog": "狗",
+ "object.horse": "马",
+ "object.sheep": "羊",
+ "object.cow": "牛",
+ "object.elephant": "大象",
+ "object.bear": "熊",
+ "object.zebra": "斑马",
+ "object.giraffe": "长颈鹿",
+ "object.hat": "帽子",
+ "object.backpack": "背包",
+ "object.umbrella": "雨伞",
+ "object.shoe": "鞋子",
+ "object.eye_glasses": "眼镜",
+ "object.handbag": "手提包",
+ "object.tie": "领带",
+ "object.suitcase": "手提箱",
+ "object.frisbee": "飞盘",
+ "object.skis": "滑雪板",
+ "object.snowboard": "滑雪板",
+ "object.sports_ball": "运动球",
+ "object.kite": "风筝",
+ "object.baseball_bat": "棒球棒",
+ "object.baseball_glove": "棒球手套",
+ "object.skateboard": "滑板",
+ "object.surfboard": "冲浪板",
+ "object.tennis_racket": "网球拍",
+ "object.bottle": "瓶子",
+ "object.plate": "盘子",
+ "object.wine_glass": "酒杯",
+ "object.cup": "杯子",
+ "object.fork": "叉子",
+ "object.knife": "刀",
+ "object.spoon": "勺子",
+ "object.bowl": "碗",
+ "object.banana": "香蕉",
+ "object.apple": "苹果",
+ "object.sandwich": "三明治",
+ "object.orange": "橙子",
+ "object.broccoli": "西兰花",
+ "object.carrot": "胡萝卜",
+ "object.hot_dog": "热狗",
+ "object.pizza": "披萨",
+ "object.donut": "甜甜圈",
+ "object.cake": "蛋糕",
+ "object.chair": "椅子",
+ "object.couch": "沙发",
+ "object.potted_plant": "盆栽植物",
+ "object.bed": "床",
+ "object.mirror": "镜子",
+ "object.dining_table": "餐桌",
+ "object.window": "窗户",
+ "object.desk": "桌子",
+ "object.toilet": "厕所",
+ "object.door": "门",
+ "object.tv": "电视",
+ "object.laptop": "笔记本电脑",
+ "object.mouse": "鼠标",
+ "object.remote": "遥控器",
+ "object.keyboard": "键盘",
+ "object.cell_phone": "手机",
+ "object.microwave": "微波炉",
+ "object.oven": "烤箱",
+ "object.toaster": "烤面包机",
+ "object.sink": "水槽",
+ "object.refrigerator": "冰箱",
+ "object.blender": "搅拌机",
+ "object.book": "书",
+ "object.clock": "时钟",
+ "object.vase": "花瓶",
+ "object.scissors": "剪刀",
+ "object.teddy_bear": "泰迪熊",
+ "object.hair_dryer": "吹风机",
+ "object.toothbrush": "牙刷",
+ "object.hair_brush": "发刷",
+ "object.vehicle": "车辆",
+ "object.squirrel": "松鼠",
+ "object.deer": "鹿",
+ "object.animal": "动物",
+ "object.bark": "树皮",
+ "object.fox": "狐狸",
+ "object.goat": "山羊",
+ "object.rabbit": "兔子",
+ "object.raccoon": "浣熊",
+ "object.robot_lawnmower": "自动割草机",
+ "object.waste_bin": "垃圾桶",
+ "object.on_demand": "手动",
+
+ "audio.crying": "哭泣",
+ "audio.laughter": "笑声",
+ "audio.scream": "尖叫",
+ "audio.speech": "谈话",
+ "audio.yell": "大喊",
+ "audio.fire_alarm": "火灾警报器",
+
+ "ui.time.ago": "{{timeAgo}} 前",
+ "ui.time.justNow": "刚才",
+ "ui.time.today": "今天",
+ "ui.time.yesterday": "昨天",
+ "ui.time.last7": "最后 7 天",
+ "ui.time.last14": "最后 14 天",
+ "ui.time.last30": "最后 30 天",
+ "ui.time.thisWeek": "本周",
+ "ui.time.lastWeek": "上个周",
+ "ui.time.thisMonth": "本月",
+ "ui.time.lastMonth": "上个月",
+
+ "ui.time.pm": "上午",
+ "ui.time.am": "下午",
+
+ "ui.time.yr": "{{time}}年",
+ "ui.time.year": "{{time}}年",
+ "ui.time.mo": "{{time}}月",
+ "ui.time.month": "{{time}}月",
+ "ui.time.d": "{{time}}天",
+ "ui.time.day": "{{time}}天",
+ "ui.time.h": "{{time}}小时",
+ "ui.time.hour": "{{time}}小时",
+ "ui.time.m": "{{time}}分钟",
+ "ui.time.minute": "{{time}}分钟",
+ "ui.time.s": "{{time}}秒",
+ "ui.time.second": "{{time}}秒",
+
+ "ui.time.formattedTimestamp": "%m月%-d日 %I:%M:%S %p",
+ "ui.time.formattedTimestamp.24hour": "%m月%-d日 %H:%M:%S",
+ "ui.time.formattedTimestampExcludeSeconds": "%m月%-d日 %I:%M %p",
+ "ui.time.formattedTimestampExcludeSeconds.24hour": "%m月%-d日 %H:%M",
+ "ui.time.formattedTimestampWithYear": "%Y年%m月%-d日 %I:%M:%S %p",
+ "ui.time.formattedTimestampWithYear.24hour": "%Y年%m月%-d日 %H:%M",
+
+ "ui.iconPicker.selectIcon": "选择图标",
+ "ui.iconPicker.search.placeholder": "搜索图标...",
+
+ "ui.dialog.restart.title": "你确定要重启 Frigate?",
+ "ui.dialog.restart.button": "重启",
+ "ui.dialog.restart.restarting.title": "Frigate 正在重启",
+ "ui.dialog.restart.restarting.content": "该页面将会在 {{countdown}} 秒后自动刷新。",
+ "ui.dialog.restart.restarting.button": "强制刷新",
+
+ "ui.dialog.export.time.fromTimeline": "从时间线选择",
+ "ui.dialog.export.time.lastHour_one": "最后1小时",
+ "ui.dialog.export.time.lastHour_other": "最后 {{count}} 小时",
+ "ui.dialog.export.time.custom": "自定义",
+ "ui.dialog.export.name.placeholder": "导出项目的名字",
+ "ui.dialog.export.select": "选择",
+ "ui.dialog.export.export": "导出",
+ "ui.dialog.export.toast.success": "导出成功。进入 /exports 目录查看文件。",
+ "ui.dialog.export.toast.error.failed": "导出失败:{{error}}",
+ "ui.dialog.export.toast.error.endTimeMustAfterStartTime": "结束时间必须在开始时间之后",
+ "ui.dialog.export.toast.error.noVaildTimeSelected": "未选择有效的时间范围",
+ "ui.dialog.export.fromTimeline.saveExport": "保存导出",
+ "ui.dialog.export.fromTimeline.previewExport": "预览导出",
+
+ "ui.dialog.streaming": "视频流",
+ "ui.dialog.streaming.restreaming.disabled": "重新流式传输未启用。",
+ "ui.dialog.streaming.restreaming.desc": "为此摄像头设置 go2rtc,以获取额外的实时预览选项和音频支持。",
+ "ui.dialog.streaming.restreaming.readTheDocumentation": "阅读文档(英文) ",
+
+ "ui.dialog.streaming.showStats": "显示视频流统计信息",
+ "ui.dialog.streaming.showStats.desc": "启用后将在摄像头画面上叠加显示视频流统计信息。",
+
+ "ui.dialog.streaming.debugView": "调试界面",
+
+
+ "ui.stats.ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%)",
+ "ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%)",
+ "ui.stats.healthy": "系统运行正常",
+
+ "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": "流数据信息通过ffprobe获取。",
+ "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": "探测",
+ "ui.menu.export": "导出",
+ "ui.menu.uiPlayground": "UI Playground",
+ "ui.menu.user.current": "当前用户:{{user}}",
+ "ui.menu.user.anonymous": "匿名",
+ "ui.menu.user.logout": "登出",
+
+ "ui.cameraGroup": "摄像头组",
+ "ui.cameraGroup.add": "添加摄像头组",
+ "ui.cameraGroup.edit": "编辑摄像头组",
+ "ui.cameraGroup.edit.desc": "编辑摄像头组",
+ "ui.cameraGroup.delete.confirm": "确认删除",
+ "ui.cameraGroup.delete.confirm.desc": "你确定要删除摄像头组 {{name}} 吗?",
+ "ui.cameraGroup.name": "名称",
+ "ui.cameraGroup.name.placeholder": "请输入名称",
+ "ui.cameraGroup.name.errorMessage.mustLeastCharacters": "摄像头组的名称必须至少有 2 个字符。",
+ "ui.cameraGroup.name.errorMessage.exists": "摄像头组名称已存在。",
+ "ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "摄像头组名称不能包含英文句号(.)。",
+ "ui.cameraGroup.name.errorMessage.invalid": "无效的摄像头组名称。",
+ "ui.cameraGroup.cameras": "摄像头",
+ "ui.cameraGroup.cameras.desc": "选择添加至该组的摄像头。",
+ "ui.cameraGroup.icon": "图标",
+ "ui.cameraGroup.toast.success": "摄像头组({{name}})保存成功。",
+ "ui.cameraGroup.toast.error": "保存设置失败: {{error}}",
+
+ "ui.eventView.alerts": "警告",
+ "ui.eventView.detections": "检测",
+ "ui.eventView.motion": "运动",
+ "ui.eventView.allCameras": "所有摄像头",
+ "ui.eventView.empty.alert": "还没有“警告”类回放",
+ "ui.eventView.empty.detection": "还没有“探测”类回放",
+
+ "ui.exploreView.trackedObjectDetails": "探测对象详情",
+ "ui.exploreView.type.details": "详情",
+ "ui.exploreView.type.snapshot": "快照",
+ "ui.exploreView.type.video": "视频",
+ "ui.exploreView.type.object_lifecycle": "对象生命周期",
+ "ui.exploreView.details.label": "标签",
+ "ui.exploreView.details.editSubLable": "编辑子标签",
+ "ui.exploreView.details.topScore": "最高得分",
+ "ui.exploreView.details.topScore.info": "最高分是跟踪对象的最高中位数得分,因此可能与搜索结果缩略图上显示的得分不同。",
+ "ui.exploreView.details.estimatedSpeed": "预计速度",
+ "ui.exploreView.details.camera": "摄像头",
+ "ui.exploreView.details.timestamp": "时间",
+ "ui.exploreView.details.button.findSimilar": "查找相似项",
+ "ui.exploreView.details.description": "描述",
+ "ui.exploreView.details.description.placeholder": "跟踪对象的描述",
+ "ui.exploreView.details.description.aiTips": "在跟踪对象的生命周期结束之前,Frigate 不会向您的生成式 AI 提供商请求描述。",
+ "ui.exploreView.details.button.regenerate": "重新生成",
+ "ui.exploreView.details.regenerateFromSnapshot": "从快照重新生成",
+ "ui.exploreView.details.regenerateFromThumbnails": "从缩略图重新生成",
+ "ui.exploreView.details.tips.descriptionSaved": "已保存描述",
+ "ui.exploreView.details.tips.saveDescriptionFailed": "更新描述失败",
+ "ui.exploreView.itemMenu.downloadVideo": "下载视频",
+ "ui.exploreView.itemMenu.downloadVideo.aria": "下载视频",
+ "ui.exploreView.itemMenu.downloadSnapshot": "下载快照",
+ "ui.exploreView.itemMenu.downloadSnapshot.aria": "下载快照",
+ "ui.exploreView.itemMenu.viewObjectLifecycle": "查看对象生命周期",
+ "ui.exploreView.itemMenu.viewObjectLifecycle.aria": "显示对象的生命周期",
+ "ui.exploreView.itemMenu.findSimilar": "查找相似项",
+ "ui.exploreView.itemMenu.findSimilar.aria": "查看相似的对象",
+ "ui.exploreView.itemMenu.submitToPlus": "提交至 Frigate+",
+ "ui.exploreView.itemMenu.submitToPlus.aria": "提交至 Frigate Plus",
+ "ui.exploreView.dialog.confirmDelete": "确认删除",
+ "ui.exploreView.dialog.confirmDelete.desc": "删除此跟踪对象将移除快照、所有已保存的嵌入数据以及任何关联的对象生命周期条目。但在历史视图中的录制视频不会 被删除。 你确定要继续删除吗?",
+
+ "ui.filter": "过滤器",
+ "ui.filter.allLabels": "所有标签",
+ "ui.filter.allLabels.short": "标签",
+ "ui.filter.countLabels": "{{count}} 个标签",
+ "ui.filter.allZones": "所有区域",
+ "ui.filter.allZones.short": "区域",
+ "ui.filter.allDates": "所有日期",
+ "ui.filter.allDates.short": "日期",
+ "ui.filter.more": "更多筛选项",
+ "ui.filter.timeRange": "时间范围",
+ "ui.filter.zones": "区域",
+ "ui.filter.subLabels": "子标签",
+ "ui.filter.allSubLabels": "所有子标签",
+ "ui.filter.score": "分值",
+ "ui.filter.features": "特性",
+ "ui.filter.features.hasSnapshot": "包含快照",
+ "ui.filter.features.hasVideoClip": "包含视频片段",
+ "ui.filter.features.submittedToFrigatePlus": "提交至 Frigate+",
+ "ui.filter.features.submittedToFrigatePlus.tips": "你必须要先筛选具有快照的探测对象。 没有快照的跟踪对象无法提交至 Frigate+.",
+ "ui.filter.sort": "排序",
+ "ui.filter.sort.dateAsc": "日期 (正序)",
+ "ui.filter.sort.dateDesc": "日期 (倒序)",
+ "ui.filter.sort.scoreAsc": "对象分值 (正序)",
+ "ui.filter.sort.scoreDesc": "对象分值 (倒序)",
+ "ui.filter.sort.relevance": "关联性",
+ "ui.filter.allCameras": "所有摄像头",
+ "ui.filter.allCameras.short": "摄像头",
+
+ "ui.reviewFilter.showReviewed": "显示已查看的项目",
+
+ "ui.apply": "应用",
+ "ui.reset": "重置",
+ "ui.enabled": "启用",
+ "ui.save": "保存",
+ "ui.saving": "保存中……",
+ "ui.cancel": "取消",
+ "ui.close": "关闭",
+ "ui.copy": "复制",
+ "ui.back": "返回",
+ "ui.history": "历史",
+ "ui.fullscreen": "全屏",
+ "ui.exitFullscreen": "退出全屏",
+ "ui.pictureInPicture": "画中画",
+ "ui.on": "开",
+ "ui.off": "关",
+ "ui.edit": "编辑",
+ "ui.delete": "删除",
+ "ui.yes": "是",
+ "ui.no": "否",
+ "ui.download": "下载",
+
+ "ui.live.documentTitle": "实时监控 - Frigate",
+ "ui.live.documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate",
+ "ui.live.twoWayTalk.enable": "开启双向对话",
+ "ui.live.twoWayTalk.disable": "关闭双向对话",
+ "ui.live.cameraAudio.enable": "开启摄像头音频",
+ "ui.live.cameraAudio.disable": "关闭摄像头音频",
+ "ui.live.ptz.move.left.label": "PTZ摄像头向左移动",
+ "ui.live.ptz.move.up.label": "PTZ摄像头向上移动",
+ "ui.live.ptz.move.down.label": "PTZ摄像头向下移动",
+ "ui.live.ptz.move.right.label": "PTZ摄像头向右移动",
+ "ui.live.ptz.zoom.in.label": "PTZ摄像头放大",
+ "ui.live.ptz.zoom.out.label": "PTZ摄像头缩小",
+ "ui.live.ptz.frame.center.label": "点击将PTZ摄像头画面居中",
+
+ "ui.live.detect.enable": "启用检测",
+ "ui.live.detect.disable": "关闭检测",
+ "ui.live.recording.enable": "启用录制",
+ "ui.live.recording.disable": "关闭录制",
+ "ui.live.snapshots.enable": "启用快照",
+ "ui.live.snapshots.disable": "关闭快照",
+ "ui.live.audioDetect.enable": "启用音频检测",
+ "ui.live.audioDetect.disable": "关闭音频检测",
+ "ui.live.autotracking.enable": "启用自动追踪",
+ "ui.live.autotracking.disable": "关闭自动追踪",
+ "ui.live.manualRecording.start": "开始手动按需录制",
+ "ui.live.manualRecording.started": "已启用手动按需录制",
+ "ui.live.manualRecording.failedToStart": "启动手动录制失败",
+ "ui.live.manualRecording.recordDisabledTips": "由于此摄像头的配置中禁用了录制或对其进行了限制,将只会保存快照。",
+ "ui.live.manualRecording.end": "停止手动按需录制",
+ "ui.live.manualRecording.ended": "已完成手动按需录制",
+ "ui.live.manualRecording.failedToEnd": "停止手动录制失败",
+
+
+ "ui.review.timeline": "时间线",
+ "ui.review.events": "事件",
+ "ui.review.events.noFoundForTimePeriod": "未找到该时间段的事件。",
+ "ui.review.documentTitle": "预览 - Frigate",
+ "ui.review.recordings.documentTitle": "回放 - Frigate",
+
+ "ui.player.noRecordingsFoundForThisTime": "找不到此次录制",
+ "ui.player.noPreviewFound": "没有找到预览",
+ "ui.player.noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览",
+
+ "ui.calendarFilter.last24Hours": "过去24小时",
+
+ "ui.searchView.noTrackedObjects": "找不到探测的对象",
+ "ui.searchView.settings": "设置",
+ "ui.searchView.settings.defaultView": "默认视图",
+ "ui.searchView.settings.defaultView.desc": "当未选择任何过滤器时,显示每个标签最近跟踪对象的摘要,或显示未过滤的网格。",
+ "ui.searchView.settings.defaultView.summary": "摘要",
+ "ui.searchView.settings.defaultView.unfilteredGrid": "未过滤网格",
+ "ui.searchView.settings.gridColumns": "网格列数",
+ "ui.searchView.settings.gridColumns.desc": "选择网格视图中的列数。",
+ "ui.searchView.settings.searchSource": "搜索源",
+ "ui.searchView.settings.searchSource.desc": "选择是搜索缩略图还是跟踪对象的描述。",
+
+ "ui.settingView.menu.uiSettings": "界面设置",
+ "ui.settingView.menu.exploreSettings": "搜索设置",
+ "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.cameraGroupStreaming": "摄像头组视频流设置",
+ "ui.settingView.generalSettings.cameraGroupStreaming.desc": "每个摄像头组的视频流设置将保存在浏览器的本地存储中。",
+ "ui.settingView.generalSettings.cameraGroupStreaming.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.exploreSettings": "探测设置",
+ "ui.settingView.exploreSettings.semanticSearch": "语义搜索",
+ "ui.settingView.exploreSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
+ "ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
+ "ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
+ "ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。关闭该设置后不要忘记重启! ",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize": "模型大小",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.small": "小",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.large": "大",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "使用 小 模型。该模型将使用较少的内存,在CPU上也能较快的运行。质量较好。",
+ "ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "使用 大 模型。该模型采用了完整的Jina模型,并在适用的情况下使用GPU。",
+
+ "ui.settingView.cameraSettings": "摄像头设置",
+ "ui.settingView.cameraSettings.review": "预览",
+ "ui.settingView.cameraSettings.review.desc": "启用/禁用摄像头的警报和检测。禁用后,不会生成新的预览项。",
+ "ui.settingView.cameraSettings.review.alerts": "警告",
+ "ui.settingView.cameraSettings.review.detections": "检测",
+ "ui.settingView.cameraSettings.reviewClassification": "预览分级",
+ "ui.settingView.cameraSettings.reviewClassification.desc": "Frigate 将回放项目分为“警告”和“检测”。默认情况下,所有的 人 、汽车 的对象都视为警告。你可以通过修改配置文件配置区域来细分。",
+ "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.documentTitle": "编辑区域 - Frigate",
+ "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": "识别指定对象前该对象必须在这个区域内出现了多少帧。默认值:3 ",
+ "ui.settingView.masksAndZonesSettings.zone.loiteringTime": "停留时间",
+ "ui.settingView.masksAndZonesSettings.zone.loiteringTime.desc": "设置对象必须在区域中活动的最小时间(单位为秒)。默认值:0 ",
+ "ui.settingView.masksAndZonesSettings.zone.objects": "对象",
+ "ui.settingView.masksAndZonesSettings.zone.objects.desc": "将在此区域应用的对象列表。",
+ "ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象",
+
+ "ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩",
+ "ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "编辑运动遮罩 - Frigate",
+ "ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
+ "ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)",
+ "ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩",
+ "ui.settingView.masksAndZonesSettings.motionMasks.edit": "编辑运动遮罩",
+ "ui.settingView.masksAndZonesSettings.motionMasks.context": "运动遮罩用于防止不需要的运动类型触发检测(例如:树枝、摄像头显示的时间等)。运动遮罩需要谨慎使用 ,过度的遮罩会导致追踪对象变得更加困难。",
+ "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.documentTitle": "编辑对象遮罩 - Frigate",
+ "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": "阈值决定像素亮度高于多少时会被认为是运动。默认值:30 ",
+ "ui.settingView.motionDetectionTuner.contourArea": "轮廓面积",
+ "ui.settingView.motionDetectionTuner.contourArea.desc": "轮廓面积决定哪些变化的像素组符合运动条件。默认值:10 ",
+ "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": "启用后,将会为每个对象标签分配不同的颜色 深蓝色细线代表该对象在当前时间点未被检测到 灰色细线代表检测到的物体静止不动 粗线表示该对象为自动跟踪的主体(在启动时) ",
+ "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": "运动区域框
将在当前检测到运动的区域内显示红色区域框。
",
+ "ui.settingView.debug.regions": "范围",
+ "ui.settingView.debug.regions.desc": "显示发送到运动检测器感兴趣范围的框。",
+ "ui.settingView.debug.regions.tips": "范围框
将在帧中发送到目标检测器的感兴趣范围上叠加绿色框。
",
+
+ "ui.settingView.users": "用户管理",
+ "ui.settingView.users.addUser": "添加用户",
+ "ui.settingView.users.updatePassword": "修改密码",
+ "ui.settingView.users.dialog.createUser": "创建用户",
+ "ui.settingView.users.dialog.createUser.user": "用户名",
+ "ui.settingView.users.dialog.createUser.password": "密码",
+ "ui.settingView.users.dialog.deleteUser": "删除该用户",
+ "ui.settingView.users.dialog.deleteUser.warn": "你确定要删除该用户吗?",
+ "ui.settingView.users.dialog.setPassword": "修改密码",
+
+ "ui.settingView.notification": "通知",
+ "ui.settingView.notification.notificationSettings": "通知设置",
+ "ui.settingView.notification.desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。",
+ "ui.settingView.notification.documentation": "阅读文档(英文)",
+ "ui.settingView.notification.email": "电子邮箱",
+ "ui.settingView.notification.email.placeholder": "例如:example@email.com",
+ "ui.settingView.notification.email.desc": "需要输入有效的电子邮件,在推送服务出现问题时,将使用此电子邮件进行通知。",
+ "ui.settingView.notification.cameras": "摄像头",
+ "ui.settingView.notification.cameras.noCameras": "没有可用的摄像头",
+ "ui.settingView.notification.cameras.desc": "选择要启用通知的摄像头。",
+ "ui.settingView.notification.deviceSpecific": "设备专用设置",
+ "ui.settingView.notification.registerDevice": "注册该设备",
+ "ui.settingView.notification.unregisterDevice": "取消注册该设备",
+
+
+ "ui.configEditorView.configEditor": "配置编辑器",
+ "ui.configEditorView.copyConfig": "复制配置",
+ "ui.configEditorView.saveAndRestart": "保存并重启",
+ "ui.configEditorView.saveOnly": "只保存",
+
+ "ui.exportView.documentTitle": "导出 - Frigate",
+ "ui.exportView.search": "搜索",
+ "ui.exportView.noExports": "没有找到导出的项目",
+ "ui.exportView.deleteExport": "删除导出的项目",
+ "ui.exportView.deleteExport.desc": "你确定要删除 {{exportName}} 吗?"
+
+}
\ No newline at end of file
diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx
index 1b20b26f6..86269ab03 100644
--- a/web/src/components/Statusbar.tsx
+++ b/web/src/components/Statusbar.tsx
@@ -5,6 +5,7 @@ import {
} from "@/context/statusbar-provider";
import useStats, { useAutoFrigateStats } from "@/hooks/use-stats";
import { useContext, useEffect, useMemo } from "react";
+import { Trans } from "react-i18next";
import { FaCheck } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io";
import { MdCircle } from "react-icons/md";
@@ -129,7 +130,7 @@ export default function Statusbar() {
{Object.entries(messages).length === 0 ? (
- System is healthy
+ ui.stats.healthy
) : (
Object.entries(messages).map(([key, messageArray]) => (
diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx
index e10e009fb..63f9d44c5 100644
--- a/web/src/components/card/ReviewCard.tsx
+++ b/web/src/components/card/ReviewCard.tsx
@@ -35,6 +35,7 @@ import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { buttonVariants } from "../ui/button";
+import { t } from "i18next";
type ReviewCardProps = {
event: ReviewSegment;
@@ -82,10 +83,9 @@ export default function ReviewCard({
)
.then((response) => {
if (response.status == 200) {
- toast.success(
- "Successfully started export. View the file in the /exports folder.",
- { position: "top-center" },
- );
+ toast.success(t("ui.dialog.export.toast.success"), {
+ position: "top-center",
+ });
}
})
.catch((error) => {
diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx
index ed98e86b4..bf2216eb2 100644
--- a/web/src/components/card/SearchThumbnail.tsx
+++ b/web/src/components/card/SearchThumbnail.tsx
@@ -8,11 +8,11 @@ import Chip from "@/components/indicators/Chip";
import useImageLoaded from "@/hooks/use-image-loaded";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
-import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { SearchResult } from "@/types/search";
import { cn } from "@/lib/utils";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import useContextMenu from "@/hooks/use-contextmenu";
+import { t } from "i18next";
type SearchThumbnailProps = {
searchResult: SearchResult;
@@ -113,7 +113,7 @@ export default function SearchThumbnail({
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
- .map((text) => capitalizeFirstLetter(text))
+ .map((text) => t("object." + text))
.sort()
.join(", ")
.replaceAll("-verified", "")}
diff --git a/web/src/components/card/SearchThumbnailFooter.tsx b/web/src/components/card/SearchThumbnailFooter.tsx
index 33db0c598..351d90fb3 100644
--- a/web/src/components/card/SearchThumbnailFooter.tsx
+++ b/web/src/components/card/SearchThumbnailFooter.tsx
@@ -6,6 +6,7 @@ import { SearchResult } from "@/types/search";
import ActivityIndicator from "../indicators/activity-indicator";
import SearchResultActions from "../menu/SearchResultActions";
import { cn } from "@/lib/utils";
+import { t } from "i18next";
type SearchThumbnailProps = {
searchResult: SearchResult;
@@ -29,7 +30,9 @@ export default function SearchThumbnailFooter({
// date
const formattedDate = useFormattedTimestamp(
searchResult.start_time,
- config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
+ config?.ui.time_format == "24hour"
+ ? t("ui.time.formattedTimestampExcludeSeconds.24hour")
+ : t("ui.time.formattedTimestampExcludeSeconds"),
config?.ui.timezone,
);
diff --git a/web/src/components/dynamic/TimeAgo.tsx b/web/src/components/dynamic/TimeAgo.tsx
index 13892180e..9945377b3 100644
--- a/web/src/components/dynamic/TimeAgo.tsx
+++ b/web/src/components/dynamic/TimeAgo.tsx
@@ -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++) {
@@ -64,11 +65,22 @@ const timeAgo = ({
if (monthDiff > 0) {
const unitAmount = monthDiff;
- return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? "" : "s"} ago`;
+ return t("ui.time.ago", {
+ timeAgo: t(
+ `ui.time.${dense ? timeUnits[i].unit : timeUnits[i].full}`,
+ {
+ time: unitAmount,
+ },
+ ),
+ });
}
} else if (elapsed >= timeUnits[i].value) {
const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
- return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? "" : "s"} ago`;
+ return t("ui.time.ago", {
+ timeAgo: t(`ui.time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, {
+ time: unitAmount,
+ }),
+ });
}
}
return "Invalid Time";
diff --git a/web/src/components/filter/CalendarFilterButton.tsx b/web/src/components/filter/CalendarFilterButton.tsx
index afa70b4e5..c8d213493 100644
--- a/web/src/components/filter/CalendarFilterButton.tsx
+++ b/web/src/components/filter/CalendarFilterButton.tsx
@@ -14,6 +14,8 @@ 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";
+import { Trans } from "react-i18next";
type CalendarFilterButtonProps = {
reviewSummary?: ReviewSummary;
@@ -46,7 +48,7 @@ export default function CalendarFilterButton({
- {day == undefined ? "Last 24 Hours" : selectedDate}
+ {day == undefined ? t("ui.calendarFilter.last24Hours") : selectedDate}
);
@@ -66,7 +68,7 @@ export default function CalendarFilterButton({
updateSelectedDay(undefined);
}}
>
- Reset
+ ui.reset
>
diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx
index 8aec2a117..69199e84a 100644
--- a/web/src/components/filter/CameraGroupSelector.tsx
+++ b/web/src/components/filter/CameraGroupSelector.tsx
@@ -70,6 +70,8 @@ import {
MobilePageHeader,
MobilePageTitle,
} from "../mobile/MobilePage";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
@@ -160,8 +162,8 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
-
- All Cameras
+
+ ui.menu.live.allCameras
@@ -349,9 +351,11 @@ function NewGroupDialog({
className={cn(isDesktop && "mt-5", "justify-center")}
onClose={() => setOpen(false)}
>
- Camera Groups
+
+ ui.cameraGroup
+
- Edit camera groups
+ ui.cameraGroup.edit
- {editState == "add" ? "Add" : "Edit"} Camera Group
+ {editState == "add" ? (
+ ui.cameraGroup.add
+ ) : (
+ ui.cameraGroup.edit
+ )}
Edit camera groups
@@ -472,8 +480,12 @@ export function EditGroupDialog({
>
setOpen(false)}>
- Edit Camera Group
- Edit camera group
+
+ ui.cameraGroup.edit
+
+
+ ui.cameraGroup.edit.desc
+
- Confirm Delete
+
+ ui.cameraGroup.delete.confirm
+
- Are you sure you want to delete the camera group{" "}
- {group[0]} ?
+
+ ui.cameraGroup.delete.confirm.desc
+
- Cancel
+
+ ui.cancel
+
- Delete
+ ui.delete
@@ -576,7 +593,9 @@ export function CameraGroupRow({
onClick={onEditGroup}
/>
- Edit
+
+ ui.edit
+
@@ -587,7 +606,9 @@ export function CameraGroupRow({
onClick={() => setDeleteDialogOpen(true)}
/>
- Delete
+
+ ui.delete
+
)}
@@ -632,7 +653,7 @@ export function CameraGroupEdit({
name: z
.string()
.min(2, {
- message: "Camera group name must be at least 2 characters.",
+ message: t("ui.cameraGroup.name.errorMessage.mustLeastCharacters"),
})
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
.refine(
@@ -643,7 +664,7 @@ export function CameraGroupEdit({
);
},
{
- message: "Camera group name already exists.",
+ message: t("ui.cameraGroup.name.errorMessage.exists"),
},
)
.refine(
@@ -651,11 +672,11 @@ export function CameraGroupEdit({
return !value.includes(".");
},
{
- message: "Camera group name must not contain a period.",
+ message: t("ui.cameraGroup.name.errorMessage.nameMustNotPeriod"),
},
)
.refine((value: string) => value.toLowerCase() !== "default", {
- message: "Invalid camera group name.",
+ message: t("ui.cameraGroup.name.errorMessage.invalid"),
}),
cameras: z.array(z.string()),
@@ -710,23 +731,31 @@ export function CameraGroupEdit({
)
.then(async (res) => {
if (res.status === 200) {
- toast.success(`Camera group (${values.name}) has been saved.`, {
- position: "top-center",
- });
+ toast.success(
+ t("ui.cameraGroup.toast.success", { name: values.name }),
+ {
+ position: "top-center",
+ },
+ );
updateConfig();
if (onSave) {
onSave();
}
setAllGroupsStreamingSettings(updatedSettings);
} else {
- toast.error(`Failed to save config changes: ${res.statusText}`, {
- position: "top-center",
- });
+ toast.error(
+ t("ui.cameraGroup.toast.error", { error: res.statusText }),
+ {
+ position: "top-center",
+ },
+ );
}
})
.catch((error) => {
toast.error(
- `Failed to save config changes: ${error.response.data.message}`,
+ t("ui.cameraGroup.toast.error", {
+ error: error.response.data.message,
+ }),
{ position: "top-center" },
);
})
@@ -767,11 +796,13 @@ export function CameraGroupEdit({
name="name"
render={({ field }) => (
- Name
+
+ ui.cameraGroup.name
+
@@ -787,9 +818,11 @@ export function CameraGroupEdit({
name="cameras"
render={({ field }) => (
- Cameras
+
+ ui.cameraGroup.cameras
+
- Select cameras for this group.
+ ui.cameraGroup.cameras.desc
{[
@@ -874,7 +907,9 @@ export function CameraGroupEdit({
name="icon"
render={({ field }) => (
- Icon
+
+ ui.cameraGroup.icon
+
- Cancel
+ ui.cancel
- Saving...
+
+ ui.saving
+
) : (
- "Save"
+ ui.save
)}
diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx
index c584dc09d..063848c6d 100644
--- a/web/src/components/filter/CamerasFilterButton.tsx
+++ b/web/src/components/filter/CamerasFilterButton.tsx
@@ -12,6 +12,8 @@ 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";
+import { Trans } from "react-i18next";
type CameraFilterButtonProps = {
allCameras: string[];
@@ -38,7 +40,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" : ""}`;
@@ -138,7 +140,7 @@ export function CamerasFilterContent({
{isMobile && (
<>
- Cameras
+ ui.filter.allCameras.short
>
@@ -146,7 +148,7 @@ export function CamerasFilterContent({
{
if (isChecked) {
setCurrentCameras(undefined);
@@ -211,7 +213,7 @@ export function CamerasFilterContent({
setOpen(false);
}}
>
- Apply
+ ui.apply
- Reset
+ ui.reset
>
diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx
index dedcb06fc..7d2bf1525 100644
--- a/web/src/components/filter/ReviewFilterGroup.tsx
+++ b/web/src/components/filter/ReviewFilterGroup.tsx
@@ -23,6 +23,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",
@@ -275,7 +277,7 @@ function ShowReviewFilter({
}
/>
- Show Reviewed
+ ui.reviewFilter.showReviewed
@@ -361,7 +363,7 @@ function GeneralFilterButton({
: "text-primary"
}`}
>
- Filter
+ ui.filter
);
@@ -442,7 +444,7 @@ export function GeneralFilterContent({
{currentSeverity && (
- All Labels
+ ui.filter.allLabels
- All Zones
+ ui.filter.allZones
- Apply
+ ui.apply
- Reset
+ ui.reset
>
diff --git a/web/src/components/filter/SearchFilterGroup.tsx b/web/src/components/filter/SearchFilterGroup.tsx
index 740a3bce7..793f64cc4 100644
--- a/web/src/components/filter/SearchFilterGroup.tsx
+++ b/web/src/components/filter/SearchFilterGroup.tsx
@@ -24,6 +24,8 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
type SearchFilterGroupProps = {
className: string;
@@ -195,7 +197,9 @@ export default function SearchFilterGroup({
to: new Date(filter.before * 1000),
}
}
- defaultText={isMobile ? "Dates" : "All Dates"}
+ defaultText={
+ isMobile ? t("ui.filter.allDates.short") : t("ui.filter.allDates")
+ }
updateSelectedRange={onUpdateSelectedRange}
/>
)}
@@ -236,18 +240,18 @@ function GeneralFilterButton({
const buttonText = useMemo(() => {
if (isMobile) {
- return "Labels";
+ return t("ui.filter.allLabels.short");
}
if (!selectedLabels || selectedLabels.length == 0) {
- return "All Labels";
+ return t("ui.filter.allLabels");
}
if (selectedLabels.length == 1) {
- return selectedLabels[0];
+ return t("object." + selectedLabels[0]);
}
- return `${selectedLabels.length} Labels`;
+ return t("ui.filter.countLabels", { count: selectedLabels.length });
}, [selectedLabels]);
// ui
@@ -331,7 +335,7 @@ export function GeneralFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
- All Labels
+ ui.filter.allLabels
(
{
if (isChecked) {
@@ -383,7 +387,7 @@ export function GeneralFilterContent({
onClose();
}}
>
- Apply
+ ui.apply
- Reset
+ ui.reset
>
@@ -441,7 +445,7 @@ function SortTypeButton({
- Sort
+ ui.filter.sort
);
@@ -497,15 +501,14 @@ export function SortTypeContent({
onClose,
}: SortTypeContentProps) {
const sortLabels = {
- date_asc: "Date (Ascending)",
- date_desc: "Date (Descending)",
- score_asc: "Object Score (Ascending)",
- score_desc: "Object Score (Descending)",
+ date_asc: t("ui.filter.sort.dateAsc"),
+ date_desc: t("ui.filter.sort.dateDesc"),
+ score_asc: t("ui.filter.sort.scoreAsc"),
+ score_desc: t("ui.filter.sort.scoreDesc"),
speed_asc: "Estimated Speed (Ascending)",
speed_desc: "Estimated Speed (Descending)",
- relevance: "Relevance",
+ relevance: t("ui.filter.sort.relevance"),
};
-
return (
<>
@@ -558,7 +561,7 @@ export function SortTypeContent({
onClose();
}}
>
- Apply
+ ui.apply
- Reset
+ ui.reset
>
diff --git a/web/src/components/graph/CameraGraph.tsx b/web/src/components/graph/CameraGraph.tsx
index ab5d6e03f..a7159735f 100644
--- a/web/src/components/graph/CameraGraph.tsx
+++ b/web/src/components/graph/CameraGraph.tsx
@@ -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,9 @@ export function CameraLineGraph({
className="size-2"
style={{ color: GRAPH_COLORS[labelIdx] }}
/>
- {label}
+
+ {t("ui.system.cameras.label." + label)}
+
{lastValues[labelIdx]}
{unit}
diff --git a/web/src/components/graph/CombinedStorageGraph.tsx b/web/src/components/graph/CombinedStorageGraph.tsx
index 2a52d82b6..1eadc8ac2 100644
--- a/web/src/components/graph/CombinedStorageGraph.tsx
+++ b/web/src/components/graph/CombinedStorageGraph.tsx
@@ -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,20 @@ export function CombinedStorageGraph({
- Camera
- Storage Used
- Percentage of Total Used
- Bandwidth
+
+ ui.system.storage.cameraStorage.camera
+
+
+ ui.system.storage.cameraStorage.storageUsed
+
+
+
+ ui.system.storage.cameraStorage.percentageOfTotalUsed
+
+
+
+ ui.system.storage.cameraStorage.bandwidth
+
@@ -191,7 +203,9 @@ export function CombinedStorageGraph({
className="size-3 rounded-md"
style={{ backgroundColor: item.color }}
>
- {item.name.replaceAll("_", " ")}
+ {item.name === "Unused"
+ ? t("ui.system.storage.cameraStorage.unused")
+ : item.name.replaceAll("_", " ")}
{item.name === "Unused" && (
@@ -207,10 +221,9 @@ export function CombinedStorageGraph({
- 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.storage.cameraStorage.unused.tips
+
diff --git a/web/src/components/icons/IconPicker.tsx b/web/src/components/icons/IconPicker.tsx
index d58b57ead..97a8e10bf 100644
--- a/web/src/components/icons/IconPicker.tsx
+++ b/web/src/components/icons/IconPicker.tsx
@@ -11,6 +11,8 @@ import { IoClose } from "react-icons/io5";
import Heading from "../ui/heading";
import { cn } from "@/lib/utils";
import { Button } from "../ui/button";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
export type IconName = keyof typeof LuIcons;
@@ -70,7 +72,7 @@ export default function IconPicker({
className="mt-2 w-full text-muted-foreground"
aria-label="Select an icon"
>
- Select an icon
+ ui.iconPicker.selectIcon
) : (
@@ -101,7 +103,9 @@ export default function IconPicker({
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
>
-
Select an icon
+
+ ui.iconPicker.selectIcon
+
setSearchTerm(e.target.value)}
diff --git a/web/src/components/menu/AccountSettings.tsx b/web/src/components/menu/AccountSettings.tsx
index 0bc968061..c50ae96d4 100644
--- a/web/src/components/menu/AccountSettings.tsx
+++ b/web/src/components/menu/AccountSettings.tsx
@@ -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,9 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
>
- Current User: {profile?.username || "anonymous"}
+ {t("ui.menu.user.current", {
+ user: profile?.username || t("ui.menu.user.anonymous"),
+ })}
- Logout
+
+ ui.menu.user.logout
+
diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx
index 7473d26d7..e288b16fc 100644
--- a/web/src/components/menu/GeneralSettings.tsx
+++ b/web/src/components/menu/GeneralSettings.tsx
@@ -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,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
- Settings
+
+ ui.settings
+
@@ -143,7 +150,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
>
)}
- System
+
+ ui.system
+
@@ -156,7 +165,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="System metrics"
>
- System metrics
+
+ ui.systemMetrics
+
@@ -169,12 +180,14 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="System logs"
>
- System logs
+
+ ui.systemLogs
+
- Configuration
+ ui.configuration
@@ -188,7 +201,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="Settings"
>
- Settings
+
+ ui.settings
+
@@ -201,11 +216,96 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="Configuration editor"
>
- Configuration editor
+
+ ui.configurationEditor
+
+
+
+
+
+ ui.languages
+
+
+
+
+
+ setLanguage("en")}
+ >
+ {language === "en" ? (
+ <>
+
+ ui.language.en
+ >
+ ) : (
+
+ ui.language.en
+
+ )}
+
+ setLanguage("zh-CN")}
+ >
+ {language === "zh-CN" ? (
+ <>
+
+ ui.language.zhCN
+ >
+ ) : (
+
+ ui.language.zhCN
+
+ )}
+
+ setLanguage(systemLanguage)}
+ >
+ {language === systemLanguage ? (
+ <>
+
+ ui.withSystem
+ >
+ ) : (
+
+ ui.withSystem
+
+ )}
+
+
+
+
- Appearance
+ ui.appearance
@@ -217,7 +317,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
}
>
- Dark Mode
+
+ ui.darkMode
+
- Light
+ ui.darkMode.light
>
) : (
- Light
+
+ ui.darkMode.light
+
)}
- Dark
+ ui.darkMode.dark
>
) : (
- Dark
+
+ ui.darkMode.dark
+
)}
- System
+ ui.withSystem
>
) : (
- System
+
+ ui.withSystem
+
)}
@@ -292,7 +400,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
}
>
- Theme
+
+ ui.theme
+
- {friendlyColorSchemeName(scheme)}
+ {friendlyColorSchemeName(scheme)}
>
) : (
- {friendlyColorSchemeName(scheme)}
+ {friendlyColorSchemeName(scheme)}
)}
@@ -329,7 +439,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
- Help
+ ui.help
@@ -337,10 +447,12 @@ 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")}
>
- Documentation
+
+ ui.documentation
+
setRestartDialogOpen(true)}
>
- Restart Frigate
+
+ ui.restart
+
diff --git a/web/src/components/menu/LiveContextMenu.tsx b/web/src/components/menu/LiveContextMenu.tsx
index 969e647a0..7f75deb24 100644
--- a/web/src/components/menu/LiveContextMenu.tsx
+++ b/web/src/components/menu/LiveContextMenu.tsx
@@ -200,7 +200,7 @@ export default function LiveContextMenu({
// notifications
const notificationsEnabledInConfig =
- config?.cameras[camera].notifications.enabled_in_config;
+ config?.cameras[camera]?.notifications?.enabled_in_config;
const { payload: notificationState, send: sendNotification } =
useNotifications(camera);
diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx
index fee12a50f..37e639974 100644
--- a/web/src/components/menu/SearchResultActions.tsx
+++ b/web/src/components/menu/SearchResultActions.tsx
@@ -38,6 +38,8 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import useSWR from "swr";
+import { t } from "i18next";
+import { Trans } from "react-i18next";
type SearchResultActionsProps = {
searchResult: SearchResult;
@@ -85,45 +87,55 @@ export default function SearchResultActions({
const menuItems = (
<>
{searchResult.has_clip && (
-
+
- Download video
+
+ ui.exploreView.itemMenu.downloadVideo
+
)}
{searchResult.has_snapshot && (
-
+
- Download snapshot
+
+ ui.exploreView.itemMenu.downloadSnapshot
+
)}
{searchResult.data.type == "object" && (
- View object lifecycle
+
+ ui.exploreView.itemMenu.viewObjectLifecycle
+
)}
{config?.semantic_search?.enabled && isContextMenu && (
- Find similar
+
+ ui.exploreView.itemMenu.findSimilar
+
)}
{isMobileOnly &&
@@ -132,9 +144,14 @@ export default function SearchResultActions({
searchResult.end_time &&
searchResult.data.type == "object" &&
!searchResult.plus_id && (
-
+
- Submit to Frigate+
+
+ ui.exploreView.itemMenu.submitToPlus
+
)}
setDeleteDialogOpen(true)}
>
- Delete
+
+ ui.delete
+
>
);
@@ -155,24 +174,22 @@ export default function SearchResultActions({
>
- Confirm Delete
+
+ ui.exploreView.dialog.confirmDelete
+
- Deleting this tracked object removes the snapshot, any saved
- embeddings, and any associated object lifecycle entries. Recorded
- footage of this tracked object in History view will NOT be
- deleted.
-
-
- Are you sure you want to proceed?
+ ui.exploreView.dialog.confirmDelete.desc
- Cancel
+
+ ui.cancel
+
- Delete
+ ui.delete
diff --git a/web/src/components/navigation/NavItem.tsx b/web/src/components/navigation/NavItem.tsx
index 98b54b3a7..dd0a494f2 100644
--- a/web/src/components/navigation/NavItem.tsx
+++ b/web/src/components/navigation/NavItem.tsx
@@ -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,9 @@ export default function NavItem({
{content}
- {item.title}
+
+ {item.title}
+
diff --git a/web/src/components/overlay/CameraInfoDialog.tsx b/web/src/components/overlay/CameraInfoDialog.tsx
index c0136a410..c38e6f263 100644
--- a/web/src/components/overlay/CameraInfoDialog.tsx
+++ b/web/src/components/overlay/CameraInfoDialog.tsx
@@ -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,13 @@ export default function CameraInfoDialog({
- {camera.name.replaceAll("_", " ")} Camera Probe Info
+ {t("ui.system.cameras.info.cameraProbeInfo", {
+ camera: camera.name.replaceAll("_", " "),
+ })}
- Stream data is obtained with ffprobe.
+ ui.system.cameras.info.streamDataFromFFPROBE
@@ -85,7 +89,7 @@ export default function CameraInfoDialog({
{ffprobeInfo.map((stream, idx) => (
- Stream {idx + 1}
+ {t("ui.system.cameras.info.stream", { idx: idx + 1 })}
{stream.return_code == 0 ? (
@@ -93,10 +97,12 @@ export default function CameraInfoDialog({
{codec.width ? (
-
Video:
+
+ ui.system.cameras.info.video
+
- Codec:
+
ui.system.cameras.info.codec
{" "}
{codec.codec_long_name}
@@ -105,7 +111,9 @@ export default function CameraInfoDialog({
{codec.width && codec.height ? (
<>
- Resolution:{" "}
+
+ ui.system.cameras.info.resolution
+ {" "}
{" "}
{codec.width}x{codec.height} (
@@ -119,7 +127,9 @@ export default function CameraInfoDialog({
>
) : (
- Resolution:{" "}
+
+ ui.system.cameras.info.resolution
+ {" "}
Unknown
@@ -127,10 +137,10 @@ export default function CameraInfoDialog({
)}
- FPS:{" "}
+ ui.system.cameras.info.fps {" "}
{codec.avg_frame_rate == "0/0"
- ? "Unknown"
+ ? t("ui.system.cameras.info.unknown")
: codec.avg_frame_rate}
@@ -140,7 +150,7 @@ export default function CameraInfoDialog({
Audio:
- Codec:{" "}
+ ui.system.cameras.info.codec {" "}
{codec.codec_long_name}
@@ -152,7 +162,11 @@ export default function CameraInfoDialog({
) : (
-
Error: {stream.stderr}
+
+ {t("ui.system.cameras.info.error", {
+ error: stream.stderr,
+ })}
+
)}
@@ -161,7 +175,9 @@ export default function CameraInfoDialog({
) : (
-
Fetching Camera Data
+
+ ui.system.cameras.info.fetching
+
)}
@@ -172,7 +188,7 @@ export default function CameraInfoDialog({
aria-label="Copy"
onClick={() => onCopyFfprobe()}
>
- Copy
+
ui.copy
diff --git a/web/src/components/overlay/CreateUserDialog.tsx b/web/src/components/overlay/CreateUserDialog.tsx
index 7d44159dd..7c6649987 100644
--- a/web/src/components/overlay/CreateUserDialog.tsx
+++ b/web/src/components/overlay/CreateUserDialog.tsx
@@ -20,6 +20,7 @@ import {
DialogHeader,
DialogTitle,
} from "../ui/dialog";
+import { Trans } from "react-i18next";
type CreateUserOverlayProps = {
show: boolean;
@@ -63,7 +64,9 @@ export default function CreateUserDialog({
- Create User
+
+ ui.settingView.users.dialog.createUser
+
diff --git a/web/src/components/overlay/DeleteUserDialog.tsx b/web/src/components/overlay/DeleteUserDialog.tsx
index 8638b9145..4e716e786 100644
--- a/web/src/components/overlay/DeleteUserDialog.tsx
+++ b/web/src/components/overlay/DeleteUserDialog.tsx
@@ -1,3 +1,4 @@
+import { Trans } from "react-i18next";
import { Button } from "../ui/button";
import {
Dialog,
@@ -21,9 +22,13 @@ export default function DeleteUserDialog({
- Delete User
+
+ ui.settingView.users.dialog.deleteUser
+
- Are you sure?
+
+ ui.settingView.users.dialog.deleteUser.warn
+
- Delete
+ ui.delete
diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx
index 00966e06a..75d01266f 100644
--- a/web/src/components/overlay/ExportDialog.tsx
+++ b/web/src/components/overlay/ExportDialog.tsx
@@ -30,6 +30,8 @@ import { getUTCOffset } from "@/utils/dateUtil";
import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
const EXPORT_OPTIONS = [
"1",
@@ -68,12 +70,14 @@ export default function ExportDialog({
const onStartExport = useCallback(() => {
if (!range) {
- toast.error("No valid time range selected", { position: "top-center" });
+ toast.error(t("ui.dialog.export.toast.error.noVaildTimeSelected"), {
+ position: "top-center",
+ });
return;
}
if (range.before < range.after) {
- toast.error("End time must be after start time", {
+ toast.error(t("ui.dialog.export.toast.error.endTimeMustAfterStartTime"), {
position: "top-center",
});
return;
@@ -89,10 +93,9 @@ export default function ExportDialog({
)
.then((response) => {
if (response.status == 200) {
- toast.success(
- "Successfully started export. View the file in the /exports folder.",
- { position: "top-center" },
- );
+ toast.success(t("ui.dialog.export.toast.success"), {
+ position: "top-center",
+ });
setName("");
setRange(undefined);
setMode("none");
@@ -100,14 +103,18 @@ export default function ExportDialog({
})
.catch((error) => {
if (error.response?.data?.message) {
+ // api error message need to be translated
toast.error(
- `Failed to start export: ${error.response.data.message}`,
+ `${t("ui.dialog.export.toast.error.failed", { error: error.response.data.message })}`,
{ position: "top-center" },
);
} else {
- toast.error(`Failed to start export: ${error.message}`, {
- position: "top-center",
- });
+ toast.error(
+ `${t("ui.dialog.export.toast.error.failed", { error: error.message })}`,
+ {
+ position: "top-center",
+ },
+ );
}
});
}, [camera, name, range, setRange, setName, setMode]);
@@ -163,7 +170,11 @@ export default function ExportDialog({
}}
>
- {isDesktop && Export
}
+ {isDesktop && (
+
+ ui.menu.export
+
+ )}
- Export
+
+ ui.menu.export
+
>
@@ -283,9 +296,11 @@ export function ExportContent({
{isNaN(parseInt(opt))
? opt == "timeline"
- ? "Select from Timeline"
- : `${opt}`
- : `Last ${opt > "1" ? `${opt} Hours` : "Hour"}`}
+ ? t("ui.dialog.export.time.fromTimeline")
+ : t("ui.dialog.export.time." + opt)
+ : t("ui.dialog.export.time.lastHour", {
+ count: parseInt(opt),
+ })}
);
@@ -301,7 +316,7 @@ export function ExportContent({
setName(e.target.value)}
/>
@@ -313,7 +328,7 @@ export function ExportContent({
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
onClick={onCancel}
>
- Cancel
+
ui.cancel
- {selectedOption == "timeline" ? "Select" : "Export"}
+ {selectedOption == "timeline"
+ ? t("ui.dialog.export.select")
+ : t("ui.dialog.export.export")}
@@ -391,14 +408,14 @@ function CustomTimeSelector({
const formattedStart = useFormattedTimestamp(
startTime,
config?.ui.time_format == "24hour"
- ? "%b %-d, %H:%M:%S"
- : "%b %-d, %I:%M:%S %p",
+ ? t("ui.time.formattedTimestamp.24hour")
+ : t("ui.time.formattedTimestamp"),
);
const formattedEnd = useFormattedTimestamp(
endTime,
config?.ui.time_format == "24hour"
- ? "%b %-d, %H:%M:%S"
- : "%b %-d, %I:%M:%S %p",
+ ? t("ui.time.formattedTimestamp.24hour")
+ : t("ui.time.formattedTimestamp"),
);
const startClock = useMemo(() => {
@@ -585,9 +602,11 @@ export function ExportPreviewDialog({
)}
>
- Preview Export
+
+ ui.dialog.export.fromTimeline.previewExport
+
- Preview Export
+ ui.dialog.export.fromTimeline.previewExport
diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx
index 0a316acc7..f63da9036 100644
--- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx
+++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx
@@ -19,6 +19,8 @@ import { toast } from "sonner";
import axios from "axios";
import SaveExportOverlay from "./SaveExportOverlay";
import { isIOS, isMobile } from "react-device-detect";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
@@ -96,10 +98,9 @@ export default function MobileReviewSettingsDrawer({
)
.then((response) => {
if (response.status == 200) {
- toast.success(
- "Successfully started export. View the file in the /exports folder.",
- { position: "top-center" },
- );
+ toast.success(t("ui.dialog.export.toast.success"), {
+ position: "top-center",
+ });
setName("");
setRange(undefined);
setMode("none");
@@ -246,7 +247,7 @@ export default function MobileReviewSettingsDrawer({
});
}}
>
- Reset
+
ui.reset
diff --git a/web/src/components/overlay/SaveExportOverlay.tsx b/web/src/components/overlay/SaveExportOverlay.tsx
index 6bb899ed8..63d207cbd 100644
--- a/web/src/components/overlay/SaveExportOverlay.tsx
+++ b/web/src/components/overlay/SaveExportOverlay.tsx
@@ -2,6 +2,7 @@ import { LuVideo, LuX } from "react-icons/lu";
import { Button } from "../ui/button";
import { FaCompactDisc } from "react-icons/fa";
import { cn } from "@/lib/utils";
+import { Trans } from "react-i18next";
type SaveExportOverlayProps = {
className: string;
@@ -33,7 +34,7 @@ export default function SaveExportOverlay({
onClick={onCancel}
>
- Cancel
+
ui.cancel
- Preview Export
+ ui.dialog.export.fromTimeline.previewExport
- Save Export
+ ui.dialog.export.fromTimeline.saveExport
diff --git a/web/src/components/overlay/SetPasswordDialog.tsx b/web/src/components/overlay/SetPasswordDialog.tsx
index 2f6cc4eaf..a65b805e5 100644
--- a/web/src/components/overlay/SetPasswordDialog.tsx
+++ b/web/src/components/overlay/SetPasswordDialog.tsx
@@ -8,6 +8,7 @@ import {
DialogHeader,
DialogTitle,
} from "../ui/dialog";
+import { Trans } from "react-i18next";
type SetPasswordProps = {
show: boolean;
@@ -25,7 +26,9 @@ export default function SetPasswordDialog({
e.preventDefault()}>
- Set Password
+
+ ui.settingView.users.dialog.setPassword
+
- Save
+ ui.save
diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx
index 2570fd033..d8775b04b 100644
--- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx
+++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx
@@ -42,6 +42,7 @@ import { DownloadVideoButton } from "@/components/button/DownloadVideoButton";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { LuSearch } from "react-icons/lu";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
+import { t } from "i18next";
type ReviewDetailDialogProps = {
review?: ReviewSegment;
@@ -95,8 +96,8 @@ export default function ReviewDetailDialog({
const formattedDate = useFormattedTimestamp(
review?.start_time ?? 0,
config?.ui.time_format == "24hour"
- ? "%b %-d %Y, %H:%M"
- : "%b %-d %Y, %I:%M %p",
+ ? t("ui.time.formattedTimestampWithYear.24hour")
+ : t("ui.time.formattedTimestampWithYear"),
config?.ui.timezone,
);
diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx
index 9d3610e49..01a13b14d 100644
--- a/web/src/components/overlay/detail/SearchDetailDialog.tsx
+++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx
@@ -73,12 +73,14 @@ import { LuInfo } from "react-icons/lu";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { FaPencilAlt } from "react-icons/fa";
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
const SEARCH_TABS = [
"details",
"snapshot",
"video",
- "object lifecycle",
+ "object_lifecycle",
] as const;
export type SearchTab = (typeof SEARCH_TABS)[number];
@@ -152,7 +154,7 @@ export default function SearchDetailDialog({
}
if (search.data.type != "object" || !search.has_clip) {
- const index = views.indexOf("object lifecycle");
+ const index = views.indexOf("object_lifecycle");
views.splice(index, 1);
}
@@ -192,8 +194,12 @@ export default function SearchDetailDialog({
)}
>
- Tracked Object Details
- Tracked object details
+
+ ui.exploreView.trackedObjectDetails
+
+
+ ui.exploreView.details
+
}
{item == "snapshot" && }
{item == "video" && }
- {item == "object lifecycle" && (
+ {item == "object_lifecycle" && (
)}
- {item}
+
+ ui.exploreView.type.{item}
+
))}
@@ -254,7 +262,7 @@ export default function SearchDetailDialog({
/>
)}
{page == "video" && }
- {page == "object lifecycle" && (
+ {page == "object_lifecycle" && (
{
if (resp.status == 200) {
- toast.success("Successfully saved description", {
+ toast.success(t("ui.exploreView.details.tips.descriptionSaved"), {
position: "top-center",
});
}
@@ -395,7 +403,7 @@ function ObjectDetailsTab({
);
})
.catch(() => {
- toast.error("Failed to update the description", {
+ toast.error(t("ui.exploreView.details.tips.saveDescriptionFailed"), {
position: "top-center",
});
setDesc(search.data.description);
@@ -506,10 +514,12 @@ function ObjectDetailsTab({
-
Label
+
+ ui.exploreView.details.label
+
{getIconForLabel(search.label, "size-4 text-primary")}
- {search.label}
+ object.{search.label}
{search.sub_label && ` (${search.sub_label})`}
@@ -523,7 +533,9 @@ function ObjectDetailsTab({
- Edit sub label
+
+ ui.exploreView.details.editSubLable
+
@@ -531,7 +543,7 @@ function ObjectDetailsTab({
- Top Score
+
ui.exploreView.details.topScore
@@ -540,9 +552,7 @@ function ObjectDetailsTab({
- The top score is the highest median score for the tracked
- object, so this may differ from the score shown on the
- search result thumbnail.
+ ui.exploreView.details.topScore.info
@@ -553,7 +563,9 @@ function ObjectDetailsTab({
{averageEstimatedSpeed && (
-
Estimated Speed
+
+ ui.exploreView.details.estimatedSpeed
+
{averageEstimatedSpeed && (
@@ -575,13 +587,17 @@ function ObjectDetailsTab({
)}
-
Camera
+
+ ui.exploreView.details.camera
+
{search.camera.replaceAll("_", " ")}
-
Timestamp
+
+ ui.exploreView.details.timestamp
+
{formattedDate}
@@ -633,17 +649,16 @@ function ObjectDetailsTab({
- Frigate will not request a description from your Generative AI
- provider until the tracked object's lifecycle has ended.
+ ui.exploreView.details.description.aiTips
>
) : (
<>
-
Description
+
diff --git a/web/src/components/overlay/dialog/RestartDialog.tsx b/web/src/components/overlay/dialog/RestartDialog.tsx
index 8e1a5c129..4c64215f1 100644
--- a/web/src/components/overlay/dialog/RestartDialog.tsx
+++ b/web/src/components/overlay/dialog/RestartDialog.tsx
@@ -18,6 +18,8 @@ import {
import { Button } from "@/components/ui/button";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { baseUrl } from "@/api/baseUrl";
+import { t } from "i18next";
+import { Trans } from "react-i18next";
type RestartDialogProps = {
isOpen: boolean;
@@ -79,13 +81,15 @@ export default function RestartDialog({
- Are you sure you want to restart Frigate?
+ ui.dialog.restart.title
- Cancel
+
+ ui.cancel
+
- Restart
+ ui.dialog.restart.button
@@ -100,10 +104,12 @@ export default function RestartDialog({
- Frigate is Restarting
+ ui.dialog.restart.restarting.title
- This page will reload in {countdown} seconds.
+
+ {t("ui.dialog.restart.restarting.content", { countdown })}
+
- Force Reload Now
+ ui.dialog.restart.restarting.button
diff --git a/web/src/components/overlay/dialog/SearchFilterDialog.tsx b/web/src/components/overlay/dialog/SearchFilterDialog.tsx
index 23deee531..05f8109b7 100644
--- a/web/src/components/overlay/dialog/SearchFilterDialog.tsx
+++ b/web/src/components/overlay/dialog/SearchFilterDialog.tsx
@@ -33,6 +33,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { Trans } from "react-i18next";
type SearchFilterDialogProps = {
config?: FrigateConfig;
@@ -93,7 +94,7 @@ export default function SearchFilterDialog({
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
)}
/>
- More Filters
+
ui.filter.more
);
const content = (
@@ -175,7 +176,7 @@ export default function SearchFilterDialog({
setOpen(false);
}}
>
- Apply
+
ui.apply
- Reset
+ ui.reset
@@ -272,7 +273,9 @@ function TimeRangeFilterContent({
return (
-
Time Range
+
+ ui.filter.timeRange
+
-
Zones
+
+ ui.filter.zones
+
{allZones && (
<>
@@ -378,7 +383,7 @@ export function ZoneFilterContent({
className="mx-2 cursor-pointer text-primary"
htmlFor="allZones"
>
- All Zones
+
ui.filter.allZones
- Sub Labels
+
+ ui.filter.subLabels
+
- All Sub Labels
+ ui.filter.allSubLabels
- Score
+
+ ui.filter.score
+
-
Features
+
+ ui.filter.features
+
@@ -658,7 +669,7 @@ export function SnapshotClipFilterContent({
htmlFor="snapshot-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
- Has a snapshot
+ ui.filter.features.hasSnapshot
- Yes
+ ui.yes
- No
+ ui.no
@@ -720,12 +731,9 @@ export function SnapshotClipFilterContent({
side="left"
sideOffset={5}
>
- You must first filter on tracked objects that have a
- snapshot.
-
-
- Tracked objects without a snapshot cannot be submitted to
- Frigate+.
+
+ ui.filter.features.submittedToFrigatePlus.tips
+
)}
@@ -734,7 +742,7 @@ export function SnapshotClipFilterContent({
htmlFor="plus-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
- Submitted to Frigate+
+
ui.filter.features.submittedToFrigatePlus
- Yes
+ ui.yes
- No
+ ui.no
@@ -796,7 +804,7 @@ export function SnapshotClipFilterContent({
htmlFor="clip-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
- Has a video clip
+ ui.filter.features.hasVideoClip
- Yes
+ ui.yes
- No
+ ui.no
diff --git a/web/src/components/player/PreviewPlayer.tsx b/web/src/components/player/PreviewPlayer.tsx
index b233c6ad4..1215c32d7 100644
--- a/web/src/components/player/PreviewPlayer.tsx
+++ b/web/src/components/player/PreviewPlayer.tsx
@@ -20,6 +20,7 @@ import {
getPreviewForTimeRange,
usePreviewForTimeRange,
} from "@/hooks/use-camera-previews";
+import { Trans } from "react-i18next";
type PreviewPlayerProps = {
className?: string;
@@ -88,7 +89,7 @@ export default function PreviewPlayer({
className,
)}
>
- No Preview Found
+ ui.player.noPreviewFound
);
}
@@ -324,7 +325,9 @@ function PreviewVideoPlayer({
{cameraPreviews && !currentPreview && (
- No Preview Found for {camera.replaceAll("_", " ")}
+
+ ui.player.noPreviewFoundFor
+
)}
{firstLoad &&
}
@@ -544,7 +547,9 @@ function PreviewFramesPlayer({
/>
{previewFrames?.length === 0 && (
- No Preview Found for {camera.replaceAll("_", " ")}
+
+ ui.player.noPreviewFoundFor
+
)}
{firstLoad &&
}
diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx
index 5c9ccb0a2..b14558735 100644
--- a/web/src/components/player/PreviewThumbnailPlayer.tsx
+++ b/web/src/components/player/PreviewThumbnailPlayer.tsx
@@ -21,6 +21,7 @@ import { cn } from "@/lib/utils";
import { InProgressPreview, VideoPreview } from "../preview/ScrubbablePreview";
import { Preview } from "@/types/preview";
import { baseUrl } from "@/api/baseUrl";
+import { t } from "i18next";
type PreviewPlayerProps = {
review: ReviewSegment;
@@ -167,7 +168,9 @@ export default function PreviewThumbnailPlayer({
const formattedDate = useFormattedTimestamp(
review.start_time,
- config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
+ config?.ui.time_format == "24hour"
+ ? t("ui.time.formattedTimestampExcludeSeconds.24hour")
+ : t("ui.time.formattedTimestampExcludeSeconds"),
config?.ui?.timezone,
);
diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx
index 6c4e28e27..07f9eb575 100644
--- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx
+++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx
@@ -12,6 +12,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
import { VideoResolutionType } from "@/types/live";
import axios from "axios";
import { cn } from "@/lib/utils";
+import { Trans } from "react-i18next";
/**
* Dynamically switches between video playback and scrubbing preview player.
@@ -247,7 +248,7 @@ export default function DynamicVideoPlayer({
)}
{!isScrubbing && !isLoading && noRecording && (
- No recordings found for this time
+ ui.player.noRecordingsFoundForThisTime
)}
>
diff --git a/web/src/components/settings/CameraStreamingDialog.tsx b/web/src/components/settings/CameraStreamingDialog.tsx
index d4e234362..35b8175d2 100644
--- a/web/src/components/settings/CameraStreamingDialog.tsx
+++ b/web/src/components/settings/CameraStreamingDialog.tsx
@@ -31,6 +31,7 @@ import useSWR from "swr";
import { LuCheck, LuExternalLink, LuInfo, LuX } from "react-icons/lu";
import { Link } from "react-router-dom";
import { LiveStreamMetadata } from "@/types/live";
+import { Trans } from "react-i18next";
type CameraStreamingDialogProps = {
camera: string;
@@ -177,10 +178,12 @@ export function CameraStreamingDialog({
{!isRestreamed && (
-
Stream
+
-
Restreaming is not enabled for this camera.
+
+ ui.dialog.streaming.restreaming.disabled
+
@@ -189,8 +192,7 @@ export function CameraStreamingDialog({
- Set up go2rtc for additional live view options and audio for
- this camera.
+ ui.dialog.streaming.restreaming.desc
- Read the documentation{" "}
+
+ ui.dialog.streaming.restreaming.readTheDocumentation
+
diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx
index 3b73c6a23..9cb19d119 100644
--- a/web/src/components/settings/MotionMaskEditPane.tsx
+++ b/web/src/components/settings/MotionMaskEditPane.tsx
@@ -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[];
@@ -206,7 +208,9 @@ export default function MotionMaskEditPane({
}
useEffect(() => {
- document.title = "Edit Motion Mask - Frigate";
+ document.title = t(
+ "ui.settingView.masksAndZonesSettings.motionMasks.documentTitle",
+ );
}, []);
if (!polygon) {
@@ -217,14 +221,15 @@ export default function MotionMaskEditPane({
<>
- {polygon.name.length ? "Edit" : "New"} Motion Mask
+ {polygon.name.length
+ ? t("ui.settingView.masksAndZonesSettings.motionMasks.edit")
+ : t("ui.settingView.masksAndZonesSettings.motionMasks.add")}
- Motion masks are used to prevent unwanted types of motion from
- triggering detection (example: tree branches, camera timestamps).
- Motion masks should be used very sparingly , over-masking will
- make it more difficult for objects to be tracked.
+
+ ui.settingView.masksAndZonesSettings.motionMasks.context
+
@@ -234,7 +239,9 @@ export default function MotionMaskEditPane({
rel="noopener noreferrer"
className="inline"
>
- Read the documentation{" "}
+
+ ui.settingView.masksAndZonesSettings.motionMasks.context.documentation
+ {" "}
@@ -243,11 +250,9 @@ export default function MotionMaskEditPane({
{polygons && activePolygonIndex !== undefined && (
- {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 && (
)}
@@ -262,7 +267,9 @@ export default function MotionMaskEditPane({
)}
- Click to draw a polygon on the image.
+
+ ui.settingView.masksAndZonesSettings.motionMasks.clickDrawPolygon
+
@@ -270,19 +277,26 @@ export default function MotionMaskEditPane({
{polygonArea && polygonArea >= 0.35 && (
<>
- 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),
+ },
+ )}
- Motion masks do not prevent objects from being detected. You should
- use a required zone instead.
+
+ ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.tips
+
- Read the documentation{" "}
+
+ ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.documentation
+ {" "}
@@ -319,7 +333,7 @@ export default function MotionMaskEditPane({
aria-label="Cancel"
onClick={onCancel}
>
- Cancel
+
ui.cancel
- Saving...
+
+ ui.saving
+
) : (
- "Save"
+
ui.save
)}
diff --git a/web/src/components/settings/ObjectMaskEditPane.tsx b/web/src/components/settings/ObjectMaskEditPane.tsx
index 2c63d2e63..81cdb6cac 100644
--- a/web/src/components/settings/ObjectMaskEditPane.tsx
+++ b/web/src/components/settings/ObjectMaskEditPane.tsx
@@ -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[];
@@ -240,7 +242,9 @@ export default function ObjectMaskEditPane({
}
useEffect(() => {
- document.title = "Edit Object Mask - Frigate";
+ document.title = t(
+ "ui.settingView.masksAndZonesSettings.objectMasks.documentTitle",
+ );
}, []);
if (!polygon) {
@@ -251,23 +255,24 @@ export default function ObjectMaskEditPane({
<>
- {polygon.name.length ? "Edit" : "New"} Object Mask
+ {polygon.name.length
+ ? t("ui.settingView.masksAndZonesSettings.objectMasks.edit")
+ : t("ui.settingView.masksAndZonesSettings.objectMasks.add")}
- Object filter masks are used to filter out false positives for a given
- object type based on location.
+
+ ui.settingView.masksAndZonesSettings.objectMasks.context
+
{polygons && activePolygonIndex !== undefined && (
- {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 && (
)}
@@ -282,6 +287,9 @@ export default function ObjectMaskEditPane({
)}
+
+ ui.settingView.masksAndZonesSettings.objectMasks.clickDrawPolygon
+
Click to draw a polygon on the image.
@@ -307,7 +315,11 @@ export default function ObjectMaskEditPane({
name="objects"
render={({ field }) => (
- Objects
+
+
+ ui.settingView.masksAndZonesSettings.objectMasks.objects
+
+
- The object type that that applies to this object mask.
+
+ ui.settingView.masksAndZonesSettings.objectMasks.objects.desc
+
@@ -420,11 +434,15 @@ export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
return (
<>
- All object types
+
+
+ ui.settingView.masksAndZonesSettings.objectMasks.objects.allObjectTypes
+
+
{allLabels.map((item) => (
- {item.replaceAll("_", " ").charAt(0).toUpperCase() + item.slice(1)}
+ {t("object." + item)}
))}
diff --git a/web/src/components/settings/SearchSettings.tsx b/web/src/components/settings/SearchSettings.tsx
index 788072ff1..b5c0bbf60 100644
--- a/web/src/components/settings/SearchSettings.tsx
+++ b/web/src/components/settings/SearchSettings.tsx
@@ -17,8 +17,10 @@ import FilterSwitch from "../filter/FilterSwitch";
import { SearchFilter, SearchSource } from "@/types/search";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
-type SearchSettingsProps = {
+type ExploreSettingsProps = {
className?: string;
columns: number;
defaultView: string;
@@ -27,7 +29,7 @@ type SearchSettingsProps = {
setDefaultView: (view: string) => void;
onUpdateFilter: (filter: SearchFilter) => void;
};
-export default function SearchSettings({
+export default function ExploreSettings({
className,
columns,
setColumns,
@@ -35,7 +37,7 @@ export default function SearchSettings({
filter,
setDefaultView,
onUpdateFilter,
-}: SearchSettingsProps) {
+}: ExploreSettingsProps) {
const { data: config } = useSWR
("config");
const [open, setOpen] = useState(false);
@@ -50,17 +52,18 @@ export default function SearchSettings({
size="sm"
>
- Settings
+ ui.searchView.settings
);
const content = (
-
Default View
+
+ ui.searchView.settings.defaultView
+
- When no filters are selected, display a summary of the most recent
- tracked objects per label, or display an unfiltered grid.
+ ui.searchView.settings.defaultView.desc
setDefaultView(value)}
>
- {defaultView == "summary" ? "Summary" : "Unfiltered Grid"}
+ {defaultView == "summary"
+ ? t("ui.searchView.settings.defaultView.summary")
+ : t("ui.searchView.settings.defaultView.unfilteredGrid")}
@@ -78,7 +83,9 @@ export default function SearchSettings({
className="cursor-pointer"
value={value}
>
- {value == "summary" ? "Summary" : "Unfiltered Grid"}
+ {value == "summary"
+ ? t("ui.searchView.settings.defaultView.summary")
+ : t("ui.searchView.settings.defaultView.unfilteredGrid")}
))}
@@ -90,9 +97,11 @@ export default function SearchSettings({
-
Grid Columns
+
+ ui.searchView.settings.gridColumns
+
- Select the number of columns in the grid view.
+ ui.searchView.settings.gridColumns.desc
@@ -153,10 +162,11 @@ export function SearchTypeContent({
-
Search Source
+
+ ui.searchView.settings.searchSource
+
- Choose whether to search the thumbnails or descriptions of your
- tracked objects.
+ ui.searchView.settings.searchSource.desc
diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx
index 247ae8991..9418b4385 100644
--- a/web/src/components/settings/ZoneEditPane.tsx
+++ b/web/src/components/settings/ZoneEditPane.tsx
@@ -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[];
@@ -451,7 +453,9 @@ export default function ZoneEditPane({
}
useEffect(() => {
- document.title = "Edit Zone - Frigate";
+ document.title = t(
+ "ui.settingView.masksAndZonesSettings.zone.documentTitle",
+ );
}, []);
if (!polygon) {
@@ -462,23 +466,23 @@ export default function ZoneEditPane({
<>
- {polygon.name.length ? "Edit" : "New"} Zone
+ {polygon.name.length
+ ? t("ui.settingView.masksAndZonesSettings.zone.edit")
+ : t("ui.settingView.masksAndZonesSettings.zone.add")}
- 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
{polygons && activePolygonIndex !== undefined && (
- {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 && (
)}
@@ -493,7 +497,9 @@ export default function ZoneEditPane({
)}
- Click to draw a polygon on the image.
+
+ ui.settingView.masksAndZonesSettings.zone.clickDrawPolygon
+
@@ -505,17 +511,22 @@ export default function ZoneEditPane({
name="name"
render={({ field }) => (
- Name
+
+ ui.settingView.masksAndZonesSettings.zone.name
+
- Name must be at least 2 characters and must not be the name of
- a camera or another zone.
+
+ ui.settingView.masksAndZonesSettings.zone.name.tips
+
@@ -527,7 +538,11 @@ export default function ZoneEditPane({
name="inertia"
render={({ field }) => (
- Inertia
+
+
+ ui.settingView.masksAndZonesSettings.zone.inertia
+
+
- Specifies how many frames that an object must be in a zone
- before they are considered in the zone. Default: 3
+
+ ui.settingView.masksAndZonesSettings.zone.inertia.desc
+
@@ -549,7 +565,11 @@ export default function ZoneEditPane({
name="loitering_time"
render={({ field }) => (
- Loitering Time
+
+
+ ui.settingView.masksAndZonesSettings.zone.loiteringTime
+
+
- Sets a minimum amount of time in seconds that the object must
- be in the zone for it to activate. Default: 0
+
+ ui.settingView.masksAndZonesSettings.zone.loiteringTime.desc
+
@@ -567,9 +588,13 @@ export default function ZoneEditPane({
/>
- Objects
+
+ ui.settingView.masksAndZonesSettings.zone.objects
+
- List of objects that apply to this zone.
+
+ ui.settingView.masksAndZonesSettings.zone.objects.desc
+
- Speed Estimation
+
+ ui.settingView.masksAndZonesSettings.zone.speedEstimation
+
0) {
toast.error(
- "Zones with loitering times greater than 0 should not be used with speed estimation.",
+ t(
+ "ui.settingView.masksAndZonesSettings.zone.speedEstimation.loiteringTimeError",
+ ),
);
}
field.onChange(checked);
@@ -634,8 +665,9 @@ export default function ZoneEditPane({
- Enable speed estimation for objects in this zone. The zone
- must have exactly 4 points.
+
+ ui.settingView.masksAndZonesSettings.zone.speedEstimation.desc
+
@@ -877,7 +909,7 @@ export function ZoneObjectSelector({
- All Objects
+ ui.settingView.masksAndZonesSettings.zone.allObjects
- {item.replaceAll("_", " ")}
+ {t("object." + item)}
- Apply
+ ui.apply
{
@@ -440,7 +442,7 @@ export function DateRangePicker({
variant="ghost"
aria-label="Reset"
>
- Reset
+ ui.reset
diff --git a/web/src/components/ui/calendar.tsx b/web/src/components/ui/calendar.tsx
index 792df0890..34e4eac08 100644
--- a/web/src/components/ui/calendar.tsx
+++ b/web/src/components/ui/calendar.tsx
@@ -1,12 +1,24 @@
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
-
+import { enUS, Locale, zhCN } from "date-fns/locale";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
+import i18n from "@/utils/i18n";
export type CalendarProps = React.ComponentProps
;
+
+let locale: Locale;
+switch(i18n.language) {
+ case "zh-CN":
+ locale = zhCN;
+ break;
+ default:
+ locale = enUS;
+ break;
+}
+
function Calendar({
className,
classNames,
@@ -15,6 +27,7 @@ function Calendar({
}: CalendarProps) {
return (
void;
+};
+
+const initialState: LanguageProviderState = {
+ language: i18next.language || "en",
+ systemLanguage: "en",
+ setLanguage: () => null,
+};
+
+const LanguageProviderContext =
+ createContext(initialState);
+
+export function LanguageProvider({
+ children,
+ defaultLanguage = "en",
+ storageKey = "frigate-ui-language",
+ ...props
+}: {
+ children: React.ReactNode;
+ defaultLanguage?: string;
+ storageKey?: string;
+}) {
+ const [language, setLanguage] = useState(() => {
+ 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(() => {
+ if (typeof window === "undefined") return "en";
+ return window.navigator.language;
+ }, []);
+
+ useEffect(() => {
+ if (language === systemLanguage) return;
+ i18next.changeLanguage(language);
+ }, [language, systemLanguage]);
+
+ const value = {
+ language,
+ systemLanguage,
+ setLanguage: (language: string) => {
+ localStorage.setItem(storageKey, language);
+ setLanguage(language);
+ window.location.reload();
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+// 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;
+};
diff --git a/web/src/context/providers.tsx b/web/src/context/providers.tsx
index 61b4a6426..7f12fea51 100644
--- a/web/src/context/providers.tsx
+++ b/web/src/context/providers.tsx
@@ -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";
import { StreamingSettingsProvider } from "./streaming-settings-provider";
type TProvidersProps = {
@@ -16,15 +17,17 @@ function providers({ children }: TProvidersProps) {
-
-
-
-
- {children}
-
-
-
-
+
+
+
+
+
+ {children}
+
+
+
+
+
diff --git a/web/src/context/theme-provider.tsx b/web/src/context/theme-provider.tsx
index 764e0d8e9..84bccda97 100644
--- a/web/src/context/theme-provider.tsx
+++ b/web/src/context/theme-provider.tsx
@@ -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 = {
diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts
index bbf70ba32..959f11f26 100644
--- a/web/src/hooks/use-camera-activity.ts
+++ b/web/src/hooks/use-camera-activity.ts
@@ -52,7 +52,7 @@ export function useCameraActivity(
// handle camera activity
const hasActiveObjects = useMemo(
- () => objects.filter((obj) => !obj.stationary).length > 0,
+ () => objects?.filter((obj) => !obj?.stationary)?.length > 0,
[objects],
);
diff --git a/web/src/hooks/use-navigation.ts b/web/src/hooks/use-navigation.ts
index daed383d3..9f773888d 100644
--- a/web/src/hooks/use-navigation.ts
+++ b/web/src/hooks/use-navigation.ts
@@ -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",
},
@@ -72,6 +72,6 @@ export default function useNavigation(
enabled: isDesktop && config?.face_recognition.enabled,
},
] as NavData[],
- [config?.face_recognition.enabled, variant],
+ [config?.face_recognition?.enabled, variant],
);
}
diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts
index 23b819327..95c64b616 100644
--- a/web/src/hooks/use-stats.ts
+++ b/web/src/hooks/use-stats.ts
@@ -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("config");
@@ -72,7 +73,10 @@ 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 +84,10 @@ 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",
});
diff --git a/web/src/main.tsx b/web/src/main.tsx
index f25366e5e..334c8f2a5 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -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(
diff --git a/web/src/pages/ConfigEditor.tsx b/web/src/pages/ConfigEditor.tsx
index bcb0c4c65..6efee3201 100644
--- a/web/src/pages/ConfigEditor.tsx
+++ b/web/src/pages/ConfigEditor.tsx
@@ -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() {
- Config Editor
+ ui.configEditorView.configEditor
handleCopyConfig()}
>
- Copy Config
+
+ ui.configEditorView.copyConfig
+
-
Save & Restart
+
+ ui.configEditorView.saveAndRestart
+
onHandleSaveConfig("saveonly")}
>
- Save Only
+
+ ui.configEditorView.saveOnly
+
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx
index 50db5211e..5e38747a7 100644
--- a/web/src/pages/Events.tsx
+++ b/web/src/pages/Events.tsx
@@ -22,6 +22,7 @@ import {
import EventView from "@/views/events/EventView";
import { RecordingView } from "@/views/recording/RecordingView";
import axios from "axios";
+import { t } from "i18next";
import { useCallback, useEffect, useMemo, useState } from "react";
import useSWR from "swr";
@@ -77,9 +78,9 @@ export default function Events() {
useEffect(() => {
if (recording) {
- document.title = "Recordings - Frigate";
+ document.title = t("ui.review.recordings.documentTitle");
} else {
- document.title = `Review - Frigate`;
+ document.title = t("ui.review.documentTitle");
}
}, [recording, severity]);
diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx
index 529bb2e26..a4f21d4d1 100644
--- a/web/src/pages/Exports.tsx
+++ b/web/src/pages/Exports.tsx
@@ -17,8 +17,10 @@ import { useSearchEffect } from "@/hooks/use-overlay-state";
import { cn } from "@/lib/utils";
import { DeleteClipType, Export } from "@/types/export";
import axios from "axios";
+import { t } from "i18next";
import { useCallback, useEffect, useMemo, useState } from "react";
import { isMobile } from "react-device-detect";
+import { Trans } from "react-i18next";
import { LuFolderX } from "react-icons/lu";
import { toast } from "sonner";
import useSWR from "swr";
@@ -27,7 +29,7 @@ function Exports() {
const { data: exports, mutate } = useSWR("exports");
useEffect(() => {
- document.title = "Export - Frigate";
+ document.title = t("ui.exportView.documentTitle");
}, []);
// Search
@@ -118,20 +120,26 @@ function Exports() {
>
- Delete Export
+
+ ui.exportView.deleteExport
+
- Are you sure you want to delete {deleteClip?.exportName}?
+
+ ui.exportView.deleteExport.desc
+
- Cancel
+
+ ui.cancel
+
onHandleDelete()}
>
- Delete
+ ui.delete
@@ -179,7 +187,7 @@ function Exports() {
setSearch(e.target.value)}
/>
@@ -207,7 +215,7 @@ function Exports() {
) : (
- No exports found
+ ui.exportView.noExports
)}
diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx
index 97e565ef1..f087b45a3 100644
--- a/web/src/pages/Live.tsx
+++ b/web/src/pages/Live.tsx
@@ -9,6 +9,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
import LiveCameraView from "@/views/live/LiveCameraView";
import LiveDashboardView from "@/views/live/LiveDashboardView";
+import { t } from "i18next";
import { useEffect, useMemo, useRef } from "react";
import useSWR from "swr";
@@ -64,11 +65,15 @@ function Live() {
.split("_")
.filter((text) => text)
.map((text) => text[0].toUpperCase() + text.substring(1));
- document.title = `${capitalized.join(" ")} - Live - Frigate`;
+ document.title = t("ui.live.documentTitle.withCamera", {
+ camera: capitalized.join(" "),
+ });
} else if (cameraGroup && cameraGroup != "default") {
- document.title = `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)} - Live - Frigate`;
+ document.title = t("ui.live.documentTitle.withCamera", {
+ camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
+ });
} else {
- document.title = "Live - Frigate";
+ document.title = t("ui.live.documentTitle");
}
}, [cameraGroup, selectedCameraName]);
diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx
index 6eeb5bcc3..af0f59438 100644
--- a/web/src/pages/Settings.tsx
+++ b/web/src/pages/Settings.tsx
@@ -37,15 +37,16 @@ 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";
import { useSearchEffect } from "@/hooks/use-overlay-state";
import { useSearchParams } from "react-router-dom";
const allSettingsViews = [
- "UI settings",
- "explore settings",
- "camera settings",
- "masks / zones",
- "motion tuner",
+ "uiSettings",
+ "exploreSettings",
+ "cameraSettings",
+ "masksAndZones",
+ "motionTuner",
"debug",
"users",
"notifications",
@@ -53,7 +54,7 @@ const allSettingsViews = [
type SettingsType = (typeof allSettingsViews)[number];
export default function Settings() {
- const [page, setPage] = useState("UI settings");
+ const [page, setPage] = useState("uiSettings");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const tabsRef = useRef(null);
@@ -150,12 +151,14 @@ export default function Settings() {
{Object.values(allSettingsViews).map((item) => (
- {item}
+
+ {t("ui.settingView.menu." + item)}
+
))}
@@ -163,11 +166,11 @@ export default function Settings() {
{(page == "debug" ||
- page == "camera settings" ||
- page == "masks / zones" ||
- page == "motion tuner") && (
+ page == "cameraSettings" ||
+ page == "masksAndZones" ||
+ page == "motionTuner") && (
- {page == "masks / zones" && (
+ {page == "masksAndZones" && (
- {page == "UI settings" &&
}
- {page == "explore settings" && (
+ {page == "uiSettings" &&
}
+ {page == "exploreSettings" && (
)}
{page == "debug" && (
)}
- {page == "camera settings" && (
+ {page == "cameraSettings" && (
)}
- {page == "masks / zones" && (
+ {page == "masksAndZones" && (
)}
- {page == "motion tuner" && (
+ {page == "motionTuner" && (
}
{item == "storage" &&
}
{item == "cameras" &&
}
- {isDesktop &&
{item}
}
+ {isDesktop && (
+
{t("ui.system." + item)}
+ )}
))}
@@ -99,7 +103,8 @@ function System() {
{lastUpdated && (
- Last refreshed:
+ ui.system.lastRefreshed
+
)}
diff --git a/web/src/utils/i18n.ts b/web/src/utils/i18n.ts
new file mode 100644
index 000000000..4ccfc326d
--- /dev/null
+++ b/web/src/utils/i18n.ts
@@ -0,0 +1,49 @@
+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
+ },
+ keySeparator: false,
+ parseMissingKeyHandler: (key: string) => {
+ const parts = key.split(".");
+ if (parts.length > 1) {
+ if (parts[0] === "object" || parts[0] === "audio") {
+ return (
+ parts[1].replaceAll("_", " ").charAt(0).toUpperCase() +
+ parts[1].slice(1)
+ );
+ }
+ return parts[parts.length - 1];
+ }
+ return key;
+ },
+ });
+
+export default i18n;
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index 583b47fe9..2f3277d91 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -54,6 +54,8 @@ 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";
+import { t } from "i18next";
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
type EventViewProps = {
@@ -197,10 +199,9 @@ export default function EventView({
)
.then((response) => {
if (response.status == 200) {
- toast.success(
- "Successfully started export. View the file in the /exports folder.",
- { position: "top-center" },
- );
+ toast.success(t("ui.dialog.export.toast.success"), {
+ position: "top-center",
+ });
}
})
.catch((error) => {
@@ -288,7 +289,7 @@ export default function EventView({
<>
- Alerts
+
ui.eventView.alerts
{reviewCounts.alert > -1 ? (
` ∙ ${reviewCounts.alert}`
) : (
@@ -324,7 +325,7 @@ export default function EventView({
<>
- Detections
+
ui.eventView.detections
{reviewCounts.detection > -1 ? (
` ∙ ${reviewCounts.detection}`
) : (
@@ -715,7 +716,7 @@ function DetectionReview({
{!loading && currentItems?.length === 0 && (
- There are no {severity.replace(/_/g, " ")}s to review
+ ui.eventView.empty.{severity.replace(/_/g, " ")}
)}
diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx
index c3ef25ad1..a6a821888 100644
--- a/web/src/views/explore/ExploreView.tsx
+++ b/web/src/views/explore/ExploreView.tsx
@@ -225,7 +225,7 @@ function ExploreThumbnailImage({
};
const handleShowObjectLifecycle = () => {
- onSelectSearch(event, false, "object lifecycle");
+ onSelectSearch(event, false, "object_lifecycle");
};
const handleShowSnapshot = () => {
diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx
index 3b85de4b3..041e1ce39 100644
--- a/web/src/views/live/DraggableGridLayout.tsx
+++ b/web/src/views/live/DraggableGridLayout.tsx
@@ -48,6 +48,7 @@ import {
} from "@/components/ui/tooltip";
import { Toaster } from "@/components/ui/sonner";
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
+import { t } from "i18next";
import LiveContextMenu from "@/components/menu/LiveContextMenu";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
@@ -692,7 +693,7 @@ export default function DraggableGridLayout({
- {fullscreen ? "Exit Fullscreen" : "Fullscreen"}
+ {fullscreen ? t("ui.exitFullscreen") : t("ui.fullscreen")}
>
diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx
index ccf06de7b..421279f9f 100644
--- a/web/src/views/live/LiveCameraView.tsx
+++ b/web/src/views/live/LiveCameraView.tsx
@@ -100,6 +100,8 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import useSWR from "swr";
import { cn } from "@/lib/utils";
import { useSessionPersistence } from "@/hooks/use-session-persistence";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
import {
Select,
SelectContent,
@@ -402,7 +404,11 @@ export default function LiveCameraView({
onClick={() => navigate(-1)}
>
- {isDesktop &&
Back
}
+ {isDesktop && (
+
+ ui.back
+
+ )}
- {isDesktop && History
}
+ {isDesktop && (
+
+ ui.history
+
+ )}
) : (
@@ -441,7 +451,9 @@ export default function LiveCameraView({
>
{isDesktop && (
-
Back
+
+ ui.back
+
)}
)}
@@ -451,7 +463,7 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"}
Icon={fullscreen ? FaCompress : FaExpand}
isActive={fullscreen}
- title={fullscreen ? "Close" : "Fullscreen"}
+ title={fullscreen ? t("ui.close") : t("ui.fullscreen")}
onClick={toggleFullscreen}
/>
)}
@@ -461,7 +473,7 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"}
Icon={LuPictureInPicture}
isActive={pip}
- title={pip ? "Close" : "Picture in Picture"}
+ title={pip ? t("ui.close") : t("ui.pictureInPicture")}
onClick={() => {
if (!pip) {
setPip(true);
@@ -719,7 +731,7 @@ function PtzControlPanel({
{ptz?.features?.includes("pt") && (
<>
{
e.preventDefault();
sendPtz("MOVE_LEFT");
@@ -734,7 +746,7 @@ function PtzControlPanel({
{
e.preventDefault();
sendPtz("MOVE_UP");
@@ -749,7 +761,7 @@ function PtzControlPanel({
{
e.preventDefault();
sendPtz("MOVE_DOWN");
@@ -764,7 +776,7 @@ function PtzControlPanel({
{
e.preventDefault();
sendPtz("MOVE_RIGHT");
@@ -783,7 +795,7 @@ function PtzControlPanel({
{ptz?.features?.includes("zoom") && (
<>
{
e.preventDefault();
sendPtz("ZOOM_IN");
@@ -798,7 +810,7 @@ function PtzControlPanel({
{
e.preventDefault();
sendPtz("ZOOM_OUT");
@@ -969,12 +981,11 @@ function FrigateCameraFeatures({
const toastId = toast.success(
- Started manual on-demand recording.
+ ui.live.manualRecording.started
{!camera.record.enabled || camera.record.retain.days == 0 ? (
- Since recording is disabled or restricted in the config for this
- camera, only a snapshot will be saved.
+ ui.live.manualRecording.recordDisabledTips
) : (
@@ -988,7 +999,7 @@ function FrigateCameraFeatures({
setActiveToastId(toastId);
}
} catch (error) {
- toast.error("Failed to start manual on-demand recording.", {
+ toast.error(t("ui.live.manualRecording.failedToStart"), {
position: "top-center",
});
}
@@ -1005,12 +1016,12 @@ function FrigateCameraFeatures({
});
recordingEventIdRef.current = null;
setIsRecording(false);
- toast.success("Ended manual on-demand recording.", {
+ toast.success(t("ui.live.manualRecording.ended"), {
position: "top-center",
});
}
} catch (error) {
- toast.error("Failed to end manual on-demand recording.", {
+ toast.error(t("ui.live.manualRecording.failedToEnd"), {
position: "top-center",
});
}
@@ -1048,7 +1059,11 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={detectState == "ON" ? MdPersonSearch : MdPersonOff}
isActive={detectState == "ON"}
- title={`${detectState == "ON" ? "Disable" : "Enable"} Detect`}
+ title={
+ detectState == "ON"
+ ? t("ui.live.detect.disable")
+ : t("ui.live.detect.enable")
+ }
onClick={() => sendDetect(detectState == "ON" ? "OFF" : "ON")}
/>
sendRecord(recordState == "ON" ? "OFF" : "ON")}
/>
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")}
/>
{audioDetectEnabled && (
@@ -1073,7 +1096,11 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={audioState == "ON" ? LuEar : LuEarOff}
isActive={audioState == "ON"}
- title={`${audioState == "ON" ? "Disable" : "Enable"} Audio Detect`}
+ title={
+ audioState == "ON"
+ ? t("ui.live.audioDetect.disable")
+ : t("ui.live.audioDetect.enable")
+ }
onClick={() => sendAudio(audioState == "ON" ? "OFF" : "ON")}
/>
)}
@@ -1083,7 +1110,11 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={autotrackingState == "ON" ? TbViewfinder : TbViewfinderOff}
isActive={autotrackingState == "ON"}
- title={`${autotrackingState == "ON" ? "Disable" : "Enable"} Autotracking`}
+ title={
+ autotrackingState == "ON"
+ ? t("ui.live.autotracking.disable")
+ : t("ui.live.autotracking.enable")
+ }
onClick={() =>
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
}
@@ -1097,7 +1128,9 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"}
Icon={isRecording ? TbRecordMail : TbRecordMailOff}
isActive={isRecording}
- title={`${isRecording ? "Stop" : "Start"} on-demand recording`}
+ title={t(
+ "ui.live.manualRecording." + (isRecording ? "stop" : "start"),
+ )}
onClick={handleEventButtonClick}
/>
@@ -1117,10 +1150,14 @@ function FrigateCameraFeatures({
{!isRestreamed && (
-
Stream
+
+ ui.dialog.streaming
+
-
Restreaming is not enabled for this camera.
+
+ ui.dialog.streaming.restreaming.disabled
+
@@ -1129,8 +1166,7 @@ function FrigateCameraFeatures({
- Set up go2rtc for additional live view options and audio
- for this camera.
+ ui.dialog.streaming.restreaming.desc
- Read the documentation{" "}
+
+ ui.dialog.streaming.restreaming.readTheDocumentation
+
@@ -1319,7 +1357,7 @@ function FrigateCameraFeatures({
className="mx-0 cursor-pointer text-primary"
htmlFor="showstats"
>
- Show stream stats
+ ui.dialog.streaming.showStats
- Enable this option to show stream statistics as an overlay on
- the camera feed.
+ ui.dialog.streaming.showStats.desc
- Debug View
+
ui.dialog.streaming.debugView
navigate(`/settings?page=debug&camera=${camera.name}`)
@@ -1414,10 +1451,14 @@ function FrigateCameraFeatures({
{!isRestreamed && (
-
Stream
+
+ ui.dialog.streaming
+
-
Restreaming is not enabled for this camera.
+
+ ui.dialog.streaming.restreaming.disabled
+
@@ -1426,8 +1467,7 @@ function FrigateCameraFeatures({
- Set up go2rtc for additional live view options and audio for
- this camera.
+ ui.dialog.streaming.restreaming.desc
- Read the documentation{" "}
+
+ ui.dialog.streaming.restreaming.readTheDocumentation
+
@@ -1588,7 +1630,9 @@ function FrigateCameraFeatures({
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
)}
>
- {isRecording ? "End" : "Start"} on-demand recording
+
+ ui.live.manualRecording.{isRecording ? "end" : "start"}
+
Start a manual event based on this camera's recording retention
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx
index 45d0d5302..2f28ca26b 100644
--- a/web/src/views/live/LiveDashboardView.tsx
+++ b/web/src/views/live/LiveDashboardView.tsx
@@ -41,6 +41,7 @@ import {
import { FaCompress, FaExpand } from "react-icons/fa";
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import { useResizeObserver } from "@/hooks/resize-observer";
+import { t } from "i18next";
import LiveContextMenu from "@/components/menu/LiveContextMenu";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
@@ -469,7 +470,9 @@ export default function LiveDashboardView({
}
const streamName =
currentGroupStreamingSettings?.[camera.name]?.streamName ||
- Object.values(camera.live.streams)?.[0];
+ camera?.live?.streams
+ ? Object?.values(camera?.live?.streams)?.[0]
+ : "";
const autoLive =
currentGroupStreamingSettings?.[camera.name]?.streamType !==
"no-streaming";
@@ -558,7 +561,7 @@ export default function LiveDashboardView({
- {fullscreen ? "Exit Fullscreen" : "Fullscreen"}
+ {fullscreen ? t("ui.exitFullscreen") : t("ui.fullscreen")}
diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx
index c5e528736..e45cb9cb9 100644
--- a/web/src/views/recording/RecordingView.tsx
+++ b/web/src/views/recording/RecordingView.tsx
@@ -47,6 +47,7 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
import { useResizeObserver } from "@/hooks/resize-observer";
import { cn } from "@/lib/utils";
import { useFullscreen } from "@/hooks/use-fullscreen";
+import { Trans } from "react-i18next";
import { useTimezone } from "@/hooks/use-date-utils";
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
@@ -398,7 +399,11 @@ export function RecordingView({
onClick={() => navigate(-1)}
>
- {isDesktop &&
Back
}
+ {isDesktop && (
+
+ ui.back
+
+ )}
- {isDesktop && Live
}
+ {isDesktop && (
+
+ ui.menu.live
+
+ )}
@@ -469,14 +478,18 @@ export function RecordingView({
value="timeline"
aria-label="Select timeline"
>
-
Timeline
+
+ ui.review.timeline
+
- Events
+
+ ui.review.events
+
) : (
@@ -783,7 +796,7 @@ function Timeline({
>
{mainCameraReviewItems.length === 0 ? (
- No events found for this time period.
+ ui.review.events.noFoundForTimePeriod
) : (
mainCameraReviewItems.map((review) => {
diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx
index adbc96413..df08abaa1 100644
--- a/web/src/views/search/SearchView.tsx
+++ b/web/src/views/search/SearchView.tsx
@@ -22,7 +22,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { isEqual } from "lodash";
import { formatDateToLocaleString } from "@/utils/dateUtil";
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
-import SearchSettings from "@/components/settings/SearchSettings";
+import ExploreSettings from "@/components/settings/SearchSettings";
import {
Tooltip,
TooltipContent,
@@ -31,6 +31,7 @@ import {
import Chip from "@/components/indicators/Chip";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import SearchActionGroup from "@/components/filter/SearchActionGroup";
+import { Trans } from "react-i18next";
type SearchViewProps = {
search: string;
@@ -484,7 +485,7 @@ export default function SearchView({
filter={searchFilter}
onUpdateFilter={onUpdateFilter}
/>
-
- No Tracked Objects Found
+ ui.searchView.noTrackedObjects
)}
@@ -605,7 +606,7 @@ export default function SearchView({
}}
refreshResults={refresh}
showObjectLifecycle={() =>
- onSelectSearch(value, false, "object lifecycle")
+ onSelectSearch(value, false, "object_lifecycle")
}
showSnapshot={() =>
onSelectSearch(value, false, "snapshot")
diff --git a/web/src/views/settings/AuthenticationView.tsx b/web/src/views/settings/AuthenticationView.tsx
index 1c6df5c52..f51cb32e2 100644
--- a/web/src/views/settings/AuthenticationView.tsx
+++ b/web/src/views/settings/AuthenticationView.tsx
@@ -15,6 +15,7 @@ import { Card } from "@/components/ui/card";
import { HiTrash } from "react-icons/hi";
import { FaUserEdit } from "react-icons/fa";
import { LuPlus } from "react-icons/lu";
+import { Trans } from "react-i18next";
export default function AuthenticationView() {
const { data: config } = useSWR("config");
@@ -91,7 +92,7 @@ export default function AuthenticationView() {
- Users
+ ui.settingView.users
- Add User
+ ui.settingView.users.addUser
@@ -123,7 +124,9 @@ export default function AuthenticationView() {
}}
>
-
Update Password
+
+ ui.settingView.users.updatePassword
+
- Delete
+
+ ui.delete
+
diff --git a/web/src/views/settings/CameraSettingsView.tsx b/web/src/views/settings/CameraSettingsView.tsx
index fa9d0ba58..008bd85aa 100644
--- a/web/src/views/settings/CameraSettingsView.tsx
+++ b/web/src/views/settings/CameraSettingsView.tsx
@@ -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";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useAlertsState, useDetectionsState } from "@/api/ws";
@@ -76,7 +78,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]);
@@ -84,7 +86,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]);
@@ -247,13 +249,13 @@ export default function CameraSettingsView({
- Camera Settings
+ ui.settingView.cameraSettings
- Review
+ ui.settingView.cameraSettings.review
@@ -267,7 +269,9 @@ export default function CameraSettingsView({
}}
/>
- Alerts
+
+ ui.settingView.cameraSettings.review.alerts
+
@@ -281,12 +285,15 @@ export default function CameraSettingsView({
}}
/>
- Detections
+
+
+ ui.settingView.cameraSettings.review.detections
+
+
- Enable/disable alerts and detections for this camera. When
- disabled, no new review items will be generated.
+ ui.settingView.cameraSettings.review.desc
@@ -294,16 +301,15 @@ export default function CameraSettingsView({
- Review Classification
+ ui.settingView.cameraSettings.reviewClassification
- Frigate categorizes review items as Alerts and Detections. By
- default, all person and car objects are
- considered Alerts. You can refine categorization of your review
- items by configuring required zones for them.
+
+ ui.settingView.cameraSettings.reviewClassification.desc
+
- Read the Documentation{" "}
+
+ ui.settingView.cameraSettings.reviewClassification.readTheDocumentation
+ {" "}
@@ -345,7 +353,9 @@ export default function CameraSettingsView({
- Select zones for Alerts
+
+ ui.settingView.cameraSettings.reviewClassification.selectAlertsZones
+
@@ -394,20 +404,40 @@ export default function CameraSettingsView({
>
) : (
- No zones are defined for this camera.
+
+ ui.settingView.cameraSettings.reviewClassification.noDefinedZones
+
)}
- 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.
+ ? 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("_", " "),
+ },
+ )}
)}
@@ -498,22 +528,56 @@ export default function CameraSettingsView({
)}
- All {detectionsLabels} objects{" "}
- not classified as Alerts {" "}
{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"}
- .
+ watchedDetectionsZones.length > 0 ? (
+ !selectDetections ? (
+
+ capitalizeFirstLetter(zone).replaceAll(
+ "_",
+ " ",
+ ),
+ )
+ .join(", "),
+ cameraName: capitalizeFirstLetter(
+ cameraConfig?.name ?? "",
+ ).replaceAll("_", " "),
+ }}
+ >
+ ) : (
+
+ capitalizeFirstLetter(zone).replaceAll(
+ "_",
+ " ",
+ ),
+ )
+ .join(", "),
+ cameraName: capitalizeFirstLetter(
+ cameraConfig?.name ?? "",
+ ).replaceAll("_", " "),
+ }}
+ />
+ )
+ ) : (
+
+ )}
)}
@@ -528,7 +592,7 @@ export default function CameraSettingsView({
onClick={onCancel}
type="button"
>
- Cancel
+
ui.cancel
- Saving...
+
+ ui.saving
+
) : (
- "Save"
+
ui.save
)}
diff --git a/web/src/views/settings/MasksAndZonesView.tsx b/web/src/views/settings/MasksAndZonesView.tsx
index 27e495766..463cf44c1 100644
--- a/web/src/views/settings/MasksAndZonesView.tsx
+++ b/web/src/views/settings/MasksAndZonesView.tsx
@@ -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";
import { useSearchEffect } from "@/hooks/use-overlay-state";
type MasksAndZoneViewProps = {
@@ -480,7 +481,7 @@ export default function MasksAndZonesView({
{editPane === undefined && (
<>
- Masks / Zones
+ ui.settingView.masksAndZonesSettings
{(selectedZoneMask === undefined ||
@@ -489,14 +490,18 @@ export default function MasksAndZonesView({
- Zones
+
+
+ ui.settingView.masksAndZonesSettings.zone
+
+
- 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{" "}
+
+ ui.settingView.masksAndZonesSettings.zone.desc.documentation
+ {" "}
@@ -526,7 +533,11 @@ export default function MasksAndZonesView({
-
Add Zone
+
+
+ ui.settingView.masksAndZonesSettings.zone.add
+
+
{allPolygons
@@ -556,16 +567,17 @@ export default function MasksAndZonesView({
- Motion Masks
+
+ ui.settingView.masksAndZonesSettings.motionMasks
+
- 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{" "}
+
+ ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation
+ {" "}
@@ -595,7 +609,11 @@ export default function MasksAndZonesView({
-
Add Motion Mask
+
+
+ ui.settingView.masksAndZonesSettings.motionMasks.add
+
+
{allPolygons
@@ -627,15 +645,17 @@ export default function MasksAndZonesView({
- Object Masks
+
+ ui.settingView.masksAndZonesSettings.objectMasks
+
- Object filter masks are used to filter out false
- positives for a given object type based on
- location.
+
+ ui.settingView.masksAndZonesSettings.objectMasks.desc
+
- Documentation{" "}
+
+ ui.settingView.masksAndZonesSettings.objectMasks.documentation
+ {" "}
@@ -665,7 +687,11 @@ export default function MasksAndZonesView({
-
Add Object Mask
+
+
+ ui.settingView.masksAndZonesSettings.objectMasks.add
+
+
{allPolygons
diff --git a/web/src/views/settings/MotionTunerView.tsx b/web/src/views/settings/MotionTunerView.tsx
index 6ccdfbf27..956ee2c66 100644
--- a/web/src/views/settings/MotionTunerView.tsx
+++ b/web/src/views/settings/MotionTunerView.tsx
@@ -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({
- Motion Detection Tuner
+ ui.settingView.motionDetectionTuner
- 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
@@ -195,7 +194,9 @@ export default function MotionTunerView({
rel="noopener noreferrer"
className="inline"
>
- Read the Motion Tuning Guide{" "}
+
+ ui.settingView.motionDetectionTuner.desc.documentation
+ {" "}
@@ -205,13 +206,13 @@ export default function MotionTunerView({
- Threshold
+ ui.settingView.motionDetectionTuner.Threshold
- The threshold value dictates how much of a change in a pixel's
- luminance is required to be considered motion.{" "}
- Default: 30
+
+ ui.settingView.motionDetectionTuner.Threshold.desc
+
@@ -236,12 +237,13 @@ export default function MotionTunerView({
- Contour Area
+ ui.settingView.motionDetectionTuner.contourArea
- The contour area value is used to decide which groups of
- changed pixels qualify as motion. Default: 10
+
+ ui.settingView.motionDetectionTuner.contourArea.desc
+
@@ -266,9 +268,15 @@ export default function MotionTunerView({
-
Improve Contrast
+
+
+ ui.settingView.motionDetectionTuner.improveContrast
+
+
- Improve contrast for darker scenes. Default: ON
+
+ ui.settingView.motionDetectionTuner.improveContrast.desc
+
- Reset
+ ui.reset
- Saving...
+
+ ui.saving
+
) : (
- "Save"
+
ui.save
)}
diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx
index edae6ba28..14263ff37 100644
--- a/web/src/views/settings/NotificationsSettingsView.tsx
+++ b/web/src/views/settings/NotificationsSettingsView.tsx
@@ -18,8 +18,10 @@ import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { FrigateConfig } from "@/types/frigateConfig";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
+import { t } from "i18next";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
+import { Trans } from "react-i18next";
import { LuAlertCircle, LuCheck, LuExternalLink, LuX } from "react-icons/lu";
import { Link } from "react-router-dom";
import { toast } from "sonner";
@@ -345,14 +347,13 @@ export default function NotificationView({
- Notification Settings
+ ui.settingView.notification.notificationSettings
- Frigate can natively send push notifications to your device
- when it is running in the browser or installed as a PWA.
+ ui.settingView.notification.desc
- Read the Documentation{" "}
+ ui.settingView.notification.documentation {" "}
@@ -378,17 +379,20 @@ export default function NotificationView({
name="email"
render={({ field }) => (
- Email
+
+ ui.settingView.notification.email
+
- Entering a valid email is required, as this is used by
- the push server in case problems occur.
+ ui.settingView.notification.email.desc
@@ -404,7 +408,9 @@ export default function NotificationView({
<>
- Cameras
+
+ ui.settingView.notification.cameras
+
@@ -413,7 +419,7 @@ export default function NotificationView({
name="allEnabled"
render={({ field }) => (
{
setChangedValue(true);
@@ -452,13 +458,17 @@ export default function NotificationView({
>
) : (
- No cameras available.
+
+ ui.settingView.notification.cameras.noCameras
+
)}
- Select the cameras to enable notifications for.
+
+ ui.settingView.notification.cameras.desc
+
)}
@@ -471,7 +481,7 @@ export default function NotificationView({
onClick={onCancel}
type="button"
>
- Cancel
+ ui.cancel
- Saving...
+
+ ui.saving
+
) : (
- "Save"
+ t("ui.save")
)}
@@ -499,7 +511,7 @@ export default function NotificationView({
- Device-Specific Settings
+ ui.settingView.notification.deviceSpecific
- {`${registration != null ? "Unregister" : "Register"} this device`}
+ {registration != null
+ ? t("ui.settingView.notification.unregisterDevice")
+ : t("ui.settingView.notification.registerDevice")}
{registration != null && registration.active && (
- Object Bounding Box Colors
+
+ ui.settingView.debug.boundingBoxes.colors
+
-
- At startup, different colors will be assigned to each object label
-
-
- A dark blue thin line indicates that object is not detected at
- this current point in time
-
-
- A gray thin line indicates that object is detected as being
- stationary
-
-
- A thick line indicates that object is the subject of autotracking
- (when enabled)
-
+ ui.settingView.debug.boundingBoxes.colors.info
>
),
},
{
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: (
<>
-
- Motion Boxes
-
-
- Red boxes will be overlaid on areas of the frame where motion is
- currently being detected
-
+ ui.settingView.debug.motion.tips
>
),
},
{
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: (
<>
-
- Region Boxes
-
-
- Bright green boxes will be overlaid on areas of interest in the
- frame that are being sent to the object detector.
-
+ ui.settingView.debug.regions.tips
>
),
},
@@ -179,24 +156,20 @@ export default function ObjectSettingsView({
- Debug
+ ui.settingView.debug
- 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(",")
+ : "",
+ })}
- 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.desc
{config?.cameras[cameraConfig.name]?.webui_url && (
@@ -217,8 +190,12 @@ export default function ObjectSettingsView({
- Debugging
- Object List
+
+ ui.settingView.debug.debugging
+
+
+ ui.settingView.debug.objectList
+
@@ -457,7 +434,9 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
);
})
) : (
-
No objects
+
+ ui.settingView.debug.noObjects
+
)}
);
diff --git a/web/src/views/settings/SearchSettingsView.tsx b/web/src/views/settings/SearchSettingsView.tsx
index 027f55070..8f5ba64b5 100644
--- a/web/src/views/settings/SearchSettingsView.tsx
+++ b/web/src/views/settings/SearchSettingsView.tsx
@@ -20,20 +20,22 @@ import {
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
+import { Trans } from "react-i18next";
+import { t } from "i18next";
-type SearchSettingsViewProps = {
+type ExploreSettingsViewProps = {
setUnsavedChanges: React.Dispatch>;
};
-type SearchSettings = {
+type ExploreSettings = {
enabled?: boolean;
reindex?: boolean;
model_size?: SearchModelSize;
};
-export default function SearchSettingsView({
+export default function ExploreSettingsView({
setUnsavedChanges,
-}: SearchSettingsViewProps) {
+}: ExploreSettingsViewProps) {
const { data: config, mutate: updateConfig } =
useSWR("config");
const [changedValue, setChangedValue] = useState(false);
@@ -41,29 +43,30 @@ export default function SearchSettingsView({
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
- const [searchSettings, setSearchSettings] = useState({
+ const [ExploreSettings, setExploreSettings] = useState({
enabled: undefined,
reindex: undefined,
model_size: undefined,
});
- const [origSearchSettings, setOrigSearchSettings] = useState({
- enabled: undefined,
- reindex: undefined,
- model_size: undefined,
- });
+ const [origExploreSettings, setOrigExploreSettings] =
+ useState({
+ enabled: undefined,
+ reindex: undefined,
+ model_size: undefined,
+ });
useEffect(() => {
if (config) {
- if (searchSettings?.enabled == undefined) {
- setSearchSettings({
+ if (ExploreSettings?.enabled == undefined) {
+ setExploreSettings({
enabled: config.semantic_search.enabled,
reindex: config.semantic_search.reindex,
model_size: config.semantic_search.model_size,
});
}
- setOrigSearchSettings({
+ setOrigExploreSettings({
enabled: config.semantic_search.enabled,
reindex: config.semantic_search.reindex,
model_size: config.semantic_search.model_size,
@@ -73,8 +76,8 @@ export default function SearchSettingsView({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
- const handleSearchConfigChange = (newConfig: Partial) => {
- setSearchSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
+ const handleSearchConfigChange = (newConfig: Partial) => {
+ setExploreSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
setUnsavedChanges(true);
setChangedValue(true);
};
@@ -84,7 +87,7 @@ export default function SearchSettingsView({
axios
.put(
- `config/set?semantic_search.enabled=${searchSettings.enabled ? "True" : "False"}&semantic_search.reindex=${searchSettings.reindex ? "True" : "False"}&semantic_search.model_size=${searchSettings.model_size}`,
+ `config/set?semantic_search.enabled=${ExploreSettings.enabled ? "True" : "False"}&semantic_search.reindex=${ExploreSettings.reindex ? "True" : "False"}&semantic_search.model_size=${ExploreSettings.model_size}`,
{
requires_restart: 0,
},
@@ -113,16 +116,16 @@ export default function SearchSettingsView({
});
}, [
updateConfig,
- searchSettings.enabled,
- searchSettings.reindex,
- searchSettings.model_size,
+ ExploreSettings.enabled,
+ ExploreSettings.reindex,
+ ExploreSettings.model_size,
]);
const onCancel = useCallback(() => {
- setSearchSettings(origSearchSettings);
+ setExploreSettings(origExploreSettings);
setChangedValue(false);
removeMessage("search_settings", "search_settings");
- }, [origSearchSettings, removeMessage]);
+ }, [origExploreSettings, removeMessage]);
useEffect(() => {
if (changedValue) {
@@ -152,18 +155,16 @@ export default function SearchSettingsView({
- Explore Settings
+ ui.settingView.exploreSettings
- Semantic Search
+ ui.settingView.exploreSettings.semanticSearch
- Semantic Search in Frigate allows you to find tracked objects
- within your review items using either the image itself, a
- user-defined text description, or an automatically generated one.
+ ui.settingView.exploreSettings.semanticSearch.desc
@@ -173,7 +174,9 @@ export default function SearchSettingsView({
rel="noopener noreferrer"
className="inline"
>
- Read the Documentation
+
+ ui.settingView.exploreSettings.semanticSearch.readTheDocumentation
+
@@ -185,14 +188,16 @@ export default function SearchSettingsView({
{
handleSearchConfigChange({ enabled: isChecked });
}}
/>
- Enabled
+
+ ui.enabled
+
@@ -200,44 +205,55 @@ export default function SearchSettingsView({
{
handleSearchConfigChange({ reindex: isChecked });
}}
/>
- Re-Index On Startup
+
+
+ ui.settingView.exploreSettings.semanticSearch.reindexOnStartup
+
+
- Re-indexing will reprocess all thumbnails and descriptions (if
- enabled) and apply the embeddings on each startup.{" "}
- Don't forget to disable the option after restarting!
+
+ ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc
+
-
Model Size
+
+
+ ui.settingView.exploreSettings.semanticSearch.modelSize
+
+
- The size of the model used for Semantic Search embeddings.
+
+ ui.settingView.exploreSettings.semanticSearch.modelSize.desc
+
- Using small employs a quantized version of the
- model that uses less RAM and runs faster on CPU with a very
- negligible difference in embedding quality.
+
+ ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc
+
- Using large employs the full Jina model and will
- automatically run on the GPU if applicable.
+
+ ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc
+
handleSearchConfigChange({
model_size: value as SearchModelSize,
@@ -245,7 +261,10 @@ export default function SearchSettingsView({
}
>
- {searchSettings.model_size}
+ {t(
+ "ui.settingView.exploreSettings.semanticSearch.modelSize." +
+ ExploreSettings.model_size,
+ )}
@@ -255,7 +274,10 @@ export default function SearchSettingsView({
className="cursor-pointer"
value={size}
>
- {size}
+ {t(
+ "ui.settingView.exploreSettings.semanticSearch.modelSize." +
+ size,
+ )}
))}
@@ -267,7 +289,7 @@ export default function SearchSettingsView({
- Reset
+ ui.reset
- Saving...
+
+ ui.saving
+
) : (
- "Save"
+ t("ui.save")
)}
diff --git a/web/src/views/settings/UiSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx
index e3b5c8c7a..965089e3a 100644
--- a/web/src/views/settings/UiSettingsView.tsx
+++ b/web/src/views/settings/UiSettingsView.tsx
@@ -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"];
@@ -82,13 +84,13 @@ export default function UiSettingsView() {
- General Settings
+ ui.settingView.generalSettings
- Live Dashboard
+ ui.settingView.generalSettings.liveDashboard
@@ -100,18 +102,16 @@ export default function UiSettingsView() {
onCheckedChange={setAutoLive}
/>
- Automatic Live View
+
+ ui.settingView.generalSettings.automaticLiveView
+
- Automatically switch to a camera's live view when activity is
- detected. Disabling this option causes static camera images on
- the your dashboards to only update once per minute.{" "}
-
- This is a global setting but can be overridden on each
- camera in camera groups only .
-
+
+ ui.settingView.generalSettings.automaticLiveView.desc
+
@@ -123,14 +123,14 @@ export default function UiSettingsView() {
onCheckedChange={setAlertVideos}
/>
- Play Alert Videos
+ ui.settingView.generalSettings.playAlertVideos
- 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.playAlertVideos.desc
+
@@ -139,12 +139,14 @@ export default function UiSettingsView() {
-
Stored Layouts
-
+
+ ui.settingView.generalSettings.storedLayouts
+
+
- 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.desc
+
@@ -152,17 +154,24 @@ export default function UiSettingsView() {
aria-label="Clear all saved layouts"
onClick={clearStoredLayouts}
>
- Clear All Layouts
+
+ ui.settingView.generalSettings.storedLayouts.clearAll
+
-
Camera Group Streaming Settings
+
+
+ ui.settingView.generalSettings.cameraGroupStreaming
+
+
- Streaming settings for each camera group are stored in your
- browser's local storage.
+
+ ui.settingView.generalSettings.cameraGroupStreaming.desc
+
@@ -170,21 +179,31 @@ export default function UiSettingsView() {
aria-label="Clear all group streaming settings"
onClick={clearStreamingSettings}
>
- Clear All Streaming Settings
+
+ ui.settingView.generalSettings.cameraGroupStreaming.clearAll
+
- Recordings Viewer
+ ui.settingView.generalSettings.recordingsViewer
-
Default Playback Rate
+
+
+ ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate
+
+
-
Default playback rate for recordings playback.
+
+
+ ui.settingView.generalSettings.recordingsViewer.defaultPlaybackRate.desc
+
+
@@ -212,14 +231,22 @@ export default function UiSettingsView() {
- Calendar
+ ui.settingView.generalSettings.calendar
-
First Weekday
+
+
+ ui.settingView.generalSettings.calendar.firstWeekday
+
+
-
The day that the weeks of the review calendar begin on.
+
+
+ ui.settingView.generalSettings.calendar.firstWeekday.desc
+
+
@@ -228,7 +255,10 @@ export default function UiSettingsView() {
onValueChange={(value) => setWeekStartsOn(parseInt(value))}
>
- {WEEK_STARTS_ON[weekStartsOn ?? 0]}
+ {t(
+ "ui.settingView.generalSettings.calendar.firstWeekday." +
+ WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(),
+ )}
@@ -238,7 +268,10 @@ export default function UiSettingsView() {
className="cursor-pointer"
value={index.toString()}
>
- {day}
+ {t(
+ "ui.settingView.generalSettings.calendar.firstWeekday." +
+ day.toLowerCase(),
+ )}
))}
diff --git a/web/src/views/system/CameraMetrics.tsx b/web/src/views/system/CameraMetrics.tsx
index 764f22e96..79f487b76 100644
--- a/web/src/views/system/CameraMetrics.tsx
+++ b/web/src/views/system/CameraMetrics.tsx
@@ -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,15 @@ export default function CameraMetrics({
return (
-
Overview
+
+ ui.system.cameras.overview
+
{statsHistory.length != 0 ? (
-
Frames / Detections
+
+ ui.system.cameras.framesAndDetections
+
- Frames / Detections
+
+ ui.system.cameras.framesAndDetections
+
- Detectors
+ ui.system.general.detector
{statsHistory.length != 0 ? (
-
Detector Inference Speed
+
+ ui.system.general.detectorInferenceSpeed
+
{detInferenceTimeSeries.map((series) => (
- Detector CPU Usage
+
+ ui.system.general.detectorCpuUsage
+
{detCpuSeries.map((series) => (
- Detector Memory Usage
+
+ ui.system.general.detectorMemoryUsage
+
{detMemSeries.map((series) => (
setShowVainfo(true)}
>
- Hardware Info
+ ui.system.general.hardwareInfo
)}
@@ -557,7 +564,9 @@ export default function GeneralMetrics({
>
{statsHistory.length != 0 ? (
-
GPU Usage
+
+ ui.system.general.gpuUsage
+
{gpuSeries.map((series) => (
{gpuMemSeries && (
-
GPU Memory
+
+ ui.system.general.gpuMemroy
+
{gpuMemSeries.map((series) => (
{gpuEncSeries && gpuEncSeries?.length != 0 && (
-
GPU Encoder
+
+ ui.system.general.gpuEncoder
+
{gpuEncSeries.map((series) => (
{gpuDecSeries && gpuDecSeries?.length != 0 && (
-
GPU Decoder
+
+ ui.system.general.gpuDecoder
+
{gpuDecSeries.map((series) => (
- Other Processes
+ ui.system.general.otherProcesses
{statsHistory.length != 0 ? (
-
Process CPU Usage
+
+ ui.system.general.processCpuUsage
+
{otherProcessCpuSeries.map((series) => (
- Process Memory Usage
+
+ ui.system.general.processMemoryUsage
+
{otherProcessMemSeries.map((series) => (
- Overview
+
+ ui.system.storage.overview
+
- Recordings
+
ui.system.storage.recordings
- 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.recordings.tips
@@ -135,7 +136,7 @@ export default function StorageMetrics({
- Camera Storage
+ ui.system.storage.cameraStorage