diff --git a/src/client/components/CategoryFilterDropdown.tsx b/src/client/components/CategoryFilterDropdown.tsx index b2bd1ee..ffacdf4 100644 --- a/src/client/components/CategoryFilterDropdown.tsx +++ b/src/client/components/CategoryFilterDropdown.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { LucideIcon } from "../lib/iconData"; interface CategoryFilterDropdownProps { @@ -12,6 +13,7 @@ export function CategoryFilterDropdown({ onChange, categories, }: CategoryFilterDropdownProps) { + const { t } = useTranslation("collection"); const [isOpen, setIsOpen] = useState(false); const [searchText, setSearchText] = useState(""); const containerRef = useRef(null); @@ -81,7 +83,7 @@ export function CategoryFilterDropdown({ {selectedCategory.name} ) : ( - All categories + {t("categoryFilter.allCategories")} )} {selectedCategory ? ( )} @@ -187,7 +189,7 @@ export function CategoryFilterDropdown({ "all categories".includes(searchText.toLowerCase()) ) && (
  • - No categories found + {t("categoryFilter.noResults")}
  • )} diff --git a/src/client/components/CategoryHeader.tsx b/src/client/components/CategoryHeader.tsx index 2b64943..dc9b7a0 100644 --- a/src/client/components/CategoryHeader.tsx +++ b/src/client/components/CategoryHeader.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories"; import { useFormatters } from "../hooks/useFormatters"; import { LucideIcon } from "../lib/iconData"; @@ -21,6 +22,7 @@ export function CategoryHeader({ totalCost, itemCount, }: CategoryHeaderProps) { + const { t } = useTranslation("collection"); const { weight, price } = useFormatters(); const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(name); @@ -67,14 +69,14 @@ export function CategoryHeader({ onClick={handleSave} className="text-sm text-gray-600 hover:text-gray-800 font-medium" > - Save + {t("categoryHeader.save")} ); @@ -85,7 +87,7 @@ export function CategoryHeader({

    {name}

    - {itemCount} {itemCount === 1 ? "item" : "items"} · {weight(totalWeight)}{" "} + {t("categoryHeader.itemCount", { count: itemCount })} · {weight(totalWeight)}{" "} · {price(totalCost)} {!isUncategorized && ( diff --git a/src/client/components/CategoryPicker.tsx b/src/client/components/CategoryPicker.tsx index 095493c..5db366e 100644 --- a/src/client/components/CategoryPicker.tsx +++ b/src/client/components/CategoryPicker.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useCategories, useCreateCategory } from "../hooks/useCategories"; import { LucideIcon } from "../lib/iconData"; import { IconPicker } from "./IconPicker"; @@ -9,6 +10,7 @@ interface CategoryPickerProps { } export function CategoryPicker({ value, onChange }: CategoryPickerProps) { + const { t } = useTranslation("collection"); const { data: categories = [] } = useCategories(); const createCategory = useCreateCategory(); const [inputValue, setInputValue] = useState(""); @@ -158,7 +160,7 @@ export function CategoryPicker({ value, onChange }: CategoryPickerProps) { value={ isOpen ? inputValue : selectedCategory ? selectedCategory.name : "" } - placeholder="Search or create category..." + placeholder={t("categoryPicker.searchOrCreate")} onChange={(e) => { setInputValue(e.target.value); setIsOpen(true); @@ -233,14 +235,14 @@ export function CategoryPicker({ value, onChange }: CategoryPickerProps) { disabled={createCategory.isPending} className="text-xs font-medium text-gray-600 hover:text-gray-800 disabled:opacity-50" > - {createCategory.isPending ? "..." : "Create"} + {createCategory.isPending ? "..." : t("categoryPicker.create")} )} {filtered.length === 0 && !showCreateOption && (
  • - No categories found + {t("categoryPicker.noCategories")}
  • )} diff --git a/src/client/components/CollectionView.tsx b/src/client/components/CollectionView.tsx index d764378..ec7f0de 100644 --- a/src/client/components/CollectionView.tsx +++ b/src/client/components/CollectionView.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useCategories } from "../hooks/useCategories"; import { useFormatters } from "../hooks/useFormatters"; import { useItems } from "../hooks/useItems"; @@ -10,6 +11,7 @@ import { CategoryHeader } from "./CategoryHeader"; import { ItemCard } from "./ItemCard"; export function CollectionView() { + const { t } = useTranslation(["collection", "common"]); const { data: items, isLoading: itemsLoading } = useItems(); const { data: totals } = useTotals(); const { data: categories } = useCategories(); @@ -58,11 +60,10 @@ export function CollectionView() { />

    - Your collection is empty + {t("collection:empty.title")}

    - Start cataloging your gear by adding your first item. Track weight, - price, and organize by category. + {t("collection:empty.description")}

    @@ -136,14 +137,14 @@ export function CollectionView() {
    - Items + {t("common:stats.items")} {totals.global.itemCount}
    - Total Weight + {t("common:stats.totalWeight")} {weight(totals.global.totalWeight)} @@ -154,7 +155,7 @@ export function CollectionView() { size={14} className="text-gray-400" /> - Total Spent + {t("common:stats.totalSpent")} {price(totals.global.totalCost)} @@ -169,7 +170,7 @@ export function CollectionView() {
    setSearchText(e.target.value)} className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" @@ -204,7 +205,7 @@ export function CollectionView() {
    {hasActiveFilters && (

    - Showing {filteredItems.length} of {items.length} items + {t("common:filter.showing", { filtered: filteredItems.length, total: items.length })}

    )}
    @@ -213,7 +214,7 @@ export function CollectionView() { {hasActiveFilters ? ( filteredItems.length === 0 ? (
    -

    No items match your search

    +

    {t("common:empty.noItems")}

    ) : (
    diff --git a/src/client/components/ItemCard.tsx b/src/client/components/ItemCard.tsx index 009e908..14b0b72 100644 --- a/src/client/components/ItemCard.tsx +++ b/src/client/components/ItemCard.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; import { useFormatters } from "../hooks/useFormatters"; import { useDuplicateItem } from "../hooks/useItems"; import { LucideIcon } from "../lib/iconData"; @@ -51,6 +52,7 @@ export function ItemCard({ linkTo, priceCurrency, }: ItemCardProps) { + const { t } = useTranslation("collection"); const { weight, price } = useFormatters(); const navigate = useNavigate(); const openExternalLink = useUIStore((s) => s.openExternalLink); @@ -102,7 +104,7 @@ export function ItemCard({ } }} className={`absolute top-2 ${productUrl ? "right-10" : "right-2"} z-10 w-6 h-6 flex items-center justify-center rounded-full bg-gray-100/80 text-gray-400 hover:bg-blue-100 hover:text-blue-500 opacity-0 group-hover:opacity-100 transition-all cursor-pointer`} - title="Duplicate item" + title={t("itemCard.duplicateItem")} > = {}; if (!form.name.trim()) { - newErrors.name = "Name is required"; + newErrors.name = t("common:errors.nameRequired"); } if ( form.weightGrams && (Number.isNaN(Number(form.weightGrams)) || Number(form.weightGrams) < 0) ) { - newErrors.weightGrams = "Must be a positive number"; + newErrors.weightGrams = t("common:errors.positiveNumber"); } if ( form.priceDollars && (Number.isNaN(Number(form.priceDollars)) || Number(form.priceDollars) < 0) ) { - newErrors.priceDollars = "Must be a positive number"; + newErrors.priceDollars = t("common:errors.positiveNumber"); } if ( form.productUrl && form.productUrl.trim() !== "" && !form.productUrl.match(/^https?:\/\//) ) { - newErrors.productUrl = "Must be a valid URL (https://...)"; + newErrors.productUrl = t("common:errors.validUrl"); } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -148,7 +150,7 @@ export function ItemForm({ mode, itemId, onClose }: ItemFormProps) { htmlFor="item-name" className="block text-sm font-medium text-gray-700 mb-1" > - Name * + {t("collection:form.nameRequired")}

    {errors.name}

    @@ -169,7 +171,7 @@ export function ItemForm({ mode, itemId, onClose }: ItemFormProps) { htmlFor="item-weight" className="block text-sm font-medium text-gray-700 mb-1" > - Weight (g) + {t("collection:form.weight")} ({ ...f, weightGrams: e.target.value })) } className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" - placeholder="e.g. 680" + placeholder={t("collection:form.weightPlaceholder")} /> {errors.weightGrams && (

    {errors.weightGrams}

    @@ -194,7 +196,7 @@ export function ItemForm({ mode, itemId, onClose }: ItemFormProps) { htmlFor="item-price" className="block text-sm font-medium text-gray-700 mb-1" > - {`Price (${currency})`} + {`${t("collection:form.price")} (${currency})`} ({ ...f, priceDollars: e.target.value })) } className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" - placeholder="e.g. 129.99" + placeholder={t("collection:form.pricePlaceholder")} /> {errors.priceDollars && (

    {errors.priceDollars}

    @@ -219,7 +221,7 @@ export function ItemForm({ mode, itemId, onClose }: ItemFormProps) { htmlFor="item-quantity" className="block text-sm font-medium text-gray-700 mb-1" > - Quantity + {t("collection:form.quantity")} - Notes + {t("collection:form.notes")}