Compare commits

..

3 Commits

Author SHA1 Message Date
Josh Hawkins
7b6d0c5e42
i18n workflow improvements and tweaks (#22586)
Some checks are pending
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* mobile button spacing

* prevent console warning about div being descendant of p

* ensure consistent spacing

* add missing i18n keys

* i18n fixes

- add missing translations
- fix dot notation keys

* use plain string

* add missing key

* add i18next-cli commands for extraction and status

also add false positives removal for several keys

* add i18n key check step to PR workflow

* formatting
2026-03-23 08:48:02 -05:00
Nicolas Mowen
57c0473e6e
Add support for RDNA4 AMD GPUs ROCm (#22585) 2026-03-23 07:26:40 -06:00
Josh Hawkins
6251b758b4
update web deps (#22584)
@rjsf/*: 6.3.1 → 6.4.1
axios: 1.7.7 → 1.13.6
hls.js: 1.5.20 → 1.6.15
swr: 2.3.2 → 2.4.1
konva: 9.3.18 → 10.2.3
framer-motion: 12.35.0 → 12.38.0
lucide-react: 0.477.0 → 0.577.0
react-hook-form: 7.52.1 → 7.72.0
@hookform/resolvers: 3.9.0 → 3.10.0
react-day-picker: 9.7.0 → 9.14.0
monaco-yaml: 5.3.1 → 5.4.1
monaco-editor: 0.52.0 → 0.52.2
postcss: 8.4.47 → 8.5.8
2026-03-23 07:23:44 -05:00
27 changed files with 1389 additions and 276 deletions

View File

@ -27,6 +27,9 @@ jobs:
- name: Lint - name: Lint
run: npm run lint run: npm run lint
working-directory: ./web working-directory: ./web
- name: Check i18n keys
run: npm run i18n:extract:ci
working-directory: ./web
web_test: web_test:
name: Web - Test name: Web - Test

View File

@ -59,12 +59,14 @@ ARG ROCM
# Copy HIP headers required for MIOpen JIT (BuildHip) / HIPRTC at runtime # Copy HIP headers required for MIOpen JIT (BuildHip) / HIPRTC at runtime
COPY --from=rocm /opt/rocm-${ROCM}/include/ /opt/rocm-${ROCM}/include/ COPY --from=rocm /opt/rocm-${ROCM}/include/ /opt/rocm-${ROCM}/include/
COPY --from=rocm /opt/rocm-$ROCM/bin/rocminfo /opt/rocm-$ROCM/bin/migraphx-driver /opt/rocm-$ROCM/bin/ COPY --from=rocm /opt/rocm-$ROCM/bin/rocminfo /opt/rocm-$ROCM/bin/migraphx-driver /opt/rocm-$ROCM/bin/
# Copy MIOpen database files for gfx10xx and gfx11xx only (RDNA2/RDNA3) # Copy MIOpen database files for gfx10xx, gfx11xx, and gfx12xx only (RDNA2/RDNA3/RDNA4)
COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx10* /opt/rocm-$ROCM/share/miopen/db/ COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx10* /opt/rocm-$ROCM/share/miopen/db/
COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx11* /opt/rocm-$ROCM/share/miopen/db/ COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx11* /opt/rocm-$ROCM/share/miopen/db/
# Copy rocBLAS library files for gfx10xx and gfx11xx only COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx12* /opt/rocm-$ROCM/share/miopen/db/
# Copy rocBLAS library files for gfx10xx, gfx11xx, and gfx12xx only
COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx10* /opt/rocm-$ROCM/lib/rocblas/library/ COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx10* /opt/rocm-$ROCM/lib/rocblas/library/
COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx11* /opt/rocm-$ROCM/lib/rocblas/library/ COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx11* /opt/rocm-$ROCM/lib/rocblas/library/
COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx12* /opt/rocm-$ROCM/lib/rocblas/library/
COPY --from=rocm /opt/rocm-dist/ / COPY --from=rocm /opt/rocm-dist/ /
####################################################################### #######################################################################

51
web/i18next.config.ts Normal file
View File

@ -0,0 +1,51 @@
import { defineConfig, type Plugin } from "i18next-cli";
/**
* Plugin to remove false positive keys generated by dynamic namespace patterns
* like useTranslation([i18nLibrary]) and t("key", { ns: configNamespace }).
* These keys already exist in their correct runtime namespaces.
*/
function ignoreDynamicNamespaceKeys(): Plugin {
// Keys that the extractor misattributes to the wrong namespace
// because it can't resolve dynamic ns values at build time.
const falsePositiveKeys = new Set([
// From useTranslation([i18nLibrary]) in ClassificationCard.tsx
// Already in views/classificationModel and views/faceLibrary
"details.unknown",
"details.none",
// From t("key", { ns: configNamespace }) in DetectorHardwareField.tsx
// Already in config/global
"detectors.type.label",
// From t(`${prefix}`) template literals producing empty/partial keys
"",
"_one",
"_other",
]);
return {
name: "ignore-dynamic-namespace-keys",
onEnd: async (keys) => {
for (const key of keys.keys()) {
// Each map key is "ns:actualKey" format
const separatorIndex = key.indexOf(":");
const actualKey =
separatorIndex >= 0 ? key.slice(separatorIndex + 1) : key;
if (falsePositiveKeys.has(actualKey)) {
keys.delete(key);
}
}
},
};
}
export default defineConfig({
locales: ["en"],
extract: {
input: ["src/**/*.{ts,tsx}"],
output: "public/locales/{{language}}/{{namespace}}.json",
defaultNS: "common",
removeUnusedKeys: false,
sort: false,
},
plugins: [ignoreDynamicNamespaceKeys()],
});

1448
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,14 @@
"preview": "vite preview", "preview": "vite preview",
"prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,js,jsx,css,html}\"", "prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,js,jsx,css,html}\"",
"test": "vitest", "test": "vitest",
"coverage": "vitest run --coverage" "coverage": "vitest run --coverage",
"i18n:extract": "i18next-cli extract",
"i18n:extract:ci": "i18next-cli extract --ci",
"i18n:status": "i18next-cli status"
}, },
"dependencies": { "dependencies": {
"@cycjimmy/jsmpeg-player": "^6.1.2", "@cycjimmy/jsmpeg-player": "^6.1.2",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.10.0",
"@melloware/react-logviewer": "^6.1.2", "@melloware/react-logviewer": "^6.1.2",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.1.2",
@ -40,38 +43,38 @@
"@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@rjsf/core": "^6.3.1", "@rjsf/core": "^6.4.1",
"@rjsf/shadcn": "^6.3.1", "@rjsf/shadcn": "^6.4.1",
"@rjsf/utils": "^6.3.1", "@rjsf/utils": "^6.4.1",
"@rjsf/validator-ajv8": "^6.3.1", "@rjsf/validator-ajv8": "^6.4.1",
"apexcharts": "^3.52.0", "apexcharts": "^3.52.0",
"axios": "^1.7.7", "axios": "^1.13.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"framer-motion": "^12.35.0", "framer-motion": "^12.38.0",
"hls.js": "^1.5.20", "hls.js": "^1.6.15",
"i18next": "^24.2.0", "i18next": "^24.2.0",
"i18next-http-backend": "^3.0.1", "i18next-http-backend": "^3.0.1",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"immer": "^10.1.1", "immer": "^10.1.1",
"konva": "^9.3.18", "konva": "^10.2.3",
"lodash": "^4.17.23", "lodash": "^4.17.23",
"lucide-react": "^0.477.0", "lucide-react": "^0.577.0",
"monaco-yaml": "^5.3.1", "monaco-yaml": "^5.4.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nosleep.js": "^0.12.0", "nosleep.js": "^0.12.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-apexcharts": "^1.4.1", "react-apexcharts": "^1.4.1",
"react-day-picker": "^9.7.0", "react-day-picker": "^9.14.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-grid-layout": "^2.2.2", "react-grid-layout": "^2.2.2",
"react-hook-form": "^7.52.1", "react-hook-form": "^7.72.0",
"react-i18next": "^15.2.0", "react-i18next": "^15.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-konva": "^19.2.3", "react-konva": "^19.2.3",
@ -84,7 +87,7 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"sort-by": "^1.2.0", "sort-by": "^1.2.0",
"strftime": "^0.10.3", "strftime": "^0.10.3",
"swr": "^2.3.2", "swr": "^2.4.1",
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.4.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@ -114,12 +117,13 @@
"eslint-plugin-react-refresh": "^0.4.8", "eslint-plugin-react-refresh": "^0.4.8",
"eslint-plugin-vitest-globals": "^1.5.0", "eslint-plugin-vitest-globals": "^1.5.0",
"fake-indexeddb": "^6.0.0", "fake-indexeddb": "^6.0.0",
"i18next-cli": "^1.5.11",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"jsdom": "^24.1.1", "jsdom": "^24.1.1",
"monaco-editor": "^0.52.0", "monaco-editor": "^0.52.2",
"msw": "^2.3.5", "msw": "^2.3.5",
"patch-package": "^8.0.1", "patch-package": "^8.0.1",
"postcss": "^8.4.47", "postcss": "^8.5.8",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.9",

View File

@ -161,7 +161,8 @@
"resetToDefault": "Reset to Default", "resetToDefault": "Reset to Default",
"saveAll": "Save All", "saveAll": "Save All",
"savingAll": "Saving All…", "savingAll": "Saving All…",
"undoAll": "Undo All" "undoAll": "Undo All",
"retry": "Retry"
}, },
"menu": { "menu": {
"system": "System", "system": "System",
@ -275,7 +276,8 @@
"error": { "error": {
"title": "Failed to save config changes: {{errorMessage}}", "title": "Failed to save config changes: {{errorMessage}}",
"noMessage": "Failed to save config changes" "noMessage": "Failed to save config changes"
} },
"success": "Successfully saved config changes."
} }
}, },
"role": { "role": {
@ -310,5 +312,7 @@
"readTheDocumentation": "Read the documentation", "readTheDocumentation": "Read the documentation",
"information": { "information": {
"pixels": "{{area}}px" "pixels": "{{area}}px"
} },
"no_items": "No items",
"validation_errors": "Validation Errors"
} }

