diff --git a/src/client/components/ClassificationBadge.tsx b/src/client/components/ClassificationBadge.tsx index 6d7c136..7eb1a71 100644 --- a/src/client/components/ClassificationBadge.tsx +++ b/src/client/components/ClassificationBadge.tsx @@ -1,8 +1,4 @@ -const CLASSIFICATION_LABELS: Record = { - base: "Base Weight", - worn: "Worn", - consumable: "Consumable", -}; +import { useTranslation } from "react-i18next"; interface ClassificationBadgeProps { classification: string; @@ -13,7 +9,10 @@ export function ClassificationBadge({ classification, onCycle, }: ClassificationBadgeProps) { - const label = CLASSIFICATION_LABELS[classification] ?? "Base Weight"; + const { t } = useTranslation("collection"); + const label = t(`classificationBadge.${classification}`, { + defaultValue: t("classificationBadge.base"), + }); return ( @@ -79,7 +81,7 @@ export function PlanningView() { : "text-gray-600 hover:bg-gray-200" }`} > - Active + {t("threads:status.active")} @@ -107,7 +109,7 @@ export function PlanningView() {

- Plan your next purchase + {t("threads:planning.emptyTitle")}

@@ -115,9 +117,9 @@ export function PlanningView() { 1
-

Create a thread

+

{t("threads:planning.step1Title")}

- Start a research thread for gear you're considering + {t("threads:planning.step1Description")}

@@ -126,9 +128,9 @@ export function PlanningView() { 2
-

Add candidates

+

{t("threads:planning.step2Title")}

- Add products you're comparing with prices and weights + {t("threads:planning.step2Description")}

@@ -137,9 +139,9 @@ export function PlanningView() { 3
-

Pick a winner

+

{t("threads:planning.step3Title")}

- Resolve the thread and the winner joins your collection + {t("threads:planning.step3Description")}

@@ -163,13 +165,13 @@ export function PlanningView() { d="M12 4v16m8-8H4" /> - Create your first thread + {t("threads:planning.createFirst")} ) : filteredThreads.length === 0 ? (
-

No threads found

+

{t("threads:empty.noThreads")}

) : (
diff --git a/src/client/components/PublicSetupCard.tsx b/src/client/components/PublicSetupCard.tsx index 64d3090..2e09af8 100644 --- a/src/client/components/PublicSetupCard.tsx +++ b/src/client/components/PublicSetupCard.tsx @@ -1,4 +1,5 @@ import { Link } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; interface PublicSetupCardProps { setup: { @@ -11,6 +12,7 @@ interface PublicSetupCardProps { } export function PublicSetupCard({ setup }: PublicSetupCardProps) { + const { t } = useTranslation("setups"); const formattedDate = new Date(setup.createdAt).toLocaleDateString( undefined, { @@ -30,13 +32,13 @@ export function PublicSetupCard({ setup }: PublicSetupCardProps) { {setup.name}

- by {setup.creatorName || "Anonymous"} + {t("card.by", { name: setup.creatorName || t("card.anonymous") })}

{setup.itemCount != null && setup.itemCount > 0 && ( - {setup.itemCount} {setup.itemCount === 1 ? "item" : "items"} + {t("card.items", { count: setup.itemCount })} )}
diff --git a/src/client/components/SetupImpactSelector.tsx b/src/client/components/SetupImpactSelector.tsx index b282ea2..536cfcf 100644 --- a/src/client/components/SetupImpactSelector.tsx +++ b/src/client/components/SetupImpactSelector.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { useSetups } from "../hooks/useSetups"; import { useUIStore } from "../stores/uiStore"; @@ -8,6 +9,7 @@ interface SetupImpactSelectorProps { export function SetupImpactSelector({ threadStatus, }: SetupImpactSelectorProps) { + const { t } = useTranslation("setups"); const { data: setups } = useSetups(); const selectedSetupId = useUIStore((s) => s.selectedSetupId); const setSelectedSetupId = useUIStore((s) => s.setSelectedSetupId); @@ -23,7 +25,7 @@ export function SetupImpactSelector({ } className="border border-gray-200 rounded-lg text-sm px-3 py-1.5 text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-gray-300" > - + {setups.map((setup) => (
@@ -76,7 +78,7 @@ export function ThreadCard({ {categoryName} - {candidateCount} {candidateCount === 1 ? "candidate" : "candidates"} + {t("card.candidates", { count: candidateCount })} {formatDate(createdAt)} diff --git a/src/client/components/ThreadTabs.tsx b/src/client/components/ThreadTabs.tsx index a365f3a..2aa93e4 100644 --- a/src/client/components/ThreadTabs.tsx +++ b/src/client/components/ThreadTabs.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from "react-i18next"; + type TabKey = "gear" | "planning" | "setups"; interface CollectionTabsProps { @@ -5,13 +7,14 @@ interface CollectionTabsProps { onChange: (tab: TabKey) => void; } -const tabs = [ - { key: "gear" as const, label: "My Gear" }, - { key: "planning" as const, label: "Planning" }, - { key: "setups" as const, label: "Setups" }, -]; - export function CollectionTabs({ active, onChange }: CollectionTabsProps) { + const { t } = useTranslation("collection"); + const tabs = [ + { key: "gear" as const, label: t("gear") }, + { key: "planning" as const, label: t("planning") }, + { key: "setups" as const, label: t("tabs.setups") }, + ]; + return (
{tabs.map((tab) => ( diff --git a/src/client/components/TotalsBar.tsx b/src/client/components/TotalsBar.tsx index 1c066b5..819757b 100644 --- a/src/client/components/TotalsBar.tsx +++ b/src/client/components/TotalsBar.tsx @@ -1,4 +1,5 @@ import { Link } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; import { useAuth } from "../hooks/useAuth"; import { LucideIcon } from "../lib/iconData"; import { UserMenu } from "./UserMenu"; @@ -9,6 +10,7 @@ interface TotalsBarProps { } export function TotalsBar({ title = "GearBox", linkTo }: TotalsBarProps) { + const { t } = useTranslation("common"); const { data: auth } = useAuth(); const isAuthenticated = !!auth?.user; @@ -43,7 +45,7 @@ export function TotalsBar({ title = "GearBox", linkTo }: TotalsBarProps) { to="/login" className="text-xs text-gray-500 hover:text-gray-700 transition-colors" > - Sign in + {t("auth.signIn")} )}
diff --git a/src/client/locales/de/collection.json b/src/client/locales/de/collection.json index 03240c3..b1648ec 100644 --- a/src/client/locales/de/collection.json +++ b/src/client/locales/de/collection.json @@ -27,5 +27,17 @@ "light": "Leicht", "medium": "Mittel", "heavy": "Schwer" + }, + "tabs": { + "setups": "Setups" + }, + "totals": { + "totalWeight": "Gesamtgewicht", + "totalCost": "Gesamtkosten" + }, + "classificationBadge": { + "base": "Basisgewicht", + "worn": "Getragen", + "consumable": "Verbrauchsmaterial" } } diff --git a/src/client/locales/de/common.json b/src/client/locales/de/common.json index 4ba9618..4934d11 100644 --- a/src/client/locales/de/common.json +++ b/src/client/locales/de/common.json @@ -82,6 +82,12 @@ "recentlyAdded": "Kürzlich hinzugefügt", "trendingCategories": "Trend-Kategorien" }, + "imageUpload": { + "clickToAdd": "Zum Hinzufügen klicken", + "invalidType": "Bitte wählen Sie ein JPG-, PNG- oder WebP-Bild aus.", + "tooLarge": "Das Bild muss kleiner als 5 MB sein.", + "uploadFailed": "Upload fehlgeschlagen. Bitte versuchen Sie es erneut." + }, "profile": { "title": "Profil", "account": "Konto", diff --git a/src/client/locales/de/setups.json b/src/client/locales/de/setups.json index 3eb2f45..79c0e10 100644 --- a/src/client/locales/de/setups.json +++ b/src/client/locales/de/setups.json @@ -6,10 +6,12 @@ "description": "Erstellen Sie ein Setup, um Ausruestung fuer bestimmte Reisen oder Aktivitaeten zu organisieren." }, "card": { - "items": "{{count}} Gegenstaende", + "items": "{{count}} Gegenstände", "items_one": "{{count}} Gegenstand", "weight": "Gewicht", - "price": "Preis" + "price": "Preis", + "by": "von {{name}}", + "anonymous": "Anonym" }, "share": { "title": "Setup teilen", @@ -37,7 +39,8 @@ }, "impact": { "title": "Auswirkungsvorschau", - "adding": "Hinzufuegen", - "removing": "Entfernen" + "adding": "Hinzufügen", + "removing": "Entfernen", + "compareWith": "Mit Setup vergleichen..." } } diff --git a/src/client/locales/de/threads.json b/src/client/locales/de/threads.json index f36d656..7601ef7 100644 --- a/src/client/locales/de/threads.json +++ b/src/client/locales/de/threads.json @@ -39,7 +39,22 @@ "message": "{{name}} als Gewinner waehlen? Der Gegenstand wird Ihrer Sammlung hinzugefuegt und der Thread archiviert." }, "empty": { - "noThreads": "Noch keine Recherche-Threads", + "noThreads": "Keine Threads gefunden", "noCandidates": "Noch keine Kandidaten" + }, + "card": { + "candidates": "{{count}} Kandidaten", + "candidates_one": "{{count}} Kandidat" + }, + "planning": { + "title": "Planungs-Threads", + "emptyTitle": "Nächsten Kauf planen", + "createFirst": "Ersten Thread erstellen", + "step1Title": "Thread erstellen", + "step1Description": "Starten Sie einen Recherche-Thread für Ausrüstung, die Sie in Betracht ziehen", + "step2Title": "Kandidaten hinzufügen", + "step2Description": "Fügen Sie Produkte hinzu, die Sie mit Preisen und Gewichten vergleichen", + "step3Title": "Gewinner wählen", + "step3Description": "Thread abschließen und der Gewinner kommt in Ihre Sammlung" } } diff --git a/src/client/locales/en/collection.json b/src/client/locales/en/collection.json index 0dfb0e4..452a41f 100644 --- a/src/client/locales/en/collection.json +++ b/src/client/locales/en/collection.json @@ -27,5 +27,17 @@ "light": "Light", "medium": "Medium", "heavy": "Heavy" + }, + "tabs": { + "setups": "Setups" + }, + "totals": { + "totalWeight": "Total Weight", + "totalCost": "Total Cost" + }, + "classificationBadge": { + "base": "Base Weight", + "worn": "Worn", + "consumable": "Consumable" } } diff --git a/src/client/locales/en/common.json b/src/client/locales/en/common.json index f91738d..d134eef 100644 --- a/src/client/locales/en/common.json +++ b/src/client/locales/en/common.json @@ -82,6 +82,12 @@ "recentlyAdded": "Recently Added", "trendingCategories": "Trending Categories" }, + "imageUpload": { + "clickToAdd": "Click to add photo", + "invalidType": "Please select a JPG, PNG, or WebP image.", + "tooLarge": "Image must be under 5MB.", + "uploadFailed": "Upload failed. Please try again." + }, "profile": { "title": "Profile", "account": "Account", diff --git a/src/client/locales/en/setups.json b/src/client/locales/en/setups.json index 4b44a0a..5c30138 100644 --- a/src/client/locales/en/setups.json +++ b/src/client/locales/en/setups.json @@ -9,7 +9,9 @@ "items": "{{count}} items", "items_one": "{{count}} item", "weight": "Weight", - "price": "Price" + "price": "Price", + "by": "by {{name}}", + "anonymous": "Anonymous" }, "share": { "title": "Share Setup", @@ -38,6 +40,7 @@ "impact": { "title": "Impact Preview", "adding": "Adding", - "removing": "Removing" + "removing": "Removing", + "compareWith": "Compare with setup..." } } diff --git a/src/client/locales/en/threads.json b/src/client/locales/en/threads.json index dcb68b6..10d780e 100644 --- a/src/client/locales/en/threads.json +++ b/src/client/locales/en/threads.json @@ -39,7 +39,22 @@ "message": "Pick {{name}} as the winner? This will add it to your collection and archive the thread." }, "empty": { - "noThreads": "No research threads yet", + "noThreads": "No threads found", "noCandidates": "No candidates yet" + }, + "card": { + "candidates": "{{count}} candidates", + "candidates_one": "{{count}} candidate" + }, + "planning": { + "title": "Planning Threads", + "emptyTitle": "Plan your next purchase", + "createFirst": "Create your first thread", + "step1Title": "Create a thread", + "step1Description": "Start a research thread for gear you're considering", + "step2Title": "Add candidates", + "step2Description": "Add products you're comparing with prices and weights", + "step3Title": "Pick a winner", + "step3Description": "Resolve the thread and the winner joins your collection" } }