i18n(web): translate the vehicle catalog detail surface
Move every hard-coded Turkish string on /dashboard/vehicles/:id and the drill-down /categories/:categoryId page into a vehicle.* message block (tr + en) — page header, error/404 panels with retry, "Araç Bilgileri" accordion, "Yedek Parça Kategorileri" + the new empty state, equipment title and show-more/less toggle, summary labels — plus the leaf-category catalog-error panel and the drill-down's loading/retry copy. Also translate the CatalogSearch component (placeholder, arias, searching/treeIncomplete/noMatch hints, section headings, parts-count badge) under a new catalogSearch.* block, since it lives inside the same surface and the rest of the page already speaks two languages. PCAT_LABELS / EMEX_LABELS / VINAPI_FIELDS / LABEL_OVERRIDES are source-specific data tables, not UI chrome — left as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { CategoryTree } from "@/components/categories/category-tree";
|
||||
import { CategoryViewToggle } from "@/components/categories/category-view-toggle";
|
||||
import { useCategoryParts } from "@/hooks/use-parts";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||
import { cleanModelName } from "@/lib/vehicle";
|
||||
import { Button, Skeleton } from "@sase/ui";
|
||||
@@ -51,6 +52,7 @@ export const Route = createFileRoute("/dashboard/vehicles_/$id/categories_/$cate
|
||||
});
|
||||
|
||||
function VehicleCategoryPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id, categoryId } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading, error, refetch, isFetching } = useCategoryParts(id, categoryId);
|
||||
@@ -93,7 +95,7 @@ function VehicleCategoryPage() {
|
||||
const cleanModel = cleanModelName(vehicle?.model);
|
||||
const vehicleLabel = vehicle?.brandName
|
||||
? `${vehicle.brandName}${cleanModel ? ` ${cleanModel}` : ""}`
|
||||
: "Araç";
|
||||
: t("vehicle.title");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -111,8 +113,8 @@ function VehicleCategoryPage() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleBack}
|
||||
title="Geri dön"
|
||||
aria-label="Geri dön"
|
||||
title={t("vehicle.back")}
|
||||
aria-label={t("vehicle.back")}
|
||||
data-faro-user-action-name="category-back"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
@@ -123,7 +125,7 @@ function VehicleCategoryPage() {
|
||||
(isLoading ? (
|
||||
<span className="inline-block h-6 w-48 animate-pulse rounded-md bg-primary/10 align-middle" />
|
||||
) : (
|
||||
"Kategori Detayı"
|
||||
t("vehicle.categoryDetail")
|
||||
))}
|
||||
</h1>
|
||||
{data?.description && (
|
||||
@@ -141,9 +143,9 @@ function VehicleCategoryPage() {
|
||||
className="flex flex-col items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-5 text-sm"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-destructive">Kategori yüklenemedi</p>
|
||||
<p className="font-medium text-destructive">{t("vehicle.categoryNotLoadedTitle")}</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{error instanceof Error ? error.message : "Veriler yüklenirken bir hata oluştu."}
|
||||
{error instanceof Error ? error.message : t("vehicle.errorHint")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -154,7 +156,7 @@ function VehicleCategoryPage() {
|
||||
disabled={isFetching}
|
||||
data-faro-user-action-name="category-retry"
|
||||
>
|
||||
{isFetching ? "Yükleniyor…" : "Tekrar dene"}
|
||||
{isFetching ? t("common.loading") : t("vehicle.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -183,10 +185,8 @@ function VehicleCategoryPage() {
|
||||
className="flex flex-col items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-5 text-sm"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-destructive">Katalog şu an yüklenemedi</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Bu kategori tedarikçi katalogundan alınamadı. Lütfen birazdan tekrar deneyin.
|
||||
</p>
|
||||
<p className="font-medium text-destructive">{t("vehicle.catalogUnavailableTitle")}</p>
|
||||
<p className="mt-1 text-muted-foreground">{t("vehicle.catalogUnavailableHint")}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -196,7 +196,7 @@ function VehicleCategoryPage() {
|
||||
disabled={isFetching}
|
||||
data-faro-user-action-name="category-loaderror-retry"
|
||||
>
|
||||
{isFetching ? "Yükleniyor…" : "Tekrar dene"}
|
||||
{isFetching ? t("common.loading") : t("vehicle.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CategoryViewToggle } from "@/components/categories/category-view-toggle
|
||||
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||
import { ApiError, api } from "@/lib/api-client";
|
||||
import { setChatwootVehicle } from "@/lib/chatwoot";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||
import { cleanModelName } from "@/lib/vehicle";
|
||||
import type { CategoryNode, Vehicle } from "@sase/shared";
|
||||
@@ -33,6 +34,7 @@ export const Route = createFileRoute("/dashboard/vehicles_/$id/")({
|
||||
});
|
||||
|
||||
function VehicleDetailPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = Route.useParams();
|
||||
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
|
||||
() => getUserSettings().categoryViewMode ?? "grid",
|
||||
@@ -81,7 +83,7 @@ function VehicleDetailPage() {
|
||||
|
||||
const vehicleLabel: string = vehicle?.brandName
|
||||
? `${vehicle.brandName}${cleanModelName(vehicle.model) ? ` ${cleanModelName(vehicle.model)}` : ""}`
|
||||
: "Araç";
|
||||
: t("vehicle.title");
|
||||
|
||||
// Surface the viewed vehicle to the support chat widget (VIN/brand/model)
|
||||
// so agents have the car context for part-compatibility questions.
|
||||
@@ -136,13 +138,15 @@ function VehicleDetailPage() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleBack}
|
||||
title="Geri dön"
|
||||
aria-label="Geri dön"
|
||||
title={t("vehicle.back")}
|
||||
aria-label={t("vehicle.back")}
|
||||
data-faro-user-action-name="vehicle-back"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h2 className="text-xl font-bold">{is404 ? "Araç bulunamadı" : "Araç yüklenemedi"}</h2>
|
||||
<h2 className="text-xl font-bold">
|
||||
{is404 ? t("vehicle.notFoundTitle") : t("vehicle.errorTitle")}
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
role="alert"
|
||||
@@ -150,14 +154,14 @@ function VehicleDetailPage() {
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-destructive">
|
||||
{is404 ? "Bu araç kaydı bulunamadı" : "Araç bilgileri yüklenemedi"}
|
||||
{is404 ? t("vehicle.notFoundMsg") : t("vehicle.errorMsg")}
|
||||
</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{is404
|
||||
? "Bağlantı geçersiz olabilir ya da kayıt kaldırılmış olabilir."
|
||||
? t("vehicle.notFoundHint")
|
||||
: vehicleErrorObj instanceof Error
|
||||
? vehicleErrorObj.message
|
||||
: "Veriler yüklenirken bir hata oluştu."}
|
||||
: t("vehicle.errorHint")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -170,11 +174,11 @@ function VehicleDetailPage() {
|
||||
disabled={vehicleFetching}
|
||||
data-faro-user-action-name="vehicle-retry"
|
||||
>
|
||||
{vehicleFetching ? "Yükleniyor…" : "Tekrar dene"}
|
||||
{vehicleFetching ? t("common.loading") : t("vehicle.retry")}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" size="sm" variant="ghost" onClick={handleBack}>
|
||||
Geri dön
|
||||
{t("vehicle.back")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,8 +200,8 @@ function VehicleDetailPage() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleBack}
|
||||
title="Geri dön"
|
||||
aria-label="Geri dön"
|
||||
title={t("vehicle.back")}
|
||||
aria-label={t("vehicle.back")}
|
||||
className="shrink-0"
|
||||
data-faro-user-action-name="vehicle-back"
|
||||
>
|
||||
@@ -222,7 +226,7 @@ function VehicleDetailPage() {
|
||||
<AccordionItem value="info" className="border-b-0">
|
||||
<AccordionTrigger className="px-6 hover:no-underline">
|
||||
<div className="flex flex-1 flex-col items-start gap-1.5 pr-3 text-left">
|
||||
<span className="text-base font-semibold">Araç Bilgileri</span>
|
||||
<span className="text-base font-semibold">{t("vehicle.info")}</span>
|
||||
<VehicleSummary vehicle={vehicle} />
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
@@ -237,7 +241,7 @@ function VehicleDetailPage() {
|
||||
{/* Categories */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-base">Yedek Parça Kategorileri</CardTitle>
|
||||
<CardTitle className="text-base">{t("vehicle.categories")}</CardTitle>
|
||||
{!searchActive && <CategoryViewToggle value={viewMode} onChange={changeViewMode} />}
|
||||
</CardHeader>
|
||||
<CardContent
|
||||
@@ -260,11 +264,10 @@ function VehicleDetailPage() {
|
||||
) : !categoryTree || categoryTree.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-6 py-10 text-center">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Bu araç için kategori bulunamadı
|
||||
{t("vehicle.noCategoriesTitle")}
|
||||
</p>
|
||||
<p className="mx-auto mt-1 max-w-md text-xs text-muted-foreground">
|
||||
Araç decode edildi ama parça kataloğu henüz hazırlanmamış olabilir. Destek
|
||||
ekibimize bildirirseniz katalog hızlandırılır.
|
||||
{t("vehicle.noCategoriesHint")}
|
||||
</p>
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
@@ -495,12 +498,11 @@ function getDisplayAttributes(vehicle: Vehicle): Attr[] {
|
||||
}
|
||||
|
||||
function VehicleAttributes({ vehicle }: { vehicle: Vehicle }) {
|
||||
const { t } = useTranslation();
|
||||
const attrs = getDisplayAttributes(vehicle);
|
||||
|
||||
if (attrs.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">Bu araç için ayrıntı bilgisi bulunamadı.</p>
|
||||
);
|
||||
return <p className="text-sm text-muted-foreground">{t("vehicle.noAttrs")}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -536,10 +538,11 @@ function getEngineCode(vehicle: Vehicle): string | undefined {
|
||||
|
||||
/** Compact at-a-glance fields shown on the collapsed Araç Bilgileri header. */
|
||||
function VehicleSummary({ vehicle }: { vehicle: Vehicle }) {
|
||||
const { t } = useTranslation();
|
||||
const fields = [
|
||||
{ label: "Model", value: cleanModelName(vehicle.model) },
|
||||
{ label: "Model yılı", value: vehicle.year },
|
||||
{ label: "Motor kodu", value: getEngineCode(vehicle) },
|
||||
{ label: t("vehicle.labelModel"), value: cleanModelName(vehicle.model) },
|
||||
{ label: t("vehicle.labelYear"), value: vehicle.year },
|
||||
{ label: t("vehicle.labelEngineCode"), value: getEngineCode(vehicle) },
|
||||
].filter((f) => f.value != null && f.value !== "");
|
||||
|
||||
if (fields.length === 0) return null;
|
||||
@@ -585,6 +588,7 @@ function getEquipment(vehicle: Vehicle): Equipment[] {
|
||||
/** Full decoded equipment / PR-code list, shown inside the expanded card. */
|
||||
const EQUIPMENT_PREVIEW = 8;
|
||||
function VehicleEquipment({ vehicle }: { vehicle: Vehicle }) {
|
||||
const { t } = useTranslation();
|
||||
const items = getEquipment(vehicle);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
if (items.length === 0) return null;
|
||||
@@ -595,7 +599,7 @@ function VehicleEquipment({ vehicle }: { vehicle: Vehicle }) {
|
||||
return (
|
||||
<div className="mt-6 border-t border-border pt-4">
|
||||
<p className="mb-3 text-xs font-semibold text-muted-foreground">
|
||||
Donanım kodları ({items.length})
|
||||
{t("vehicle.equipmentTitle", { count: items.length })}
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-2">
|
||||
{visible.map((it, i) => (
|
||||
@@ -617,7 +621,7 @@ function VehicleEquipment({ vehicle }: { vehicle: Vehicle }) {
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="mt-3 h-8 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{expanded ? "Daha az göster" : `Tümünü göster (${items.length})`}
|
||||
{expanded ? t("vehicle.showLess") : t("vehicle.showAll", { count: items.length })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user