From 6fd8874970ae457314b1d21e904aac5a980bdf60 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 18 Apr 2026 13:44:26 +0200 Subject: [PATCH] feat(34-02): extract hardcoded strings from thread/candidate components - CandidateCard: replace all hardcoded titles and badge text with t() - CandidateListItem: add useTranslation, replace winner/delete/open labels and +/- Notes badge - CandidateForm: add useTranslation, replace all form labels, placeholders, validation errors, submit button - ComparisonTable: move STATUS_LABELS inside component with t(), replace all ATTRIBUTE_ROWS labels, View button, impact row labels - StatusBadge: refactor STATUS_CONFIG to STATUS_ICONS + runtime STATUS_LABELS via t() - CreateThreadModal: replace title, thread name label, category label, placeholder, cancel/submit buttons, error messages - AddToThreadModal: replace modal titles, labels, placeholders, back/cancel/submit buttons, error messages - threads.json: extend candidateForm with category, notes, pros, cons, product link labels and all placeholders --- src/client/components/AddToThreadModal.tsx | 28 +++++----- src/client/components/CandidateCard.tsx | 12 +++-- src/client/components/CandidateForm.tsx | 44 ++++++++-------- src/client/components/CandidateListItem.tsx | 12 +++-- src/client/components/ComparisonTable.tsx | 39 +++++++------- src/client/components/CreateThreadModal.tsx | 20 +++---- src/client/components/StatusBadge.tsx | 29 ++++++----- src/client/locales/en/threads.json | 58 +++++++++++++++++++++ 8 files changed, 158 insertions(+), 84 deletions(-) diff --git a/src/client/components/AddToThreadModal.tsx b/src/client/components/AddToThreadModal.tsx index 67e696a..c107b2a 100644 --- a/src/client/components/AddToThreadModal.tsx +++ b/src/client/components/AddToThreadModal.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useCategories } from "../hooks/useCategories"; import { useGlobalItem } from "../hooks/useGlobalItems"; @@ -8,6 +9,7 @@ import { apiPost } from "../lib/api"; import { useUIStore } from "../stores/uiStore"; export function AddToThreadModal() { + const { t } = useTranslation(["threads", "common"]); const { open, globalItemId, globalItemName } = useUIStore( (s) => s.addToThreadModal, ); @@ -114,7 +116,7 @@ export function AddToThreadModal() { toast.success(`Added to "${thread?.name ?? "thread"}"`); closeAddToThread(); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to add candidate"); + setError(err instanceof Error ? err.message : t("addToThread.failedToAdd")); } finally { setIsSubmitting(false); } @@ -142,7 +144,7 @@ export function AddToThreadModal() { toast.success(`Created "${trimmedName}" with first candidate`); closeAddToThread(); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to create thread"); + setError(err instanceof Error ? err.message : t("addToThread.failedToCreate")); } finally { setIsSubmitting(false); } @@ -173,7 +175,7 @@ export function AddToThreadModal() { onKeyDown={() => {}} >

- {mode === "pick" ? "Add to Thread" : "New Thread + Candidate"} + {mode === "pick" ? t("addToThread.title") : t("addToThread.newThreadTitle")}

{globalItemName}

@@ -184,7 +186,7 @@ export function AddToThreadModal() { htmlFor="thread-select" className="block text-sm font-medium text-gray-700 mb-1" > - Thread + {t("addToThread.thread")} ) : ( @@ -207,14 +209,14 @@ export function AddToThreadModal() { htmlFor="new-thread-name" className="block text-sm font-medium text-gray-700 mb-1" > - Thread name + {t("addToThread.threadName")} setNewThreadName(e.target.value)} - placeholder="e.g. Lightweight sleeping bag" + placeholder={t("create.namePlaceholder")} 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" /> @@ -224,7 +226,7 @@ export function AddToThreadModal() { htmlFor="new-thread-category" className="block text-sm font-medium text-gray-700 mb-1" > - Category + {t("create.category")} setForm((f) => ({ ...f, name: 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. Osprey Talon 22" + placeholder={t("candidateForm.namePlaceholder")} /> {errors.name && (

{errors.name}

@@ -181,7 +183,7 @@ export function CandidateForm({ htmlFor="candidate-weight" className="block text-sm font-medium text-gray-700 mb-1" > - Weight (g) + {t("candidateForm.weightLabel")} ({ ...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("candidateForm.weightPlaceholder")} /> {errors.weightGrams && (

{errors.weightGrams}

@@ -218,7 +220,7 @@ export function CandidateForm({ setForm((f) => ({ ...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("candidateForm.pricePlaceholder")} /> {errors.priceDollars && (

{errors.priceDollars}

@@ -228,7 +230,7 @@ export function CandidateForm({ {/* Category */}
- Notes + {t("candidateForm.notesLabel")}