diff --git a/package-lock.json b/package-lock.json index 8b65844..39641ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5015,6 +5015,12 @@ "node": ">=8" } }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.26.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", @@ -5049,6 +5055,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -9585,12 +9601,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/nan": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9719,16 +9729,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "nan": "^2.17.0" - } - }, "node_modules/node-releases": { "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", @@ -13128,4 +13128,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index c54bc9d..2239f3b 100644 --- a/package.json +++ b/package.json @@ -90,4 +90,4 @@ "overrides": { "prismjs": "^1.30.0" } -} +} \ No newline at end of file diff --git a/src/app/_components/CategorySidebar.tsx b/src/app/_components/CategorySidebar.tsx index 6786f98..a0d0794 100644 --- a/src/app/_components/CategorySidebar.tsx +++ b/src/app/_components/CategorySidebar.tsx @@ -1,7 +1,8 @@ -'use client'; +"use client"; -import { useState } from 'react'; -import { ContextualHelpIcon } from './ContextualHelpIcon'; +import { useState } from "react"; +import { useTranslation } from "@/lib/i18n/useTranslation"; +import { ContextualHelpIcon } from "./ContextualHelpIcon"; interface CategorySidebarProps { categories: string[]; @@ -12,218 +13,509 @@ interface CategorySidebarProps { } // Icon mapping for categories -const CategoryIcon = ({ iconName, className = "w-5 h-5" }: { iconName: string; className?: string }) => { +const CategoryIcon = ({ + iconName, + className = "w-5 h-5", +}: { + iconName: string; + className?: string; +}) => { const iconMap: Record = { server: ( - - + + ), monitor: ( - - + + ), box: ( - - + + ), shield: ( - - + + ), "shield-check": ( - - + + ), key: ( - - + + ), archive: ( - - + + ), database: ( - - + + ), "chart-bar": ( - - + + ), template: ( - - + + ), "folder-open": ( - - + + ), "document-text": ( - - + + ), film: ( - - + + ), download: ( - - + + ), "video-camera": ( - - + + ), home: ( - - + + ), wifi: ( - - + + ), "chat-alt": ( - - + + ), clock: ( - - + + ), code: ( - - + + ), "external-link": ( - - + + ), sparkles: ( - - + + ), "currency-dollar": ( - - + + ), puzzle: ( - - + + ), office: ( - - + + ), }; - return iconMap[iconName] ?? ( - - - + return ( + iconMap[iconName] ?? ( + + + + ) ); }; -export function CategorySidebar({ - categories, - categoryCounts, - totalScripts, - selectedCategory, - onCategorySelect +export function CategorySidebar({ + categories, + categoryCounts, + totalScripts, + selectedCategory, + onCategorySelect, }: CategorySidebarProps) { const [isCollapsed, setIsCollapsed] = useState(false); + const { t } = useTranslation("categorySidebar"); + + const formatCategoryLabel = (category: string) => { + const defaultLabel = category.replace(/[_-]/g, " "); + return t(`categories.${category}`, { fallback: defaultLabel }); + }; + + const formatCategoryTooltip = (categoryLabel: string, count: number) => + t("tooltips.category", { values: { category: categoryLabel, count } }); // Category to icon mapping (based on metadata.json) const categoryIconMapping: Record = { - 'Proxmox & Virtualization': 'server', - 'Operating Systems': 'monitor', - 'Containers & Docker': 'box', - 'Network & Firewall': 'shield', - 'Adblock & DNS': 'shield-check', - 'Authentication & Security': 'key', - 'Backup & Recovery': 'archive', - 'Databases': 'database', - 'Monitoring & Analytics': 'chart-bar', - 'Dashboards & Frontends': 'template', - 'Files & Downloads': 'folder-open', - 'Documents & Notes': 'document-text', - 'Media & Streaming': 'film', - '*Arr Suite': 'download', - 'NVR & Cameras': 'video-camera', - 'IoT & Smart Home': 'home', - 'ZigBee, Z-Wave & Matter': 'wifi', - 'MQTT & Messaging': 'chat-alt', - 'Automation & Scheduling': 'clock', - 'AI / Coding & Dev-Tools': 'code', - 'Webservers & Proxies': 'external-link', - 'Bots & ChatOps': 'sparkles', - 'Finance & Budgeting': 'currency-dollar', - 'Gaming & Leisure': 'puzzle', - 'Business & ERP': 'office', - 'Miscellaneous': 'box' + "Proxmox & Virtualization": "server", + "Operating Systems": "monitor", + "Containers & Docker": "box", + "Network & Firewall": "shield", + "Adblock & DNS": "shield-check", + "Authentication & Security": "key", + "Backup & Recovery": "archive", + Databases: "database", + "Monitoring & Analytics": "chart-bar", + "Dashboards & Frontends": "template", + "Files & Downloads": "folder-open", + "Documents & Notes": "document-text", + "Media & Streaming": "film", + "*Arr Suite": "download", + "NVR & Cameras": "video-camera", + "IoT & Smart Home": "home", + "ZigBee, Z-Wave & Matter": "wifi", + "MQTT & Messaging": "chat-alt", + "Automation & Scheduling": "clock", + "AI / Coding & Dev-Tools": "code", + "Webservers & Proxies": "external-link", + "Bots & ChatOps": "sparkles", + "Finance & Budgeting": "currency-dollar", + "Gaming & Leisure": "puzzle", + "Business & ERP": "office", + Miscellaneous: "box", }; // Sort categories by count (descending) and then alphabetically const sortedCategories = categories - .map(category => [category, categoryCounts[category] ?? 0] as const) + .map((category) => [category, categoryCounts[category] ?? 0] as const) .sort(([a, countA], [b, countB]) => { if (countB !== countA) return countB - countA; return a.localeCompare(b); }); return ( -
+
{/* Header */} -
+
{!isCollapsed && ( -
+
-

Categories

-

{totalScripts} Total scripts

+

+ {t("headerTitle")} +

+

+ {t("totalScripts", { values: { count: totalScripts } })} +

- +
)}
@@ -235,24 +527,26 @@ export function CategorySidebar({ {/* "All Categories" option */} @@ -260,31 +554,32 @@ export function CategorySidebar({ {/* Individual Categories */} {sortedCategories.map(([category, count]) => { const isSelected = selectedCategory === category; - + const categoryLabel = formatCategoryLabel(category); + return ( @@ -296,66 +591,71 @@ export function CategorySidebar({ {/* Collapsed state - show only icons with counters and tooltips */} {isCollapsed && ( -
+
{/* "All Categories" option */}
- + {/* Tooltip */} -
- All Categories ({totalScripts}) +
+ {t("all.tooltip", { values: { count: totalScripts } })}
{/* Individual Categories */} {sortedCategories.map(([category, count]) => { const isSelected = selectedCategory === category; - + const categoryLabel = formatCategoryLabel(category); + return (
- + {/* Tooltip */} -
- {category} ({count}) +
+ {formatCategoryTooltip(categoryLabel, count)}
); @@ -364,4 +664,4 @@ export function CategorySidebar({ )}
); -} \ No newline at end of file +} diff --git a/src/app/_components/FilterBar.tsx b/src/app/_components/FilterBar.tsx index 6b10f07..e7527bb 100644 --- a/src/app/_components/FilterBar.tsx +++ b/src/app/_components/FilterBar.tsx @@ -1,9 +1,19 @@ "use client"; -import React, { useState } from "react"; +import { useState } from "react"; import { Button } from "./ui/button"; import { ContextualHelpIcon } from "./ContextualHelpIcon"; -import { Package, Monitor, Wrench, Server, FileText, Calendar, RefreshCw, Filter } from "lucide-react"; +import { + Package, + Monitor, + Wrench, + Server, + FileText, + Calendar, + RefreshCw, + Filter, +} from "lucide-react"; +import { useTranslation } from "~/lib/i18n/useTranslation"; export interface FilterState { searchQuery: string; @@ -24,10 +34,10 @@ interface FilterBarProps { } const SCRIPT_TYPES = [ - { value: "ct", label: "LXC Container", Icon: Package }, - { value: "vm", label: "Virtual Machine", Icon: Monitor }, - { value: "addon", label: "Add-on", Icon: Wrench }, - { value: "pve", label: "PVE Host", Icon: Server }, + { value: "ct", labelKey: "types.options.ct", Icon: Package }, + { value: "vm", labelKey: "types.options.vm", Icon: Monitor }, + { value: "addon", labelKey: "types.options.addon", Icon: Wrench }, + { value: "pve", labelKey: "types.options.pve", Icon: Server }, ]; export function FilterBar({ @@ -39,6 +49,7 @@ export function FilterBar({ saveFiltersEnabled = false, isLoadingFilters = false, }: FilterBarProps) { + const { t } = useTranslation("filterBar"); const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false); const [isSortDropdownOpen, setIsSortDropdownOpen] = useState(false); @@ -64,50 +75,55 @@ export function FilterBar({ filters.sortOrder !== "asc"; const getUpdatableButtonText = () => { - if (filters.showUpdatable === null) return "Updatable: All"; - if (filters.showUpdatable === true) - return `Updatable: Yes (${updatableCount})`; - return "Updatable: No"; + if (filters.showUpdatable === null) return t("updatable.all"); + if (filters.showUpdatable === true) { + return t("updatable.yes", { values: { count: updatableCount } }); + } + return t("updatable.no"); }; const getTypeButtonText = () => { - if (filters.selectedTypes.length === 0) return "All Types"; + if (filters.selectedTypes.length === 0) return t("types.all"); if (filters.selectedTypes.length === 1) { const type = SCRIPT_TYPES.find( (t) => t.value === filters.selectedTypes[0], ); - return type?.label ?? filters.selectedTypes[0]; + return type ? t(type.labelKey) : filters.selectedTypes[0]; } - return `${filters.selectedTypes.length} Types`; + return t("types.multiple", { + values: { count: filters.selectedTypes.length }, + }); }; return ( -
+
{/* Loading State */} {isLoadingFilters && (
-
-
- Loading saved filters... +
+
+ {t("loading")}
)} - {/* Filter Header */} {!isLoadingFilters && (
-

Filter Scripts

- +

{t("header")}

+
)} {/* Search Bar */}
-
+
+
{/* Updateable Filter */} {isTypeDropdownOpen && ( -
+
{SCRIPT_TYPES.map((type) => { const IconComponent = type.Icon; return ( ); })}
-
+
@@ -270,14 +286,18 @@ export function FilterBar({ onClick={() => setIsSortDropdownOpen(!isSortDropdownOpen)} variant="outline" size="default" - className="w-full sm:w-auto flex items-center justify-center space-x-2 bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground" + className="bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground flex w-full items-center justify-center space-x-2 sm:w-auto" > {filters.sortBy === "name" ? ( ) : ( )} - {filters.sortBy === "name" ? "By Name" : "By Created Date"} + + {filters.sortBy === "name" + ? t("sort.byName") + : t("sort.byCreated")} + {isSortDropdownOpen && ( -
+
@@ -334,7 +358,7 @@ export function FilterBar({ } variant="outline" size="default" - className="w-full sm:w-auto flex items-center justify-center space-x-1 bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground" + className="bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground flex w-full items-center justify-center space-x-1 sm:w-auto" > {filters.sortOrder === "asc" ? ( <> @@ -352,7 +376,9 @@ export function FilterBar({ /> - {filters.sortBy === "created" ? "Oldest First" : "A-Z"} + {filters.sortBy === "created" + ? t("sort.oldestFirst") + : t("sort.aToZ")} ) : ( @@ -371,7 +397,9 @@ export function FilterBar({ /> - {filters.sortBy === "created" ? "Newest First" : "Z-A"} + {filters.sortBy === "created" + ? t("sort.newestFirst") + : t("sort.zToA")} )} @@ -379,30 +407,38 @@ export function FilterBar({
{/* Filter Summary and Clear All */} -
+
-
+
{filteredCount === totalScripts ? ( - Showing all {totalScripts} scripts + + {t("summary.showingAll", { values: { count: totalScripts } })} + ) : ( - {filteredCount} of {totalScripts} scripts{" "} + {t("summary.showingFiltered", { + values: { filtered: filteredCount, total: totalScripts }, + })}{" "} {hasActiveFilters && ( - - (filtered) + + {t("summary.filteredSuffix")} )} )}
- + {/* Filter Persistence Status */} {!isLoadingFilters && saveFiltersEnabled && ( -
+
- + - Filters are being saved automatically + {t("persistence.enabled")}
)}
@@ -412,7 +448,7 @@ export function FilterBar({ onClick={clearAllFilters} variant="ghost" size="sm" - className="flex items-center space-x-1 text-error hover:bg-error/10 hover:text-error-foreground w-full sm:w-auto justify-center sm:justify-start" + className="text-error hover:bg-error/10 hover:text-error-foreground flex w-full items-center justify-center space-x-1 sm:w-auto sm:justify-start" > - Clear all filters + {t("actions.clearFilters")} )}
diff --git a/src/app/_components/Footer.tsx b/src/app/_components/Footer.tsx index 91f9b96..c4e96b1 100644 --- a/src/app/_components/Footer.tsx +++ b/src/app/_components/Footer.tsx @@ -1,8 +1,9 @@ -'use client'; +"use client"; -import { api } from '~/trpc/react'; -import { Button } from './ui/button'; -import { ExternalLink, FileText } from 'lucide-react'; +import { api } from "~/trpc/react"; +import { Button } from "./ui/button"; +import { ExternalLink, FileText } from "lucide-react"; +import { useTranslation } from "~/lib/i18n/useTranslation"; interface FooterProps { onOpenReleaseNotes: () => void; @@ -10,41 +11,43 @@ interface FooterProps { export function Footer({ onOpenReleaseNotes }: FooterProps) { const { data: versionData } = api.version.getCurrentVersion.useQuery(); + const { t } = useTranslation("footer"); + const currentYear = new Date().getFullYear(); return ( -