View File

@ -81,6 +81,7 @@
"zones": "Zones", "zones": "Zones",
"mask": "Mask", "mask": "Mask",
"motion": "Motion", "motion": "Motion",
"regions": "Regions" "regions": "Regions",
"paths": "Paths"
} }
} }

View File

@ -23,15 +23,17 @@
}, },
"toast": { "toast": {
"success": { "success": {
"deletedCategory": "Deleted Class",
"deletedImage": "Deleted Images",
"deletedModel_one": "Successfully deleted {{count}} model", "deletedModel_one": "Successfully deleted {{count}} model",
"deletedModel_other": "Successfully deleted {{count}} models", "deletedModel_other": "Successfully deleted {{count}} models",
"categorizedImage": "Successfully Classified Image", "categorizedImage": "Successfully Classified Image",
"trainedModel": "Successfully trained model.", "trainedModel": "Successfully trained model.",
"trainingModel": "Successfully started model training.", "trainingModel": "Successfully started model training.",
"updatedModel": "Successfully updated model configuration", "updatedModel": "Successfully updated model configuration",
"renamedCategory": "Successfully renamed class to {{name}}" "renamedCategory": "Successfully renamed class to {{name}}",
"deletedCategory_one": "Deleted {{count}} class",
"deletedCategory_other": "Deleted {{count}} classes",
"deletedImage_one": "Deleted {{count}} image",
"deletedImage_other": "Deleted {{count}} images"
}, },
"error": { "error": {
"deleteImageFailed": "Failed to delete: {{errorMessage}}", "deleteImageFailed": "Failed to delete: {{errorMessage}}",

View File

@ -15,8 +15,10 @@
"description": "Review items can only be created for a camera when recordings are enabled for that camera." "description": "Review items can only be created for a camera when recordings are enabled for that camera."
} }
}, },
"timeline": "Timeline", "timeline": {
"timeline.aria": "Select timeline", "label": "Timeline",
"aria": "Select timeline"
},
"zoomIn": "Zoom In", "zoomIn": "Zoom In",
"zoomOut": "Zoom Out", "zoomOut": "Zoom Out",
"events": { "events": {

View File

@ -169,7 +169,8 @@
}, },
"title": { "title": {
"label": "Title" "label": "Title"
} },
"scoreInfo": "Score Information"
}, },
"itemMenu": { "itemMenu": {
"downloadVideo": { "downloadVideo": {
@ -220,12 +221,18 @@
"debugReplay": { "debugReplay": {
"label": "Debug replay", "label": "Debug replay",
"aria": "View this tracked object in the debug replay view" "aria": "View this tracked object in the debug replay view"
},
"more": {
"aria": "More"
} }
}, },
"dialog": { "dialog": {
"confirmDelete": { "confirmDelete": {
"title": "Confirm Delete", "title": "Confirm Delete",
"desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated tracking details 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 tracking details 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?"
},
"toast": {
"error": "Error deleting this tracked object: {{errorMessage}}"
} }
}, },
"noTrackedObjects": "No Tracked Objects Found", "noTrackedObjects": "No Tracked Objects Found",
@ -248,5 +255,8 @@
}, },
"concerns": { "concerns": {
"label": "Concerns" "label": "Concerns"
},
"objectLifecycle": {
"noImageFound": "No image found for this tracked object."
} }
} }

