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:
2026-05-31 21:42:48 +03:00
parent 73fe3e33c1
commit b9dd154bfd
5 changed files with 136 additions and 50 deletions

View File

@@ -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>
) : (

View File

@@ -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>