chore: add more translation key

This commit is contained in:
ZhaiSoul 2025-03-09 22:30:22 +08:00
parent e72e27e058
commit 10fa4ec2ca
56 changed files with 1026 additions and 507 deletions

20
web/package-lock.json generated
View File

@ -5477,15 +5477,6 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
"dev": true,
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/i18next": { "node_modules/i18next": {
"version": "24.2.0", "version": "24.2.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz",
@ -8590,7 +8581,7 @@
"version": "5.8.2", "version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@ -9016,6 +9007,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": { "node_modules/vscode-jsonrpc": {
"version": "8.2.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",

View File

@ -64,6 +64,7 @@
"info": "Info" "info": "Info"
}, },
"menu": { "menu": {
"system": "System",
"systemMetrics": "System metrics", "systemMetrics": "System metrics",
"configuration": "Configuration", "configuration": "Configuration",
"systemLogs": "System logs", "systemLogs": "System logs",
@ -112,5 +113,11 @@
"error": "Failed to save config changes: {{errorMessage}}", "error": "Failed to save config changes: {{errorMessage}}",
"error.noMessage": "Failed to save config changes" "error.noMessage": "Failed to save config changes"
} }
},
"role": {
"title": "Role",
"admin": "Admin",
"viewer": "Viewer",
"desc": "Admins have full access to all features in the Frigate UI. Viewers are limited to viewing cameras, review items, and historical footage in the UI."
} }
} }

View File

@ -29,7 +29,7 @@
"regenerateFromThumbnails": "Regenerate from Thumbnails", "regenerateFromThumbnails": "Regenerate from Thumbnails",
"tips": { "tips": {
"descriptionSaved": "Successfully saved description", "descriptionSaved": "Successfully saved description",
"saveDescriptionFailed": "Failed to update the description" "saveDescriptionFailed": "Failed to update the description: {{errorMessage}}"
} }
}, },
"itemMenu": { "itemMenu": {
@ -59,5 +59,6 @@
"title": "Confirm Delete", "title": "Confirm Delete",
"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 <em>NOT</em> be deleted.<br /><br />Are you sure you want to proceed?" "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 <em>NOT</em> be deleted.<br /><br />Are you sure you want to proceed?"
} }
} },
"noTrackedObjects": "No Tracked Objects Found"
} }

View File

@ -91,6 +91,10 @@
"noDefinedZones": "No zones are defined for this camera.", "noDefinedZones": "No zones are defined for this camera.",
"objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.", "objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.",
"zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.", "zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.",
"objectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in.",
"zoneObjectDetectionsTips": "All {{detectionsLabels}} objects not categorized in {{zone}} on {{cameraName}} will be shown as Detections.",
"zoneObjectDetectionsTips.notSelectDetections": "All {{detectionsLabels}} objects detected in {{zone}} on {{cameraName}} not categorized as Alerts will be shown as Detections regardless of which zone they are in.",
"zoneObjectDetectionsTips.regardlessOfZoneObjectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in.",
"selectAlertsZones": "Select zones for Alerts", "selectAlertsZones": "Select zones for Alerts",
"selectDetectionsZones": "Select zones for Detections", "selectDetectionsZones": "Select zones for Detections",
"limitDetections": "Limit detections to specific zones", "limitDetections": "Limit detections to specific zones",
@ -103,9 +107,24 @@
"filter": { "filter": {
"all": "All Masks and Zones" "all": "All Masks and Zones"
}, },
"polygonDrawing": { "form": {
"error": { "zoneName": {
"mustBeFinished": "多边形绘制必须完成闭合后才能保存。" "error": {
"mustBeAtLeastTwoCharacters": "Zone name must be at least 2 characters.",
"mustNotBeSameWithCamera": "Zone name must not be the same as camera name.",
"alreadyExists": "A zone with this name already exists for this camera.",
"mustNotContainPeriod": "Zone name must not contain periods.",
"hasIllegalCharacter": "Zone name contains illegal characters."
}
},
"distance.error": "Distance must be greater than or equal to 0.1.",
"distance.error.mustBeFilled": "All distance fields must be filled to use speed estimation.",
"inertia.error.mustBeAboveZero": "Inertia must be above 0.",
"loiteringTime.error.mustBeGreaterOrEqualZero": "Loitering time must be greater than or equal to 0.",
"polygonDrawing": {
"error": {
"mustBeFinished": "Polygon drawing must be finished before saving."
}
} }
}, },
"zones": { "zones": {
@ -182,7 +201,9 @@
"contourArea.desc": "The contour area value is used to decide which groups of changed pixels qualify as motion. <em>Default: 10</em>", "contourArea.desc": "The contour area value is used to decide which groups of changed pixels qualify as motion. <em>Default: 10</em>",
"improveContrast": "Improve Contrast", "improveContrast": "Improve Contrast",
"improveContrast.desc": "Improve contrast for darker scenes. <em>Default: ON</em>", "improveContrast.desc": "Improve contrast for darker scenes. <em>Default: ON</em>",
"toast.success": "Motion settings have been saved.", "toast": {
"success": "Motion settings have been saved."
}
}, },
"debug": { "debug": {
"title": "Debug", "title": "Debug",
@ -230,29 +251,86 @@
}, },
"users": { "users": {
"title": "Users", "title": "Users",
"management": "Users Management",
"management.desc": "Manage this Frigate instance's user accounts.",
"addUser": "Add User", "addUser": "Add User",
"updatePassword": "Update Password", "updatePassword": "Update Password",
"toast": { "toast": {
"success": {
"createUser": "User {{user}} created successfully",
"deleteUser": "User {{user}} deleted successfully"
},
"error": { "error": {
"setPasswordFailed": "Error setting password", "setPasswordFailed": "Failed to save password: {{errorMessage}}",
"createUserFailed": "Error creating user. Check server logs.", "createUserFailed": "Failed to create user: {{errorMessage}}",
"deleteUserFailed": "Error deleting user. Check server logs." "deleteUserFailed": "Failed to delete user: {{errorMessage}}"
} }
}, },
"table": {
"username": "Username",
"actions": "Actions",
"role": "Role",
"noUsers": "No users found.",
"changeRole": "Change user role",
"password": "Password",
"deleteUser": "Delete user"
},
"dialog": { "dialog": {
"form": {
"user": "Username",
"user.desc": "Only letters, numbers, periods and underscores allowed.",
"user.placeholder": "Enter username",
"password": "Password",
"password.placeholder": "Enter password",
"password.confirm": "Confirm Password",
"password.confirm.placeholder": "Confirm Password",
"password.strength": "password strength: ",
"password.strength.weak": "Weak",
"password.strength.medium": "Medium",
"password.strength.strong": "Strong",
"password.strength.veryStrong": "Very strong",
"password.match": "Passwords match",
"password.notMatch": "Passwords don't match",
"newPassword": "New Password",
"newPassword.placeholder": "Enter new password",
"newPassword.confirm.placeholder": "Re-enter new password",
"usernameIsRequired": "Username is required"
},
"createUser": { "createUser": {
"title": "Create User", "title": "Create New User",
"user": "user", "desc": "Add a new user account and specify an role for access to areas of the Frigate UI.",
"password": "password",
"usernameOnlyInclude": "Username may only include letters, numbers, . or _" "usernameOnlyInclude": "Username may only include letters, numbers, . or _"
}, },
"deleteUser": { "deleteUser": {
"title": "Delete User", "title": "Delete User",
"warn": "Are you sure?" "desc": "This action cannot be undone. This will permanently delete the user account and remove all associated data.",
"warn": "Are you sure you want to delete <span className=\"font-bold\">{{username}}</span>?"
}, },
"setPassword": { "passwordSetting": {
"title": "Set Password" "updatePassword": "Update Password for {{username}}",
"setPassword": "Set Password",
"desc": "Create a strong password to secure this account."
},
"changeRole": {
"title": "Change User Role",
"desc": "Update permissions for <span className=\"font-medium\">{{username}}</span>",
"roleInfo": "<p>Select the appropriate role for this user:</p><ul className=\"mt-2 space-y-1 pl-5\"><li> • <span className=\"font-medium\">Admin:</span> Full access to all features. </li><li> • <span className=\"font-medium\">Viewer:</span> Limited to Live dashboards, Review, Explore, and Exports only.</li></ul>"
} }
} }
},
"notification": {
"title": "Notifications",
"notificationSettings": "Notification Settings",
"desc": "Frigate can send native push notifications to your device when running in the browser or installed as a PWA.",
"documentation": "Read the Documentation",
"email": "Email",
"email.placeholder": "e.g. example@email.com",
"email.desc": "A valid email is required and will be used to notify you if there are any issues with the push service.",
"cameras": "Cameras",
"cameras.noCameras": "No cameras available",
"cameras.desc": "Select which cameras to enable notifications for.",
"deviceSpecific": "Device Specific Settings",
"registerDevice": "Register This Device",
"unregisterDevice": "Unregister This Device"
} }
} }

View File

@ -112,5 +112,11 @@
"error": "保存配置信息失败: {{errorMessage}}", "error": "保存配置信息失败: {{errorMessage}}",
"error.noMessage": "保存配置信息失败" "error.noMessage": "保存配置信息失败"
} }
},
"role": {
"title": "权限组",
"admin": "管理员",
"viewer": "查看者",
"desc": "管理员可以完全访问 Frigate UI 的所有功能。查看者则仅限于在 UI 中查看摄像头、审核项和历史录像。"
} }
} }

View File

