mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-06 05:27:44 +03:00
feat: add search page i18n
This commit is contained in:
parent
a52592bcac
commit
118ef6b2fe
35
web/public/locales/en/views/search.json
Normal file
35
web/public/locales/en/views/search.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
web/public/locales/zh-CN/views/search.json
Normal file
35
web/public/locales/zh-CN/views/search.json
Normal 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": "清除相似搜索"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user