i18n(web): translate the search page
Move every hard-coded Turkish string on /dashboard/search into a search.* message block (tr + en) and resolve them via t() — title, subtitle, input label/placeholder, counter, example button, submit, the error banner with the subscription nudge, the report-VIN flow + toasts, preview loading + identified badge + hint, recent-searches header / see-all / open hint / row aria-label, validation/generic/toast errors, auto-correction toast, decoding skeleton sr-only text, and the candidate selection failure toast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -551,5 +551,36 @@
|
||||
},
|
||||
"categories": {
|
||||
"root": "Root"
|
||||
},
|
||||
"search": {
|
||||
"title": "VIN Search",
|
||||
"subtitle": "Enter the vehicle's VIN to access the parts catalog",
|
||||
"inputLabel": "VIN",
|
||||
"inputPlaceholder": "Enter VIN (17 characters)",
|
||||
"counter": "{count}/17 characters",
|
||||
"tryExample": "Try an example VIN →",
|
||||
"submit": "Decode VIN",
|
||||
"errorTitle": "Couldn't decode VIN",
|
||||
"errorSubscriptionPrefix": "You have no active subscription. To access vehicle data,",
|
||||
"subscribeLink": "subscribe",
|
||||
"reportVin": "VIN is correct — send to administrator",
|
||||
"reportSent": "Report sent. We'll review it shortly.",
|
||||
"reportToastTitle": "Report sent",
|
||||
"reportToastDesc": "The VIN was forwarded to the administrator.",
|
||||
"reportToastError": "Report failed",
|
||||
"previewLoading": "Fetching vehicle info...",
|
||||
"vehicleIdentified": "Vehicle identified",
|
||||
"previewHint": "Click Decode VIN to access the full parts catalog.",
|
||||
"recent": "Recent searches",
|
||||
"seeAll": "See all →",
|
||||
"openHint": "Click to reopen the vehicle with its parts catalog",
|
||||
"historyAria": "{brand} {model} — VIN {vin}, open from history",
|
||||
"errorInvalidVin": "Invalid VIN. Must be 17 characters; I, O, Q are not allowed.",
|
||||
"errorGeneric": "An error occurred. Please try again.",
|
||||
"errorToast": "VIN search failed",
|
||||
"decodingSkeleton": "Loading vehicle info…",
|
||||
"pasteInvalidChars": "VIN cannot contain I, O, Q",
|
||||
"candidateSelectFailed": "Vehicle selection failed",
|
||||
"autoCorrected": "Auto-corrected: {list}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,5 +551,36 @@
|
||||
},
|
||||
"categories": {
|
||||
"root": "Kök"
|
||||
},
|
||||
"search": {
|
||||
"title": "Şase Arama",
|
||||
"subtitle": "Araç şase numarasını girerek yedek parça kataloğuna erişin",
|
||||
"inputLabel": "Şase numarası",
|
||||
"inputPlaceholder": "Şase numarasını girin (17 karakter)",
|
||||
"counter": "{count}/17 karakter",
|
||||
"tryExample": "Örnek şase deneyin →",
|
||||
"submit": "Şase Çöz",
|
||||
"errorTitle": "Şase çözümlenemedi",
|
||||
"errorSubscriptionPrefix": "Aktif aboneliğiniz yok. Araç verilerine erişmek için",
|
||||
"subscribeLink": "abone olun",
|
||||
"reportVin": "Şase no doğru, sistem yöneticisine gönder",
|
||||
"reportSent": "Bildirim gönderildi. En kısa sürede incelenecektir.",
|
||||
"reportToastTitle": "Bildirim gönderildi",
|
||||
"reportToastDesc": "Şase numarası sistem yöneticisine iletildi.",
|
||||
"reportToastError": "Bildirim gönderilemedi",
|
||||
"previewLoading": "Araç bilgileri alınıyor...",
|
||||
"vehicleIdentified": "Araç tanımlandı",
|
||||
"previewHint": "Şase Çöz butonuna tıklayarak tam parça kataloğuna erişin.",
|
||||
"recent": "Son Aramalar",
|
||||
"seeAll": "Tümünü Gör →",
|
||||
"openHint": "Aracı parça kataloğuyla birlikte yeniden açmak için tıklayın",
|
||||
"historyAria": "{brand} {model} — şase {vin}, geçmişten aç",
|
||||
"errorInvalidVin": "Geçersiz şase. 17 karakter olmalı, I, O, Q harfleri kullanılamaz.",
|
||||
"errorGeneric": "Bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
"errorToast": "Şase arama başarısız",
|
||||
"decodingSkeleton": "Araç bilgileri yükleniyor…",
|
||||
"pasteInvalidChars": "Şase numarasında I, O, Q harfleri kullanılamaz",
|
||||
"candidateSelectFailed": "Araç seçimi başarısız",
|
||||
"autoCorrected": "Otomatik düzeltildi: {list}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { VinBrandIcon } from "@/components/ui/vin-brand-icon";
|
||||
import { VehicleSelectModal } from "@/components/vehicles/vehicle-select-modal";
|
||||
import { ApiError, api } from "@/lib/api-client";
|
||||
import { startAction } from "@/lib/faro";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { KEYS_6, KEYS_17 } from "@/lib/keys";
|
||||
import { capture } from "@/lib/posthog";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -63,9 +64,10 @@ function ShimmerBox({ className }: { className?: string }) {
|
||||
* a vehicle-page-shaped skeleton so the wait feels like the page is loading
|
||||
* rather than an unresponsive button. */
|
||||
function DecodingSkeleton() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-6" aria-busy="true" aria-live="polite">
|
||||
<span className="sr-only">Araç bilgileri yükleniyor…</span>
|
||||
<span className="sr-only">{t("search.decodingSkeleton")}</span>
|
||||
{/* Header: back + brand logo + 2-line title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<ShimmerBox className="h-9 w-9 rounded-md" />
|
||||
@@ -108,6 +110,7 @@ export const Route = createFileRoute("/dashboard/search")({
|
||||
});
|
||||
|
||||
function SearchPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
vin: vinParam,
|
||||
@@ -286,8 +289,7 @@ function SearchPage() {
|
||||
navigate({ to: "/dashboard/vehicles/$id", params: { id: data.id } });
|
||||
} catch (err) {
|
||||
const responseTimeMs = Math.round(performance.now() - decodeStart);
|
||||
const message =
|
||||
err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin.";
|
||||
const message = err instanceof ApiError ? err.message : t("search.errorGeneric");
|
||||
capture("vin_decode_error", {
|
||||
vin: cleanVin,
|
||||
error: message,
|
||||
@@ -297,7 +299,7 @@ function SearchPage() {
|
||||
attempt,
|
||||
});
|
||||
setError(message);
|
||||
toast.error("Şase arama başarısız");
|
||||
toast.error(t("search.errorToast"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -319,7 +321,7 @@ function SearchPage() {
|
||||
error_type: cleanVin.length !== 17 ? "invalid_length" : "invalid_chars",
|
||||
input_length: cleanVin.length,
|
||||
});
|
||||
setError("Geçersiz şase. 17 karakter olmalı, I, O, Q harfleri kullanılamaz.");
|
||||
setError(t("search.errorInvalidVin"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -334,7 +336,7 @@ function SearchPage() {
|
||||
querySourceRef.current = "landing";
|
||||
capture("vin_decoded", { vin: cleanVin, query_source: "landing" });
|
||||
if (!isValidVin(cleanVin)) {
|
||||
setError("Geçersiz şase. 17 karakter olmalı, I, O, Q harfleri kullanılamaz.");
|
||||
setError(t("search.errorInvalidVin"));
|
||||
return;
|
||||
}
|
||||
lastAttemptedVinRef.current = cleanVin;
|
||||
@@ -380,11 +382,11 @@ function SearchPage() {
|
||||
try {
|
||||
await api.post("/vehicles/report-vin", { vin: vin.toUpperCase().trim() });
|
||||
setReportSent(true);
|
||||
toast.success("Bildirim gönderildi", {
|
||||
description: "Şase numarası sistem yöneticisine iletildi.",
|
||||
toast.success(t("search.reportToastTitle"), {
|
||||
description: t("search.reportToastDesc"),
|
||||
});
|
||||
} catch {
|
||||
toast.error("Bildirim gönderilemedi");
|
||||
toast.error(t("search.reportToastError"));
|
||||
} finally {
|
||||
setReportSending(false);
|
||||
}
|
||||
@@ -402,11 +404,11 @@ function SearchPage() {
|
||||
if (correctionsRef.current.size === 0) return;
|
||||
const list = [...correctionsRef.current];
|
||||
correctionsRef.current = new Set();
|
||||
toast.info(`Otomatik düzeltildi: ${list.join(", ")}`, {
|
||||
description: "Şase numarasında I, O, Q harfleri kullanılamaz",
|
||||
toast.info(t("search.autoCorrected", { list: list.join(", ") }), {
|
||||
description: t("search.pasteInvalidChars"),
|
||||
duration: 2500,
|
||||
});
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -465,12 +467,11 @@ function SearchPage() {
|
||||
params: { id: data.id },
|
||||
});
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin.";
|
||||
const message = err instanceof ApiError ? err.message : t("search.errorGeneric");
|
||||
setError(message);
|
||||
setCandidates(null);
|
||||
setCandidateSource(null);
|
||||
toast.error("Araç seçimi başarısız");
|
||||
toast.error(t("search.candidateSelectFailed"));
|
||||
} finally {
|
||||
setSelectLoading(false);
|
||||
}
|
||||
@@ -508,11 +509,9 @@ function SearchPage() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h2 className="font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight">
|
||||
Şase Arama
|
||||
{t("search.title")}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Araç şase numarasını girerek yedek parça kataloğuna erişin
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t("search.subtitle")}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -521,14 +520,14 @@ function SearchPage() {
|
||||
<form onSubmit={handleSearch} className="space-y-4">
|
||||
{/* Input */}
|
||||
<Label htmlFor="vin" className="sr-only">
|
||||
Şase numarası
|
||||
{t("search.inputLabel")}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<VinBrandIcon vin={vin} className="absolute left-4 top-1/2 -translate-y-1/2" />
|
||||
<Input
|
||||
id="vin"
|
||||
ref={inputRef}
|
||||
placeholder="Şase numarasını girin (17 karakter)"
|
||||
placeholder={t("search.inputPlaceholder")}
|
||||
value={vin}
|
||||
onChange={(e) => handleVinChange(e.target.value)}
|
||||
onFocus={() => {
|
||||
@@ -580,14 +579,16 @@ function SearchPage() {
|
||||
|
||||
{/* Counter + Example VIN */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="tabular-nums text-muted-foreground">{vin.length}/17 karakter</span>
|
||||
<span className="tabular-nums text-muted-foreground">
|
||||
{t("search.counter", { count: vin.length })}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fillExampleVin}
|
||||
data-faro-user-action-name="fill-example-vin"
|
||||
className="text-muted-foreground underline underline-offset-4 transition hover:text-foreground"
|
||||
>
|
||||
Örnek şase deneyin →
|
||||
{t("search.tryExample")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -602,7 +603,7 @@ function SearchPage() {
|
||||
) : (
|
||||
<Search className="mr-2 size-4" />
|
||||
)}
|
||||
Şase Çöz
|
||||
{t("search.submit")}
|
||||
</Button>
|
||||
|
||||
{/* Error banner — the main submit button above doubles as the retry,
|
||||
@@ -616,16 +617,16 @@ function SearchPage() {
|
||||
>
|
||||
<AlertCircle className="mt-0.5 size-5 shrink-0 text-destructive" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-destructive">Şase çözümlenemedi</p>
|
||||
<p className="font-medium text-destructive">{t("search.errorTitle")}</p>
|
||||
<p className="mt-1 text-sm text-destructive/90">
|
||||
{error.includes("abone olun") ? (
|
||||
<>
|
||||
Aktif aboneliğiniz yok. Araç verilerine erişmek için{" "}
|
||||
{t("search.errorSubscriptionPrefix")}{" "}
|
||||
<Link
|
||||
to="/dashboard/subscription"
|
||||
className="inline-flex items-center font-semibold underline underline-offset-4 transition hover:text-destructive/80"
|
||||
>
|
||||
abone olun
|
||||
{t("search.subscribeLink")}
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
@@ -651,13 +652,11 @@ function SearchPage() {
|
||||
) : (
|
||||
<Send className="mr-2 size-4" />
|
||||
)}
|
||||
Şase no doğru, sistem yöneticisine gönder
|
||||
{t("search.reportVin")}
|
||||
</Button>
|
||||
)}
|
||||
{reportSent && (
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Bildirim gönderildi. En kısa sürede incelenecektir.
|
||||
</p>
|
||||
<p className="text-center text-sm text-muted-foreground">{t("search.reportSent")}</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
@@ -666,7 +665,7 @@ function SearchPage() {
|
||||
{previewLoading && (
|
||||
<div className="flex items-center justify-center gap-3 rounded-2xl border border-border bg-background p-6">
|
||||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Araç bilgileri alınıyor...</span>
|
||||
<span className="text-sm text-muted-foreground">{t("search.previewLoading")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -689,15 +688,13 @@ function SearchPage() {
|
||||
variant="default"
|
||||
className="bg-brand text-xs text-brand-foreground hover:bg-brand/90"
|
||||
>
|
||||
Araç tanımlandı
|
||||
{t("search.vehicleIdentified")}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4 bg-border" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Şase Çöz butonuna tıklayarak tam parça kataloğuna erişin.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{t("search.previewHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -708,14 +705,14 @@ function SearchPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="size-4 text-muted-foreground" />
|
||||
<h3 className="font-[family-name:var(--font-display)] text-lg font-bold tracking-tight">
|
||||
Son Aramalar
|
||||
{t("search.recent")}
|
||||
</h3>
|
||||
</div>
|
||||
<Link
|
||||
to="/dashboard/history"
|
||||
className="text-sm text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
Tümünü Gör →
|
||||
{t("search.seeAll")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -724,7 +721,11 @@ function SearchPage() {
|
||||
<button
|
||||
key={v.id}
|
||||
type="button"
|
||||
aria-label={`${v.brandName} ${v.model ?? ""} — şase ${v.vin}, geçmişten aç`}
|
||||
aria-label={t("search.historyAria", {
|
||||
brand: v.brandName,
|
||||
model: v.model ?? "",
|
||||
vin: v.vin,
|
||||
})}
|
||||
onClick={() => {
|
||||
querySourceRef.current = "history";
|
||||
const ageDays = v.lastAccessedAt
|
||||
@@ -757,9 +758,7 @@ function SearchPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
Aracı parça kataloğuyla birlikte yeniden açmak için tıklayın
|
||||
</p>
|
||||
<p className="text-center text-xs text-muted-foreground">{t("search.openHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user