@ -29,7 +29,7 @@
"regenerateFromThumbnails": "从缩略图重新生成", "regenerateFromThumbnails": "从缩略图重新生成",
"tips": { "tips": {
"descriptionSaved": "已保存描述", "descriptionSaved": "已保存描述",
"saveDescriptionFailed": "更新描述失败" "saveDescriptionFailed": "更新描述失败{{errorMessage}}"
} }
}, },
"itemMenu": { "itemMenu": {

View File

@ -158,7 +158,7 @@
"motionMasks": { "motionMasks": {
"label": "运动遮罩", "label": "运动遮罩",
"documentTitle": "编辑运动遮罩 - Frigate", "documentTitle": "编辑运动遮罩 - Frigate",
"desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪", "desc": "运动遮罩用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
"desc.documentation": "文档(英文)", "desc.documentation": "文档(英文)",
"add": "添加运动遮罩", "add": "添加运动遮罩",
"edit": "编辑运动遮罩", "edit": "编辑运动遮罩",
@ -200,7 +200,10 @@
"contourArea": "轮廓面积", "contourArea": "轮廓面积",
"contourArea.desc": "轮廓面积决定哪些变化的像素组符合运动条件。<em>默认值10</em>", "contourArea.desc": "轮廓面积决定哪些变化的像素组符合运动条件。<em>默认值10</em>",
"improveContrast": "提高对比度", "improveContrast": "提高对比度",
"improveContrast.desc": "提高较暗场景的对比度。默认值:开启" "improveContrast.desc": "提高较暗场景的对比度。默认值:开启",
"toast": {
"success": "运动设置已保存。"
}
}, },
"debug": { "debug": {
"title": "调试", "title": "调试",
@ -248,29 +251,73 @@
}, },
"users": { "users": {
"title": "用户", "title": "用户",
"management": "用户管理",
"management.desc": "管理此 Frigate 实例的用户账户。",
"addUser": "添加用户", "addUser": "添加用户",
"updatePassword": "修改密码", "updatePassword": "修改密码",
"toast": { "toast": {
"success": {
"createUser": "用户 {{user}} 创建成功",
"deleteUser": "用户 {{user}} 删除成功"
},
"error": { "error": {
"setPasswordFailed": "保存密码出现错误", "setPasswordFailed": "保存密码出现错误{{errorMessage}}",
"createUserFailed": "创建用户失败!请检查后台日志。", "createUserFailed": "创建用户失败{{errorMessage}}",
"deleteUserFailed": "删除用户失败!请检查后台日志。" "deleteUserFailed": "删除用户失败{{errorMessage}}"
} }
}, },
"table": {
"username": "用户名",
"actions": "操作",
"role": "权限组",
"noUsers": "未找到用户。",
"changeRole": "更改用户角色",
"password": "密码",
"deleteUser": "删除用户"
},
"dialog": { "dialog": {
"form": {
"user": "用户名",
"user.desc": "仅允许使用字母、数字、句点和下划线。",
"user.placeholder": "请输入用户名",
"password": "密码",
"password.placeholder": "请输入密码",
"password.confirm": "确认密码",
"password.confirm.placeholder": "请再次输入密码",
"password.strength": "密码强度:",
"password.strength.weak": "弱",
"password.strength.medium": "中等",
"password.strength.strong": "强",
"password.strength.veryStrong": "非常强",
"password.match": "密码匹配",
"password.notMatch": "密码不匹配",
"newPassword": "新密码",
"newPassword.placeholder": "请输入新密码",
"newPassword.confirm.placeholder": "请再次输入新密码",
"usernameIsRequired": "用户名为必填项"
},
"createUser": { "createUser": {
"title": "创建用户", "title": "创建新用户",
"desc": "创建一个新用户账户,并指定一个角色以控制访问 Frigate UI 的权限。",
"user": "用户", "user": "用户",
"password": "密码", "password": "密码",
"usernameOnlyInclude": "用户名只能包含字母、数字和 _" "usernameOnlyInclude": "用户名只能包含字母、数字和 _"
}, },
"deleteUser": { "deleteUser": {
"title": "删除该用户", "title": "删除该用户",
"warn": "你确定要删除该用户吗?" "desc": "此操作无法撤销。这将永久删除用户账户并移除所有相关数据。",
"warn": "你确定要删除 <span className=\"font-bold\">{{username}}</span> 吗?"
}, },
"setPassword": { "passwordSetting": {
"title": "修改密码" "updatePassword": "更新 {{username}} 的密码",
} "setPassword": "设置密码",
"desc": "创建一个强密码来保护此账户。"
},
"changeRole": {
"title": "更改用户权限组",
"desc": "更新 <span className=\"font-medium\">{{username}}</span> 的权限",
"roleInfo": "<p>请选择此用户的适当角色:</p><ul className=\"mt-2 space-y-1 pl-5\"><li> • <span className=\"font-medium\">管理员 (Admin)</span> 拥有所有功能的完整访问权限。</li><li> • <span className=\"font-medium\">查看者 (Viewer)</span> 仅限访问实时监控、回放、探测和导出功能。</li></ul>"
}
} }
}, },
"notification": { "notification": {

View File

@ -83,9 +83,12 @@ export default function ReviewCard({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success(t("export.toast.success", { ns: "components/dialog"}), { toast.success(
position: "top-center", t("export.toast.success", { ns: "components/dialog" }),
}); {
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {

View File

@ -66,12 +66,9 @@ const timeAgo = ({
if (monthDiff > 0) { if (monthDiff > 0) {
const unitAmount = monthDiff; const unitAmount = monthDiff;
return t("time.ago", { return t("time.ago", {
timeAgo: t( timeAgo: t(`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, {
`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, time: unitAmount,
{ }),
time: unitAmount,
},
),
}); });
} }
} else if (elapsed >= timeUnits[i].value) { } else if (elapsed >= timeUnits[i].value) {

View File

@ -32,7 +32,7 @@ export default function CalendarFilterButton({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const selectedDate = useFormattedTimestamp( const selectedDate = useFormattedTimestamp(
day == undefined ? 0 : day?.getTime() / 1000 + 1, day == undefined ? 0 : day?.getTime() / 1000 + 1,
"%b %-d", t("time.formattedTimestampOnlyMonthAndDay"),
); );
const trigger = ( const trigger = (
@ -48,7 +48,9 @@ export default function CalendarFilterButton({
<div <div
className={`hidden md:block ${day == undefined ? "text-primary" : "text-selected-foreground"}`} className={`hidden md:block ${day == undefined ? "text-primary" : "text-selected-foreground"}`}
> >
{day == undefined ? t("calendarFilter.last24Hours", {ns: "views/events"}) : selectedDate} {day == undefined
? t("calendarFilter.last24Hours", { ns: "views/events" })
: selectedDate}
</div> </div>
</Button> </Button>
); );

View File

@ -656,7 +656,9 @@ export function CameraGroupEdit({
name: z name: z
.string() .string()
.min(2, { .min(2, {
message: t("group.name.errorMessage.mustLeastCharacters", { ns: "components/camera" }), message: t("group.name.errorMessage.mustLeastCharacters", {
ns: "components/camera",
}),
}) })
.transform((val: string) => val.trim().replace(/\s+/g, "_")) .transform((val: string) => val.trim().replace(/\s+/g, "_"))
.refine( .refine(
@ -667,7 +669,9 @@ export function CameraGroupEdit({
); );
}, },
{ {
message: t("group.name.errorMessage.exists", { ns: "components/camera" }), message: t("group.name.errorMessage.exists", {
ns: "components/camera",
}),
}, },
) )
.refine( .refine(
@ -679,7 +683,9 @@ export function CameraGroupEdit({
}, },
) )
.refine((value: string) => value.toLowerCase() !== "default", { .refine((value: string) => value.toLowerCase() !== "default", {
message: t("group.name.errorMessage.invalid", { ns: "components/camera" }), message: t("group.name.errorMessage.invalid", {
ns: "components/camera",
}),
}), }),
cameras: z.array(z.string()), cameras: z.array(z.string()),
@ -735,7 +741,10 @@ export function CameraGroupEdit({
.then(async (res) => { .then(async (res) => {
if (res.status === 200) { if (res.status === 200) {
toast.success( toast.success(
t("group.toast.success", { name: values.name, ns: "components/camera"}), t("group.toast.success", {
name: values.name,
ns: "components/camera",
}),
{ {
position: "top-center", position: "top-center",
}, },
@ -756,9 +765,9 @@ export function CameraGroupEdit({
}) })
.catch((error) => { .catch((error) => {
const errorMessage = const errorMessage =
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error( toast.error(
t("toast.save.error", { t("toast.save.error", {
errorMessage, errorMessage,
@ -809,7 +818,9 @@ export function CameraGroupEdit({
<FormControl> <FormControl>
<Input <Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder={t("group.name.placeholder", { ns: "components/camera" })} placeholder={t("group.name.placeholder", {
ns: "components/camera",
})}
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@ -153,7 +153,7 @@ export function CamerasFilterContent({
<div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4"> <div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
<FilterSwitch <FilterSwitch
isChecked={currentCameras == undefined} isChecked={currentCameras == undefined}
label={t("cameras.all", { ns: "components/filter"})} label={t("cameras.all", { ns: "components/filter" })}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {
setCurrentCameras(undefined); setCurrentCameras(undefined);

View File

@ -198,7 +198,9 @@ export default function SearchFilterGroup({
} }
} }
defaultText={ defaultText={
isMobile ? t("dates.all.short", {ns: "components/filter"}) : t("dates.all", {ns: "components/filter"}) isMobile
? t("dates.all.short", { ns: "components/filter" })
: t("dates.all", { ns: "components/filter" })
} }
updateSelectedRange={onUpdateSelectedRange} updateSelectedRange={onUpdateSelectedRange}
/> />
@ -240,18 +242,21 @@ function GeneralFilterButton({
const buttonText = useMemo(() => { const buttonText = useMemo(() => {
if (isMobile) { if (isMobile) {
return t("labels.all.short", {ns: "components/filter"}); return t("labels.all.short", { ns: "components/filter" });
} }
if (!selectedLabels || selectedLabels.length == 0) { if (!selectedLabels || selectedLabels.length == 0) {
return t("labels.all", {ns: "components/filter"}); return t("labels.all", { ns: "components/filter" });
} }
if (selectedLabels.length == 1) { if (selectedLabels.length == 1) {
return t(selectedLabels[0], { ns: "objects"}); return t(selectedLabels[0], { ns: "objects" });
} }
return t("labels.count", { count: selectedLabels.length, ns: "components/filter" }); return t("labels.count", {
count: selectedLabels.length,
ns: "components/filter",
});
}, [selectedLabels]); }, [selectedLabels]);
// ui // ui
@ -352,7 +357,7 @@ export function GeneralFilterContent({
{allLabels.map((item) => ( {allLabels.map((item) => (
<FilterSwitch <FilterSwitch
key={item} key={item}
label={t(item, {ns: "objects"})} label={t(item, { ns: "objects" })}
isChecked={currentLabels?.includes(item) ?? false} isChecked={currentLabels?.includes(item) ?? false}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {
@ -501,13 +506,13 @@ export function SortTypeContent({
onClose, onClose,
}: SortTypeContentProps) { }: SortTypeContentProps) {
const sortLabels = { const sortLabels = {
date_asc: t("sort.dateAsc", {ns: "components/filter"}), date_asc: t("sort.dateAsc", { ns: "components/filter" }),
date_desc: t("sort.dateDesc", {ns: "components/filter"}), date_desc: t("sort.dateDesc", { ns: "components/filter" }),
score_asc: t("sort.scoreAsc", {ns: "components/filter"}), score_asc: t("sort.scoreAsc", { ns: "components/filter" }),
score_desc: t("sort.scoreDesc", {ns: "components/filter"}), score_desc: t("sort.scoreDesc", { ns: "components/filter" }),
speed_asc: t("sort.speedAsc", {ns: "components/filter"}), speed_asc: t("sort.speedAsc", { ns: "components/filter" }),
speed_desc: t("sort.speedDesc", {ns: "components/filter"}), speed_desc: t("sort.speedDesc", { ns: "components/filter" }),
relevance: t("sort.relevance", {ns: "components/filter"}), relevance: t("sort.relevance", { ns: "components/filter" }),
}; };
return ( return (
<> <>

View File

@ -128,7 +128,7 @@ export function CameraLineGraph({
style={{ color: GRAPH_COLORS[labelIdx] }} style={{ color: GRAPH_COLORS[labelIdx] }}
/> />
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{t("cameras.label." + label, {ns: "views/settings"})} {t("cameras.label." + label, { ns: "views/settings" })}
</div> </div>
<div className="text-xs text-primary"> <div className="text-xs text-primary">
{lastValues[labelIdx]} {lastValues[labelIdx]}

View File

@ -182,7 +182,9 @@ export function CombinedStorageGraph({
<Trans ns="views/system">storage.cameraStorage.camera</Trans> <Trans ns="views/system">storage.cameraStorage.camera</Trans>
</TableHead> </TableHead>
<TableHead> <TableHead>
<Trans ns="views/system">storage.cameraStorage.storageUsed</Trans> <Trans ns="views/system">
storage.cameraStorage.storageUsed
</Trans>
</TableHead> </TableHead>
<TableHead> <TableHead>
<Trans ns="views/system"> <Trans ns="views/system">
@ -205,8 +207,8 @@ export function CombinedStorageGraph({
></div> ></div>
{item.name === "Unused" {item.name === "Unused"
? t("storage.cameraStorage.unused", { ? t("storage.cameraStorage.unused", {
ns: "views/system", ns: "views/system",
}) })
: item.name.replaceAll("_", " ")} : item.name.replaceAll("_", " ")}
{item.name === "Unused" && ( {item.name === "Unused" && (
<Popover> <Popover>

View File

@ -117,7 +117,9 @@ export default function IconPicker({
</div> </div>
<Input <Input
type="text" type="text"
placeholder={t("iconPicker.search.placeholder", {ns: "components/icons"})} placeholder={t("iconPicker.search.placeholder", {
ns: "components/icons",
})}
className="text-md mb-3 md:text-sm" className="text-md mb-3 md:text-sm"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}

View File

@ -37,7 +37,7 @@ export function SaveSearchDialog({
toast.success( toast.success(
t("search.saveSearch.success", { t("search.saveSearch.success", {
searchName: searchName.trim(), searchName: searchName.trim(),
ns: "components/dialog" ns: "components/dialog",
}), }),
{ {
position: "top-center", position: "top-center",
@ -73,7 +73,9 @@ export function SaveSearchDialog({
value={searchName} value={searchName}
className="text-md" className="text-md"
onChange={(e) => setSearchName(e.target.value)} onChange={(e) => setSearchName(e.target.value)}
placeholder={t("search.saveSearch.placeholder", {ns: "components/dialog"})} placeholder={t("search.saveSearch.placeholder", {
ns: "components/dialog",
})}
/> />
{overwrite && ( {overwrite && (
<div className="ml-1 text-sm text-danger"> <div className="ml-1 text-sm text-danger">

View File

@ -11,6 +11,7 @@ import {
LuSettings, LuSettings,
LuSun, LuSun,
LuSunMoon, LuSunMoon,
LuEarth,
} from "react-icons/lu"; } from "react-icons/lu";
import { import {
DropdownMenu, DropdownMenu,
@ -195,7 +196,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
)} )}
{isAdmin && ( {isAdmin && (
<> <>
<DropdownMenuLabel><Trans>menu.system</Trans></DropdownMenuLabel> <DropdownMenuLabel>
<Trans>menu.system</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}> <DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
<Link to="/system#general"> <Link to="/system#general">
@ -208,7 +211,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="System metrics" aria-label="System metrics"
> >
<LuActivity className="mr-2 size-4" /> <LuActivity className="mr-2 size-4" />
<span><Trans>menu.systemMetrics</Trans></span> <span>
<Trans>menu.systemMetrics</Trans>
</span>
</MenuItem> </MenuItem>
</Link> </Link>
<Link to="/logs"> <Link to="/logs">
@ -221,7 +226,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="System logs" aria-label="System logs"
> >
<LuList className="mr-2 size-4" /> <LuList className="mr-2 size-4" />
<span><Trans>menu.systemLogs</Trans></span> <span>
<Trans>menu.systemLogs</Trans>
</span>
</MenuItem> </MenuItem>
</Link> </Link>
</DropdownMenuGroup> </DropdownMenuGroup>
@ -261,7 +268,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
aria-label="Configuration editor" aria-label="Configuration editor"
> >
<LuSquarePen className="mr-2 size-4" /> <LuSquarePen className="mr-2 size-4" />
<span><Trans>menu.configurationEditor</Trans></span> <span>
<Trans>menu.configurationEditor</Trans>
</span>
</MenuItem> </MenuItem>
</Link> </Link>
</> </>
@ -271,6 +280,87 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<Trans>menu.appearance</Trans> <Trans>menu.appearance</Trans>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<SubItem>
<SubItemTrigger
className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
}
>
<LuLanguages className="mr-2 size-4" />
<span>
<Trans>menu.languages</Trans>
</span>
</SubItemTrigger>
<Portal>
<SubItemContent
className={
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
<span tabIndex={0} className="sr-only" />
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Light mode"
onClick={() => setLanguage("en")}
>
{language.trim() === "en" ? (
<>
<LuLanguages className="mr-2 size-4" />
<Trans>menu.language.en</Trans>
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.language.en</Trans>
</span>
)}
</MenuItem>
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Dark mode"
onClick={() => setLanguage("zh-CN")}
>
{language === "zh-CN" ? (
<>
<LuLanguages className="mr-2 size-4" />
<Trans>menu.language.zhCN</Trans>
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.language.zhCN</Trans>
</span>
)}
</MenuItem>
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Use the system settings for light or dark mode"
onClick={() => setLanguage(systemLanguage)}
>
{language === systemLanguage ? (
<>
<LuEarth className="mr-2 size-4 scale-100 transition-all" />
<Trans>menu.withSystem</Trans>
</>
) : (
<span className="ml-6 mr-2">
<Trans>menu.withSystem</Trans>
</span>
)}
</MenuItem>
</SubItemContent>
</Portal>
</SubItem>
<SubItem> <SubItem>
<SubItemTrigger <SubItemTrigger
className={ className={
@ -278,7 +368,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
} }
> >
<LuSunMoon className="mr-2 size-4" /> <LuSunMoon className="mr-2 size-4" />
<span><Trans>menu.darkMode.label</Trans></span> <span>
<Trans>menu.darkMode.label</Trans>
</span>
</SubItemTrigger> </SubItemTrigger>
<Portal> <Portal>
<SubItemContent <SubItemContent
@ -302,7 +394,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<Trans>menu.darkMode.light</Trans> <Trans>menu.darkMode.light</Trans>
</> </>
) : ( ) : (
<span className="ml-6 mr-2"><Trans>menu.darkMode.light</Trans></span> <span className="ml-6 mr-2">
<Trans>menu.darkMode.light</Trans>
</span>
)} )}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
@ -320,7 +414,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<Trans>menu.darkMode.dark</Trans> <Trans>menu.darkMode.dark</Trans>
</> </>
) : ( ) : (
<span className="ml-6 mr-2"><Trans>menu.darkMode.dark</Trans></span> <span className="ml-6 mr-2">
<Trans>menu.darkMode.dark</Trans>
</span>
)} )}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
@ -338,7 +434,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<Trans>menu.withSystem</Trans> <Trans>menu.withSystem</Trans>
</> </>
) : ( ) : (
<span className="ml-6 mr-2"><Trans>menu.withSystem</Trans></span> <span className="ml-6 mr-2">
<Trans>menu.withSystem</Trans>
</span>
)} )}
</MenuItem> </MenuItem>
</SubItemContent> </SubItemContent>
@ -351,7 +449,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
} }
> >
<LuSunMoon className="mr-2 size-4" /> <LuSunMoon className="mr-2 size-4" />
<span><Trans>menu.theme.label</Trans></span> <span>
<Trans>menu.theme.label</Trans>
</span>
</SubItemTrigger> </SubItemTrigger>
<Portal> <Portal>
<SubItemContent <SubItemContent
@ -432,7 +532,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
onClick={() => setRestartDialogOpen(true)} onClick={() => setRestartDialogOpen(true)}
> >
<LuRotateCw className="mr-2 size-4" /> <LuRotateCw className="mr-2 size-4" />
<span><Trans>menu.restart</Trans></span> <span>
<Trans>menu.restart</Trans>
</span>
</MenuItem> </MenuItem>
</> </>
)} )}

View File

@ -242,7 +242,10 @@ export default function LiveContextMenu({
time_style: "medium", time_style: "medium",
date_style: "medium", date_style: "medium",
timezone: config?.ui.timezone, timezone: config?.ui.timezone,
strftime_fmt: config?.ui.time_format == "24hour" ? t("time.formattedTimestampExcludeSeconds.24hour"): t("time.formattedTimestampExcludeSeconds"), strftime_fmt:
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour")
: t("time.formattedTimestampExcludeSeconds"),
}); });
}; };
@ -359,7 +362,9 @@ export default function LiveContextMenu({
className="flex w-full cursor-pointer items-center justify-start gap-2" className="flex w-full cursor-pointer items-center justify-start gap-2"
onClick={isEnabled ? resetPreferredLiveMode : undefined} onClick={isEnabled ? resetPreferredLiveMode : undefined}
> >
<div className="text-primary"><Trans>button</Trans></div> <div className="text-primary">
<Trans>button</Trans>
</div>
</div> </div>
</ContextMenuItem> </ContextMenuItem>
</> </>

View File

@ -92,7 +92,9 @@ export default function SearchResultActions({
const menuItems = ( const menuItems = (
<> <>
{searchResult.has_clip && ( {searchResult.has_clip && (
<MenuItem aria-label={t("itemMenu.downloadVideo.aria", {ns: "views/explore"})}> <MenuItem
aria-label={t("itemMenu.downloadVideo.aria", { ns: "views/explore" })}
>
<a <a
className="flex items-center" className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`} href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
@ -107,7 +109,9 @@ export default function SearchResultActions({
)} )}
{searchResult.has_snapshot && ( {searchResult.has_snapshot && (
<MenuItem <MenuItem
aria-label={t("itemMenu.downloadSnapshot.aria", {ns: "views/explore"})} aria-label={t("itemMenu.downloadSnapshot.aria", {
ns: "views/explore",
})}
> >
<a <a
className="flex items-center" className="flex items-center"
@ -123,7 +127,9 @@ export default function SearchResultActions({
)} )}
{searchResult.data.type == "object" && ( {searchResult.data.type == "object" && (
<MenuItem <MenuItem
aria-label={t("itemMenu.viewObjectLifecycle.aria", {ns: "views/explore"})} aria-label={t("itemMenu.viewObjectLifecycle.aria", {
ns: "views/explore",
})}
onClick={showObjectLifecycle} onClick={showObjectLifecycle}
> >
<FaArrowsRotate className="mr-2 size-4" /> <FaArrowsRotate className="mr-2 size-4" />
@ -134,7 +140,7 @@ export default function SearchResultActions({
)} )}
{config?.semantic_search?.enabled && isContextMenu && ( {config?.semantic_search?.enabled && isContextMenu && (
<MenuItem <MenuItem
aria-label={t("itemMenu.findSimilar.aria", {ns: "views/explore"})} aria-label={t("itemMenu.findSimilar.aria", { ns: "views/explore" })}
onClick={findSimilar} onClick={findSimilar}
> >
<MdImageSearch className="mr-2 size-4" /> <MdImageSearch className="mr-2 size-4" />
@ -150,7 +156,9 @@ export default function SearchResultActions({
searchResult.data.type == "object" && searchResult.data.type == "object" &&
!searchResult.plus_id && ( !searchResult.plus_id && (
<MenuItem <MenuItem
aria-label={t("itemMenu.submitToPlus.aria", {ns: "views/explore"})} aria-label={t("itemMenu.submitToPlus.aria", {
ns: "views/explore",
})}
onClick={showSnapshot} onClick={showSnapshot}
> >
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" /> <FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
@ -215,7 +223,9 @@ export default function SearchResultActions({
onClick={findSimilar} onClick={findSimilar}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><Trans ns="views/explore">itemMenu.findSimilar.label</Trans></TooltipContent> <TooltipContent>
<Trans ns="views/explore">itemMenu.findSimilar.label</Trans>
</TooltipContent>
</Tooltip> </Tooltip>
)} )}
@ -232,7 +242,9 @@ export default function SearchResultActions({
onClick={showSnapshot} onClick={showSnapshot}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><Trans ns="views/explore">itemMenu.submitToPlus.label</Trans></TooltipContent> <TooltipContent>
<Trans ns="views/explore">itemMenu.submitToPlus.label</Trans>
</TooltipContent>
</Tooltip> </Tooltip>
)} )}

View File

@ -74,9 +74,9 @@ export default function CameraInfoDialog({
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle className="capitalize"> <DialogTitle className="capitalize">
{t("system.cameras.info.cameraProbeInfo", { {t("cameras.info.cameraProbeInfo", {
camera: camera.name.replaceAll("_", " "), camera: camera.name.replaceAll("_", " "),
ns: "views/system" ns: "views/system",
})} })}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
@ -90,7 +90,10 @@ export default function CameraInfoDialog({
{ffprobeInfo.map((stream, idx) => ( {ffprobeInfo.map((stream, idx) => (
<div key={idx} className="mb-5"> <div key={idx} className="mb-5">
<div className="mb-1 rounded-md bg-secondary p-2 text-lg text-primary"> <div className="mb-1 rounded-md bg-secondary p-2 text-lg text-primary">
{t("cameras.info.stream", { idx: idx + 1, ns: "views/system" })} {t("cameras.info.stream", {
idx: idx + 1,
ns: "views/system",
})}
</div> </div>
{stream.return_code == 0 ? ( {stream.return_code == 0 ? (
<div> <div>
@ -99,11 +102,15 @@ export default function CameraInfoDialog({
{codec.width ? ( {codec.width ? (
<div className="text-muted-foreground"> <div className="text-muted-foreground">
<div className="ml-2"> <div className="ml-2">
<Trans ns="views/system">cameras.info.video</Trans> <Trans ns="views/system">
cameras.info.video
</Trans>
</div> </div>
<div className="ml-5"> <div className="ml-5">
<div> <div>
<Trans ns="views/system">cameras.info.codec</Trans> <Trans ns="views/system">
cameras.info.codec
</Trans>
<span className="text-primary"> <span className="text-primary">
{" "} {" "}
{codec.codec_long_name} {codec.codec_long_name}
@ -138,10 +145,14 @@ export default function CameraInfoDialog({
)} )}
</div> </div>
<div> <div>
<Trans ns="views/system">cameras.info.fps</Trans>{" "} <Trans ns="views/system">
cameras.info.fps
</Trans>{" "}
<span className="text-primary"> <span className="text-primary">
{codec.avg_frame_rate == "0/0" {codec.avg_frame_rate == "0/0"
? t("cameras.info.unknown", { ns: "views/system" }) ? t("cameras.info.unknown", {
ns: "views/system",
})
: codec.avg_frame_rate} : codec.avg_frame_rate}
</span> </span>
</div> </div>
@ -151,7 +162,9 @@ export default function CameraInfoDialog({
<div className="text-muted-foreground"> <div className="text-muted-foreground">
<div className="ml-2 mt-1">Audio:</div> <div className="ml-2 mt-1">Audio:</div>
<div className="ml-4"> <div className="ml-4">
<Trans ns="views/system">cameras.info.codec</Trans>{" "} <Trans ns="views/system">
cameras.info.codec
</Trans>{" "}
<span className="text-primary"> <span className="text-primary">
{codec.codec_long_name} {codec.codec_long_name}
</span> </span>
@ -166,7 +179,7 @@ export default function CameraInfoDialog({
<div> <div>
{t("cameras.info.error", { {t("cameras.info.error", {
error: stream.stderr, error: stream.stderr,
ns: "views/system" ns: "views/system",
})} })}
</div> </div>
</div> </div>

View File

@ -51,16 +51,22 @@ export default function CreateUserDialog({
.object({ .object({
user: z user: z
.string() .string()
.min(1, "Username is required") .min(1, t("users.dialog.form.usernameIsRequired", {
ns: "views/settings",
}))
.regex(/^[A-Za-z0-9._]+$/, { .regex(/^[A-Za-z0-9._]+$/, {
message: t("users.dialog.createUser.usernameOnlyInclude", {ns: "views/settings"}), message: t("users.dialog.createUser.usernameOnlyInclude", {
ns: "views/settings",
}),
}), }),
password: z.string().min(1, "Password is required"), password: z.string().min(1, "Password is required"),
confirmPassword: z.string().min(1, "Please confirm your password"), confirmPassword: z.string().min(1, "Please confirm your password"),
role: z.enum(["admin", "viewer"]), role: z.enum(["admin", "viewer"]),
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match", message: t("users.dialog.form.password.notMatch", {
ns: "views/settings",
}),
path: ["confirmPassword"], path: ["confirmPassword"],
}); });
@ -111,9 +117,11 @@ export default function CreateUserDialog({
<Dialog open={show} onOpenChange={onCancel}> <Dialog open={show} onOpenChange={onCancel}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle><Trans ns="views/settings">users.dialog.createUser.title</Trans></DialogTitle> <DialogTitle>
<Trans ns="views/settings">users.dialog.createUser.title</Trans>
</DialogTitle>
<DialogDescription> <DialogDescription>
<Trans ns="views/settings">users.dialog.createUser.desc</Trans> <Trans ns="views/settings">users.dialog.createUser.desc</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -127,17 +135,21 @@ export default function CreateUserDialog({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="text-sm font-medium"> <FormLabel className="text-sm font-medium">
<Trans ns="views/settings">users.dialog.createUser.user</Trans> <Trans ns="views/settings">
users.dialog.form.user
</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="Enter username" placeholder={t("users.dialog.form.user.placeholder", { ns: "views/settings" })}
className="h-10" className="h-10"
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-xs text-muted-foreground"> <FormDescription className="text-xs text-muted-foreground">
<Trans ns="views/settings">users.dialog.createUser.user.desc</Trans> <Trans ns="views/settings">
users.dialog.form.user.desc
</Trans>
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -149,11 +161,13 @@ export default function CreateUserDialog({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="text-sm font-medium"> <FormLabel className="text-sm font-medium">
<Trans ns="views/settings">users.dialog.createUser.password</Trans> <Trans ns="views/settings">
users.dialog.form.password
</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="Enter password" placeholder={t("users.dialog.form.password.placeholder", { ns: "views/settings" })}
type="password" type="password"
className="h-10" className="h-10"
{...field} {...field}
@ -169,11 +183,13 @@ export default function CreateUserDialog({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="text-sm font-medium"> <FormLabel className="text-sm font-medium">
<Trans ns="views/settings">users.dialog.createUser.confirmPassword</Trans> <Trans ns="views/settings">
users.dialog.form.password.confirm
</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="Confirm password" placeholder={t("users.dialog.form.password.confirm.placeholder", { ns: "views/settings" })}
type="password" type="password"
className="h-10" className="h-10"
{...field} {...field}
@ -185,14 +201,18 @@ export default function CreateUserDialog({
<> <>
<LuCheck className="size-3.5 text-green-500" /> <LuCheck className="size-3.5 text-green-500" />
<span className="text-green-600"> <span className="text-green-600">
<Trans ns="views/settings">users.dialog.createUser.password.match</Trans> <Trans ns="views/settings">
users.dialog.form.password.match
</Trans>
</span> </span>
</> </>
) : ( ) : (
<> <>
<LuX className="size-3.5 text-red-500" /> <LuX className="size-3.5 text-red-500" />
<span className="text-red-600"> <span className="text-red-600">
<Trans ns="views/settings">users.dialog.createUser.password.notMatch</Trans> <Trans ns="views/settings">
users.dialog.form.password.notMatch
</Trans>
</span> </span>
</> </>
)} )}
@ -207,7 +227,9 @@ export default function CreateUserDialog({
name="role" name="role"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="text-sm font-medium"><Trans>role.title</Trans></FormLabel> <FormLabel className="text-sm font-medium">
<Trans>role.title</Trans>
</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
@ -224,7 +246,9 @@ export default function CreateUserDialog({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-primary" /> <Shield className="h-4 w-4 text-primary" />
<span><Trans>role.admin</Trans></span> <span>
<Trans>role.admin</Trans>
</span>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem <SelectItem
@ -233,7 +257,9 @@ export default function CreateUserDialog({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" /> <User className="h-4 w-4 text-muted-foreground" />
<span><Trans>role.viewer</Trans></span> <span>
<Trans>role.viewer</Trans>
</span>
</div> </div>
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -256,7 +282,7 @@ export default function CreateUserDialog({
onClick={handleCancel} onClick={handleCancel}
type="button" type="button"
> >
Cancel <Trans>button.cancel</Trans>
</Button> </Button>
<Button <Button
variant="select" variant="select"
@ -268,7 +294,9 @@ export default function CreateUserDialog({
{isLoading ? ( {isLoading ? (
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<ActivityIndicator /> <ActivityIndicator />
<span><Trans>button.saving</Trans></span> <span>
<Trans>button.saving</Trans>
</span>
</div> </div>
) : ( ) : (
<Trans>button.save</Trans> <Trans>button.save</Trans>

View File

@ -5,7 +5,6 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle,
} from "../ui/dialog"; } from "../ui/dialog";
import { DialogDescription } from "@radix-ui/react-dialog"; import { DialogDescription } from "@radix-ui/react-dialog";
@ -35,7 +34,9 @@ export default function DeleteUserDialog({
<div className="my-4 rounded-md border border-destructive/20 bg-destructive/5 p-4 text-center text-sm"> <div className="my-4 rounded-md border border-destructive/20 bg-destructive/5 p-4 text-center text-sm">
<p className="font-medium text-destructive"> <p className="font-medium text-destructive">
<Trans ns="views/settings" values={{username}}>users.dialog.deleteUser.warn</Trans> <Trans ns="views/settings" values={{ username }}>
users.dialog.deleteUser.warn
</Trans>
</p> </p>
</div> </div>

View File

@ -70,16 +70,26 @@ export default function ExportDialog({
const onStartExport = useCallback(() => { const onStartExport = useCallback(() => {
if (!range) { if (!range) {
toast.error(t("export.toast.error.noVaildTimeSelected", {ns: "components/dialog"}), { toast.error(
position: "top-center", t("export.toast.error.noVaildTimeSelected", {
}); ns: "components/dialog",
}),
{
position: "top-center",
},
);
return; return;
} }
if (range.before < range.after) { if (range.before < range.after) {
toast.error(t("export.toast.error.endTimeMustAfterStartTime", {ns: "components/dialog"}), { toast.error(
position: "top-center", t("export.toast.error.endTimeMustAfterStartTime", {
}); ns: "components/dialog",
}),
{
position: "top-center",
},
);
return; return;
} }
@ -93,9 +103,12 @@ export default function ExportDialog({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success(t("export.toast.success", {ns: "components/dialog"}), { toast.success(
position: "top-center", t("export.toast.success", { ns: "components/dialog" }),
}); {
position: "top-center",
},
);
setName(""); setName("");
setRange(undefined); setRange(undefined);
setMode("none"); setMode("none");
@ -106,10 +119,13 @@ export default function ExportDialog({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error( toast.error(
t("export.toast.error.failed", { error: errorMessage, ns: "components/dialog" }), t("export.toast.error.failed", {
{ position: "top-center" }, error: errorMessage,
); ns: "components/dialog",
}),
{ position: "top-center" },
);
}); });
}, [camera, name, range, setRange, setName, setMode]); }, [camera, name, range, setRange, setName, setMode]);
@ -290,11 +306,11 @@ export function ExportContent({
<Label className="cursor-pointer capitalize" htmlFor={opt}> <Label className="cursor-pointer capitalize" htmlFor={opt}>
{isNaN(parseInt(opt)) {isNaN(parseInt(opt))
? opt == "timeline" ? opt == "timeline"
? t("export.time.fromTimeline", {ns: "components/dialog"}) ? t("export.time.fromTimeline", { ns: "components/dialog" })
: t("export.time." + opt, {ns: "components/dialog"}) : t("export.time." + opt, { ns: "components/dialog" })
: t("export.time.lastHour", { : t("export.time.lastHour", {
count: parseInt(opt), count: parseInt(opt),
ns: "components/dialog" ns: "components/dialog",
})} })}
</Label> </Label>
</div> </div>
@ -311,7 +327,7 @@ export function ExportContent({
<Input <Input
className="text-md my-6" className="text-md my-6"
type="search" type="search"
placeholder={t("export.name.placeholder", {ns: "components/dialog"})} placeholder={t("export.name.placeholder", { ns: "components/dialog" })}
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
/> />
@ -342,8 +358,8 @@ export function ExportContent({
}} }}
> >
{selectedOption == "timeline" {selectedOption == "timeline"
? t("export.select", {ns: "components/dialog"}) ? t("export.select", { ns: "components/dialog" })
: t("export.export", {ns: "components/dialog"})} : t("export.export", { ns: "components/dialog" })}
</Button> </Button>
</DialogFooter> </DialogFooter>
</div> </div>
@ -598,10 +614,14 @@ export function ExportPreviewDialog({
> >
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
<Trans ns="components/dialog">export.fromTimeline.previewExport</Trans> <Trans ns="components/dialog">
export.fromTimeline.previewExport
</Trans>
</DialogTitle> </DialogTitle>
<DialogDescription className="sr-only"> <DialogDescription className="sr-only">
<Trans ns="components/dialog">export.fromTimeline.previewExport</Trans> <Trans ns="components/dialog">
export.fromTimeline.previewExport
</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<GenericVideoPlayer source={source} /> <GenericVideoPlayer source={source} />

View File

@ -98,9 +98,12 @@ export default function MobileReviewSettingsDrawer({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success(t("export.toast.success", { ns: "components/dialog"}), { toast.success(
position: "top-center", t("export.toast.success", { ns: "components/dialog" }),
}); {
position: "top-center",
},
);
setName(""); setName("");
setRange(undefined); setRange(undefined);
setMode("none"); setMode("none");

View File

@ -1,3 +1,4 @@
import { Trans } from "react-i18next";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { import {
Dialog, Dialog,
@ -41,27 +42,16 @@ export default function RoleChangeDialog({
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl font-semibold"> <DialogTitle className="text-xl font-semibold">
Change User Role <Trans ns="views/settings">users.dialog.changeRole.title</Trans>
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
Update permissions for{" "} <Trans ns="views/settings" values={{username}}>users.dialog.changeRole.desc</Trans>
<span className="font-medium">{username}</span>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="py-6"> <div className="py-6">
<div className="mb-4 text-sm text-muted-foreground"> <div className="mb-4 text-sm text-muted-foreground">
<p>Select the appropriate role for this user:</p> <Trans ns="views/settings">users.dialog.changeRole.roleInfo</Trans>
<ul className="mt-2 space-y-1 pl-5">
<li>
<span className="font-medium">Admin:</span> Full access to all
features.
</li>
<li>
<span className="font-medium">Viewer:</span> Limited to Live
dashboards, Review, Explore, and Exports only.
</li>
</ul>
</div> </div>
<Select <Select
@ -77,13 +67,13 @@ export default function RoleChangeDialog({
<SelectItem value="admin" className="flex items-center gap-2"> <SelectItem value="admin" className="flex items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LuShield className="size-4 text-primary" /> <LuShield className="size-4 text-primary" />
<span>Admin</span> <span><Trans>role.admin</Trans></span>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value="viewer" className="flex items-center gap-2"> <SelectItem value="viewer" className="flex items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LuUser className="size-4 text-primary" /> <LuUser className="size-4 text-primary" />
<span>Viewer</span> <span><Trans>role.viewer</Trans></span>
</div> </div>
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -99,7 +89,7 @@ export default function RoleChangeDialog({
onClick={onCancel} onClick={onCancel}
type="button" type="button"
> >
Cancel <Trans>button.cancel</Trans>
</Button> </Button>
<Button <Button
variant="select" variant="select"
@ -108,7 +98,7 @@ export default function RoleChangeDialog({
onClick={() => onSave(selectedRole)} onClick={() => onSave(selectedRole)}
disabled={selectedRole === currentRole} disabled={selectedRole === currentRole}
> >
Save <Trans>button.save</Trans>
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -43,7 +43,9 @@ export default function SaveExportOverlay({
onClick={onPreview} onClick={onPreview}
> >
<LuVideo /> <LuVideo />
<Trans ns="components/dialog">export.fromTimeline.previewExport</Trans> <Trans ns="components/dialog">
export.fromTimeline.previewExport
</Trans>
</Button> </Button>
<Button <Button
className="flex items-center gap-1" className="flex items-center gap-1"

View File

@ -79,10 +79,10 @@ export default function SetPasswordDialog({
const getStrengthLabel = () => { const getStrengthLabel = () => {
if (!password) return ""; if (!password) return "";
if (passwordStrength <= 1) return "Weak"; if (passwordStrength <= 1) return t("users.dialog.form.password.strength.weak", { ns: "views/settings" });
if (passwordStrength === 2) return "Medium"; if (passwordStrength === 2) return t("users.dialog.form.password.strength.medium", { ns: "views/settings" });
if (passwordStrength === 3) return "Strong"; if (passwordStrength === 3) return t("users.dialog.form.password.strength.strong", { ns: "views/settings" });
return "Very Strong"; return t("users.dialog.form.password.strength.veryStrong", { ns: "views/settings" });
}; };
const getStrengthColor = () => { const getStrengthColor = () => {
@ -98,7 +98,14 @@ export default function SetPasswordDialog({
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader className="space-y-2"> <DialogHeader className="space-y-2">
<DialogTitle> <DialogTitle>
{username ? t("users.dialog.passwordSetting.updatePassword", {username, ns: "views/settings"}) : t("users.dialog.passwordSetting.setPassword", {ns: "views/settings"})} {username
? t("users.dialog.passwordSetting.updatePassword", {
username,
ns: "views/settings",
})
: t("users.dialog.passwordSetting.setPassword", {
ns: "views/settings",
})}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
<Trans ns="views/settings">users.dialog.passwordSetting.desc</Trans> <Trans ns="views/settings">users.dialog.passwordSetting.desc</Trans>
@ -107,7 +114,9 @@ export default function SetPasswordDialog({
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password"><Trans ns="views/settings">users.dialog.form.newPassword</Trans></Label> <Label htmlFor="password">
<Trans ns="views/settings">users.dialog.form.newPassword</Trans>
</Label>
<Input <Input
id="password" id="password"
className="h-10" className="h-10"
@ -117,7 +126,9 @@ export default function SetPasswordDialog({
setPassword(event.target.value); setPassword(event.target.value);
setError(null); setError(null);
}} }}
placeholder={t("users.dialog.form.newPassword.placeholder", {ns: "views/settings"})} placeholder={t("users.dialog.form.newPassword.placeholder", {
ns: "views/settings",
})}
autoFocus autoFocus
/> />
@ -131,7 +142,7 @@ export default function SetPasswordDialog({
/> />
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Password strength:{" "} <Trans ns="views/settings">users.dialog.form.password.strength</Trans>
<span className="font-medium">{getStrengthLabel()}</span> <span className="font-medium">{getStrengthLabel()}</span>
</p> </p>
</div> </div>
@ -139,7 +150,11 @@ export default function SetPasswordDialog({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="confirm-password"><Trans ns="views/settings">users.dialog.form.password.confirm</Trans></Label> <Label htmlFor="confirm-password">
<Trans ns="views/settings">
users.dialog.form.password.confirm
</Trans>
</Label>
<Input <Input
id="confirm-password" id="confirm-password"
className="h-10" className="h-10"
@ -149,7 +164,7 @@ export default function SetPasswordDialog({
setConfirmPassword(event.target.value); setConfirmPassword(event.target.value);
setError(null); setError(null);
}} }}
placeholder="Confirm new password" placeholder={t("users.dialog.form.newPassword.confirm.placeholder", { ns: "views/settings"})}
/> />
{/* Password match indicator */} {/* Password match indicator */}
@ -158,12 +173,20 @@ export default function SetPasswordDialog({
{password === confirmPassword ? ( {password === confirmPassword ? (
<> <>
<LuCheck className="size-3.5 text-green-500" /> <LuCheck className="size-3.5 text-green-500" />
<span className="text-green-600"><Trans ns="views/settings">users.dialog.form.password.match</Trans></span> <span className="text-green-600">
<Trans ns="views/settings">
users.dialog.form.password.match
</Trans>
</span>
</> </>
) : ( ) : (
<> <>
<LuX className="size-3.5 text-red-500" /> <LuX className="size-3.5 text-red-500" />
<span className="text-red-600"><Trans ns="views/settings">users.dialog.form.password.notMatch</Trans></span> <span className="text-red-600">
<Trans ns="views/settings">
users.dialog.form.password.notMatch
</Trans>
</span>
</> </>
)} )}
</div> </div>

View File

@ -374,9 +374,12 @@ function ObjectDetailsTab({
.post(`events/${search.id}/description`, { description: desc }) .post(`events/${search.id}/description`, { description: desc })
.then((resp) => { .then((resp) => {
if (resp.status == 200) { if (resp.status == 200) {
toast.success(t("details.tips.descriptionSaved", {ns: "views/explore"}), { toast.success(
position: "top-center", t("details.tips.descriptionSaved", { ns: "views/explore" }),
}); {
position: "top-center",
},
);
} }
mutate( mutate(
(key) => (key) =>
@ -407,9 +410,15 @@ function ObjectDetailsTab({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("details.tips.saveDescriptionFailed", {ns: "views/explore", errorMessage}), { toast.error(
position: "top-center", t("details.tips.saveDescriptionFailed", {
}); ns: "views/explore",
errorMessage,
}),
{
position: "top-center",
},
);
setDesc(search.data.description); setDesc(search.data.description);
}); });
}, [desc, search, mutate]); }, [desc, search, mutate]);
@ -580,7 +589,9 @@ function ObjectDetailsTab({
{averageEstimatedSpeed && ( {averageEstimatedSpeed && (
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
{averageEstimatedSpeed}{" "} {averageEstimatedSpeed}{" "}
{config?.ui.unit_system == "imperial" ? t("unit.speed.mph") : t("unit.speed.kph")}{" "} {config?.ui.unit_system == "imperial"
? t("unit.speed.mph")
: t("unit.speed.kph")}{" "}
{velocityAngle != undefined && ( {velocityAngle != undefined && (
<span className="text-primary/40"> <span className="text-primary/40">
<FaArrowRight <FaArrowRight
@ -668,7 +679,9 @@ function ObjectDetailsTab({
<div className="text-sm text-primary/40"></div> <div className="text-sm text-primary/40"></div>
<Textarea <Textarea
className="h-64" className="h-64"
placeholder={t("details.description.placeholder", {ns: "views/explore"})} placeholder={t("details.description.placeholder", {
ns: "views/explore",
})}
value={desc} value={desc}
onChange={(e) => setDesc(e.target.value)} onChange={(e) => setDesc(e.target.value)}
onFocus={handleDescriptionFocus} onFocus={handleDescriptionFocus}
@ -734,13 +747,16 @@ function ObjectDetailsTab({
<TextEntryDialog <TextEntryDialog
open={isSubLabelDialogOpen} open={isSubLabelDialogOpen}
setOpen={setIsSubLabelDialogOpen} setOpen={setIsSubLabelDialogOpen}
title={t("details.editSubLable", {ns: "views/explore"})} title={t("details.editSubLable", { ns: "views/explore" })}
description={ description={
search.label search.label
? t("details.editSubLable.desc", { ? t("details.editSubLable.desc", {
label: t(search.label, { ns: "objects" }), ns: "views/explore", label: t(search.label, { ns: "objects" }),
ns: "views/explore",
})
: t("details.editSubLable.desc.noLabel", {
ns: "views/explore",
}) })
: t("details.editSubLable.desc.noLabel", { ns: "views/explore" })
} }
onSave={handleSubLabelSave} onSave={handleSubLabelSave}
defaultValue={search?.sub_label || ""} defaultValue={search?.sub_label || ""}

View File

@ -108,7 +108,10 @@ export default function RestartDialog({
</SheetTitle> </SheetTitle>
<SheetDescription className="text-center"> <SheetDescription className="text-center">
<div> <div>
{t("restart.restarting.content", { countdown, ns: "components/dialog" })} {t("restart.restarting.content", {
countdown,
ns: "components/dialog",
})}
</div> </div>
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>

View File

@ -753,7 +753,9 @@ export function SnapshotClipFilterContent({
htmlFor="plus-filter" htmlFor="plus-filter"
className="cursor-pointer text-sm font-medium leading-none" className="cursor-pointer text-sm font-medium leading-none"
> >
<Trans ns="components/filter">features.submittedToFrigatePlus.label</Trans> <Trans ns="components/filter">
features.submittedToFrigatePlus.label
</Trans>
</Label> </Label>
</div> </div>
<ToggleGroup <ToggleGroup

View File

@ -325,7 +325,10 @@ function PreviewVideoPlayer({
</video> </video>
{cameraPreviews && !currentPreview && ( {cameraPreviews && !currentPreview && (
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl"> <div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
<Trans ns="components/player" value={{ camera: camera.replaceAll("_", " ") }}> <Trans
ns="components/player"
value={{ camera: camera.replaceAll("_", " ") }}
>
noPreviewFoundFor noPreviewFoundFor
</Trans> </Trans>
</div> </div>
@ -547,7 +550,10 @@ function PreviewFramesPlayer({
/> />
{previewFrames?.length === 0 && ( {previewFrames?.length === 0 && (
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl"> <div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
<Trans ns="components/player" values={{ cameraName: camera.replaceAll("_", " ") }}> <Trans
ns="components/player"
values={{ cameraName: camera.replaceAll("_", " ") }}
>
noPreviewFoundFor noPreviewFoundFor
</Trans> </Trans>
</div> </div>

View File

@ -168,7 +168,10 @@ export function CameraStreamingDialog({
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader className="mb-4"> <DialogHeader className="mb-4">
<DialogTitle className="capitalize"> <DialogTitle className="capitalize">
<Trans ns="components/camera" values={{ cameraName: camera.replaceAll("_", " ") }}> <Trans
ns="components/camera"
values={{ cameraName: camera.replaceAll("_", " ") }}
>
group.camera.setting.title group.camera.setting.title
</Trans> </Trans>
</DialogTitle> </DialogTitle>
@ -183,17 +186,23 @@ export function CameraStreamingDialog({
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground"> <div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" /> <LuX className="size-4 text-danger" />
<div> <div>
<Trans ns="components/dialog">streaming.restreaming.disabled</Trans> <Trans ns="components/dialog">
streaming.restreaming.disabled
</Trans>
</div> </div>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div className="cursor-pointer p-0"> <div className="cursor-pointer p-0">
<LuInfo className="size-4" /> <LuInfo className="size-4" />
<span className="sr-only"><Trans>button.info</Trans></span> <span className="sr-only">
<Trans>button.info</Trans>
</span>
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80 text-xs"> <PopoverContent className="w-80 text-xs">
<Trans ns="components/dialog">streaming.restreaming.desc</Trans> <Trans ns="components/dialog">
streaming.restreaming.desc
</Trans>
<div className="mt-2 flex items-center text-primary"> <div className="mt-2 flex items-center text-primary">
<Link <Link
to="https://docs.frigate.video/configuration/live" to="https://docs.frigate.video/configuration/live"
@ -286,7 +295,9 @@ export function CameraStreamingDialog({
)} )}
<div className="flex flex-col items-start gap-2"> <div className="flex flex-col items-start gap-2">
<Label htmlFor="streaming-method" className="text-right"> <Label htmlFor="streaming-method" className="text-right">
<Trans ns="components/camera">group.camera.setting.streamMethod</Trans> <Trans ns="components/camera">
group.camera.setting.streamMethod
</Trans>
</Label> </Label>
<Select <Select
value={streamType} value={streamType}
@ -357,7 +368,9 @@ export function CameraStreamingDialog({
htmlFor="compatibility" htmlFor="compatibility"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
<Trans ns="components/camera">group.camera.setting.compatibilityMode</Trans> <Trans ns="components/camera">
group.camera.setting.compatibilityMode
</Trans>
</Label> </Label>
</div> </div>
<div className="flex flex-col gap-2 leading-none"> <div className="flex flex-col gap-2 leading-none">

View File

@ -107,7 +107,9 @@ export default function MotionMaskEditPane({
polygon: z.object({ name: z.string(), isFinished: z.boolean() }), polygon: z.object({ name: z.string(), isFinished: z.boolean() }),
}) })
.refine(() => polygon?.isFinished === true, { .refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {ns: "views/settings"}), message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
ns: "views/settings",
}),
path: ["polygon.isFinished"], path: ["polygon.isFinished"],
}); });
@ -166,19 +168,13 @@ export default function MotionMaskEditPane({
if (res.status === 200) { if (res.status === 200) {
toast.success( toast.success(
polygon.name polygon.name
? t( ? t("masksAndZones.motionMasks.toast.success", {
"masksAndZones.motionMasks.toast.success", polygonName: polygon.name,
{ ns: "views/settings",
polygonName: polygon.name, })
ns: "views/settings" : t("masksAndZones.motionMasks.toast.success.noName", {
}, ns: "views/settings",
) }),
: t(
"masksAndZones.motionMasks.toast.success.noName",
{
ns: "views/settings"
}
),
{ {
position: "top-center", position: "top-center",
}, },
@ -224,12 +220,9 @@ export default function MotionMaskEditPane({
} }
useEffect(() => { useEffect(() => {
document.title = t( document.title = t("masksAndZones.motionMasks.documentTitle", {
"masksAndZones.motionMasks.documentTitle", ns: "views/settings",
{ });
ns: "views/settings"
}
);
}, []); }, []);
if (!polygon) { if (!polygon) {
@ -241,14 +234,12 @@ export default function MotionMaskEditPane({
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
{polygon.name.length {polygon.name.length
? t("masksAndZones.motionMasks.edit", {ns: "views/settings"}) ? t("masksAndZones.motionMasks.edit", { ns: "views/settings" })
: t("masksAndZones.motionMasks.add", {ns: "views/settings"})} : t("masksAndZones.motionMasks.add", { ns: "views/settings" })}
</Heading> </Heading>
<div className="my-3 space-y-3 text-sm text-muted-foreground"> <div className="my-3 space-y-3 text-sm text-muted-foreground">
<p> <p>
<Trans ns="views/settings"> <Trans ns="views/settings">masksAndZones.motionMasks.context</Trans>
masksAndZones.motionMasks.context
</Trans>
</p> </p>
<div className="flex items-center text-primary"> <div className="flex items-center text-primary">
@ -271,7 +262,7 @@ export default function MotionMaskEditPane({
<div className="my-1 inline-flex"> <div className="my-1 inline-flex">
{t("masksAndZones.motionMasks.point", { {t("masksAndZones.motionMasks.point", {
count: polygons[activePolygonIndex].points.length, count: polygons[activePolygonIndex].points.length,
ns: "views/settings" ns: "views/settings",
})} })}
{polygons[activePolygonIndex].isFinished && ( {polygons[activePolygonIndex].isFinished && (
<FaCheckCircle className="ml-2 size-5" /> <FaCheckCircle className="ml-2 size-5" />
@ -297,13 +288,10 @@ export default function MotionMaskEditPane({
{polygonArea && polygonArea >= 0.35 && ( {polygonArea && polygonArea >= 0.35 && (
<> <>
<div className="mb-3 text-sm text-danger"> <div className="mb-3 text-sm text-danger">
{t( {t("masksAndZones.motionMasks.polygonAreaTooLarge", {
"masksAndZones.motionMasks.polygonAreaTooLarge", polygonArea: Math.round(polygonArea * 100),
{ ns: "views/settings",
polygonArea: Math.round(polygonArea * 100), })}
ns: "views/settings"
},
)}
</div> </div>
<div className="mb-3 text-sm text-primary"> <div className="mb-3 text-sm text-primary">
<Trans ns="views/settings"> <Trans ns="views/settings">

View File

@ -109,7 +109,9 @@ export default function ObjectMaskEditPane({
polygon: z.object({ isFinished: z.boolean(), name: z.string() }), polygon: z.object({ isFinished: z.boolean(), name: z.string() }),
}) })
.refine(() => polygon?.isFinished === true, { .refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {ns: "views/settings"}), message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
ns: "views/settings",
}),
path: ["polygon.isFinished"], path: ["polygon.isFinished"],
}); });
@ -198,19 +200,13 @@ export default function ObjectMaskEditPane({
if (res.status === 200) { if (res.status === 200) {
toast.success( toast.success(
polygon.name polygon.name
? t( ? t("masksAndZones.objectMasks.toast.success", {
"masksAndZones.objectMasks.toast.success", polygonName: polygon.name,
{ ns: "views/settings",
polygonName: polygon.name, })
ns: "views/settings" : t("masksAndZones.objectMasks.toast.success.noName", {
}, ns: "views/settings",
) }),
: t(
"masksAndZones.objectMasks.toast.success.noName",
{
ns: "views/settings"
}
),
{ {
position: "top-center", position: "top-center",
}, },
@ -232,11 +228,14 @@ export default function ObjectMaskEditPane({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("toast.save.error", { toast.error(
errorMessage t("toast.save.error", {
}), { errorMessage,
position: "top-center", }),
}); {
position: "top-center",
},
);
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
@ -265,12 +264,9 @@ export default function ObjectMaskEditPane({
} }
useEffect(() => { useEffect(() => {
document.title = t( document.title = t("masksAndZones.objectMasks.documentTitle", {
"masksAndZones.objectMasks.documentTitle", ns: "views/settings",
{ });
ns: "views/settings"
}
);
}, []); }, []);
if (!polygon) { if (!polygon) {
@ -283,17 +279,15 @@ export default function ObjectMaskEditPane({
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
{polygon.name.length {polygon.name.length
? t("masksAndZones.objectMasks.edit", { ? t("masksAndZones.objectMasks.edit", {
ns: "views/settings" ns: "views/settings",
}) })
: t("masksAndZones.objectMasks.add", { : t("masksAndZones.objectMasks.add", {
ns: "views/settings" ns: "views/settings",
})} })}
</Heading> </Heading>
<div className="my-2 text-sm text-muted-foreground"> <div className="my-2 text-sm text-muted-foreground">
<p> <p>
<Trans ns="views/settings"> <Trans ns="views/settings">masksAndZones.objectMasks.context</Trans>
masksAndZones.objectMasks.context
</Trans>
</p> </p>
</div> </div>
<Separator className="my-3 bg-secondary" /> <Separator className="my-3 bg-secondary" />
@ -302,7 +296,7 @@ export default function ObjectMaskEditPane({
<div className="my-1 inline-flex"> <div className="my-1 inline-flex">
{t("masksAndZones.objectMasks.point", { {t("masksAndZones.objectMasks.point", {
count: polygons[activePolygonIndex].points.length, count: polygons[activePolygonIndex].points.length,
ns: "views/settings" ns: "views/settings",
})} })}
{polygons[activePolygonIndex].isFinished && ( {polygons[activePolygonIndex].isFinished && (
<FaCheckCircle className="ml-2 size-5" /> <FaCheckCircle className="ml-2 size-5" />
@ -474,7 +468,7 @@ export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
<SelectSeparator className="bg-secondary" /> <SelectSeparator className="bg-secondary" />
{allLabels.map((item) => ( {allLabels.map((item) => (
<SelectItem key={item} value={item}> <SelectItem key={item} value={item}>
{t(item, {ns: "objects"})} {t(item, { ns: "objects" })}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>

View File

@ -63,7 +63,9 @@ export default function ExploreSettings({
<Trans ns="components/filter">explore.settings.defaultView</Trans> <Trans ns="components/filter">explore.settings.defaultView</Trans>
</div> </div>
<div className="space-y-1 text-xs text-muted-foreground"> <div className="space-y-1 text-xs text-muted-foreground">
<Trans ns="components/filter">explore.settings.defaultView.desc</Trans> <Trans ns="components/filter">
explore.settings.defaultView.desc
</Trans>
</div> </div>
</div> </div>
<Select <Select
@ -72,8 +74,12 @@ export default function ExploreSettings({
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
{defaultView == "summary" {defaultView == "summary"
? t("explore.settings.defaultView.summary", {ns: "components/filter"}) ? t("explore.settings.defaultView.summary", {
: t("explore.settings.defaultView.unfilteredGrid", {ns: "components/filter"})} ns: "components/filter",
})
: t("explore.settings.defaultView.unfilteredGrid", {
ns: "components/filter",
})}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@ -84,8 +90,12 @@ export default function ExploreSettings({
value={value} value={value}
> >
{value == "summary" {value == "summary"
? t("explore.settings.defaultView.summary", {ns: "components/filter"}) ? t("explore.settings.defaultView.summary", {
: t("explore.settings.defaultView.unfilteredGrid", {ns: "components/filter"})} ns: "components/filter",
})
: t("explore.settings.defaultView.unfilteredGrid", {
ns: "components/filter",
})}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@ -98,10 +108,14 @@ export default function ExploreSettings({
<div className="flex w-full flex-col space-y-4"> <div className="flex w-full flex-col space-y-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="text-md"> <div className="text-md">
<Trans ns="components/filter">explore.settings.gridColumns</Trans> <Trans ns="components/filter">
explore.settings.gridColumns
</Trans>
</div> </div>
<div className="space-y-1 text-xs text-muted-foreground"> <div className="space-y-1 text-xs text-muted-foreground">
<Trans ns="components/filter">explore.settings.gridColumns.desc</Trans> <Trans ns="components/filter">
explore.settings.gridColumns.desc
</Trans>
</div> </div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
@ -166,7 +180,9 @@ export function SearchTypeContent({
<Trans ns="components/filter">explore.settings.searchSource</Trans> <Trans ns="components/filter">explore.settings.searchSource</Trans>
</div> </div>
<div className="space-y-1 text-xs text-muted-foreground"> <div className="space-y-1 text-xs text-muted-foreground">
<Trans ns="components/filter">explore.settings.searchSource.desc</Trans> <Trans ns="components/filter">
explore.settings.searchSource.desc
</Trans>
</div> </div>
</div> </div>
<div className="mt-2.5 flex flex-col gap-2.5"> <div className="mt-2.5 flex flex-col gap-2.5">

View File

@ -106,7 +106,7 @@ export default function ZoneEditPane({
.min(2, { .min(2, {
message: t( message: t(
"masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters", "masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters",
{ ns: "views/settings"} { ns: "views/settings" },
), ),
}) })
.transform((val: string) => val.trim().replace(/\s+/g, "_")) .transform((val: string) => val.trim().replace(/\s+/g, "_"))
@ -117,7 +117,7 @@ export default function ZoneEditPane({
{ {
message: t( message: t(
"masksAndZones.form.zoneName.error.mustNotBeSameWithCamera", "masksAndZones.form.zoneName.error.mustNotBeSameWithCamera",
{ ns: "views/settings"} { ns: "views/settings" },
), ),
}, },
) )
@ -131,9 +131,9 @@ export default function ZoneEditPane({
return !otherPolygonNames.includes(value); return !otherPolygonNames.includes(value);
}, },
{ {
message: t("masksAndZones.form.zoneName.error.alreadyExists", message: t("masksAndZones.form.zoneName.error.alreadyExists", {
{ ns: "views/settings"} ns: "views/settings",
), }),
}, },
) )
.refine( .refine(
@ -141,22 +141,23 @@ export default function ZoneEditPane({
return !value.includes("."); return !value.includes(".");
}, },
{ {
message: t("masksAndZones.form.zoneName.error.mustNotContainPeriod", message: t(
{ ns: "views/settings"} "masksAndZones.form.zoneName.error.mustNotContainPeriod",
{ ns: "views/settings" },
), ),
}, },
) )
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), { .refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter", message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter", {
{ ns: "views/settings"} ns: "views/settings",
), }),
}), }),
inertia: z.coerce inertia: z.coerce
.number() .number()
.min(1, { .min(1, {
message: t("masksAndZones.form.inertia.error.mustBeAboveZero", message: t("masksAndZones.form.inertia.error.mustBeAboveZero", {
{ ns: "views/settings"} ns: "views/settings",
), }),
}) })
.or(z.literal("")), .or(z.literal("")),
loitering_time: z.coerce loitering_time: z.coerce
@ -164,13 +165,15 @@ export default function ZoneEditPane({
.min(0, { .min(0, {
message: t( message: t(
"masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero", "masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero",
{ ns: "views/settings"} { ns: "views/settings" },
), ),
}) })
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
isFinished: z.boolean().refine(() => polygon?.isFinished === true, { isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", { ns: "views/settings" }), message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
ns: "views/settings",
}),
}), }),
objects: z.array(z.string()).optional(), objects: z.array(z.string()).optional(),
review_alerts: z.boolean().default(false).optional(), review_alerts: z.boolean().default(false).optional(),
@ -179,28 +182,36 @@ export default function ZoneEditPane({
lineA: z.coerce lineA: z.coerce
.number() .number()
.min(0.1, { .min(0.1, {
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}), message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
}) })
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
lineB: z.coerce lineB: z.coerce
.number() .number()
.min(0.1, { .min(0.1, {
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}), message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
}) })
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
lineC: z.coerce lineC: z.coerce
.number() .number()
.min(0.1, { .min(0.1, {
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}), message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
}) })
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
lineD: z.coerce lineD: z.coerce
.number() .number()
.min(0.1, { .min(0.1, {
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}), message: t("masksAndZones.form.distance.error", {
ns: "views/settings",
}),
}) })
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
@ -220,7 +231,9 @@ export default function ZoneEditPane({
return true; return true;
}, },
{ {
message: t("masksAndZones.form.distance.error.mustBeFilled", { ns: "views/settings"}), message: t("masksAndZones.form.distance.error.mustBeFilled", {
ns: "views/settings",
}),
path: ["speedEstimation"], path: ["speedEstimation"],
}, },
) )
@ -237,8 +250,8 @@ export default function ZoneEditPane({
message: t( message: t(
"masksAndZones.zones.speedThreshold.toast.error.loiteringTimeError", "masksAndZones.zones.speedThreshold.toast.error.loiteringTimeError",
{ {
ns: "views/settings" ns: "views/settings",
} },
), ),
path: ["loitering_time"], path: ["loitering_time"],
}, },
@ -278,12 +291,9 @@ export default function ZoneEditPane({
polygon.points.length !== 4 polygon.points.length !== 4
) { ) {
toast.error( toast.error(
t( t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError", {
"masksAndZones.zones.speedThreshold.toast.error.pointLengthError", ns: "views/settings",
{ }),
ns: "views/settings"
}
),
); );
form.setValue("speedEstimation", false); form.setValue("speedEstimation", false);
} }
@ -452,11 +462,14 @@ export default function ZoneEditPane({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("toast.save.error", { toast.error(
errorMessage, t("toast.save.error", {
}), { errorMessage,
position: "top-center", }),
}); {
position: "top-center",
},
);
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
@ -490,12 +503,9 @@ export default function ZoneEditPane({
} }
useEffect(() => { useEffect(() => {
document.title = t( document.title = t("masksAndZones.zones.documentTitle", {
"masksAndZones.zones.documentTitle", ns: "views/settings",
{ });
ns: "views/settings"
}
);
}, []); }, []);
if (!polygon) { if (!polygon) {
@ -507,12 +517,12 @@ export default function ZoneEditPane({
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
{polygon.name.length {polygon.name.length
? t("masksAndZones.zones.edit",{ ? t("masksAndZones.zones.edit", {
ns: "views/settings" ns: "views/settings",
}) })
: t("masksAndZones.zones.add",{ : t("masksAndZones.zones.add", {
ns: "views/settings" ns: "views/settings",
})} })}
</Heading> </Heading>
<div className="my-2 text-sm text-muted-foreground"> <div className="my-2 text-sm text-muted-foreground">
<p> <p>
@ -525,7 +535,7 @@ export default function ZoneEditPane({
<div className="my-1 inline-flex"> <div className="my-1 inline-flex">
{t("masksAndZones.zones.point", { {t("masksAndZones.zones.point", {
count: polygons[activePolygonIndex].points.length, count: polygons[activePolygonIndex].points.length,
ns: "views/settings" ns: "views/settings",
})} })}
{polygons[activePolygonIndex].isFinished && ( {polygons[activePolygonIndex].isFinished && (
@ -542,9 +552,7 @@ export default function ZoneEditPane({
</div> </div>
)} )}
<div className="mb-3 text-sm text-muted-foreground"> <div className="mb-3 text-sm text-muted-foreground">
<Trans ns="views/settings"> <Trans ns="views/settings">masksAndZones.zones.clickDrawPolygon</Trans>
masksAndZones.zones.clickDrawPolygon
</Trans>
</div> </div>
<Separator className="my-3 bg-secondary" /> <Separator className="my-3 bg-secondary" />
@ -565,8 +573,8 @@ export default function ZoneEditPane({
placeholder={t( placeholder={t(
"masksAndZones.zones.name.inputPlaceHolder", "masksAndZones.zones.name.inputPlaceHolder",
{ {
ns: "views/settings" ns: "views/settings",
} },
)} )}
{...field} {...field}
/> />
@ -587,9 +595,7 @@ export default function ZoneEditPane({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans ns="views/settings"> <Trans ns="views/settings">masksAndZones.zones.inertia</Trans>
masksAndZones.zones.inertia
</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@ -693,8 +699,8 @@ export default function ZoneEditPane({
t( t(
"masksAndZones.zones.speedEstimation.pointLengthError", "masksAndZones.zones.speedEstimation.pointLengthError",
{ {
ns: "views/settings" ns: "views/settings",
} },
), ),
); );
return; return;
@ -707,8 +713,8 @@ export default function ZoneEditPane({
t( t(
"masksAndZones.zones.speedEstimation.loiteringTimeError", "masksAndZones.zones.speedEstimation.loiteringTimeError",
{ {
ns: "views/settings" ns: "views/settings",
} },
), ),
); );
} }
@ -996,7 +1002,7 @@ export function ZoneObjectSelector({
className="w-full cursor-pointer capitalize text-primary" className="w-full cursor-pointer capitalize text-primary"
htmlFor={item} htmlFor={item}
> >
{t(item, {ns: "objects"})} {t(item, { ns: "objects" })}
</Label> </Label>
<Switch <Switch
key={item} key={item}

View File

@ -76,7 +76,7 @@ export default function useStats(stats: FrigateStats | undefined) {
text: t("stats.ffmpegHighCpuUsage", { text: t("stats.ffmpegHighCpuUsage", {
camera: capitalizeFirstLetter(name.replaceAll("_", " ")), camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
ffmpegAvg, ffmpegAvg,
ns: "views/system" ns: "views/system",
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`, }), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
color: "text-danger", color: "text-danger",
relevantLink: "/system#cameras", relevantLink: "/system#cameras",
@ -88,7 +88,7 @@ export default function useStats(stats: FrigateStats | undefined) {
text: t("stats.detectHighCpuUsage", { text: t("stats.detectHighCpuUsage", {
camera: capitalizeFirstLetter(name.replaceAll("_", " ")), camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
detectAvg, detectAvg,
ns: "views/system" ns: "views/system",
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`, }), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
color: "text-danger", color: "text-danger",
relevantLink: "/system#cameras", relevantLink: "/system#cameras",

View File

@ -78,9 +78,9 @@ export default function Events() {
useEffect(() => { useEffect(() => {
if (recording) { if (recording) {
document.title = t("recordings.documentTitle", {ns: "views/events"}); document.title = t("recordings.documentTitle", { ns: "views/events" });
} else { } else {
document.title = t("documentTitle", {ns: "views/events"}); document.title = t("documentTitle", { ns: "views/events" });
} }
}, [recording, severity]); }, [recording, severity]);

View File

@ -121,7 +121,10 @@ function Exports() {
<Trans ns="views/exports">deleteExport</Trans> <Trans ns="views/exports">deleteExport</Trans>
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<Trans ns="views/exports" values={{ exportName: deleteClip?.exportName }}> <Trans
ns="views/exports"
values={{ exportName: deleteClip?.exportName }}
>
deleteExport.desc deleteExport.desc
</Trans> </Trans>
</AlertDialogDescription> </AlertDialogDescription>

View File

@ -67,15 +67,15 @@ function Live() {
.map((text) => text[0].toUpperCase() + text.substring(1)); .map((text) => text[0].toUpperCase() + text.substring(1));
document.title = t("documentTitle.withCamera", { document.title = t("documentTitle.withCamera", {
camera: capitalized.join(" "), camera: capitalized.join(" "),
ns: "views/live" ns: "views/live",
}); });
} else if (cameraGroup && cameraGroup != "default") { } else if (cameraGroup && cameraGroup != "default") {
document.title = t("documentTitle.withCamera", { document.title = t("documentTitle.withCamera", {
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`, camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
ns: "views/live" ns: "views/live",
}); });
} else { } else {
document.title = t("documentTitle", {ns: "views/live"}); document.title = t("documentTitle", { ns: "views/live" });
} }
}, [cameraGroup, selectedCameraName]); }, [cameraGroup, selectedCameraName]);

View File

@ -94,7 +94,9 @@ function System() {
{item == "storage" && <LuHardDrive className="size-4" />} {item == "storage" && <LuHardDrive className="size-4" />}
{item == "cameras" && <FaVideo className="size-4" />} {item == "cameras" && <FaVideo className="size-4" />}
{isDesktop && ( {isDesktop && (
<div className="capitalize">{t(item+".title", {ns:"views/system"})}</div> <div className="capitalize">
{t(item + ".title", { ns: "views/system" })}
</div>
)} )}
</ToggleGroupItem> </ToggleGroupItem>
))} ))}
@ -110,7 +112,9 @@ function System() {
</div> </div>
</div> </div>
<div className="mt-2 flex items-end gap-2"> <div className="mt-2 flex items-end gap-2">
<div className="h-full content-center font-medium"><Trans ns="views/system">title</Trans></div> <div className="h-full content-center font-medium">
<Trans ns="views/system">title</Trans>
</div>
{statsSnapshot && ( {statsSnapshot && (
<div className="h-full content-center text-sm text-muted-foreground"> <div className="h-full content-center text-sm text-muted-foreground">
{statsSnapshot.service.version} {statsSnapshot.service.version}

View File

@ -13,23 +13,23 @@ i18n
}, },
ns: [ ns: [
'common', "common",
'objects', "objects",
'audio', "audio",
'components/camera', "components/camera",
'components/dialog', "components/dialog",
'components/filter', "components/filter",
'components/icons', "components/icons",
'components/player', "components/player",
'views/events', "views/events",
'views/explore', "views/explore",
'views/live', "views/live",
'views/settings', "views/settings",
'views/system', "views/system",
'views/exports', "views/exports",
'views/explore' "views/explore",
], ],
defaultNS: 'common', defaultNS: "common",
react: { react: {
transSupportBasicHtmlNodes: true, transSupportBasicHtmlNodes: true,
@ -41,6 +41,11 @@ i18n
"li", "li",
"p", "p",
"code", "code",
"span",
"p",
"ul",
"li",
"ol",
], ],
}, },
interpolation: { interpolation: {
@ -52,10 +57,15 @@ i18n
// Handle special cases for objects and audio // Handle special cases for objects and audio
if (parts[0] === "object" || parts[0] === "audio") { if (parts[0] === "object" || parts[0] === "audio") {
return parts[1] return (
?.split("_") parts[1]
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) ?.split("_")
.join(" ") || key; .map(
(word) =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join(" ") || key
);
} }
// For nested keys, try to make them more readable // For nested keys, try to make them more readable
@ -63,14 +73,19 @@ i18n
const lastPart = parts[parts.length - 1]; const lastPart = parts[parts.length - 1];
return lastPart return lastPart
.split("_") .split("_")
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .map(
(word) =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join(" "); .join(" ");
} }
// For single keys, just capitalize and format // For single keys, just capitalize and format
return key return key
.split("_") .split("_")
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .map(
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join(" "); .join(" ");
}, },
}); });

View File

@ -199,9 +199,12 @@ export default function EventView({
) )
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
toast.success(t("export.toast.success", { ns: "components/dialog"}), { toast.success(
position: "top-center", t("export.toast.success", { ns: "components/dialog" }),
}); {
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {
@ -209,9 +212,15 @@ export default function EventView({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("export.toast.error", { ns: "components/dialog", message: errorMessage }), { toast.error(
position: "top-center", t("export.toast.error", {
}); ns: "components/dialog",
message: errorMessage,
}),
{
position: "top-center",
},
);
}); });
}, },
[reviewItems], [reviewItems],

View File

@ -695,7 +695,9 @@ export default function DraggableGridLayout({
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{fullscreen ? t("button.exitFullscreen") : t("button.fullscreen")} {fullscreen
? t("button.exitFullscreen")
: t("button.fullscreen")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</> </>

View File

@ -493,7 +493,9 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"} variant={fullscreen ? "overlay" : "primary"}
Icon={fullscreen ? FaCompress : FaExpand} Icon={fullscreen ? FaCompress : FaExpand}
isActive={fullscreen} isActive={fullscreen}
title={fullscreen ? t("button.close") : t("button.fullscreen")} title={
fullscreen ? t("button.close") : t("button.fullscreen")
}
onClick={toggleFullscreen} onClick={toggleFullscreen}
/> />
)} )}
@ -764,7 +766,7 @@ function PtzControlPanel({
{ptz?.features?.includes("pt") && ( {ptz?.features?.includes("pt") && (
<> <>
<TooltipButton <TooltipButton
label={t("ptz.move.left.label", { ns: "views/live"})} label={t("ptz.move.left.label", { ns: "views/live" })}
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_LEFT"); sendPtz("MOVE_LEFT");
@ -779,7 +781,7 @@ function PtzControlPanel({
<FaAngleLeft /> <FaAngleLeft />
</TooltipButton> </TooltipButton>
<TooltipButton <TooltipButton
label={t("ptz.move.up.label", { ns: "views/live"})} label={t("ptz.move.up.label", { ns: "views/live" })}
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_UP"); sendPtz("MOVE_UP");
@ -794,7 +796,7 @@ function PtzControlPanel({
<FaAngleUp /> <FaAngleUp />
</TooltipButton> </TooltipButton>
<TooltipButton <TooltipButton
label={t("ptz.move.down.label", { ns: "views/live"})} label={t("ptz.move.down.label", { ns: "views/live" })}
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_DOWN"); sendPtz("MOVE_DOWN");
@ -809,7 +811,7 @@ function PtzControlPanel({
<FaAngleDown /> <FaAngleDown />
</TooltipButton> </TooltipButton>
<TooltipButton <TooltipButton
label={t("ptz.move.right.label", { ns: "views/live"})} label={t("ptz.move.right.label", { ns: "views/live" })}
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_RIGHT"); sendPtz("MOVE_RIGHT");
@ -828,7 +830,7 @@ function PtzControlPanel({
{ptz?.features?.includes("zoom") && ( {ptz?.features?.includes("zoom") && (
<> <>
<TooltipButton <TooltipButton
label={t("ptz.zoom.in.label", { ns: "views/live"})} label={t("ptz.zoom.in.label", { ns: "views/live" })}
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("ZOOM_IN"); sendPtz("ZOOM_IN");
@ -843,7 +845,7 @@ function PtzControlPanel({
<MdZoomIn /> <MdZoomIn />
</TooltipButton> </TooltipButton>
<TooltipButton <TooltipButton
label={t("ptz.zoom.out.label", { ns: "views/live"})} label={t("ptz.zoom.out.label", { ns: "views/live" })}
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("ZOOM_OUT"); sendPtz("ZOOM_OUT");
@ -1027,7 +1029,9 @@ function FrigateCameraFeatures({
</div> </div>
{!camera.record.enabled || camera.record.retain.days == 0 ? ( {!camera.record.enabled || camera.record.retain.days == 0 ? (
<div> <div>
<Trans ns="views/live">manualRecording.recordDisabledTips</Trans> <Trans ns="views/live">
manualRecording.recordDisabledTips
</Trans>
</div> </div>
) : ( ) : (
<OnDemandRetentionMessage camera={camera} /> <OnDemandRetentionMessage camera={camera} />
@ -1041,7 +1045,7 @@ function FrigateCameraFeatures({
setActiveToastId(toastId); setActiveToastId(toastId);
} }
} catch (error) { } catch (error) {
toast.error(t("manualRecording.failedToStart", { ns: "views/live"}), { toast.error(t("manualRecording.failedToStart", { ns: "views/live" }), {
position: "top-center", position: "top-center",
}); });
} }
@ -1058,12 +1062,12 @@ function FrigateCameraFeatures({
}); });
recordingEventIdRef.current = null; recordingEventIdRef.current = null;
setIsRecording(false); setIsRecording(false);
toast.success(t("manualRecording.ended", { ns: "views/live"}), { toast.success(t("manualRecording.ended", { ns: "views/live" }), {
position: "top-center", position: "top-center",
}); });
} }
} catch (error) { } catch (error) {
toast.error(t("manualRecording.failedToEnd", { ns: "views/live"}), { toast.error(t("manualRecording.failedToEnd", { ns: "views/live" }), {
position: "top-center", position: "top-center",
}); });
} }
@ -1170,9 +1174,9 @@ function FrigateCameraFeatures({
variant={fullscreen ? "overlay" : "primary"} variant={fullscreen ? "overlay" : "primary"}
Icon={isRecording ? TbRecordMail : TbRecordMailOff} Icon={isRecording ? TbRecordMail : TbRecordMailOff}
isActive={isRecording} isActive={isRecording}
title={t( title={t("manualRecording." + (isRecording ? "stop" : "start"), {
"manualRecording." + (isRecording ? "stop" : "start"), { ns: "views/live"}) ns: "views/live",
} })}
onClick={handleEventButtonClick} onClick={handleEventButtonClick}
disabled={!cameraEnabled} disabled={!cameraEnabled}
/> />
@ -1199,7 +1203,9 @@ function FrigateCameraFeatures({
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground"> <div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" /> <LuX className="size-4 text-danger" />
<div> <div>
<Trans ns="components/dialog">streaming.restreaming.disabled</Trans> <Trans ns="components/dialog">
streaming.restreaming.disabled
</Trans>
</div> </div>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -1209,7 +1215,9 @@ function FrigateCameraFeatures({
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80 text-xs"> <PopoverContent className="w-80 text-xs">
<Trans ns="components/dialog">streaming.restreaming.desc</Trans> <Trans ns="components/dialog">
streaming.restreaming.desc
</Trans>
<div className="mt-2 flex items-center text-primary"> <div className="mt-2 flex items-center text-primary">
<Link <Link
to="https://docs.frigate.video/configuration/live" to="https://docs.frigate.video/configuration/live"
@ -1512,7 +1520,9 @@ function FrigateCameraFeatures({
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground"> <div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" /> <LuX className="size-4 text-danger" />
<div> <div>
<Trans ns="components/dialog">streaming.restreaming.disabled</Trans> <Trans ns="components/dialog">
streaming.restreaming.disabled
</Trans>
</div> </div>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -1522,7 +1532,9 @@ function FrigateCameraFeatures({
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80 text-xs"> <PopoverContent className="w-80 text-xs">
<Trans ns="components/dialog">streaming.restreaming.desc</Trans> <Trans ns="components/dialog">
streaming.restreaming.desc
</Trans>
<div className="mt-2 flex items-center text-primary"> <div className="mt-2 flex items-center text-primary">
<Link <Link
to="https://docs.frigate.video/configuration/live" to="https://docs.frigate.video/configuration/live"

View File

@ -563,7 +563,9 @@ export default function LiveDashboardView({
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{fullscreen ? t("button.exitFullscreen") : t("button.fullscreen")} {fullscreen
? t("button.exitFullscreen")
: t("button.fullscreen")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -67,9 +67,15 @@ export default function AuthenticationView() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("users.toast.error.setPasswordFailed", {ns: "views/settings", errorMessage}), { toast.error(
position: "top-center", t("users.toast.error.setPasswordFailed", {
}); ns: "views/settings",
errorMessage,
}),
{
position: "top-center",
},
);
}); });
}, []); }, []);
@ -87,9 +93,12 @@ export default function AuthenticationView() {
users?.push({ username: user, role: role }); users?.push({ username: user, role: role });
return users; return users;
}, false); }, false);
toast.success(t("users.toast.success.createUser", {ns: "views/settings", user}), { toast.success(
position: "top-center", t("users.toast.success.createUser", { ns: "views/settings", user }),
}); {
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {
@ -97,9 +106,15 @@ export default function AuthenticationView() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("users.toast.error.createUserFailed", {ns: "views/settings", errorMessage}), { toast.error(
position: "top-center", t("users.toast.error.createUserFailed", {
}); ns: "views/settings",
errorMessage,
}),
{
position: "top-center",
},
);
}); });
}; };
@ -113,9 +128,12 @@ export default function AuthenticationView() {
(users) => users?.filter((u) => u.username !== user), (users) => users?.filter((u) => u.username !== user),
false, false,
); );
toast.success(t("users.toast.success.deleteUser", {ns: "views/settings", user}), { toast.success(
position: "top-center", t("users.toast.success.deleteUser", { ns: "views/settings", user }),
}); {
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {
@ -123,9 +141,15 @@ export default function AuthenticationView() {
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("users.toast.error.deleteUserFailed", {ns: "views/settings", errorMessage}), { toast.error(
position: "top-center", t("users.toast.error.deleteUserFailed", {
}); ns: "views/settings",
errorMessage,
}),
{
position: "top-center",
},
);
}); });
}; };
@ -188,7 +212,7 @@ export default function AuthenticationView() {
onClick={() => setShowCreate(true)} onClick={() => setShowCreate(true)}
> >
<LuPlus className="size-4" /> <LuPlus className="size-4" />
Add User <Trans ns="views/settings">users.addUser</Trans>
</Button> </Button>
</div> </div>
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
@ -197,9 +221,13 @@ export default function AuthenticationView() {
<Table> <Table>
<TableHeader className="sticky top-0 bg-muted/50"> <TableHeader className="sticky top-0 bg-muted/50">
<TableRow> <TableRow>
<TableHead className="w-[250px]"><Trans ns="views/settings">users.table.username</Trans></TableHead> <TableHead className="w-[250px]">
<TableHead>Role</TableHead> <Trans ns="views/settings">users.table.username</Trans>
<TableHead className="text-right"><Trans ns="views/settings">users.table.actions</Trans></TableHead> </TableHead>
<TableHead><Trans ns="views/settings">users.table.role</Trans></TableHead>
<TableHead className="text-right">
<Trans ns="views/settings">users.table.actions</Trans>
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@ -233,7 +261,7 @@ export default function AuthenticationView() {
: "" : ""
} }
> >
{user.role || "viewer"} <Trans>role.{user.role || "viewer"}</Trans>
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
@ -262,7 +290,11 @@ export default function AuthenticationView() {
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p><Trans ns="views/settings">users.table.changeRole</Trans></p> <p>
<Trans ns="views/settings">
users.table.changeRole
</Trans>
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
@ -280,12 +312,18 @@ export default function AuthenticationView() {
> >
<FaUserEdit className="size-3.5" /> <FaUserEdit className="size-3.5" />
<span className="ml-1.5 hidden sm:inline-block"> <span className="ml-1.5 hidden sm:inline-block">
<Trans ns="views/settings">users.table.password</Trans> <Trans ns="views/settings">
users.table.password
</Trans>
</span> </span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p><Trans ns="views/settings">users.updatePassword</Trans></p> <p>
<Trans ns="views/settings">
users.updatePassword
</Trans>
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -308,7 +346,11 @@ export default function AuthenticationView() {
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p><Trans ns="views/settings">users.table.deleteUser</Trans></p> <p>
<Trans ns="views/settings">
users.table.deleteUser
</Trans>
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}

View File

@ -78,7 +78,7 @@ export default function CameraSettingsView({
const alertsLabels = useMemo(() => { const alertsLabels = useMemo(() => {
return cameraConfig?.review.alerts.labels return cameraConfig?.review.alerts.labels
? cameraConfig.review.alerts.labels ? cameraConfig.review.alerts.labels
.map((label) => t(label, {ns: "objects"})) .map((label) => t(label, { ns: "objects" }))
.join(", ") .join(", ")
: ""; : "";
}, [cameraConfig]); }, [cameraConfig]);
@ -86,7 +86,7 @@ export default function CameraSettingsView({
const detectionsLabels = useMemo(() => { const detectionsLabels = useMemo(() => {
return cameraConfig?.review.detections.labels return cameraConfig?.review.detections.labels
? cameraConfig.review.detections.labels ? cameraConfig.review.detections.labels
.map((label) => t(label, {ns: "objects"})) .map((label) => t(label, { ns: "objects" }))
.join(", ") .join(", ")
: ""; : "";
}, [cameraConfig]); }, [cameraConfig]);
@ -159,14 +159,9 @@ export default function CameraSettingsView({
}) })
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
toast.success( toast.success(t("camera.reviewClassification.toast.success"), {
t( position: "top-center",
"camera.reviewClassification.toast.success", });
),
{
position: "top-center",
},
);
updateConfig(); updateConfig();
} else { } else {
toast.error( toast.error(
@ -182,11 +177,14 @@ export default function CameraSettingsView({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("toast.save.error", { toast.error(
errorMessage t("toast.save.error", {
}), { errorMessage,
position: "top-center", }),
}); {
position: "top-center",
},
);
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
@ -322,9 +320,7 @@ export default function CameraSettingsView({
/> />
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="detections-enabled"> <Label htmlFor="detections-enabled">
<Trans ns="views/settings"> <Trans ns="views/settings">camera.review.detections</Trans>
camera.review.detections
</Trans>
</Label> </Label>
</div> </div>
</div> </div>
@ -465,19 +461,16 @@ export default function CameraSettingsView({
cameraName: capitalizeFirstLetter( cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "", cameraConfig?.name ?? "",
).replaceAll("_", " "), ).replaceAll("_", " "),
ns: "views/settings" ns: "views/settings",
}, },
) )
: t( : t("camera.reviewClassification.objectAlertsTips", {
"camera.reviewClassification.objectAlertsTips", alertsLabels,
{ cameraName: capitalizeFirstLetter(
alertsLabels, cameraConfig?.name ?? "",
cameraName: capitalizeFirstLetter( ).replaceAll("_", " "),
cameraConfig?.name ?? "", ns: "views/settings",
).replaceAll("_", " "), })}
ns: "views/settings"
},
)}
</div> </div>
</FormItem> </FormItem>
)} )}

View File

@ -120,7 +120,7 @@ export default function MotionTunerView({
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
toast.success( toast.success(
t("motionDetectionTuner.toast.success", { ns: "views/settings"}), t("motionDetectionTuner.toast.success", { ns: "views/settings" }),
{ {
position: "top-center", position: "top-center",
}, },
@ -128,12 +128,9 @@ export default function MotionTunerView({
setChangedValue(false); setChangedValue(false);
updateConfig(); updateConfig();
} else { } else {
toast.error( toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
t("toast.save.error", { errorMessage: res.statusText }), position: "top-center",
{ });
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {
@ -215,7 +212,9 @@ export default function MotionTunerView({
<div className="mt-2 space-y-6"> <div className="mt-2 space-y-6">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md"> <Label htmlFor="motion-threshold" className="text-md">
<Trans ns="views/settings">motionDetectionTuner.Threshold</Trans> <Trans ns="views/settings">
motionDetectionTuner.Threshold
</Trans>
</Label> </Label>
<div className="my-2 text-sm text-muted-foreground"> <div className="my-2 text-sm text-muted-foreground">
<p> <p>
@ -246,7 +245,9 @@ export default function MotionTunerView({
<div className="mt-2 space-y-6"> <div className="mt-2 space-y-6">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md"> <Label htmlFor="motion-threshold" className="text-md">
<Trans ns="views/settings">motionDetectionTuner.contourArea</Trans> <Trans ns="views/settings">
motionDetectionTuner.contourArea
</Trans>
</Label> </Label>
<div className="my-2 text-sm text-muted-foreground"> <div className="my-2 text-sm text-muted-foreground">
<p> <p>

View File

@ -351,7 +351,9 @@ export default function NotificationView({
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
<Trans ns="views/settings">notification.notificationSettings</Trans> <Trans ns="views/settings">
notification.notificationSettings
</Trans>
</Heading> </Heading>
<div className="max-w-6xl"> <div className="max-w-6xl">
@ -366,7 +368,9 @@ export default function NotificationView({
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
<Trans ns="views/settings">notification.documentation</Trans>{" "} <Trans ns="views/settings">
notification.documentation
</Trans>{" "}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </div>
@ -389,15 +393,16 @@ export default function NotificationView({
<FormControl> <FormControl>
<Input <Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72"
placeholder={t( placeholder={t("notification.email.placeholder", {
"notification.email.placeholder", ns: "views/settings",
{ ns: "views/settings" } })}
)}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
<Trans ns="views/settings">notification.email.desc</Trans> <Trans ns="views/settings">
notification.email.desc
</Trans>
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -424,7 +429,9 @@ export default function NotificationView({
name="allEnabled" name="allEnabled"
render={({ field }) => ( render={({ field }) => (
<FilterSwitch <FilterSwitch
label={t("cameras.all", { ns: "components/filter" })} label={t("cameras.all", {
ns: "components/filter",
})}
isChecked={field.value} isChecked={field.value}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setChangedValue(true); setChangedValue(true);
@ -516,7 +523,9 @@ export default function NotificationView({
<div className="flex flex-col gap-2 md:max-w-[50%]"> <div className="flex flex-col gap-2 md:max-w-[50%]">
<Separator className="my-2 flex bg-secondary md:hidden" /> <Separator className="my-2 flex bg-secondary md:hidden" />
<Heading as="h4" className="my-2"> <Heading as="h4" className="my-2">
<Trans ns="views/settings">notification.deviceSpecific</Trans> <Trans ns="views/settings">
notification.deviceSpecific
</Trans>
</Heading> </Heading>
<Button <Button
aria-label="Register or unregister notifications for this device" aria-label="Register or unregister notifications for this device"
@ -560,8 +569,12 @@ export default function NotificationView({
}} }}
> >
{registration != null {registration != null
? t("notification.unregisterDevice", { ns: "views/settings" }) ? t("notification.unregisterDevice", {
: t("notification.registerDevice", { ns: "views/settings" })} ns: "views/settings",
})
: t("notification.registerDevice", {
ns: "views/settings",
})}
</Button> </Button>
{registration != null && registration.active && ( {registration != null && registration.active && (
<Button <Button
@ -656,7 +669,10 @@ export function CameraNotificationSwitch({
time_style: "medium", time_style: "medium",
date_style: "medium", date_style: "medium",
timezone: config?.ui.timezone, timezone: config?.ui.timezone,
strftime_fmt: config?.ui.time_format == "24hour" ? t("time.formattedTimestampExcludeSeconds.24hour"): t("time.formattedTimestampExcludeSeconds"), strftime_fmt:
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour")
: t("time.formattedTimestampExcludeSeconds"),
}); });
}; };

View File

@ -166,7 +166,7 @@ export default function ObjectSettingsView({
.map((detector) => capitalizeFirstLetter(detector)) .map((detector) => capitalizeFirstLetter(detector))
.join(",") .join(",")
: "", : "",
ns: "views/settings", ns: "views/settings",
})} })}
</p> </p>
<p> <p>
@ -264,7 +264,9 @@ export default function ObjectSettingsView({
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div className="cursor-pointer p-0"> <div className="cursor-pointer p-0">
<LuInfo className="size-4" /> <LuInfo className="size-4" />
<span className="sr-only"><Trans>button.info</Trans></span> <span className="sr-only">
<Trans>button.info</Trans>
</span>
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80 text-sm"> <PopoverContent className="w-80 text-sm">

View File

@ -100,12 +100,9 @@ export default function ExploreSettingsView({
setChangedValue(false); setChangedValue(false);
updateConfig(); updateConfig();
} else { } else {
toast.error( toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
t("toast.save.error", { errorMessage: res.statusText }), position: "top-center",
{ });
position: "top-center",
},
);
} }
}) })
.catch((error) => { .catch((error) => {
@ -113,11 +110,14 @@ export default function ExploreSettingsView({
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
toast.error(t("toast.save.error", { toast.error(
errorMessage t("toast.save.error", {
}), { errorMessage,
position: "top-center", }),
}); {
position: "top-center",
},
);
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
@ -272,7 +272,7 @@ export default function ExploreSettingsView({
{t( {t(
"explore.semanticSearch.modelSize." + "explore.semanticSearch.modelSize." +
ExploreSettings.model_size, ExploreSettings.model_size,
{ ns: "views/settings"} { ns: "views/settings" },
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -283,11 +283,9 @@ export default function ExploreSettingsView({
className="cursor-pointer" className="cursor-pointer"
value={size} value={size}
> >
{t( {t("explore.semanticSearch.modelSize." + size, {
"explore.semanticSearch.modelSize." + ns: "views/settings",
size, })}
{ ns: "views/settings"}
)}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>

View File

@ -129,7 +129,9 @@ export default function UiSettingsView() {
onCheckedChange={setAlertVideos} onCheckedChange={setAlertVideos}
/> />
<Label className="cursor-pointer" htmlFor="images-only"> <Label className="cursor-pointer" htmlFor="images-only">
<Trans ns="views/settings">general.liveDashboard.playAlertVideos.label</Trans> <Trans ns="views/settings">
general.liveDashboard.playAlertVideos.label
</Trans>
</Label> </Label>
</div> </div>
<div className="my-2 max-w-5xl text-sm text-muted-foreground"> <div className="my-2 max-w-5xl text-sm text-muted-foreground">
@ -264,7 +266,7 @@ export default function UiSettingsView() {
{t( {t(
"general.calendar.firstWeekday." + "general.calendar.firstWeekday." +
WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(), WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(),
{ns: "views/settings"} { ns: "views/settings" },
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -275,11 +277,9 @@ export default function UiSettingsView() {
className="cursor-pointer" className="cursor-pointer"
value={index.toString()} value={index.toString()}
> >
{t( {t("general.calendar.firstWeekday." + day.toLowerCase(), {
"general.calendar.firstWeekday." + ns: "views/settings",
day.toLowerCase(), })}
{ns: "views/settings"}
)}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>

View File

@ -300,7 +300,9 @@ export default function CameraMetrics({
{Object.keys(cameraFpsSeries).includes(camera.name) ? ( {Object.keys(cameraFpsSeries).includes(camera.name) ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/system">cameras.framesAndDetections</Trans> <Trans ns="views/system">
cameras.framesAndDetections
</Trans>
</div> </div>
<CameraLineGraph <CameraLineGraph
graphId={`${camera.name}-dps`} graphId={`${camera.name}-dps`}

View File

@ -449,7 +449,7 @@ export default function GeneralMetrics({
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto"> <div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
<div className="text-sm font-medium text-muted-foreground"> <div className="text-sm font-medium text-muted-foreground">
<Trans ns="views/settings">general.detector</Trans> <Trans ns="views/system">general.detector.title</Trans>
</div> </div>
<div <div
className={cn( className={cn(
@ -460,7 +460,7 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.detectorInferenceSpeed</Trans> <Trans ns="views/system">general.detector.inferenceSpeed</Trans>
</div> </div>
{detInferenceTimeSeries.map((series) => ( {detInferenceTimeSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -500,7 +500,7 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.detectorCpuUsage</Trans> <Trans ns="views/system">general.detector.cpuUsage</Trans>
</div> </div>
{detCpuSeries.map((series) => ( {detCpuSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -520,7 +520,7 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.detectorMemoryUsage</Trans> <Trans ns="views/system">general.detector.memoryUsage</Trans>
</div> </div>
{detMemSeries.map((series) => ( {detMemSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -552,7 +552,7 @@ export default function GeneralMetrics({
size="sm" size="sm"
onClick={() => setShowVainfo(true)} onClick={() => setShowVainfo(true)}
> >
<Trans ns="views/settings">general.hardwareInfo</Trans> <Trans ns="views/system">general.hardwareInfo.title</Trans>
</Button> </Button>
)} )}
</div> </div>
@ -565,7 +565,9 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.gpuUsage</Trans> <Trans ns="views/system">
general.hardwareInfo.gpuUsage
</Trans>
</div> </div>
{gpuSeries.map((series) => ( {gpuSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -587,7 +589,9 @@ export default function GeneralMetrics({
{gpuMemSeries && ( {gpuMemSeries && (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.gpuMemroy</Trans> <Trans ns="views/system">
general.hardwareInfo.gpuMemroy
</Trans>
</div> </div>
{gpuMemSeries.map((series) => ( {gpuMemSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -611,7 +615,9 @@ export default function GeneralMetrics({
{gpuEncSeries && gpuEncSeries?.length != 0 && ( {gpuEncSeries && gpuEncSeries?.length != 0 && (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.gpuEncoder</Trans> <Trans ns="views/system">
general.hardwareInfo.gpuEncoder
</Trans>
</div> </div>
{gpuEncSeries.map((series) => ( {gpuEncSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -635,7 +641,9 @@ export default function GeneralMetrics({
{gpuDecSeries && gpuDecSeries?.length != 0 && ( {gpuDecSeries && gpuDecSeries?.length != 0 && (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.gpuDecoder</Trans> <Trans ns="views/system">
general.hardwareInfo.gpuDecoder
</Trans>
</div> </div>
{gpuDecSeries.map((series) => ( {gpuDecSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -659,13 +667,15 @@ export default function GeneralMetrics({
)} )}
<div className="mt-4 text-sm font-medium text-muted-foreground"> <div className="mt-4 text-sm font-medium text-muted-foreground">
<Trans ns="views/settings">general.otherProcesses</Trans> <Trans ns="views/system">general.otherProcesses.title</Trans>
</div> </div>
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2"> <div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.processCpuUsage</Trans> <Trans ns="views/system">
general.otherProcesses.processCpuUsage
</Trans>
</div> </div>
{otherProcessCpuSeries.map((series) => ( {otherProcessCpuSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -685,7 +695,9 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5"> <div className="mb-5">
<Trans ns="views/settings">general.processMemoryUsage</Trans> <Trans ns="views/system">
general.otherProcesses.processMemoryUsage
</Trans>
</div> </div>
{otherProcessMemSeries.map((series) => ( {otherProcessMemSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph