i18n workflow improvements and tweaks (#22586)

* 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
This commit is contained in:
Josh Hawkins
2026-03-23 08:48:02 -05:00
committed by GitHub
parent 57c0473e6e
commit 7b6d0c5e42
26 changed files with 1262 additions and 162 deletions

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ export default function MobileTimelineDrawer({
setDrawer(false);
}}
>
{t("timeline")}
{t("timeline.label")}
</div>
<div
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">
<div className="flex items-center justify-between">
<h4 className="font-medium">
{t("cameraWizard.step2.streamTitle", {
{t("cameraWizard.step3.streamTitle", {
number: index + 1,
})}
</h4>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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