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
This commit is contained in:
2026-04-18 13:44:26 +02:00
parent c5af1247c0
commit 6fd8874970
8 changed files with 158 additions and 84 deletions

View File

@@ -1,9 +1,11 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useCategories } from "../hooks/useCategories";
import { useCreateThread } from "../hooks/useThreads";
import { useUIStore } from "../stores/uiStore";
export function CreateThreadModal() {
const { t } = useTranslation(["threads", "common"]);
const isOpen = useUIStore((s) => s.createThreadModalOpen);
const closeModal = useUIStore((s) => s.closeCreateThreadModal);
@@ -38,11 +40,11 @@ export function CreateThreadModal() {
e.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setError("Thread name is required");
setError(t("create.nameRequired"));
return;
}
if (categoryId === null) {
setError("Please select a category");
setError(t("create.selectCategory"));
return;
}
setError(null);
@@ -55,7 +57,7 @@ export function CreateThreadModal() {
},
onError: (err) => {
setError(
err instanceof Error ? err.message : "Failed to create thread",
err instanceof Error ? err.message : t("create.createFailed"),
);
},
},
@@ -77,7 +79,7 @@ export function CreateThreadModal() {
onClick={(e) => e.stopPropagation()}
onKeyDown={() => {}}
>
<h2 className="text-lg font-semibold text-gray-900 mb-4">New Thread</h2>
<h2 className="text-lg font-semibold text-gray-900 mb-4">{t("create.title")}</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
@@ -85,14 +87,14 @@ export function CreateThreadModal() {
htmlFor="thread-name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Thread name
{t("create.threadName")}
</label>
<input
id="thread-name"
type="text"
value={name}
onChange={(e) => setName(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"
/>
</div>
@@ -102,7 +104,7 @@ export function CreateThreadModal() {
htmlFor="thread-category"
className="block text-sm font-medium text-gray-700 mb-1"
>
Category
{t("create.category")}
</label>
<select
id="thread-category"
@@ -126,14 +128,14 @@ export function CreateThreadModal() {
onClick={handleClose}
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("common:actions.cancel")}
</button>
<button
type="submit"
disabled={createThread.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"
>
{createThread.isPending ? "Creating..." : "Create Thread"}
{createThread.isPending ? t("common:actions.creating") : t("create.createThread")}
</button>
</div>
</form>