mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(web): group home grid, tabs, and breadcrumb by section
This commit is contained in:
@@ -240,7 +240,7 @@ export function App() {
|
|||||||
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
||||||
<Route path="/analytics-consent" element={<AnalyticsConsentPage />} />
|
<Route path="/analytics-consent" element={<AnalyticsConsentPage />} />
|
||||||
<Route path="/editor" element={<EditorPage />} />
|
<Route path="/editor" element={<EditorPage />} />
|
||||||
<Route path="/:modality/:toolId" element={<ToolPage />} />
|
<Route path="/:section/:toolId" element={<ToolPage />} />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Tool } from "@snapotter/shared";
|
import type { Tool } from "@snapotter/shared";
|
||||||
import { MODALITIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP } from "@snapotter/shared";
|
import { PYTHON_SIDECAR_TOOLS, SECTIONS, TOOL_BUNDLE_MAP, toolSection } from "@snapotter/shared";
|
||||||
import { Clock, Download, FileImage, Loader2 } from "lucide-react";
|
import { Clock, Download, FileImage, Loader2 } from "lucide-react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -15,8 +15,8 @@ interface ToolCardProps {
|
|||||||
showModalityBadge?: boolean;
|
showModalityBadge?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODALITY_COLOR_MAP: Record<string, string> = Object.fromEntries(
|
const SECTION_COLOR_MAP: Record<string, string> = Object.fromEntries(
|
||||||
MODALITIES.map((m) => [m.id, m.color]),
|
SECTIONS.map((s) => [s.id, s.color]),
|
||||||
);
|
);
|
||||||
|
|
||||||
export function ToolCard({ tool, variant = "compact", showModalityBadge }: ToolCardProps) {
|
export function ToolCard({ tool, variant = "compact", showModalityBadge }: ToolCardProps) {
|
||||||
@@ -38,17 +38,18 @@ export function ToolCard({ tool, variant = "compact", showModalityBadge }: ToolC
|
|||||||
return bundle?.status === "installed" ? "installed" : "not_installed";
|
return bundle?.status === "installed" ? "installed" : "not_installed";
|
||||||
}, [isAiTool, tool.id, bundles, installing, queued]);
|
}, [isAiTool, tool.id, bundles, installing, queued]);
|
||||||
|
|
||||||
const modalityColor = MODALITY_COLOR_MAP[tool.modality] ?? "#6B7280";
|
const section = toolSection(tool);
|
||||||
|
const sectionColor = SECTION_COLOR_MAP[section] ?? "#6B7280";
|
||||||
|
|
||||||
const modalityBadge = showModalityBadge ? (
|
const sectionBadge = showModalityBadge ? (
|
||||||
<span
|
<span
|
||||||
className="text-[10px] px-1.5 py-0.5 rounded-full font-medium shrink-0"
|
className="text-[10px] px-1.5 py-0.5 rounded-full font-medium shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${modalityColor}20`,
|
backgroundColor: `${sectionColor}20`,
|
||||||
color: modalityColor,
|
color: sectionColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{MODALITIES.find((m) => m.id === tool.modality)?.name ?? tool.modality}
|
{SECTIONS.find((s) => s.id === section)?.name ?? section}
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ export function ToolCard({ tool, variant = "compact", showModalityBadge }: ToolC
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="p-2 rounded-lg shrink-0 mt-0.5"
|
className="p-2 rounded-lg shrink-0 mt-0.5"
|
||||||
style={{ backgroundColor: `${modalityColor}12`, color: modalityColor }}
|
style={{ backgroundColor: `${sectionColor}12`, color: sectionColor }}
|
||||||
>
|
>
|
||||||
<IconComponent className="h-5 w-5" />
|
<IconComponent className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +86,7 @@ export function ToolCard({ tool, variant = "compact", showModalityBadge }: ToolC
|
|||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{getToolName(t, tool.id, tool.name)}
|
{getToolName(t, tool.id, tool.name)}
|
||||||
</span>
|
</span>
|
||||||
{modalityBadge}
|
{sectionBadge}
|
||||||
{tool.experimental && (
|
{tool.experimental && (
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">
|
||||||
{t.common.experimental}
|
{t.common.experimental}
|
||||||
@@ -114,7 +115,7 @@ export function ToolCard({ tool, variant = "compact", showModalityBadge }: ToolC
|
|||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{getToolName(t, tool.id, tool.name)}
|
{getToolName(t, tool.id, tool.name)}
|
||||||
</span>
|
</span>
|
||||||
{modalityBadge}
|
{sectionBadge}
|
||||||
{tool.experimental && (
|
{tool.experimental && (
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">
|
||||||
{t.common.experimental}
|
{t.common.experimental}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function TopNav({
|
|||||||
{breadcrumb.modality && (
|
{breadcrumb.modality && (
|
||||||
<span className={isDark ? "text-[#aaa]" : "text-muted-foreground"}>
|
<span className={isDark ? "text-[#aaa]" : "text-muted-foreground"}>
|
||||||
{breadcrumb.modalityTab ? (
|
{breadcrumb.modalityTab ? (
|
||||||
<Link to={`/?modality=${breadcrumb.modalityTab}`} className="hover:underline">
|
<Link to={`/?section=${breadcrumb.modalityTab}`} className="hover:underline">
|
||||||
{breadcrumb.modality}
|
{breadcrumb.modality}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@@ -163,7 +163,7 @@ export function TopNav({
|
|||||||
/>
|
/>
|
||||||
{breadcrumb.modalityTab ? (
|
{breadcrumb.modalityTab ? (
|
||||||
<Link
|
<Link
|
||||||
to={`/?modality=${breadcrumb.modalityTab}`}
|
to={`/?section=${breadcrumb.modalityTab}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 hover:underline",
|
"shrink-0 hover:underline",
|
||||||
isDark
|
isDark
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Tool } from "@snapotter/shared";
|
import type { Tool } from "@snapotter/shared";
|
||||||
import { CATEGORIES, MODALITIES, TOOLS } from "@snapotter/shared";
|
import { CATEGORIES, SECTIONS, TOOLS, toolSection } from "@snapotter/shared";
|
||||||
import { ChevronDown, Search, X } from "lucide-react";
|
import { ChevronDown, Search, X } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -20,9 +20,9 @@ interface TabDef {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLLAPSE_STORAGE_KEY = "snapotter-collapsed-modalities";
|
const COLLAPSE_STORAGE_KEY = "snapotter-collapsed-sections";
|
||||||
|
|
||||||
function getCollapsedModalities(): Set<string> {
|
function getCollapsedSections(): Set<string> {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(COLLAPSE_STORAGE_KEY);
|
const raw = localStorage.getItem(COLLAPSE_STORAGE_KEY);
|
||||||
return raw ? new Set(JSON.parse(raw)) : new Set();
|
return raw ? new Set(JSON.parse(raw)) : new Set();
|
||||||
@@ -31,19 +31,11 @@ function getCollapsedModalities(): Set<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCollapsedModalities(collapsed: Set<string>) {
|
function saveCollapsedSections(collapsed: Set<string>) {
|
||||||
localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify([...collapsed]));
|
localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify([...collapsed]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODALITY_TAB_ORDER = [
|
const SECTION_TABS = new Set<string>(["all", ...SECTIONS.map((s) => s.id)]);
|
||||||
{ modalityId: "image", tabKey: "image", label: "Image" },
|
|
||||||
{ modalityId: "video", tabKey: "video", label: "Video" },
|
|
||||||
{ modalityId: "audio", tabKey: "audio", label: "Audio" },
|
|
||||||
{ modalityId: "document", tabKey: "document", label: "PDF" },
|
|
||||||
{ modalityId: "file", tabKey: "data", label: "Data" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const MODALITY_TABS = new Set<string>(["all", ...MODALITY_TAB_ORDER.map((m) => m.tabKey)]);
|
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -60,12 +52,12 @@ export function HomePage() {
|
|||||||
fetchSettings();
|
fetchSettings();
|
||||||
}, [fetchSettings]);
|
}, [fetchSettings]);
|
||||||
|
|
||||||
// Open a specific modality tab when arriving via a breadcrumb link
|
// Open a specific section tab when arriving via a breadcrumb link
|
||||||
// (/?modality=<tabKey>), then clean the URL so refresh/back doesn't re-pin it.
|
// (/?section=<sectionId>), then clean the URL so refresh/back doesn't re-pin it.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const m = new URLSearchParams(location.search).get("modality");
|
const s = new URLSearchParams(location.search).get("section");
|
||||||
if (m && MODALITY_TABS.has(m)) {
|
if (s && SECTION_TABS.has(s)) {
|
||||||
setActiveTab(m);
|
setActiveTab(s);
|
||||||
navigate("/", { replace: true });
|
navigate("/", { replace: true });
|
||||||
}
|
}
|
||||||
}, [location.search, navigate]);
|
}, [location.search, navigate]);
|
||||||
@@ -73,11 +65,10 @@ export function HomePage() {
|
|||||||
const tabs: TabDef[] = useMemo(
|
const tabs: TabDef[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ key: "all", label: t.homePage.all },
|
{ key: "all", label: t.homePage.all },
|
||||||
{ key: "image", label: "Image" },
|
...SECTIONS.map((s) => ({
|
||||||
{ key: "video", label: "Video" },
|
key: s.id,
|
||||||
{ key: "audio", label: "Audio" },
|
label: s.id === "pdf" ? t.homePage.pdf : s.id === "files" ? t.homePage.files : s.name,
|
||||||
{ key: "document", label: t.homePage.documents },
|
})),
|
||||||
{ key: "data", label: t.homePage.data },
|
|
||||||
],
|
],
|
||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
@@ -96,8 +87,7 @@ export function HomePage() {
|
|||||||
|
|
||||||
const tabTools = useMemo(() => {
|
const tabTools = useMemo(() => {
|
||||||
if (activeTab === "all") return visibleTools;
|
if (activeTab === "all") return visibleTools;
|
||||||
const modalityKey = activeTab === "data" ? "file" : activeTab;
|
return visibleTools.filter((tool) => toolSection(tool) === activeTab);
|
||||||
return visibleTools.filter((tool) => tool.modality === modalityKey);
|
|
||||||
}, [visibleTools, activeTab]);
|
}, [visibleTools, activeTab]);
|
||||||
|
|
||||||
const groupedTools = useMemo(() => {
|
const groupedTools = useMemo(() => {
|
||||||
@@ -116,8 +106,8 @@ export function HomePage() {
|
|||||||
const tabCounts = useMemo(() => {
|
const tabCounts = useMemo(() => {
|
||||||
const counts: Record<string, number> = { all: visibleTools.length };
|
const counts: Record<string, number> = { all: visibleTools.length };
|
||||||
for (const tool of visibleTools) {
|
for (const tool of visibleTools) {
|
||||||
const tabKey = tool.modality === "file" ? "data" : tool.modality;
|
const sec = toolSection(tool);
|
||||||
counts[tabKey] = (counts[tabKey] ?? 0) + 1;
|
counts[sec] = (counts[sec] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
return counts;
|
return counts;
|
||||||
}, [visibleTools]);
|
}, [visibleTools]);
|
||||||
@@ -296,7 +286,7 @@ function SearchResults({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── All Tab: Grouped by modality, then by category ───────────────
|
// ── All Tab: Grouped by section, then by category ───────────────
|
||||||
|
|
||||||
function AllTabContent({
|
function AllTabContent({
|
||||||
recentTools,
|
recentTools,
|
||||||
@@ -306,29 +296,30 @@ function AllTabContent({
|
|||||||
visibleTools: Tool[];
|
visibleTools: Tool[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [collapsed, setCollapsed] = useState<Set<string>>(getCollapsedModalities);
|
const [collapsed, setCollapsed] = useState<Set<string>>(getCollapsedSections);
|
||||||
|
|
||||||
const toggleModality = useCallback((modalityId: string) => {
|
const toggleSection = useCallback((sectionId: string) => {
|
||||||
setCollapsed((prev) => {
|
setCollapsed((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(modalityId)) next.delete(modalityId);
|
if (next.has(sectionId)) next.delete(sectionId);
|
||||||
else next.add(modalityId);
|
else next.add(sectionId);
|
||||||
saveCollapsedModalities(next);
|
saveCollapsedSections(next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toolsByModality = useMemo(() => {
|
const toolsBySection = useMemo(() => {
|
||||||
const map = new Map<string, Map<string, Tool[]>>();
|
const map = new Map<string, Map<string, Tool[]>>();
|
||||||
for (const tool of visibleTools) {
|
for (const tool of visibleTools) {
|
||||||
let modMap = map.get(tool.modality);
|
const sec = toolSection(tool);
|
||||||
if (!modMap) {
|
let catMap = map.get(sec);
|
||||||
modMap = new Map();
|
if (!catMap) {
|
||||||
map.set(tool.modality, modMap);
|
catMap = new Map();
|
||||||
|
map.set(sec, catMap);
|
||||||
}
|
}
|
||||||
const existing = modMap.get(tool.category);
|
const existing = catMap.get(tool.category);
|
||||||
if (existing) existing.push(tool);
|
if (existing) existing.push(tool);
|
||||||
else modMap.set(tool.category, [tool]);
|
else catMap.set(tool.category, [tool]);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [visibleTools]);
|
}, [visibleTools]);
|
||||||
@@ -355,37 +346,36 @@ function AllTabContent({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modality sections */}
|
{/* Section groups */}
|
||||||
{MODALITY_TAB_ORDER.map(({ modalityId, label }) => {
|
{SECTIONS.map((sec) => {
|
||||||
const categoryMap = toolsByModality.get(modalityId);
|
const categoryMap = toolsBySection.get(sec.id);
|
||||||
if (!categoryMap || categoryMap.size === 0) return null;
|
if (!categoryMap || categoryMap.size === 0) return null;
|
||||||
|
|
||||||
const totalCount = [...categoryMap.values()].reduce((sum, arr) => sum + arr.length, 0);
|
const totalCount = [...categoryMap.values()].reduce((sum, arr) => sum + arr.length, 0);
|
||||||
const isCollapsed = collapsed.has(modalityId);
|
const isCollapsed = collapsed.has(sec.id);
|
||||||
const modality = MODALITIES.find((m) => m.id === modalityId);
|
const SectionIcon = ICON_MAP[sec.icon] as
|
||||||
const ModalityIcon = modality
|
| React.ComponentType<{ className?: string }>
|
||||||
? (ICON_MAP[modality.icon] as React.ComponentType<{ className?: string }>)
|
| undefined;
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section key={modalityId}>
|
<section key={sec.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleModality(modalityId)}
|
onClick={() => toggleSection(sec.id)}
|
||||||
className="w-full flex items-center gap-2 py-2 mb-2 border-b border-border/40 group cursor-pointer"
|
className="w-full flex items-center gap-2 py-2 mb-2 border-b border-border/40 group cursor-pointer"
|
||||||
>
|
>
|
||||||
{ModalityIcon && (
|
{SectionIcon && (
|
||||||
<div
|
<div
|
||||||
className="p-1 rounded"
|
className="p-1 rounded"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${modality?.color ?? "#6B7280"}15`,
|
backgroundColor: `${sec.color}15`,
|
||||||
color: modality?.color ?? "#6B7280",
|
color: sec.color,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalityIcon className="h-4 w-4" />
|
<SectionIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-semibold text-foreground">{label}</span>
|
<span className="text-sm font-semibold text-foreground">{sec.name}</span>
|
||||||
<span className="text-xs text-muted-foreground">{totalCount}</span>
|
<span className="text-xs text-muted-foreground">{totalCount}</span>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { MODALITIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
|
import {
|
||||||
|
PYTHON_SIDECAR_TOOLS,
|
||||||
|
SECTIONS,
|
||||||
|
TOOL_BUNDLE_MAP,
|
||||||
|
TOOLS,
|
||||||
|
toolSection,
|
||||||
|
} from "@snapotter/shared";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -233,11 +239,11 @@ export function ToolPage() {
|
|||||||
|
|
||||||
const breadcrumb = useMemo(() => {
|
const breadcrumb = useMemo(() => {
|
||||||
if (!tool) return undefined;
|
if (!tool) return undefined;
|
||||||
const modalityInfo = MODALITIES.find((m) => m.id === tool.modality);
|
const section = toolSection(tool);
|
||||||
const modalityDisplay = modalityInfo?.name ?? tool.modality;
|
const sectionInfo = SECTIONS.find((s) => s.id === section);
|
||||||
return {
|
return {
|
||||||
modality: modalityDisplay,
|
modality: sectionInfo?.name ?? section,
|
||||||
modalityTab: tool.modality === "file" ? "data" : tool.modality,
|
modalityTab: section,
|
||||||
toolName: getToolName(t, tool.id, tool.name),
|
toolName: getToolName(t, tool.id, tool.name),
|
||||||
};
|
};
|
||||||
}, [tool, t]);
|
}, [tool, t]);
|
||||||
|
|||||||
@@ -2489,6 +2489,8 @@ export const ar: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2503,6 +2503,8 @@ export const de: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2453,6 +2453,8 @@ export const en = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2485,6 +2485,8 @@ export const es: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2504,6 +2504,8 @@ export const fr: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2486,6 +2486,8 @@ export const hi: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2498,6 +2498,8 @@ export const id: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2497,6 +2497,8 @@ export const it: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2456,6 +2456,8 @@ export const ja: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2441,6 +2441,8 @@ export const ko: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2500,6 +2500,8 @@ export const nl: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2501,6 +2501,8 @@ export const pl: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2497,6 +2497,8 @@ export const ptBR: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2499,6 +2499,8 @@ export const ru: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2496,6 +2496,8 @@ export const sv: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2478,6 +2478,8 @@ export const th: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2501,6 +2501,8 @@ export const tr: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2499,6 +2499,8 @@ export const uk: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2498,6 +2498,8 @@ export const vi: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2431,6 +2431,8 @@ export const zhCN: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2429,6 +2429,8 @@ export const zhTW: TranslationKeys = {
|
|||||||
all: "All",
|
all: "All",
|
||||||
documents: "PDF",
|
documents: "PDF",
|
||||||
data: "Data",
|
data: "Data",
|
||||||
|
pdf: "PDF",
|
||||||
|
files: "Files",
|
||||||
toolCount: "{count} tools",
|
toolCount: "{count} tools",
|
||||||
heading: "SnapOtter Tools",
|
heading: "SnapOtter Tools",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user