Fix i18n (#20857)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled

* fix: fix the missing i18n key

* fix: fix trackedObject i18n keys count variable

* fix: fix some pages audio label missing i18n

* fix: add 6214d52 missing variable

* fix: add more missing i18n

* fix: add menu missing key
This commit is contained in:
GuoQing Liu 2025-11-12 07:23:30 +08:00 committed by GitHub
parent f1a05d0f9b
commit de066d0062
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 84 additions and 17 deletions

View File

@ -72,7 +72,10 @@
"formattedTimestampFilename": { "formattedTimestampFilename": {
"12hour": "MM-dd-yy-h-mm-ss-a", "12hour": "MM-dd-yy-h-mm-ss-a",
"24hour": "MM-dd-yy-HH-mm-ss" "24hour": "MM-dd-yy-HH-mm-ss"
} },
"inProgress": "In progress",
"invalidStartTime": "Invalid start time",
"invalidEndTime": "Invalid end time"
}, },
"unit": { "unit": {
"speed": { "speed": {
@ -144,7 +147,8 @@
"unselect": "Unselect", "unselect": "Unselect",
"export": "Export", "export": "Export",
"deleteNow": "Delete Now", "deleteNow": "Delete Now",
"next": "Next" "next": "Next",
"continue": "Continue"
}, },
"menu": { "menu": {
"system": "System", "system": "System",
@ -237,6 +241,7 @@
"export": "Export", "export": "Export",
"uiPlayground": "UI Playground", "uiPlayground": "UI Playground",
"faceLibrary": "Face Library", "faceLibrary": "Face Library",
"classification": "Classification",
"user": { "user": {
"title": "User", "title": "User",
"account": "Account", "account": "Account",

View File

@ -24,8 +24,8 @@
"label": "Detail", "label": "Detail",
"noDataFound": "No detail data to review", "noDataFound": "No detail data to review",
"aria": "Toggle detail view", "aria": "Toggle detail view",
"trackedObject_one": "object", "trackedObject_one": "{{count}} object",
"trackedObject_other": "objects", "trackedObject_other": "{{count}} objects",
"noObjectDetailData": "No object detail data available.", "noObjectDetailData": "No object detail data available.",
"settings": "Detail View Settings", "settings": "Detail View Settings",
"alwaysExpandActive": { "alwaysExpandActive": {

View File

@ -35,7 +35,7 @@
"snapshot": "snapshot", "snapshot": "snapshot",
"thumbnail": "thumbnail", "thumbnail": "thumbnail",
"video": "video", "video": "video",
"object_lifecycle": "object lifecycle" "tracking_details": "tracking details"
}, },
"trackingDetails": { "trackingDetails": {
"title": "Tracking Details", "title": "Tracking Details",

View File

@ -454,6 +454,24 @@ export function GeneralFilterContent({
onClose, onClose,
}: GeneralFilterContentProps) { }: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter", "views/events"]); const { t } = useTranslation(["components/filter", "views/events"]);
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const allAudioListenLabels = useMemo<string[]>(() => {
if (!config) {
return [];
}
const labels = new Set<string>();
Object.values(config.cameras).forEach((camera) => {
if (camera?.audio?.enabled) {
camera.audio.listen.forEach((label) => {
labels.add(label);
});
}
});
return [...labels].sort();
}, [config]);
return ( return (
<> <>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden"> <div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
@ -504,7 +522,10 @@ export function GeneralFilterContent({
{allLabels.map((item) => ( {allLabels.map((item) => (
<FilterSwitch <FilterSwitch
key={item} key={item}
label={getTranslatedLabel(item)} label={getTranslatedLabel(
item,
allAudioListenLabels.includes(item) ? "audio" : "object",
)}
isChecked={filter.labels?.includes(item) ?? false} isChecked={filter.labels?.includes(item) ?? false}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {

View File

@ -81,6 +81,43 @@ export default function InputWithTags({
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
const allAudioListenLabels = useMemo<Set<string>>(() => {
if (!config) {
return new Set<string>();
}
const labels = new Set<string>();
Object.values(config.cameras).forEach((camera) => {
if (camera?.audio?.enabled) {
camera.audio.listen.forEach((label) => {
labels.add(label);
});
}
});
return labels;
}, [config]);
const translatedAudioLabelMap = useMemo<Map<string, string>>(() => {
const map = new Map<string, string>();
if (!config) return map;
allAudioListenLabels.forEach((label) => {
// getTranslatedLabel likely depends on i18n internally; including `lang`
// in deps ensures this map is rebuilt when language changes
map.set(label, getTranslatedLabel(label, "audio"));
});
return map;
}, [allAudioListenLabels, config]);
function resolveLabel(value: string) {
const mapped = translatedAudioLabelMap.get(value);
if (mapped) return mapped;
return getTranslatedLabel(
value,
allAudioListenLabels.has(value) ? "audio" : "object",
);
}
const [inputValue, setInputValue] = useState(search || ""); const [inputValue, setInputValue] = useState(search || "");
const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>( const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>(
null, null,
@ -421,7 +458,8 @@ export default function InputWithTags({
? t("button.yes", { ns: "common" }) ? t("button.yes", { ns: "common" })
: t("button.no", { ns: "common" }); : t("button.no", { ns: "common" });
} else if (filterType === "labels") { } else if (filterType === "labels") {
return getTranslatedLabel(String(filterValues)); const value = String(filterValues);
return resolveLabel(value);
} else if (filterType === "search_type") { } else if (filterType === "search_type") {
return t("filter.searchType." + String(filterValues)); return t("filter.searchType." + String(filterValues));
} else { } else {
@ -828,7 +866,7 @@ export default function InputWithTags({
> >
{t("filter.label." + filterType)}:{" "} {t("filter.label." + filterType)}:{" "}
{filterType === "labels" ? ( {filterType === "labels" ? (
getTranslatedLabel(value) resolveLabel(value)
) : filterType === "cameras" ? ( ) : filterType === "cameras" ? (
<CameraNameLabel camera={value} /> <CameraNameLabel camera={value} />
) : filterType === "zones" ? ( ) : filterType === "zones" ? (

View File

@ -1155,7 +1155,7 @@ function ObjectDetailsTab({
</div> </div>
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize"> <div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
{getIconForLabel(search.label, "size-4 text-primary")} {getIconForLabel(search.label, "size-4 text-primary")}
{getTranslatedLabel(search.label)} {getTranslatedLabel(search.label, search.data.type)}
{search.sub_label && ` (${search.sub_label})`} {search.sub_label && ` (${search.sub_label})`}
{isAdmin && search.end_time && ( {isAdmin && search.end_time && (
<Tooltip> <Tooltip>
@ -1394,7 +1394,9 @@ function ObjectDetailsTab({
{state == "submitted" && ( {state == "submitted" && (
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
<FaCheckCircle className="size-4 text-success" /> <FaCheckCircle className="size-4 text-success" />
{t("explore.plus.review.state.submitted")} {t("explore.plus.review.state.submitted", {
ns: "components/dialog",
})}
</div> </div>
)} )}
</div> </div>

View File

@ -349,7 +349,7 @@ function ReviewGroup({
? fetchedEvents.length ? fetchedEvents.length
: (review.data.objects ?? []).length; : (review.data.objects ?? []).length;
return `${objectCount} ${t("detail.trackedObject", { count: objectCount })}`; return `${t("detail.trackedObject", { count: objectCount })}`;
}, [review, t, fetchedEvents]); }, [review, t, fetchedEvents]);
const reviewDuration = useMemo( const reviewDuration = useMemo(
@ -478,7 +478,7 @@ function ReviewGroup({
<div className="rounded-full bg-muted-foreground p-1"> <div className="rounded-full bg-muted-foreground p-1">
{getIconForLabel(audioLabel, "size-3 text-white")} {getIconForLabel(audioLabel, "size-3 text-white")}
</div> </div>
<span>{getTranslatedLabel(audioLabel)}</span> <span>{getTranslatedLabel(audioLabel, "audio")}</span>
</div> </div>
</div> </div>
))} ))}
@ -513,7 +513,8 @@ function EventList({
const isSelected = selectedObjectIds.includes(event.id); const isSelected = selectedObjectIds.includes(event.id);
const label = event.sub_label || getTranslatedLabel(event.label); const label =
event.sub_label || getTranslatedLabel(event.label, event.data.type);
const handleObjectSelect = (event: Event | undefined) => { const handleObjectSelect = (event: Event | undefined) => {
if (event) { if (event) {

View File

@ -244,12 +244,12 @@ export const getDurationFromTimestamps = (
abbreviated: boolean = false, abbreviated: boolean = false,
): string => { ): string => {
if (isNaN(start_time)) { if (isNaN(start_time)) {
return "Invalid start time"; return i18n.t("time.invalidStartTime", { ns: "common" });
} }
let duration = "In Progress"; let duration = i18n.t("time.inProgress", { ns: "common" });
if (end_time !== null) { if (end_time !== null) {
if (isNaN(end_time)) { if (isNaN(end_time)) {
return "Invalid end time"; return i18n.t("time.invalidEndTime", { ns: "common" });
} }
const start = fromUnixTime(start_time); const start = fromUnixTime(start_time);
const end = fromUnixTime(end_time); const end = fromUnixTime(end_time);

View File

@ -649,7 +649,7 @@ export function RecordingView({
value="detail" value="detail"
aria-label="Detail Stream" aria-label="Detail Stream"
> >
<div className="">Detail</div> <div className="">{t("detail.label")}</div>
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
) : ( ) : (