feat: add search page i18n

This commit is contained in:
ZhaiSoul 2025-03-11 13:58:00 +08:00
parent a52592bcac
commit 118ef6b2fe
4 changed files with 118 additions and 65 deletions

View File

@ -0,0 +1,35 @@
{
"search": "Search",
"savedSearches": "Saved Searches",
"searchFor": "Search for {{inputValue}}",
"button": {
"clear": "Clear search",
"save": "Save search",
"delete": "Delete saved search",
"filterInformation": "Filter information",
"filterActive": "Filters active"
},
"filter": {
"toast": {
"error": {
"beforeDateBeLaterAfter": "The 'before' date must be later than the 'after' date.",
"afterDatebeEarlierBefore": "The 'after' date must be earlier than the 'before' date.",
"minScoreMustBeLessOrEqualMaxScore": "The 'min_score' must be less than or equal to the 'max_score'.",
"maxScoreMustBeGreaterOrEqualMinScore": "The 'max_score' must be greater than or equal to the 'min_score'.",
"minSpeedMustBeLessOrEqualMaxSpeed": "The 'min_speed' must be less than or equal to the 'max_speed'.",
"maxSpeedMustBeGreaterOrEqualMinSpeed": "The 'max_speed' must be greater than or equal to the 'min_speed'."
}
},
"tips": {
"title": "How to use text filters",
"desc": "Filters help you narrow down your search results. Here's how to use them in the input field:",
"desc.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>",
"desc.example": "Example: <code className=\"text-primary\">cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM </code>"
}
},
"similaritySearch": {
"title": "Similarity Search",
"active": "Similarity search active",
"clear": "Clear similarity search"
}
}

View File

@ -0,0 +1,35 @@
{
"search": "搜索",
"savedSearches": "已保存的搜索",
"searchFor": "搜索 {{inputValue}}",
"button": {
"clear": "清除搜索",
"save": "保存搜索",
"delete": "删除已保存的搜索",
"filterInformation": "筛选信息",
"filterActive": "筛选器已激活"
},
"filter": {
"toast": {
"error": {
"beforeDateBeLaterAfter": "“之前”日期必须晚于“之后”日期。",
"afterDatebeEarlierBefore": "“之后”日期必须早于“之前”日期。",
"minScoreMustBeLessOrEqualMaxScore": "最小分值 必须小于或等于 最大分值。",
"maxScoreMustBeGreaterOrEqualMinScore": "最大分值 必须大于或等于 最小分值",
"minSpeedMustBeLessOrEqualMaxSpeed": "最低速度 必须小于或等于 最高速度",
"maxSpeedMustBeGreaterOrEqualMinSpeed": "最高速度 必须大于或等于 最低速度"
}
},
"tips": {
"title": "如何使用文本筛选器(英文)",
"desc": "筛选器可帮助您缩小搜索范围。注意,目前还暂不支持中文搜索。以下是在输入字段中使用筛选器的方法:",
"desc.step": "<ul className=\"list-disc pl-5 text-sm text-primary-variant\"><li>输入筛选器名称后跟一个冒号例如“cameras:”)。</li><li>从建议中选择一个值或输入您自己的值。</li><li>使用多个筛选器时,可以在它们之间用空格分隔。</li><li>日期筛选器before: 和 after:)使用 <em>{{DateFormat}}</em> 格式。</li><li>时间范围筛选器使用 <em>{{exampleTime}}</em> 格式。</li><li>点击筛选器旁边的“x”即可移除筛选条件。</li></ul>",
"desc.example": "示例:<code className=\"text-primary\">cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM</code>"
}
},
"similaritySearch": {
"title": "相似搜索",
"active": "相似搜索已激活",
"clear": "清除相似搜索"
}
}

View File

