feat: add i18n (translation/localization) (#16877)

* Translation module init

* Add more i18n keys

* fix: fix string wrong

* refactor: use namespace translation file

* chore: add more translation key

* fix: fix some page name error

* refactor: change Trans tag for t function

* chore: fix some key not work

* chore: fix SearchFilterDialog i18n key error

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* chore: fix en i18n file filter missing some keys

* chore: add some i18n keys

* chore: add more i18n keys again

* feat: add search page i18n

* feat: add explore model i18n keys

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* feat: add more live i18n keys

* feat: add more search setting i18n keys

* fix: remove some comment

* fix: fix some setting page url error

* Update web/src/views/settings/SearchSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* fix: add system missing keys

* fix: update password update i18n keys

* chore: remove outdate translation.json file

* fix: fix exploreSettings error

* chore: add object setting i18n keys

* Update web/src/views/recording/RecordingView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/public/locales/en/components/filter.json

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/overlay/ExportDialog.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* feat: add more i18n keys

* fix: fix motionDetectionTuner html node

* feat: add more page i18n keys

* fix: cameraStream i18n keys error

* feat: add Player i18n keys

* feat: add more toast i18n keys

* feat: change explore setting name

* feat: add more document title i18n keys

* feat: add more search i18n keys

* fix: fix accessDenied i18n keys error

* chore: add objectType i18n

* chore: add  inputWithTags i18n

* chore: add SearchFilterDialog i18n

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* chore: add some missing i18n keys

* chore: remove most import { t } from "i18next";

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
GuoQing Liu
2025-03-16 10:36:20 -05:00
committed by GitHub
co-authored by Josh Hawkins
parent db541abed4
commit d34533981f
150 changed files with 6809 additions and 1926 deletions
@@ -18,6 +18,7 @@ import { Skeleton } from "../ui/skeleton";
import { Button } from "../ui/button";
import { FaCircleCheck } from "react-icons/fa6";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
type AnimatedEventCardProps = {
event: ReviewSegment;
@@ -29,6 +30,7 @@ export function AnimatedEventCard({
selectedGroup,
updateEvents,
}: AnimatedEventCardProps) {
const { t } = useTranslation(["views/events"]);
const { data: config } = useSWR<FrigateConfig>("config");
const apiHost = useApiHost();
@@ -121,7 +123,7 @@ export function AnimatedEventCard({
<Button
className="absolute right-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
size="xs"
aria-label="Mark as Reviewed"
aria-label={t("markAsReviewed")}
onClick={async () => {
await axios.post(`reviews/viewed`, { ids: [event.id] });
updateEvents();
@@ -130,7 +132,7 @@ export function AnimatedEventCard({
<FaCircleCheck className="size-3 text-white" />
</Button>
</TooltipTrigger>
<TooltipContent>Mark as Reviewed</TooltipContent>
<TooltipContent>{t("markAsReviewed")}</TooltipContent>
</Tooltip>
)}
{previews != undefined && (
+7 -7
View File
@@ -20,6 +20,7 @@ import { MdEditSquare } from "react-icons/md";
import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import { shareOrCopy } from "@/utils/browserUtil";
import { useTranslation } from "react-i18next";
type ExportProps = {
className: string;
@@ -36,6 +37,7 @@ export default function ExportCard({
onRename,
onDelete,
}: ExportProps) {
const { t } = useTranslation(["views/exports"]);
const [hovered, setHovered] = useState(false);
const [loading, setLoading] = useState(
exportedRecording.thumb_path.length > 0,
@@ -89,10 +91,8 @@ export default function ExportCard({
}
}}
>
<DialogTitle>Rename Export</DialogTitle>
<DialogDescription>
Enter a new name for this export.
</DialogDescription>
<DialogTitle>{t("editExport.title")}</DialogTitle>
<DialogDescription>{t("editExport.desc")}</DialogDescription>
{editName && (
<>
<Input
@@ -113,13 +113,13 @@ export default function ExportCard({
/>
<DialogFooter>
<Button
aria-label="Save Export"
aria-label={t("editExport.saveExport")}
size="sm"
variant="select"
disabled={(editName?.update?.length ?? 0) == 0}
onClick={() => submitRename()}
>
Save
{t("button.save", { ns: "common" })}
</Button>
</DialogFooter>
</>
@@ -207,7 +207,7 @@ export default function ExportCard({
{!exportedRecording.in_progress && (
<Button
className="absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white"
aria-label="Play"
aria-label={t("button.play", { ns: "common" })}
variant="ghost"
onClick={() => {
onSelect(exportedRecording);
+41 -37
View File
@@ -35,6 +35,7 @@ import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next";
type ReviewCardProps = {
event: ReviewSegment;
@@ -46,6 +47,7 @@ export default function ReviewCard({
currentTime,
onClick,
}: ReviewCardProps) {
const { t } = useTranslation(["components/dialog"]);
const { data: config } = useSWR<FrigateConfig>("config");
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
const formattedDate = useFormattedTimestamp(
@@ -82,26 +84,20 @@ export default function ReviewCard({
)
.then((response) => {
if (response.status == 200) {
toast.success(
"Successfully started export. View the file in the /exports folder.",
{ position: "top-center" },
);
}
})
.catch((error) => {
if (error.response?.data?.message) {
toast.error(
`Failed to start export: ${error.response.data.message}`,
{ position: "top-center" },
);
} else {
toast.error(`Failed to start export: ${error.message}`, {
toast.success(t("export.toast.success"), {
position: "top-center",
});
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message || error.message || "Unknown error";
toast.error(t("export.toast.error.failed", { error: errorMessage }), {
position: "top-center",
});
});
setOptionsOpen(false);
}, [event]);
}, [event, t]);
const onDelete = useCallback(async () => {
await axios.post(`reviews/delete`, { ids: [event.id] });
@@ -216,24 +212,24 @@ export default function ReviewCard({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
<AlertDialogTitle>
{t("recording.confirmDelete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
this review item?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
<Trans ns="components/dialog">
recording.confirmDelete.title
</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
Cancel
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={onDelete}
>
Delete
{t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -247,7 +243,9 @@ export default function ReviewCard({
onClick={onExport}
>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
<div className="text-primary">
{t("recording.button.export")}
</div>
</div>
</ContextMenuItem>
{!event.has_been_reviewed && (
@@ -257,7 +255,9 @@ export default function ReviewCard({
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
<div className="text-primary">
{t("recording.button.markAsReviewed")}
</div>
</div>
</ContextMenuItem>
)}
@@ -268,7 +268,9 @@ export default function ReviewCard({
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">
{bypassDialogRef.current ? "Delete Now" : "Delete"}
{bypassDialogRef.current
? t("recording.button.deleteNow")
: t("button.delete", { ns: "common" })}
</div>
</div>
</ContextMenuItem>
@@ -286,24 +288,22 @@ export default function ReviewCard({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
<AlertDialogTitle>
{t("recording.confirmDelete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
this review item?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
<Trans ns="components/dialog">recording.confirmDelete.desc</Trans>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
Cancel
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={onDelete}
>
Delete
{t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -316,7 +316,7 @@ export default function ReviewCard({
onClick={onExport}
>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
<div className="text-primary">{t("recording.button.export")}</div>
</div>
{!event.has_been_reviewed && (
<div
@@ -324,7 +324,9 @@ export default function ReviewCard({
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
<div className="text-primary">
{t("recording.button.markAsReviewed")}
</div>
</div>
)}
<div
@@ -333,7 +335,9 @@ export default function ReviewCard({
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">
{bypassDialogRef.current ? "Delete Now" : "Delete"}
{bypassDialogRef.current
? t("recording.button.deleteNow")
: t("button.delete", { ns: "common" })}
</div>
</div>
</DrawerContent>
+3 -2
View File
@@ -8,11 +8,11 @@ import Chip from "@/components/indicators/Chip";
import useImageLoaded from "@/hooks/use-image-loaded";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { SearchResult } from "@/types/search";
import { cn } from "@/lib/utils";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import useContextMenu from "@/hooks/use-contextmenu";
import { useTranslation } from "react-i18next";
type SearchThumbnailProps = {
searchResult: SearchResult;
@@ -23,6 +23,7 @@ export default function SearchThumbnail({
searchResult,
onClick,
}: SearchThumbnailProps) {
const { t } = useTranslation(["views/search"]);
const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config");
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
@@ -113,7 +114,7 @@ export default function SearchThumbnail({
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
.map((text) => capitalizeFirstLetter(text))
.map((text) => t(text, { ns: "objects" }))
.sort()
.join(", ")
.replaceAll("-verified", "")}
@@ -6,6 +6,7 @@ import { SearchResult } from "@/types/search";
import ActivityIndicator from "../indicators/activity-indicator";
import SearchResultActions from "../menu/SearchResultActions";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
type SearchThumbnailProps = {
searchResult: SearchResult;
@@ -24,12 +25,15 @@ export default function SearchThumbnailFooter({
showObjectLifecycle,
showSnapshot,
}: SearchThumbnailProps) {
const { t } = useTranslation(["views/search"]);
const { data: config } = useSWR<FrigateConfig>("config");
// date
const formattedDate = useFormattedTimestamp(
searchResult.start_time,
config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
config?.ui.time_format == "24hour"
? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" })
: t("time.formattedTimestampExcludeSeconds", { ns: "common" }),
config?.ui.timezone,
);