feat(i18n): extract strings from navigation, dialogs, onboarding, settings, and login

- Add useTranslation() to TopNav, BottomTabBar, FabMenu, UserMenu
- Internationalize ConfirmDialog, AuthPromptModal, ExternalLinkDialog
- Extract all onboarding flow strings (Welcome, HobbyPicker, ItemBrowser, Review, Done)
- Internationalize settings page (weight unit, currency, API keys, import/export)
- Internationalize login page and root error boundary
- All dialogs in __root.tsx use t() for UI chrome

Phase 34, Plan 02 (core navigation and global UI)
This commit is contained in:
2026-04-13 18:19:29 +02:00
parent 8c0fb31df2
commit 672b17fd13
15 changed files with 123 additions and 98 deletions

View File

@@ -1,7 +1,9 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
export function AuthPromptModal() { export function AuthPromptModal() {
const { t } = useTranslation();
const showAuthPrompt = useUIStore((s) => s.showAuthPrompt); const showAuthPrompt = useUIStore((s) => s.showAuthPrompt);
const closeAuthPrompt = useUIStore((s) => s.closeAuthPrompt); const closeAuthPrompt = useUIStore((s) => s.closeAuthPrompt);
@@ -18,10 +20,10 @@ export function AuthPromptModal() {
/> />
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full"> <div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-gray-900 mb-2">
Join GearBox {t("auth.joinGearBox")}
</h3> </h3>
<p className="text-sm text-gray-600 mb-6"> <p className="text-sm text-gray-600 mb-6">
To manage your own collection, sign in or sign up. {t("auth.signInDescription")}
</p> </p>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<Link <Link
@@ -29,14 +31,14 @@ export function AuthPromptModal() {
className="w-full text-center px-4 py-2.5 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors" className="w-full text-center px-4 py-2.5 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors"
onClick={closeAuthPrompt} onClick={closeAuthPrompt}
> >
Sign in {t("auth.signIn")}
</Link> </Link>
<Link <Link
to="/login" to="/login"
className="w-full text-center px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" className="w-full text-center px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
onClick={closeAuthPrompt} onClick={closeAuthPrompt}
> >
Create account {t("auth.createAccount")}
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { Link, useMatchRoute } from "@tanstack/react-router"; import { Link, useMatchRoute } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
@@ -26,6 +27,7 @@ function TabItemWrapper({ icon, label, isActive }: TabItemProps) {
} }
export function BottomTabBar() { export function BottomTabBar() {
const { t } = useTranslation();
const { data: auth } = useAuth(); const { data: auth } = useAuth();
const isAuthenticated = !!auth?.user; const isAuthenticated = !!auth?.user;
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch); const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
@@ -46,7 +48,7 @@ export function BottomTabBar() {
<div className="flex justify-around"> <div className="flex justify-around">
{/* Home tab — always a Link */} {/* Home tab — always a Link */}
<Link to="/"> <Link to="/">
<TabItemWrapper icon="house" label="Home" isActive={isHome} /> <TabItemWrapper icon="house" label={t("nav.home")} isActive={isHome} />
</Link> </Link>
{/* Collection tab — Link if authenticated, button if anonymous */} {/* Collection tab — Link if authenticated, button if anonymous */}
@@ -54,7 +56,7 @@ export function BottomTabBar() {
<Link to="/collection"> <Link to="/collection">
<TabItemWrapper <TabItemWrapper
icon="package" icon="package"
label="Collection" label={t("nav.collection")}
isActive={isCollection} isActive={isCollection}
/> />
</Link> </Link>
@@ -62,7 +64,7 @@ export function BottomTabBar() {
<button type="button" onClick={openAuthPrompt}> <button type="button" onClick={openAuthPrompt}>
<TabItemWrapper <TabItemWrapper
icon="package" icon="package"
label="Collection" label={t("nav.collection")}
isActive={isCollection} isActive={isCollection}
/> />
</button> </button>
@@ -71,17 +73,17 @@ export function BottomTabBar() {
{/* Setups tab — Link if authenticated, button if anonymous */} {/* Setups tab — Link if authenticated, button if anonymous */}
{isAuthenticated ? ( {isAuthenticated ? (
<Link to="/setups"> <Link to="/setups">
<TabItemWrapper icon="layers" label="Setups" isActive={isSetups} /> <TabItemWrapper icon="layers" label={t("nav.setups")} isActive={isSetups} />
</Link> </Link>
) : ( ) : (
<button type="button" onClick={openAuthPrompt}> <button type="button" onClick={openAuthPrompt}>
<TabItemWrapper icon="layers" label="Setups" isActive={isSetups} /> <TabItemWrapper icon="layers" label={t("nav.setups")} isActive={isSetups} />
</button> </button>
)} )}
{/* Search tab — always a button, opens CatalogSearchOverlay */} {/* Search tab — always a button, opens CatalogSearchOverlay */}
<button type="button" onClick={() => openCatalogSearch("collection")}> <button type="button" onClick={() => openCatalogSearch("collection")}>
<TabItemWrapper icon="search" label="Search" isActive={false} /> <TabItemWrapper icon="search" label={t("nav.search")} isActive={false} />
</button> </button>
</div> </div>
</motion.div> </motion.div>

View File

@@ -1,7 +1,9 @@
import { useTranslation } from "react-i18next";
import { useDeleteItem, useItems } from "../hooks/useItems"; import { useDeleteItem, useItems } from "../hooks/useItems";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
export function ConfirmDialog() { export function ConfirmDialog() {
const { t } = useTranslation();
const confirmDeleteItemId = useUIStore((s) => s.confirmDeleteItemId); const confirmDeleteItemId = useUIStore((s) => s.confirmDeleteItemId);
const closeConfirmDelete = useUIStore((s) => s.closeConfirmDelete); const closeConfirmDelete = useUIStore((s) => s.closeConfirmDelete);
const deleteItem = useDeleteItem(); const deleteItem = useDeleteItem();
@@ -30,12 +32,10 @@ export function ConfirmDialog() {
/> />
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full"> <div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-gray-900 mb-2">
Delete Item {t("confirm.deleteItem")}
</h3> </h3>
<p className="text-sm text-gray-600 mb-6"> <p className="text-sm text-gray-600 mb-6">
Are you sure you want to delete{" "} {t("confirm.deleteItemMessage", { name: itemName })}
<span className="font-medium">{itemName}</span>? This action cannot be
undone.
</p> </p>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
@@ -43,7 +43,7 @@ export function ConfirmDialog() {
onClick={closeConfirmDelete} onClick={closeConfirmDelete}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
> >
Cancel {t("actions.cancel")}
</button> </button>
<button <button
type="button" type="button"
@@ -51,7 +51,7 @@ export function ConfirmDialog() {
disabled={deleteItem.isPending} disabled={deleteItem.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 rounded-lg transition-colors"
> >
{deleteItem.isPending ? "Deleting..." : "Delete"} {deleteItem.isPending ? t("actions.deleting") : t("actions.delete")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,9 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
export function ExternalLinkDialog() { export function ExternalLinkDialog() {
const { t } = useTranslation();
const externalLinkUrl = useUIStore((s) => s.externalLinkUrl); const externalLinkUrl = useUIStore((s) => s.externalLinkUrl);
const closeExternalLink = useUIStore((s) => s.closeExternalLink); const closeExternalLink = useUIStore((s) => s.closeExternalLink);
@@ -35,9 +37,9 @@ export function ExternalLinkDialog() {
/> />
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full"> <div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-gray-900 mb-2">
You are about to leave GearBox {t("externalLink.title")}
</h3> </h3>
<p className="text-sm text-gray-600 mb-1">You will be redirected to:</p> <p className="text-sm text-gray-600 mb-1">{t("externalLink.redirectMessage")}</p>
<p className="text-sm text-gray-600 break-all mb-6"> <p className="text-sm text-gray-600 break-all mb-6">
{externalLinkUrl} {externalLinkUrl}
</p> </p>
@@ -47,14 +49,14 @@ export function ExternalLinkDialog() {
onClick={closeExternalLink} onClick={closeExternalLink}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
> >
Cancel {t("actions.cancel")}
</button> </button>
<button <button
type="button" type="button"
onClick={handleContinue} onClick={handleContinue}
className="px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors"
> >
Continue {t("actions.continue")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { Package, Plus, Search } from "lucide-react"; import { Package, Plus, Search } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
interface FabMenuProps { interface FabMenuProps {
@@ -15,6 +16,7 @@ interface MenuItem {
} }
export function FabMenu({ isSetupsPage }: FabMenuProps) { export function FabMenu({ isSetupsPage }: FabMenuProps) {
const { t } = useTranslation();
const fabMenuOpen = useUIStore((s) => s.fabMenuOpen); const fabMenuOpen = useUIStore((s) => s.fabMenuOpen);
const openFabMenu = useUIStore((s) => s.openFabMenu); const openFabMenu = useUIStore((s) => s.openFabMenu);
const closeFabMenu = useUIStore((s) => s.closeFabMenu); const closeFabMenu = useUIStore((s) => s.closeFabMenu);
@@ -26,12 +28,12 @@ export function FabMenu({ isSetupsPage }: FabMenuProps) {
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ {
label: "Add to Collection", label: t("fab.addToCollection"),
icon: <Package className="w-5 h-5 text-gray-600" />, icon: <Package className="w-5 h-5 text-gray-600" />,
onClick: () => openCatalogSearch("collection"), onClick: () => openCatalogSearch("collection"),
}, },
{ {
label: "Start New Thread", label: t("fab.startNewThread"),
icon: <Search className="w-5 h-5 text-gray-600" />, icon: <Search className="w-5 h-5 text-gray-600" />,
onClick: () => openCatalogSearch("thread"), onClick: () => openCatalogSearch("thread"),
}, },
@@ -39,7 +41,7 @@ export function FabMenu({ isSetupsPage }: FabMenuProps) {
if (isSetupsPage) { if (isSetupsPage) {
menuItems.push({ menuItems.push({
label: "New Setup", label: t("fab.newSetup"),
icon: <Plus className="w-5 h-5 text-gray-600" />, icon: <Plus className="w-5 h-5 text-gray-600" />,
onClick: () => { onClick: () => {
closeFabMenu(); closeFabMenu();

View File

@@ -1,5 +1,6 @@
import { Link, useMatchRoute, useNavigate } from "@tanstack/react-router"; import { Link, useMatchRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
@@ -42,6 +43,7 @@ function NavLinkOrButton({
} }
export function TopNav() { export function TopNav() {
const { t } = useTranslation();
const { data: auth } = useAuth(); const { data: auth } = useAuth();
const isAuthenticated = !!auth?.user; const isAuthenticated = !!auth?.user;
const openAuthPrompt = useUIStore((s) => s.openAuthPrompt); const openAuthPrompt = useUIStore((s) => s.openAuthPrompt);
@@ -82,7 +84,7 @@ export function TopNav() {
isAuthenticated={isAuthenticated} isAuthenticated={isAuthenticated}
onAuthPrompt={openAuthPrompt} onAuthPrompt={openAuthPrompt}
> >
Home {t("nav.home")}
</NavLinkOrButton> </NavLinkOrButton>
<NavLinkOrButton <NavLinkOrButton
to="/collection" to="/collection"
@@ -91,7 +93,7 @@ export function TopNav() {
isAuthenticated={isAuthenticated} isAuthenticated={isAuthenticated}
onAuthPrompt={openAuthPrompt} onAuthPrompt={openAuthPrompt}
> >
Collection {t("nav.collection")}
</NavLinkOrButton> </NavLinkOrButton>
<NavLinkOrButton <NavLinkOrButton
to="/setups" to="/setups"
@@ -100,7 +102,7 @@ export function TopNav() {
isAuthenticated={isAuthenticated} isAuthenticated={isAuthenticated}
onAuthPrompt={openAuthPrompt} onAuthPrompt={openAuthPrompt}
> >
Setups {t("nav.setups")}
</NavLinkOrButton> </NavLinkOrButton>
</nav> </nav>
@@ -124,7 +126,7 @@ export function TopNav() {
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleSearch(); if (e.key === "Enter") handleSearch();
}} }}
placeholder="Search catalog..." placeholder={t("nav.searchPlaceholder")}
className="bg-gray-50 border border-gray-200 rounded-lg pl-9 pr-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-gray-300 w-48 lg:w-64 transition-colors" className="bg-gray-50 border border-gray-200 rounded-lg pl-9 pr-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-gray-300 w-48 lg:w-64 transition-colors"
/> />
</div> </div>
@@ -137,7 +139,7 @@ export function TopNav() {
to="/login" to="/login"
className="text-xs text-gray-500 hover:text-gray-700 transition-colors" className="text-xs text-gray-500 hover:text-gray-700 transition-colors"
> >
Sign in {t("auth.signIn")}
</Link> </Link>
)} )}
</div> </div>

View File

@@ -1,10 +1,12 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAuth, useLogout } from "../hooks/useAuth"; import { useAuth, useLogout } from "../hooks/useAuth";
import { usePublicProfile } from "../hooks/useProfile"; import { usePublicProfile } from "../hooks/useProfile";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
export function UserMenu() { export function UserMenu() {
const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const { logout } = useLogout(); const { logout } = useLogout();
@@ -34,7 +36,7 @@ export function UserMenu() {
{avatarUrl ? ( {avatarUrl ? (
<img <img
src={avatarUrl} src={avatarUrl}
alt="Profile" alt={t("nav.profile")}
className="w-8 h-8 rounded-full object-cover" className="w-8 h-8 rounded-full object-cover"
/> />
) : ( ) : (
@@ -53,7 +55,7 @@ export function UserMenu() {
size={16} size={16}
className="text-gray-400" className="text-gray-400"
/> />
Profile {t("nav.profile")}
</Link> </Link>
<Link <Link
to="/settings" to="/settings"
@@ -61,7 +63,7 @@ export function UserMenu() {
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors" className="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
> >
<LucideIcon name="settings" size={16} className="text-gray-400" /> <LucideIcon name="settings" size={16} className="text-gray-400" />
Settings {t("nav.settings")}
</Link> </Link>
<div className="border-t border-gray-100 my-1" /> <div className="border-t border-gray-100 my-1" />
<button <button
@@ -73,7 +75,7 @@ export function UserMenu() {
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors" className="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
> >
<LucideIcon name="log-out" size={16} className="text-gray-400" /> <LucideIcon name="log-out" size={16} className="text-gray-400" />
Sign out {t("auth.signOut")}
</button> </button>
</div> </div>
)} )}

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { LucideIcon } from "../../lib/iconData"; import { LucideIcon } from "../../lib/iconData";
interface OnboardingDoneProps { interface OnboardingDoneProps {
@@ -9,6 +10,7 @@ export function OnboardingDone({
itemsCreated: _itemsCreated, itemsCreated: _itemsCreated,
onFinish, onFinish,
}: OnboardingDoneProps) { }: OnboardingDoneProps) {
const { t } = useTranslation("onboarding");
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen px-8"> <div className="flex flex-col items-center justify-center min-h-screen px-8">
<div className="max-w-2xl text-center"> <div className="max-w-2xl text-center">
@@ -20,18 +22,17 @@ export function OnboardingDone({
/> />
</div> </div>
<h1 className="text-3xl font-bold text-gray-900 mb-2"> <h1 className="text-3xl font-bold text-gray-900 mb-2">
You're all set! {t("done.title")}
</h1> </h1>
<p className="text-base text-gray-500 mb-8"> <p className="text-base text-gray-500 mb-8">
Your collection is ready. Browse the catalog anytime to discover more {t("done.subtitle")}
gear.
</p> </p>
<button <button
type="button" type="button"
onClick={onFinish} onClick={onFinish}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors" className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors"
> >
Start exploring {t("done.cta")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { HOBBIES } from "@/shared/hobbyConfig"; import { HOBBIES } from "@/shared/hobbyConfig";
import { useTranslation } from "react-i18next";
import { HobbyCard } from "./HobbyCard"; import { HobbyCard } from "./HobbyCard";
interface OnboardingHobbyPickerProps { interface OnboardingHobbyPickerProps {
@@ -12,14 +13,15 @@ export function OnboardingHobbyPicker({
onToggleHobby, onToggleHobby,
onContinue, onContinue,
}: OnboardingHobbyPickerProps) { }: OnboardingHobbyPickerProps) {
const { t } = useTranslation("onboarding");
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen px-8"> <div className="flex flex-col items-center justify-center min-h-screen px-8">
<div className="max-w-2xl text-center"> <div className="max-w-2xl text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-2"> <h1 className="text-3xl font-bold text-gray-900 mb-2">
What are you into? {t("hobby.title")}
</h1> </h1>
<p className="text-base text-gray-500 mb-8"> <p className="text-base text-gray-500 mb-8">
Pick one or more we'll show you popular gear for each. {t("hobby.subtitle")}
</p> </p>
<div className="flex flex-wrap justify-center gap-4 mb-8"> <div className="flex flex-wrap justify-center gap-4 mb-8">
{HOBBIES.map((hobby) => ( {HOBBIES.map((hobby) => (
@@ -39,7 +41,7 @@ export function OnboardingHobbyPicker({
disabled={selectedHobbies.length === 0} disabled={selectedHobbies.length === 0}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors" className="px-8 py-3 bg-gray-700 hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors"
> >
Continue {t("hobby.continue")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { getTagsForHobbies } from "@/shared/hobbyConfig"; import { getTagsForHobbies } from "@/shared/hobbyConfig";
import { useTranslation } from "react-i18next";
import { usePopularItems } from "../../hooks/useOnboarding"; import { usePopularItems } from "../../hooks/useOnboarding";
import { SelectableItemCard } from "./SelectableItemCard"; import { SelectableItemCard } from "./SelectableItemCard";
@@ -17,6 +18,7 @@ export function OnboardingItemBrowser({
onContinue, onContinue,
onSkip, onSkip,
}: OnboardingItemBrowserProps) { }: OnboardingItemBrowserProps) {
const { t } = useTranslation("onboarding");
const tags = getTagsForHobbies(selectedHobbies); const tags = getTagsForHobbies(selectedHobbies);
const { data: items, isLoading } = usePopularItems(tags); const { data: items, isLoading } = usePopularItems(tags);
@@ -48,11 +50,12 @@ export function OnboardingItemBrowser({
<div className="flex flex-col items-center min-h-screen px-8 py-16"> <div className="flex flex-col items-center min-h-screen px-8 py-16">
<div className="max-w-5xl w-full text-center"> <div className="max-w-5xl w-full text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-2"> <h1 className="text-3xl font-bold text-gray-900 mb-2">
Popular gear for{" "} {selectedHobbies.length === 1
{selectedHobbies.length === 1 ? selectedHobbies[0] : "your hobbies"} ? t("items.title", { hobby: selectedHobbies[0] })
: t("items.titleMultiple")}
</h1> </h1>
<p className="text-base text-gray-500 mb-8"> <p className="text-base text-gray-500 mb-8">
Tap items you already own. We'll add them to your collection. {t("items.subtitle")}
</p> </p>
{isLoading && ( {isLoading && (
@@ -64,11 +67,10 @@ export function OnboardingItemBrowser({
{!isLoading && !hasItems && ( {!isLoading && !hasItems && (
<div className="py-12 text-center"> <div className="py-12 text-center">
<h2 className="text-lg font-semibold text-gray-900 mb-2"> <h2 className="text-lg font-semibold text-gray-900 mb-2">
No gear cataloged yet {t("items.noCatalog")}
</h2> </h2>
<p className="text-base text-gray-500 mb-8"> <p className="text-base text-gray-500 mb-8">
We're still building our catalog for this hobby. You can skip this {t("items.noCatalogDescription")}
step and add gear manually later.
</p> </p>
</div> </div>
)} )}
@@ -105,8 +107,7 @@ export function OnboardingItemBrowser({
onClick={onContinue} onClick={onContinue}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors" className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors"
> >
Review {selectedItemIds.size}{" "} {t("items.reviewCount", { count: selectedItemIds.size })}
{selectedItemIds.size === 1 ? "item" : "items"}
</button> </button>
)} )}
<button <button
@@ -114,7 +115,7 @@ export function OnboardingItemBrowser({
onClick={onSkip} onClick={onSkip}
className="text-sm text-gray-400 hover:text-gray-600 transition-colors" className="text-sm text-gray-400 hover:text-gray-600 transition-colors"
> >
Skip this step {t("common:actions.skipStep")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { LucideIcon } from "../../lib/iconData"; import { LucideIcon } from "../../lib/iconData";
interface ReviewItem { interface ReviewItem {
@@ -23,6 +24,7 @@ export function OnboardingReview({
onSkip, onSkip,
isSubmitting, isSubmitting,
}: OnboardingReviewProps) { }: OnboardingReviewProps) {
const { t } = useTranslation("onboarding");
// Group by category // Group by category
const grouped = new Map<string, ReviewItem[]>(); const grouped = new Map<string, ReviewItem[]>();
for (const item of items) { for (const item of items) {
@@ -35,12 +37,12 @@ export function OnboardingReview({
<div className="flex flex-col items-center justify-center min-h-screen px-8"> <div className="flex flex-col items-center justify-center min-h-screen px-8">
<div className="max-w-2xl w-full text-center"> <div className="max-w-2xl w-full text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-2"> <h1 className="text-3xl font-bold text-gray-900 mb-2">
Your starting collection {t("review.title")}
</h1> </h1>
<p className="text-base text-gray-500 mb-8"> <p className="text-base text-gray-500 mb-8">
{items.length > 0 {items.length > 0
? `${items.length} ${items.length === 1 ? "item" : "items"} ready to add` ? t("review.itemsReady", { count: items.length })
: "No items selected — you can always add gear later from the catalog."} : t("review.noItemsSelected")}
</p> </p>
{items.length > 0 && ( {items.length > 0 && (
@@ -101,7 +103,7 @@ export function OnboardingReview({
disabled={isSubmitting} disabled={isSubmitting}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 disabled:opacity-50 text-white font-medium rounded-lg transition-colors" className="px-8 py-3 bg-gray-700 hover:bg-gray-800 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
> >
{isSubmitting ? "Adding..." : "Add to my collection"} {isSubmitting ? t("review.adding") : t("review.addToCollection")}
</button> </button>
) : ( ) : (
<button <button
@@ -109,7 +111,7 @@ export function OnboardingReview({
onClick={onSkip} onClick={onSkip}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors" className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors"
> >
Continue {t("common:actions.continue")}
</button> </button>
)} )}
{items.length > 0 && ( {items.length > 0 && (
@@ -118,7 +120,7 @@ export function OnboardingReview({
onClick={onSkip} onClick={onSkip}
className="text-sm text-gray-400 hover:text-gray-600 transition-colors" className="text-sm text-gray-400 hover:text-gray-600 transition-colors"
> >
Skip this step {t("common:actions.skipStep")}
</button> </button>
)} )}
</div> </div>

View File

@@ -1,24 +1,26 @@
import { useTranslation } from "react-i18next";
interface OnboardingWelcomeProps { interface OnboardingWelcomeProps {
onContinue: () => void; onContinue: () => void;
} }
export function OnboardingWelcome({ onContinue }: OnboardingWelcomeProps) { export function OnboardingWelcome({ onContinue }: OnboardingWelcomeProps) {
const { t } = useTranslation("onboarding");
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen px-8"> <div className="flex flex-col items-center justify-center min-h-screen px-8">
<div className="max-w-2xl text-center"> <div className="max-w-2xl text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-4"> <h1 className="text-3xl font-bold text-gray-900 mb-4">
Welcome to GearBox {t("welcome.title")}
</h1> </h1>
<p className="text-base text-gray-500 mb-8 leading-relaxed"> <p className="text-base text-gray-500 mb-8 leading-relaxed">
Tell us what you're into, and we'll help you set up your collection {t("welcome.subtitle")}
with gear that people actually use.
</p> </p>
<button <button
type="button" type="button"
onClick={onContinue} onClick={onContinue}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors" className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors"
> >
Let's go {t("welcome.cta")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@ import {
useRouter, useRouter,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import "../app.css"; import "../app.css";
import { AddToCollectionModal } from "../components/AddToCollectionModal"; import { AddToCollectionModal } from "../components/AddToCollectionModal";
@@ -33,6 +34,7 @@ export const Route = createRootRoute({
function RootErrorBoundary({ error, reset }: ErrorComponentProps) { function RootErrorBoundary({ error, reset }: ErrorComponentProps) {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation();
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
@@ -53,12 +55,12 @@ function RootErrorBoundary({ error, reset }: ErrorComponentProps) {
</svg> </svg>
</div> </div>
<h1 className="text-xl font-semibold text-gray-900 mb-2"> <h1 className="text-xl font-semibold text-gray-900 mb-2">
Something went wrong {t("errors.somethingWentWrong")}
</h1> </h1>
<p className="text-sm text-gray-500 mb-6"> <p className="text-sm text-gray-500 mb-6">
{error instanceof Error {error instanceof Error
? error.message ? error.message
: "An unexpected error occurred"} : t("errors.unexpectedError")}
</p> </p>
<button <button
type="button" type="button"
@@ -68,7 +70,7 @@ function RootErrorBoundary({ error, reset }: ErrorComponentProps) {
}} }}
className="px-5 py-2.5 bg-gray-700 hover:bg-gray-800 text-white text-sm font-medium rounded-lg transition-colors" className="px-5 py-2.5 bg-gray-700 hover:bg-gray-800 text-white text-sm font-medium rounded-lg transition-colors"
> >
Try again {t("actions.tryAgain")}
</button> </button>
</div> </div>
</div> </div>
@@ -205,6 +207,7 @@ function CandidateDeleteDialog({
threadId: number; threadId: number;
onClose: () => void; onClose: () => void;
}) { }) {
const { t } = useTranslation();
const deleteCandidate = useDeleteCandidate(threadId); const deleteCandidate = useDeleteCandidate(threadId);
const { data: thread } = useThread(threadId); const { data: thread } = useThread(threadId);
const candidate = thread?.candidates.find((c) => c.id === candidateId); const candidate = thread?.candidates.find((c) => c.id === candidateId);
@@ -227,12 +230,10 @@ function CandidateDeleteDialog({
/> />
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full"> <div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-gray-900 mb-2">
Delete Candidate {t("confirm.deleteCandidate")}
</h3> </h3>
<p className="text-sm text-gray-600 mb-6"> <p className="text-sm text-gray-600 mb-6">
Are you sure you want to delete{" "} {t("confirm.deleteCandidateMessage", { name: candidateName })}
<span className="font-medium">{candidateName}</span>? This action
cannot be undone.
</p> </p>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
@@ -240,7 +241,7 @@ function CandidateDeleteDialog({
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
> >
Cancel {t("actions.cancel")}
</button> </button>
<button <button
type="button" type="button"
@@ -248,7 +249,7 @@ function CandidateDeleteDialog({
disabled={deleteCandidate.isPending} disabled={deleteCandidate.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 rounded-lg transition-colors"
> >
{deleteCandidate.isPending ? "Deleting..." : "Delete"} {deleteCandidate.isPending ? t("actions.deleting") : t("actions.delete")}
</button> </button>
</div> </div>
</div> </div>
@@ -267,6 +268,7 @@ function ResolveDialog({
onClose: () => void; onClose: () => void;
onResolved: () => void; onResolved: () => void;
}) { }) {
const { t } = useTranslation();
const resolveThread = useResolveThread(); const resolveThread = useResolveThread();
const { data: thread } = useThread(threadId); const { data: thread } = useThread(threadId);
const candidate = thread?.candidates.find((c) => c.id === candidateId); const candidate = thread?.candidates.find((c) => c.id === candidateId);
@@ -290,11 +292,10 @@ function ResolveDialog({
/> />
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full"> <div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-gray-900 mb-2">
Pick Winner {t("confirm.pickWinner")}
</h3> </h3>
<p className="text-sm text-gray-600 mb-6"> <p className="text-sm text-gray-600 mb-6">
Pick <span className="font-medium">{candidateName}</span> as the {t("confirm.pickWinnerMessage", { name: candidateName })}
winner? This will add it to your collection and archive the thread.
</p> </p>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
@@ -302,7 +303,7 @@ function ResolveDialog({
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
> >
Cancel {t("actions.cancel")}
</button> </button>
<button <button
type="button" type="button"
@@ -310,7 +311,7 @@ function ResolveDialog({
disabled={resolveThread.isPending} disabled={resolveThread.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 disabled:opacity-50 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 disabled:opacity-50 rounded-lg transition-colors"
> >
{resolveThread.isPending ? "Resolving..." : "Pick Winner"} {resolveThread.isPending ? t("actions.saving") : t("confirm.pickWinner")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
export const Route = createFileRoute("/login")({ export const Route = createFileRoute("/login")({
@@ -7,6 +8,7 @@ export const Route = createFileRoute("/login")({
}); });
function LoginPage() { function LoginPage() {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { data: auth, isLoading } = useAuth(); const { data: auth, isLoading } = useAuth();
@@ -19,7 +21,7 @@ function LoginPage() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-gray-500 text-sm">Loading...</p> <p className="text-gray-500 text-sm">{t("actions.loading")}</p>
</div> </div>
); );
} }
@@ -28,11 +30,11 @@ function LoginPage() {
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4"> <div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
<h1 className="text-xl font-semibold text-gray-900 text-center mb-6"> <h1 className="text-xl font-semibold text-gray-900 text-center mb-6">
Sign in to GearBox {t("auth.signInToGearBox")}
</h1> </h1>
<div className="bg-white rounded-xl border border-gray-100 p-6 space-y-4"> <div className="bg-white rounded-xl border border-gray-100 p-6 space-y-4">
<p className="text-sm text-gray-500 text-center"> <p className="text-sm text-gray-500 text-center">
You will be redirected to sign in with your account. {t("auth.redirectDescription")}
</p> </p>
<button <button
type="button" type="button"
@@ -41,7 +43,7 @@ function LoginPage() {
}} }}
className="w-full py-2 px-4 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors" className="w-full py-2 px-4 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors"
> >
Sign In {t("auth.signIn")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { import {
useApiKeys, useApiKeys,
useAuth, useAuth,
@@ -27,6 +28,7 @@ export const Route = createFileRoute("/settings")({
}); });
function ApiKeySection() { function ApiKeySection() {
const { t } = useTranslation("settings");
const { data: keys } = useApiKeys(); const { data: keys } = useApiKeys();
const createKey = useCreateApiKey(); const createKey = useCreateApiKey();
const deleteKey = useDeleteApiKey(); const deleteKey = useDeleteApiKey();
@@ -42,16 +44,15 @@ function ApiKeySection() {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900">API Keys</h3> <h3 className="text-sm font-medium text-gray-900">{t("apiKeys.title")}</h3>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
API keys allow programmatic access to GearBox (e.g., from Claude Desktop {t("apiKeys.description")}
or scripts).
</p> </p>
{newKey && ( {newKey && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3"> <div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<p className="text-xs font-medium text-amber-800 mb-1"> <p className="text-xs font-medium text-amber-800 mb-1">
Copy this key now it won't be shown again: {t("apiKeys.copyWarning")}
</p> </p>
<code className="text-xs text-amber-900 break-all select-all"> <code className="text-xs text-amber-900 break-all select-all">
{newKey} {newKey}
@@ -61,7 +62,7 @@ function ApiKeySection() {
onClick={() => setNewKey(null)} onClick={() => setNewKey(null)}
className="mt-2 block text-xs text-amber-700 hover:text-amber-900" className="mt-2 block text-xs text-amber-700 hover:text-amber-900"
> >
Dismiss {t("common:actions.dismiss")}
</button> </button>
</div> </div>
)} )}
@@ -69,7 +70,7 @@ function ApiKeySection() {
<form onSubmit={handleCreate} className="flex gap-2"> <form onSubmit={handleCreate} className="flex gap-2">
<input <input
type="text" type="text"
placeholder="Key name (e.g., claude-desktop)" placeholder={t("apiKeys.namePlaceholder")}
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
@@ -80,7 +81,7 @@ function ApiKeySection() {
disabled={createKey.isPending} disabled={createKey.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 disabled:opacity-50 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 disabled:opacity-50 rounded-lg transition-colors"
> >
Create {t("common:actions.create")}
</button> </button>
</form> </form>
@@ -102,7 +103,7 @@ function ApiKeySection() {
onClick={() => deleteKey.mutate(key.id)} onClick={() => deleteKey.mutate(key.id)}
className="text-xs text-red-500 hover:text-red-700" className="text-xs text-red-500 hover:text-red-700"
> >
Revoke {t("common:actions.revoke")}
</button> </button>
</div> </div>
))} ))}
@@ -113,6 +114,7 @@ function ApiKeySection() {
} }
function ImportExportSection() { function ImportExportSection() {
const { t } = useTranslation("settings");
const exportItems = useExportItems(); const exportItems = useExportItems();
const importItems = useImportItems(); const importItems = useImportItems();
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -142,9 +144,9 @@ function ImportExportSection() {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900">Import / Export</h3> <h3 className="text-sm font-medium text-gray-900">{t("importExport.title")}</h3>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Export your gear collection as a CSV file, or import items from a CSV. {t("importExport.description")}
</p> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@@ -153,11 +155,11 @@ function ImportExportSection() {
onClick={exportItems} onClick={exportItems}
className="px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors"
> >
Export CSV {t("importExport.export")}
</button> </button>
<label className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 hover:bg-gray-50 rounded-lg transition-colors cursor-pointer"> <label className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 hover:bg-gray-50 rounded-lg transition-colors cursor-pointer">
{importItems.isPending ? "Importing..." : "Import CSV"} {importItems.isPending ? t("importExport.importing") : t("importExport.import")}
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
@@ -179,12 +181,11 @@ function ImportExportSection() {
> >
{importResult.imported > 0 && ( {importResult.imported > 0 && (
<p className="font-medium"> <p className="font-medium">
{importResult.imported} item {t("importExport.imported", { count: importResult.imported })}
{importResult.imported !== 1 ? "s" : ""} imported.
</p> </p>
)} )}
{importResult.createdCategories.length > 0 && ( {importResult.createdCategories.length > 0 && (
<p>New categories: {importResult.createdCategories.join(", ")}</p> <p>{t("importExport.newCategories", { categories: importResult.createdCategories.join(", ") })}</p>
)} )}
{importResult.errors.map((err, i) => ( {importResult.errors.map((err, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: static error list // biome-ignore lint/suspicious/noArrayIndexKey: static error list
@@ -193,7 +194,7 @@ function ImportExportSection() {
</p> </p>
))} ))}
{importResult.imported === 0 && importResult.errors.length === 0 && ( {importResult.imported === 0 && importResult.errors.length === 0 && (
<p>No items found in the CSV.</p> <p>{t("importExport.noItemsFound")}</p>
)} )}
</div> </div>
)} )}
@@ -228,6 +229,7 @@ function getSuggestedCurrency(): Currency | null {
} }
function SettingsPage() { function SettingsPage() {
const { t } = useTranslation("settings");
const unit = useWeightUnit(); const unit = useWeightUnit();
const { currency, showConversions } = useCurrency(); const { currency, showConversions } = useCurrency();
const updateSetting = useUpdateSetting(); const updateSetting = useUpdateSetting();
@@ -245,9 +247,9 @@ function SettingsPage() {
to="/" to="/"
className="text-sm text-gray-500 hover:text-gray-700 mb-2 inline-block" className="text-sm text-gray-500 hover:text-gray-700 mb-2 inline-block"
> >
&larr; Back &larr; {t("common:actions.back")}
</Link> </Link>
<h1 className="text-xl font-semibold text-gray-900">Settings</h1> <h1 className="text-xl font-semibold text-gray-900">{t("title")}</h1>
</div> </div>
{showSuggestion && ( {showSuggestion && (
@@ -279,9 +281,9 @@ function SettingsPage() {
<div className="bg-white rounded-xl border border-gray-100 p-5 space-y-6"> <div className="bg-white rounded-xl border border-gray-100 p-5 space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-medium text-gray-900">Weight Unit</h3> <h3 className="text-sm font-medium text-gray-900">{t("weightUnit.title")}</h3>
<p className="text-xs text-gray-500 mt-0.5"> <p className="text-xs text-gray-500 mt-0.5">
Choose the unit used to display weights across the app {t("weightUnit.description")}
</p> </p>
</div> </div>
<div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5"> <div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">
@@ -312,10 +314,10 @@ function SettingsPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-medium text-gray-900"> <h3 className="text-sm font-medium text-gray-900">
Market & Currency {t("currency.title")}
</h3> </h3>
<p className="text-xs text-gray-500 mt-0.5"> <p className="text-xs text-gray-500 mt-0.5">
Sets your market region and currency for price display {t("currency.description")}
</p> </p>
</div> </div>
<div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5"> <div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">