View File

@ -6,8 +6,10 @@
"cases": "Cases", "cases": "Cases",
"uncategorizedExports": "Uncategorized Exports" "uncategorizedExports": "Uncategorized Exports"
}, },
"deleteExport": "Delete Export", "deleteExport": {
"deleteExport.desc": "Are you sure you want to delete {{exportName}}?", "label": "Delete Export",
"desc": "Are you sure you want to delete {{exportName}}?"
},
"editExport": { "editExport": {
"title": "Rename Export", "title": "Rename Export",
"desc": "Enter a new name for this export.", "desc": "Enter a new name for this export.",

View File

@ -1,6 +1,8 @@
{ {
"documentTitle": "Live - Frigate", "documentTitle": {
"documentTitle.withCamera": "{{camera}} - Live - Frigate", "default": "Live - Frigate",
"withCamera": "{{camera}} - Live - Frigate"
},
"lowBandwidthMode": "Low-bandwidth Mode", "lowBandwidthMode": "Low-bandwidth Mode",
"twoWayTalk": { "twoWayTalk": {
"enable": "Enable Two Way Talk", "enable": "Enable Two Way Talk",

View File

@ -515,7 +515,6 @@
"reviewClassification": { "reviewClassification": {
"title": "Review Classification", "title": "Review Classification",
"desc": "Frigate categorizes review items as Alerts and Detections. By default, all <em>person</em> and <em>car</em> objects are considered Alerts. You can refine categorization of your review items by configuring required zones for them.", "desc": "Frigate categorizes review items as Alerts and Detections. By default, all <em>person</em> and <em>car</em> objects are considered Alerts. You can refine categorization of your review items by configuring required zones for them.",
"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.",
@ -553,6 +552,17 @@
"motionMaskLabel": "Motion Mask {{number}}", "motionMaskLabel": "Motion Mask {{number}}",
"objectMaskLabel": "Object Mask {{number}}", "objectMaskLabel": "Object Mask {{number}}",
"form": { "form": {
"id": {
"error": {
"mustNotBeEmpty": "ID must not be empty.",
"alreadyExists": "A mask with this ID already exists for this camera."
}
},
"name": {
"error": {
"mustNotBeEmpty": "Name must not be empty."
}
},
"zoneName": { "zoneName": {
"error": { "error": {
"mustBeAtLeastTwoCharacters": "Zone name must be at least 2 characters.", "mustBeAtLeastTwoCharacters": "Zone name must be at least 2 characters.",
@ -1286,7 +1296,8 @@
}, },
"camera": { "camera": {
"title": "Camera Settings", "title": "Camera Settings",
"description": "These settings apply only to this camera and override the global settings." "description": "These settings apply only to this camera and override the global settings.",
"noCameras": "No cameras available"
}, },
"advancedSettingsCount": "Advanced Settings ({{count}})", "advancedSettingsCount": "Advanced Settings ({{count}})",
"advancedCount": "Advanced ({{count}})", "advancedCount": "Advanced ({{count}})",

View File

@ -35,7 +35,8 @@
"cameras_count_other": "{{count}} Cameras" "cameras_count_other": "{{count}} Cameras"
}, },
"empty": "No messages captured yet", "empty": "No messages captured yet",
"count": "{{count}} messages", "count_one": "{{count}} message",
"count_other": "{{count}} messages",
"expanded": { "expanded": {
"payload": "Payload" "payload": "Payload"
} }

View File

@ -60,7 +60,7 @@ export default function DeleteRoleDialog({
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
<p> <p>
<Trans <Trans
ns={"views/settings"} ns="views/settings"
values={{ role }} values={{ role }}
components={{ strong: <span className="font-medium" /> }} components={{ strong: <span className="font-medium" /> }}
> >

View File

@ -35,7 +35,7 @@ export default function DeleteTriggerDialog({
<DialogTitle>{t("triggers.dialog.deleteTrigger.title")}</DialogTitle> <DialogTitle>{t("triggers.dialog.deleteTrigger.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
<Trans <Trans
ns={"views/settings"} ns="views/settings"
values={{ triggerName }} values={{ triggerName }}
components={{ strong: <span className="font-medium" /> }} components={{ strong: <span className="font-medium" /> }}
> >

View File

@ -90,7 +90,7 @@ export default function EditRoleCamerasDialog({
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
<Trans <Trans
ns={"views/settings"} ns="views/settings"
values={{ role }} values={{ role }}
components={{ strong: <span className="font-medium" /> }} components={{ strong: <span className="font-medium" /> }}
> >

View File

@ -40,7 +40,7 @@ export default function MobileTimelineDrawer({
setDrawer(false); setDrawer(false);
}} }}
> >
{t("timeline")} {t("timeline.label")}
</div> </div>
<div <div
className={`mx-4 w-full py-2 text-center smart-capitalize ${selected == "events" ? "rounded-lg bg-secondary" : ""}`} className={`mx-4 w-full py-2 text-center smart-capitalize ${selected == "events" ? "rounded-lg bg-secondary" : ""}`}

View File

@ -494,7 +494,7 @@ export default function CameraEditForm({
<CardContent className="space-y-4 p-4"> <CardContent className="space-y-4 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium"> <h4 className="font-medium">
{t("cameraWizard.step2.streamTitle", { {t("cameraWizard.step3.streamTitle", {
number: index + 1, number: index + 1,
})} })}
</h4> </h4>

View File

@ -338,8 +338,8 @@ export default function CameraWizardDialog({
} }
} else { } else {
toast.success( toast.success(
t("camera.cameraConfig.toast.success", { t("cameraWizard.save.success", {
cameraName: wizardData.cameraName, cameraName: friendlyName || finalCameraName,
}), }),
{ position: "top-center" }, { position: "top-center" },
); );

View File

@ -785,7 +785,7 @@ export default function ZoneEditPane({
</div> </div>
<FormDescription> <FormDescription>
{t("masksAndZones.zones.speedEstimation.desc")} {t("masksAndZones.zones.speedEstimation.desc")}
<div className="mt-2 flex items-center text-primary"> <span className="mt-2 flex items-center text-primary">
<Link <Link
to={getLocaleDocUrl( to={getLocaleDocUrl(
"configuration/zones#speed-estimation", "configuration/zones#speed-estimation",
@ -797,7 +797,7 @@ export default function ZoneEditPane({
{t("readTheDocumentation", { ns: "common" })} {t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </span>
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@ -220,7 +220,7 @@ function Exports() {
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t("deleteExport")}</AlertDialogTitle> <AlertDialogTitle>{t("deleteExport.label")}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t("deleteExport.desc", { exportName: deleteClip?.exportName })} {t("deleteExport.desc", { exportName: deleteClip?.exportName })}
</AlertDialogDescription> </AlertDialogDescription>

View File

@ -81,7 +81,7 @@ function Live() {
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`, camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
}); });
} else { } else {
document.title = t("documentTitle", { ns: "views/live" }); document.title = t("documentTitle.default", { ns: "views/live" });
} }
}, [cameraGroup, selectedCameraName, t]); }, [cameraGroup, selectedCameraName, t]);

View File

@ -1397,10 +1397,12 @@ export default function Settings() {
: "bg-selected"; : "bg-selected";
return ( return (
<div className="flex w-full items-center justify-between pr-4 md:pr-0"> <div className="flex w-full min-w-0 items-center justify-between pr-4 md:pr-0">
<div>{t("menu." + key)}</div> <div className="min-w-0 flex-1 whitespace-normal break-words">
{t("menu." + key)}
</div>
{(showOverrideDot || showUnsavedDot) && ( {(showOverrideDot || showUnsavedDot) && (
<div className="ml-2 flex items-center gap-2"> <div className="ml-2 flex shrink-0 items-center gap-2">
{showOverrideDot && ( {showOverrideDot && (
<span <span
className={cn("inline-block size-2 rounded-full", dotColor)} className={cn("inline-block size-2 rounded-full", dotColor)}
@ -1747,7 +1749,7 @@ export default function Settings() {
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton <SidebarMenuButton
className="ml-0" className="ml-0 h-auto min-h-8 py-1.5"
isActive={pageToggle === filteredItems[0].key} isActive={pageToggle === filteredItems[0].key}
onClick={() => { onClick={() => {
if ( if (
@ -1788,6 +1790,7 @@ export default function Settings() {
{filteredItems.map((item) => ( {filteredItems.map((item) => (
<SidebarMenuSubItem key={item.key}> <SidebarMenuSubItem key={item.key}>
<SidebarMenuSubButton <SidebarMenuSubButton
className="h-auto w-full py-1.5"
isActive={pageToggle === item.key} isActive={pageToggle === item.key}
onClick={() => { onClick={() => {
if ( if (

View File

@ -1,3 +1,5 @@
import { TFunction } from "i18next";
export const calculatePasswordStrength = (password: string): number => { export const calculatePasswordStrength = (password: string): number => {
if (!password) return 0; if (!password) return 0;
@ -16,13 +18,18 @@ export const getPasswordRequirements = (password: string) => ({
export const getPasswordStrengthLabel = ( export const getPasswordStrengthLabel = (
password: string, password: string,
t: (key: string) => string, t: TFunction,
): string => { ): string => {
const strength = calculatePasswordStrength(password); const strength = calculatePasswordStrength(password);
if (!password) return ""; if (!password) return "";
if (strength < 1) return t("users.dialog.form.password.strength.weak"); if (strength < 1)
return t("users.dialog.form.password.strength.veryStrong"); return t("users.dialog.form.password.strength.weak", {
ns: "views/settings",
});
return t("users.dialog.form.password.strength.veryStrong", {
ns: "views/settings",
});
}; };
export const getPasswordStrengthColor = (password: string): string => { export const getPasswordStrengthColor = (password: string): string => {

View File

@ -700,7 +700,7 @@ export function RecordingView({
value="timeline" value="timeline"
aria-label={t("timeline.aria")} aria-label={t("timeline.aria")}
> >
<div className="">{t("timeline")}</div> <div className="">{t("timeline.label")}</div>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem <ToggleGroupItem
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`} className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}

View File

@ -618,7 +618,7 @@ export default function ProfilesView({
ns: "views/settings", ns: "views/settings",
})} })}
/> />
<DialogFooter> <DialogFooter className="gap-2 md:gap-0">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"