From d14bcb7dcae1238d1354e75908c64c124e55c8b8 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 19 May 2026 10:45:12 +0300 Subject: [PATCH 1/2] feat(pcat): translate part descriptions + vehicle attrs (body/engine/transmission) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PCAT was missing parity with EMEX in two places: - Part `description` (notice) was stored raw English alongside translated `name`. Now batched into translateMany so users see Turkish notices. - Vehicle body/engine/transmission attrs from VIN decode were raw upstream values. Wire up the existing emex.mapper dictionaries on the PCAT single-car, PCAT resolveById, and EMEX single-vehicle result paths. - emex.mapper translateToTurkish now falls back to the original term on dictionary miss instead of null — upstream values are heterogeneous (engine codes, multi-word descriptors); losing them was worse than leaving them untranslated. Co-Authored-By: Claude Opus 4.7 --- apps/api/src/categories/categories.service.ts | 9 +++--- apps/api/src/integrations/emex/emex.mapper.ts | 6 +++- apps/api/src/vehicles/vehicles.service.ts | 31 +++++++++++++------ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/apps/api/src/categories/categories.service.ts b/apps/api/src/categories/categories.service.ts index b386fc5..ed06b60 100644 --- a/apps/api/src/categories/categories.service.ts +++ b/apps/api/src/categories/categories.service.ts @@ -705,9 +705,10 @@ export class CategoriesService { } } - const trMap = await this.translationsService.translateMany( - rawParts.map((p) => p.name).filter(Boolean), - ); + const trMap = await this.translationsService.translateMany([ + ...rawParts.map((p) => p.name).filter(Boolean), + ...rawParts.map((p) => p.notice).filter((n): n is string => !!n), + ]); const allParts: Array = rawParts.map((p) => ({ vehicleId: vehicle.id, @@ -715,7 +716,7 @@ export class CategoriesService { oemCode: p.number, name: trMap.get(p.name) ?? p.name ?? "Unknown", nameOriginal: p.name || null, - description: p.notice, + description: p.notice ? (trMap.get(p.notice) ?? p.notice) : null, quantity: null, position: p.positionNumber, hotspotIndex: p.positionNumber diff --git a/apps/api/src/integrations/emex/emex.mapper.ts b/apps/api/src/integrations/emex/emex.mapper.ts index edefaa3..b85890d 100644 --- a/apps/api/src/integrations/emex/emex.mapper.ts +++ b/apps/api/src/integrations/emex/emex.mapper.ts @@ -91,7 +91,11 @@ function translateToTurkish( ): string | null { if (!term) return null; const normalized = term.toLowerCase().trim(); - return dictionary[normalized] || null; + // Dictionary miss → return original term unchanged. Upstream values are + // often heterogeneous (engine codes, multi-word descriptors) that the + // single-word dictionary can't cover; losing them would be worse than + // leaving them untranslated. + return dictionary[normalized] ?? term; } export function translateBodyType(bodyType: string | null): string | null { diff --git a/apps/api/src/vehicles/vehicles.service.ts b/apps/api/src/vehicles/vehicles.service.ts index 92c7b0e..c27c126 100644 --- a/apps/api/src/vehicles/vehicles.service.ts +++ b/apps/api/src/vehicles/vehicles.service.ts @@ -21,6 +21,11 @@ import { vehicles, } from "../database/schema/core"; import { CorgiService } from "../integrations/corgi/corgi.service"; +import { + translateBodyType, + translateEngineType, + translateTransmission, +} from "../integrations/emex/emex.mapper"; import { EmexService } from "../integrations/emex/emex.service"; import { EmexCandidate } from "../integrations/emex/emex.service"; import { PartsCatalogsService } from "../integrations/parts-catalogs/parts-catalogs.service"; @@ -581,9 +586,11 @@ export class VehiclesService { brandName, model: car.name || null, year: this.extractYearFromPcatCar(car) || null, - engine: this.extractParamFromPcatCar(car, "engine") || null, - transmission: this.extractParamFromPcatCar(car, "transmission") || null, - bodyType: this.extractParamFromPcatCar(car, "body") || null, + engine: translateEngineType(this.extractParamFromPcatCar(car, "engine") || null), + transmission: translateTransmission( + this.extractParamFromPcatCar(car, "transmission") || null, + ), + bodyType: translateBodyType(this.extractParamFromPcatCar(car, "body") || null), rawData: { source: "parts-catalogs", catalogId: car.catalogId, @@ -611,9 +618,13 @@ export class VehiclesService { brandName, model: emexSingleVehicle.model || null, year: emexSingleVehicle.year || null, - engine: emexSingleVehicle.engineCode || emexSingleVehicle.engineType || null, - transmission: emexSingleVehicle.transmission || null, - bodyType: emexSingleVehicle.bodyType || null, + // engineCode is alphanumeric (e.g. "N20B20") — leave raw; only the + // engineType fallback ("Petrol"/"Diesel") goes through the dict. + engine: + emexSingleVehicle.engineCode || + translateEngineType(emexSingleVehicle.engineType || null), + transmission: translateTransmission(emexSingleVehicle.transmission || null), + bodyType: translateBodyType(emexSingleVehicle.bodyType || null), rawData: emexSingleVehicle.raw || null, source: "emex", corgiKnown, @@ -738,9 +749,11 @@ export class VehiclesService { brandName, model: car.name || null, year: this.extractYearFromPcatCar(car) || null, - engine: this.extractParamFromPcatCar(car, "engine") || null, - transmission: this.extractParamFromPcatCar(car, "transmission") || null, - bodyType: this.extractParamFromPcatCar(car, "body") || null, + engine: translateEngineType(this.extractParamFromPcatCar(car, "engine") || null), + transmission: translateTransmission( + this.extractParamFromPcatCar(car, "transmission") || null, + ), + bodyType: translateBodyType(this.extractParamFromPcatCar(car, "body") || null), rawData: { source: "parts-catalogs", catalogId: car.catalogId, From 39ad0d4c2a21dc5ee793f042fc6cd3e267900067 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 19 May 2026 11:05:11 +0300 Subject: [PATCH 2/2] fix(translation): backfill parts.description after async LLM translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker was updating parts.name but not parts.description, so PCAT part notices stayed raw English forever once cached. Match on raw value still being present — once translated, the row no longer matches and we stop touching it. Co-Authored-By: Claude Opus 4.7 --- apps/api/src/jobs/processors/translation.processor.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/api/src/jobs/processors/translation.processor.ts b/apps/api/src/jobs/processors/translation.processor.ts index 5f28362..cc9f4fe 100644 --- a/apps/api/src/jobs/processors/translation.processor.ts +++ b/apps/api/src/jobs/processors/translation.processor.ts @@ -127,6 +127,15 @@ export async function processTranslation( AND name_original = ${orig} AND name = name_original `); + // PCAT also stores the part `notice` field as `description`. We don't + // track a `description_original`, so match on the raw English value + // still being present — once translated, the row won't re-match. + await db.execute(drizzleSql` + UPDATE parts + SET description = ${tr} + WHERE source = 'parts-catalogs' + AND description = ${orig} + `); } // Invalidate translation lookup cache so next read sees DB value.