@ -51,6 +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";
type InputWithTagsProps = { type InputWithTagsProps = {
inputFocused: boolean; inputFocused: boolean;
@ -73,6 +74,7 @@ export default function InputWithTags({
setSearch, setSearch,
allSuggestions, allSuggestions,
}: InputWithTagsProps) { }: InputWithTagsProps) {
const { t } = useTranslation(["views/search"]);
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
@ -236,12 +238,9 @@ export default function InputWithTags({
filters.after && filters.after &&
timestamp <= filters.after * 1000 timestamp <= filters.after * 1000
) { ) {
toast.error( toast.error(t("filter.toast.error.beforeDateBeLaterAfter"), {
"The 'before' date must be later than the 'after' date.", position: "top-center",
{ });
position: "top-center",
},
);
return; return;
} }
if ( if (
@ -249,12 +248,9 @@ export default function InputWithTags({
filters.before && filters.before &&
timestamp >= filters.before * 1000 timestamp >= filters.before * 1000
) { ) {
toast.error( toast.error(t("afterDatebeEarlierBefore"), {
"The 'after' date must be earlier than the 'before' date.", position: "top-center",
{ });
position: "top-center",
},
);
return; return;
} }
if (type === "before") { if (type === "before") {
@ -274,7 +270,7 @@ export default function InputWithTags({
score > filters.max_score * 100 score > filters.max_score * 100
) { ) {
toast.error( toast.error(
"The 'min_score' must be less than or equal to the 'max_score'.", t("filter.toast.error.minScoreMustBeLessOrEqualMaxScore"),
{ {
position: "top-center", position: "top-center",
}, },
@ -287,7 +283,7 @@ export default function InputWithTags({
score < filters.min_score * 100 score < filters.min_score * 100
) { ) {
toast.error( toast.error(
"The 'max_score' must be greater than or equal to the 'min_score'.", t("filter.toast.error.maxScoreMustBeGreaterOrEqualMinScore"),
{ {
position: "top-center", position: "top-center",
}, },
@ -308,7 +304,7 @@ export default function InputWithTags({
speed > filters.max_speed speed > filters.max_speed
) { ) {
toast.error( toast.error(
"The 'min_speed' must be less than or equal to the 'max_speed'.", t("filter.toast.error.minSpeedMustBeLessOrEqualMaxSpeed"),
{ {
position: "top-center", position: "top-center",
}, },
@ -321,7 +317,7 @@ export default function InputWithTags({
speed < filters.min_speed speed < filters.min_speed
) { ) {
toast.error( toast.error(
"The 'max_speed' must be greater than or equal to the 'min_speed'.", t("filter.toast.error.maxSpeedMustBeGreaterOrEqualMinSpeed"),
{ {
position: "top-center", position: "top-center",
}, },
@ -380,7 +376,7 @@ export default function InputWithTags({
setCurrentFilterType(null); setCurrentFilterType(null);
} }
}, },
[filters, setFilters, allSuggestions], [filters, setFilters, allSuggestions, t],
); );
function formatFilterValues( function formatFilterValues(
@ -408,7 +404,11 @@ export default function InputWithTags({
return Math.round(Number(filterValues) * 100).toString() + "%"; return Math.round(Number(filterValues) * 100).toString() + "%";
} else if (filterType === "min_speed" || filterType === "max_speed") { } else if (filterType === "min_speed" || filterType === "max_speed") {
return ( return (
filterValues + (config?.ui.unit_system == "metric" ? " kph" : " mph") filterValues +
" " +
(config?.ui.unit_system == "metric"
? t("unit.speed.kph", { ns: "common" })
: t("unit.speed.mph", { ns: "common" }))
); );
} else if ( } else if (
filterType === "has_clip" || filterType === "has_clip" ||
@ -665,7 +665,7 @@ export default function InputWithTags({
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>Clear search</TooltipContent> <TooltipContent>{t("button.clear")}</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
)} )}
@ -679,7 +679,7 @@ export default function InputWithTags({
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>Save search</TooltipContent> <TooltipContent>{t("button.save")}</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
)} )}
@ -688,12 +688,14 @@ export default function InputWithTags({
<Tooltip> <Tooltip>
<TooltipTrigger className="cursor-default"> <TooltipTrigger className="cursor-default">
<MdImageSearch <MdImageSearch
aria-label="Similarity search active" aria-label={t("similaritySearch.active")}
className="size-4 text-selected" className="size-4 text-selected"
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>Similarity search active</TooltipContent> <TooltipContent>
{t("similaritySearch.active")}
</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
)} )}
@ -702,10 +704,10 @@ export default function InputWithTags({
<PopoverTrigger asChild> <PopoverTrigger asChild>
<button <button
className="focus:outline-none" className="focus:outline-none"
aria-label="Filter information" aria-label={t("button.filterInformation")}
> >
<LuFilter <LuFilter
aria-label="Filters active" aria-label={t("button.filterActive")}
className={cn( className={cn(
"size-4", "size-4",
Object.keys(filters).length > 0 Object.keys(filters).length > 0
@ -717,43 +719,24 @@ export default function InputWithTags({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80"> <PopoverContent className="w-80">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-medium">How to use text filters</h3> <h3 className="font-medium">{t("filter.tips.title")}</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Filters help you narrow down your search results. Here's how {t("filter.tips.desc")}
to use them in the input field:
</p> </p>
<ul className="list-disc pl-5 text-sm text-primary-variant"> <Trans
<li> ns="views/search"
Type a filter name followed by a colon (e.g., "cameras:"). values={{
</li> DateFormat: getIntlDateFormat(),
<li> exampleTime:
Select a value from the suggestions or type your own. config?.ui.time_format == "24hour"
</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>{getIntlDateFormat()}</em> format.
</li>
<li>
Time range filter uses{" "}
<em>
{config?.ui.time_format == "24hour"
? "15:00-16:00" ? "15:00-16:00"
: "3:00PM-4:00PM"}{" "} : "3:00PM-4:00PM",
</em> }}
format. >
</li> filter.tips.desc.step
<li>Remove filters by clicking the 'x' next to them.</li> </Trans>
</ul>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Example:{" "} <Trans ns="views/search">filter.tips.desc.example</Trans>
<code className="text-primary">
cameras:front_door label:person before:01012024
time_range:3:00PM-4:00PM
</code>
</p> </p>
</div> </div>
</PopoverContent> </PopoverContent>
@ -780,13 +763,13 @@ export default function InputWithTags({
)} )}
> >
{!currentFilterType && inputValue && ( {!currentFilterType && inputValue && (
<CommandGroup heading="Search"> <CommandGroup heading={t("search")}>
<CommandItem <CommandItem
className="cursor-pointer" className="cursor-pointer"
onSelect={() => handleSearch(inputValue)} onSelect={() => handleSearch(inputValue)}
> >
<LuSearch className="mr-2 h-4 w-4" /> <LuSearch className="mr-2 h-4 w-4" />
Search for "{inputValue}" {t("searchFor", { inputValue })}
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
)} )}
@ -796,11 +779,11 @@ export default function InputWithTags({
<div className="my-2 flex flex-wrap gap-2 px-2"> <div className="my-2 flex flex-wrap gap-2 px-2">
{isSimilaritySearch && ( {isSimilaritySearch && (
<span className="inline-flex items-center whitespace-nowrap rounded-full bg-blue-100 px-2 py-0.5 text-sm text-blue-800"> <span className="inline-flex items-center whitespace-nowrap rounded-full bg-blue-100 px-2 py-0.5 text-sm text-blue-800">
Similarity Search {t("similaritySearch.title")}
<button <button
onClick={handleClearSimilarity} onClick={handleClearSimilarity}
className="ml-1 focus:outline-none" className="ml-1 focus:outline-none"
aria-label="Clear similarity search" aria-label={t("similaritySearch.clear")}
> >
<LuX className="h-3 w-3" /> <LuX className="h-3 w-3" />
</button> </button>
@ -863,7 +846,7 @@ export default function InputWithTags({
!inputValue && !inputValue &&
searchHistoryLoaded && searchHistoryLoaded &&
(searchHistory?.length ?? 0) > 0 && ( (searchHistory?.length ?? 0) > 0 && (
<CommandGroup heading="Saved Searches"> <CommandGroup heading={t("savedSearches")}>
{searchHistory?.map((suggestion, index) => ( {searchHistory?.map((suggestion, index) => (
<CommandItem <CommandItem
key={index} key={index}
@ -884,7 +867,7 @@ export default function InputWithTags({
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent>Delete saved search</TooltipContent> <TooltipContent>{t("button.delete")}</TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
</CommandItem> </CommandItem>

View File

@ -558,8 +558,8 @@ export function SpeedFilterContent({
{t("estimatedSpeed", { {t("estimatedSpeed", {
unit: unit:
config?.ui.unit_system == "metric" config?.ui.unit_system == "metric"
? t("unit.speed.kph") ? t("unit.speed.kph", { ns: "common" })
: t("unit.speed.mph"), : t("unit.speed.mph", { ns: "common" }),
})} })}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">