i18n fixes (#17725)

* fix key

* add french

* Fix random 0

* fix i18n in role change dialog

* fix delete user dialog

* fix filter tips steps

* remove classes from i18n files

* combine disjointed norweigan localized files

* change submit to plus to use a question with yes/no

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins 2025-04-17 08:34:24 -05:00 committed by GitHub
parent 1315a28252
commit 84c1ad59a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 134 additions and 72 deletions

View File

@ -15,15 +15,11 @@
"desc": "Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model." "desc": "Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model."
}, },
"review": { "review": {
"true": { "question": {
"label": "Confirm this label for Frigate Plus", "label": "Confirm this label for Frigate Plus",
"true_one": "This is a {{label}}", "ask_a": "Is this object a <code>{{label}}</code>?",
"true_other": "This is an {{label}}" "ask_an": "Is this object an <code>{{label}}</code>?",
}, "ask_full": "Is this object a <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
"false": {
"label": "Do not confirm this label for Frigate Plus",
"false_one": "This is not a {{label}}",
"false_other": "This is not an {{label}}"
}, },
"state": { "state": {
"submitted": "Submitted" "submitted": "Submitted"

View File

@ -46,8 +46,13 @@
"title": "How to use text filters", "title": "How to use text filters",
"desc": { "desc": {
"text": "Filters help you narrow down your search results. Here's how to use them in the input field:", "text": "Filters help you narrow down your search results. Here's how to use them in the input field:",
"step": "<ul className=\"list-disc pl-5 text-sm text-primary-variant\"><li>Type a filter name followed by a colon (e.g., \"cameras:\").</li><li>Select a value from the suggestions or type your own.</li><li>Use multiple filters by adding them one after another with a space in between.</li><li>Date filters (before: and after:) use <em>{{DateFormat}}</em> format.</li><li>Time range filter uses <em>{{exampleTime}}</em> format.</li><li>Remove filters by clicking the 'x' next to them.</li></ul>", "step1": "Type a filter key name followed by a colon (e.g., \"cameras:\").",
"example": "Example: <code className=\"text-primary\">cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM </code>" "step2": "Select a value from the suggestions or type your own.",
"step3": "Use multiple filters by adding them one after another with a space in between.",
"step4": "Date filters (before: and after:) use {{DateFormat}} format.",
"step5": "Time range filter uses {{exampleTime}} format.",
"step6": "Remove filters by clicking the 'x' next to them.",
"exampleLabel": "Example:"
} }
}, },
"header": { "header": {

View File

@ -385,12 +385,12 @@
"motion": { "motion": {
"title": "Motion boxes", "title": "Motion boxes",
"desc": "Show boxes around areas where motion is detected", "desc": "Show boxes around areas where motion is detected",
"tips": "<p className=\"mb-2\"><strong>Motion Boxes</strong></p><br><p>Red boxes will be overlaid on areas of the frame where motion is currently being detected</p>" "tips": "<p><strong>Motion Boxes</strong></p><br><p>Red boxes will be overlaid on areas of the frame where motion is currently being detected</p>"
}, },
"regions": { "regions": {
"title": "Regions", "title": "Regions",
"desc": "Show a box of the region of interest sent to the object detector", "desc": "Show a box of the region of interest sent to the object detector",
"tips": "<p className=\"mb-2\"><strong>Region Boxes</strong></p><br><p>Bright green boxes will be overlaid on areas of interest in the frame that are being sent to the object detector.</p>" "tips": "<p><strong>Region Boxes</strong></p><br><p>Bright green boxes will be overlaid on areas of interest in the frame that are being sent to the object detector.</p>"
}, },
"objectShapeFilterDrawing": { "objectShapeFilterDrawing": {
"title": "Object Shape Filter Drawing", "title": "Object Shape Filter Drawing",
@ -474,7 +474,7 @@
"deleteUser": { "deleteUser": {
"title": "Delete User", "title": "Delete User",
"desc": "This action cannot be undone. This will permanently delete the user account and remove all associated data.", "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>?" "warn": "Are you sure you want to delete"
}, },
"passwordSetting": { "passwordSetting": {
"updatePassword": "Update Password for {{username}}", "updatePassword": "Update Password for {{username}}",
@ -483,8 +483,14 @@
}, },
"changeRole": { "changeRole": {
"title": "Change User Role", "title": "Change User Role",
"desc": "Update permissions for <span className=\"font-medium\">{{username}}</span>", "desc": "Update permissions for",
"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>" "roleInfo": {
"intro": "Select the appropriate role for this user:",
"admin": "Admin",
"adminDesc": "Full access to all features.",
"viewer": "Viewer",
"viewerDesc": "Limited to Live dashboards, Review, Explore, and Exports only."
}
} }
} }
}, },

View File

@ -51,7 +51,7 @@ import { toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { MdImageSearch } from "react-icons/md"; import { MdImageSearch } from "react-icons/md";
import { Trans, useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
type InputWithTagsProps = { type InputWithTagsProps = {
inputFocused: boolean; inputFocused: boolean;
@ -729,20 +729,31 @@ export default function InputWithTags({
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("filter.tips.desc.text")} {t("filter.tips.desc.text")}
</p> </p>
<Trans <ul className="list-disc pl-5 text-sm text-primary-variant">
ns="views/search" <li>{t("filter.tips.desc.step1")}</li>
values={{ <li>{t("filter.tips.desc.step2")}</li>
DateFormat: getIntlDateFormat(), <li>{t("filter.tips.desc.step3")}</li>
exampleTime: <li>
config?.ui.time_format == "24hour" {t("filter.tips.desc.step4", {
? "15:00-16:00" DateFormat: getIntlDateFormat(),
: "3:00PM-4:00PM", })}
}} </li>
> <li>
filter.tips.desc.step {t("filter.tips.desc.step5", {
</Trans> exampleTime:
config?.ui.time_format == "24hour"
? "15:00-16:00"
: "3:00PM-4:00PM",
})}
</li>
<li>{t("filter.tips.desc.step6")}</li>
</ul>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
<Trans ns="views/search">filter.tips.desc.example</Trans> {t("filter.tips.desc.exampleLabel")}{" "}
<code className="text-primary">
cameras:front_door label:person before:01012024
time_range:3:00PM-4:00PM
</code>
</p> </p>
</div> </div>
</PopoverContent> </PopoverContent>

View File

@ -78,6 +78,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
const languages = [ const languages = [
{ code: "en", label: t("menu.language.en") }, { code: "en", label: t("menu.language.en") },
{ code: "es", label: t("menu.language.es") }, { code: "es", label: t("menu.language.es") },
{ code: "fr", label: t("menu.language.fr") },
{ code: "zh-CN", label: t("menu.language.zhCN") }, { code: "zh-CN", label: t("menu.language.zhCN") },
{ code: "tr", label: t("menu.language.tr") }, { code: "tr", label: t("menu.language.tr") },
{ code: "nl", label: t("menu.language.nl") }, { code: "nl", label: t("menu.language.nl") },

View File

@ -35,7 +35,8 @@ 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">
{t("users.dialog.deleteUser.warn", { username })} {t("users.dialog.deleteUser.warn")}
<span className="font-medium"> {username}</span>?
</p> </p>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Trans, useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { import {
Dialog, Dialog,
@ -46,13 +46,28 @@ export default function RoleChangeDialog({
{t("users.dialog.changeRole.title")} {t("users.dialog.changeRole.title")}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{t("users.dialog.changeRole.desc", { username })} {t("users.dialog.changeRole.desc")}
<span className="font-medium"> {username}</span>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="py-6"> <div className="py-3">
<div className="mb-4 text-sm text-muted-foreground"> <div className="mb-4 text-sm text-muted-foreground">
<Trans ns="views/settings">users.dialog.changeRole.roleInfo</Trans> <p>{t("users.dialog.changeRole.roleInfo.intro")}</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>
<span className="font-medium">
{t("users.dialog.changeRole.roleInfo.admin")}
</span>
: {t("users.dialog.changeRole.roleInfo.adminDesc")}
</li>
<li>
<span className="font-medium">
{t("users.dialog.changeRole.roleInfo.viewer")}
</span>
: {t("users.dialog.changeRole.roleInfo.viewerDesc")}
</li>
</ul>
</div> </div>
<Select <Select

View File

@ -73,7 +73,7 @@ import { LuInfo, LuSearch } from "react-icons/lu";
import { TooltipPortal } from "@radix-ui/react-tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip";
import { FaPencilAlt } from "react-icons/fa"; import { FaPencilAlt } from "react-icons/fa";
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
import { useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { TbFaceId } from "react-icons/tb"; import { TbFaceId } from "react-icons/tb";
import { useIsAdmin } from "@/hooks/use-is-admin"; import { useIsAdmin } from "@/hooks/use-is-admin";
import FaceSelectionDialog from "../FaceSelectionDialog"; import FaceSelectionDialog from "../FaceSelectionDialog";
@ -366,7 +366,7 @@ function ObjectDetailsTab({
const snapScore = useMemo(() => { const snapScore = useMemo(() => {
if (!search?.has_snapshot) { if (!search?.has_snapshot) {
return 0; return undefined;
} }
const value = search.data.score ?? search.score ?? 0; const value = search.data.score ?? search.score ?? 0;
@ -1017,7 +1017,7 @@ export function ObjectSnapshotTab({
search, search,
onEventUploaded, onEventUploaded,
}: ObjectSnapshotTabProps) { }: ObjectSnapshotTabProps) {
const { t } = useTranslation(["components/dialog"]); const { t, i18n } = useTranslation(["components/dialog"]);
type SubmissionState = "reviewing" | "uploading" | "submitted"; type SubmissionState = "reviewing" | "uploading" | "submitted";
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
@ -1128,42 +1128,67 @@ export function ObjectSnapshotTab({
</div> </div>
</div> </div>
<div className="flex flex-row justify-center gap-2 md:justify-end"> <div className="flex w-full flex-1 flex-col justify-center gap-2 md:ml-8 md:w-auto md:justify-end">
{state == "reviewing" && ( {state == "reviewing" && (
<> <>
<Button <div>
className="bg-success" {i18n.language === "en" ? (
aria-label={t("explore.plus.review.true.label")} // English with a/an logic plus label
onClick={() => { <>
setState("uploading"); {/^[aeiou]/i.test(search?.label || "") ? (
onSubmitToPlus(false); <Trans
}} ns="components/dialog"
> values={{ label: search?.label }}
{/^[aeiou]/i.test(search?.label || "") >
? t("explore.plus.review.true.true_other", { explore.plus.review.question.ask_an
label: search?.label, </Trans>
}) ) : (
: t("explore.plus.review.true.true_one", { <Trans
label: search?.label, ns="components/dialog"
})} values={{ label: search?.label }}
</Button> >
<Button explore.plus.review.question.ask_a
className="text-white" </Trans>
aria-label={t("explore.plus.review.false.label")} )}
variant="destructive" </>
onClick={() => { ) : (
setState("uploading"); // For other languages
onSubmitToPlus(true); <Trans
}} ns="components/dialog"
> values={{
{/^[aeiou]/i.test(search?.label || "") untranslatedLabel: search?.label,
? t("explore.plus.review.false.false_other", { translatedLabel: t(
label: search?.label, "filter.label." + search?.label,
}) ),
: t("explore.plus.review.false.false_one", { }}
label: search?.label, >
})} explore.plus.review.question.ask_full
</Button> </Trans>
)}
</div>
<div className="flex w-full flex-row gap-2">
<Button
className="flex-1 bg-success"
aria-label={t("button.yes", { ns: "common" })}
onClick={() => {
setState("uploading");
onSubmitToPlus(false);
}}
>
{t("button.yes", { ns: "common" })}
</Button>
<Button
className="flex-1 text-white"
aria-label={t("button.no", { ns: "common" })}
variant="destructive"
onClick={() => {
setState("uploading");
onSubmitToPlus(true);
}}
>
{t("button.no", { ns: "common" })}
</Button>
</div>
</> </>
)} )}
{state == "uploading" && <ActivityIndicator />} {state == "uploading" && <ActivityIndicator />}

View File

@ -242,7 +242,9 @@ export default function MotionMaskEditPane({
</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">masksAndZones.motionMasks.context</Trans> <Trans ns="views/settings">
masksAndZones.motionMasks.context.title
</Trans>
</p> </p>
<div className="flex items-center text-primary"> <div className="flex items-center text-primary">