""" Nexpart Taxonomy — Universal parts classification used in Local catalog mode. Source of truth: /home/Autopartes/CapturasWeb/nexpart_hierarchy.txt Total: 14 Groups → 103 Subgroups → 558 Part Types This module loads the Nexpart hierarchy from the .txt file and provides helpers to: 1. List all groups / subgroups / part types 2. Map a TecDoc `parts.name_part` value to (group, subgroup, part_type) 3. Translate any node name to Spanish using the existing translations.py Business decisions (locked in by user 2026-04-08): 1. AMBIGUITY: first match wins (the order in nexpart_hierarchy.txt is Nexpart's own canonical order, so the first match is also Nexpart's primary classification). 2. UNMAPPED: drop. Parts without a clean Nexpart match do NOT appear in Local mode. Local mode is intentionally smaller and more consistent. 3. LANGUAGE: bilingual via translations.py — single source of truth. The hierarchy is stored in English; the UI translates each node on-the-fly using `translate_taxonomy_node()`. """ import os import re from typing import Optional # ============================================================================ # CONSTANTS # ============================================================================ UNMAPPED_STRATEGY = "drop" LANGUAGE_STRATEGY = "bilingual_taxonomy" # Path to the source-of-truth hierarchy text file _HIERARCHY_PATH = os.path.join( os.path.dirname(os.path.abspath(__file__)), "..", "..", "CapturasWeb", "nexpart_hierarchy.txt" ) # ============================================================================ # HIERARCHY PARSER # ============================================================================ # The list of valid groups, in canonical order (matches Nexpart's own order # from the screenshots). Used to disambiguate "is this line a group header?" # from "is this line a subgroup name?" — both can be capitalized. _KNOWN_GROUPS = ( "IGNITION & FILTERS", "BELTS, HOSES, WATER PUMPS & COOLING SYSTEM PARTS", "STARTING & CHARGING SYSTEM PARTS (ALTERNATORS, BATTERIES & CABLES)", "BRAKE SYSTEM, WHEEL BEARINGS, STUDS, NUTS & HARDWARE", "FUEL & EMISSIONS PARTS", "HEATING & AIR CONDITIONING", "ENGINE PARTS", "DRIVETRAIN PARTS", "STEERING & SUSPENSION PARTS", "EXHAUST, CLUTCH & FLYWHEEL PARTS", "WIPERS, LAMPS & FUSES", "BODY PARTS, CABLES, CAPS, ELECTRICAL MOTORS, SWITCHES & OTHER MISCELLANEOUS PARTS", "CHEMICALS, WAXES & LUBRICANTS", "TIRES, WHEELS, TOOLS & ACCESSORY PARTS", ) def _parse_hierarchy_file() -> dict: """Parse nexpart_hierarchy.txt into a nested dict. Returns: { "Ignition & Filters": { "Computers & Relays": ["Engine Control Module (ECM)", ...], ... }, ... } """ taxonomy = {} current_group = None current_subgroup = None if not os.path.exists(_HIERARCHY_PATH): return taxonomy with open(_HIERARCHY_PATH, "r", encoding="utf-8") as f: for line in f: line = line.rstrip("\n") # Skip comments, blank lines, and decoration rules if not line or line.startswith("#"): continue if set(line.strip()) <= {"═", " "}: continue if line.strip() == "SUMMARY": break # End-of-file marker # Group header: ALL CAPS line that matches a known group if line.strip().upper() in _KNOWN_GROUPS: # Convert to title case for display, preserving the original # casing from the .txt file (which already mixes Title Case) current_group = line.strip().title() \ .replace("Ac ", "AC ") \ .replace("Pcv", "PCV") \ .replace("Ecm", "ECM") \ .replace("Cv ", "CV ") \ .replace("Vvt", "VVT") \ .replace("Tpms", "TPMS") \ .replace("Hvac", "HVAC") \ .replace("Abs ", "ABS ") \ .replace("Egr", "EGR") taxonomy.setdefault(current_group, {}) current_subgroup = None continue # Part type: lines with leading " - " if line.lstrip().startswith("- "): if current_group and current_subgroup: pt = line.lstrip()[2:].strip() taxonomy[current_group][current_subgroup].append(pt) continue # Subgroup: a non-empty line that's not a comment, not a header, # not a part type, and starts with a non-space character. if line[0] not in (" ", "\t"): current_subgroup = line.strip() if current_group: taxonomy[current_group].setdefault(current_subgroup, []) return taxonomy # Load at import time NEXPART_TAXONOMY = _parse_hierarchy_file() # ============================================================================ # FLAT INDEX FOR FAST LOOKUP # ============================================================================ # Building these once at import time means O(1) lookups during requests. def _build_indexes(): """Build flat lookup tables from the nested taxonomy.""" # part_type_lower → list of (group, subgroup, original_part_type) # We use lowercase keys so the matcher is case-insensitive. part_type_index = {} all_part_types = [] # ordered list, in canonical Nexpart order for group, subgroups in NEXPART_TAXONOMY.items(): for subgroup, part_types in subgroups.items(): for pt in part_types: key = pt.strip().lower() part_type_index.setdefault(key, []).append((group, subgroup, pt)) all_part_types.append((group, subgroup, pt)) return part_type_index, all_part_types _PART_TYPE_INDEX, _ALL_PART_TYPES = _build_indexes() # ============================================================================ # DECISION 1 — RESOLVE AMBIGUITY (first-match wins) # ============================================================================ # Manual overrides for ambiguous part names. Key = lowercase TecDoc name # (as fed to the matcher). Value = the subgroup WHERE the part should # canonically live when a mechanic thinks about it. # # These beat the first-match rule. Add entries when you see that your users # expect a part in a different subgroup than the one Nexpart's canonical # order picks. Leave empty at start — grow incrementally from feedback. # # Example: a Mexican mechanic troubleshooting a failed emissions test will # look for an O2 sensor under "Catalytic Converter" (system-level thinking), # not "Emission Sensors, Relays, Solenoids & Switches" (component-level). AMBIGUITY_OVERRIDES = { # tecdoc name (lowercase) -> preferred subgroup name (exact string) # (populated as real usage surfaces mismatches) # 'oxygen sensor': 'Catalytic Converter', } def resolve_ambiguous_subgroup(tecdoc_name: str, candidates: list) -> tuple: """Pick the canonical (group, subgroup, part_type) for an ambiguous name. Resolution order: 1. AMBIGUITY_OVERRIDES dict — manual curation wins over everything. 2. First-match in canonical Nexpart order (Decision 1 locked in). Search by the user still finds the part from anywhere via the flat index; the override only affects which subgroup the part "lives in" during hierarchical navigation. Args: tecdoc_name: e.g. "Oxygen Sensor" candidates: list of (group, subgroup, part_type) tuples Returns: A single (group, subgroup, part_type) tuple. """ # 1. Manual override wins key = (tecdoc_name or '').strip().lower() preferred_subgroup = AMBIGUITY_OVERRIDES.get(key) if preferred_subgroup: for cand in candidates: if cand[1] == preferred_subgroup: return cand # Override pointed to a subgroup not in the candidate set — # log and fall through to first-match. # (Using print to stay import-free; swap for logger if available.) print(f"[taxonomy] AMBIGUITY_OVERRIDES['{key}'] = '{preferred_subgroup}' " f"not in candidates {[c[1] for c in candidates]}; falling back") # 2. First-match in canonical order return candidates[0] # ============================================================================ # DECISION 2 — UNMAPPED HANDLING (drop) # ============================================================================ # When a TecDoc name doesn't match any Nexpart Part Type, the matcher # returns None and the caller filters it out of Local mode results. # ============================================================================ # CORE MATCHER: tecdoc_to_nexpart() # ============================================================================ def tecdoc_to_nexpart(tecdoc_name: str) -> Optional[tuple]: """Map a TecDoc part name to its Nexpart (group, subgroup, part_type). Matching strategy (in order of preference): 1. Exact match (case-insensitive) on the full Part Type name. 2. Substring match — TecDoc name CONTAINS a known Part Type. Example: "Front Brake Pad Set" contains "Brake Pad Set" → match. 3. Reverse substring — known Part Type contains the TecDoc name. Example: TecDoc "Wiper" matches Nexpart "Wiper Arm". Less precise, used as last resort. Args: tecdoc_name: value from `parts.name_part` (English) Returns: (group, subgroup, part_type) if matched, None otherwise. Per Decision 2, callers should filter out None values. """ if not tecdoc_name: return None name_lower = tecdoc_name.strip().lower() if not name_lower: return None # 1. Exact match if name_lower in _PART_TYPE_INDEX: candidates = _PART_TYPE_INDEX[name_lower] return resolve_ambiguous_subgroup(tecdoc_name, candidates) # 2. Substring match (TecDoc contains Nexpart Part Type) # Prefer the LONGEST match — more specific wins on a tie of position. best_match = None best_len = 0 for pt_key, candidates in _PART_TYPE_INDEX.items(): if pt_key in name_lower and len(pt_key) > best_len: best_match = candidates best_len = len(pt_key) if best_match: return resolve_ambiguous_subgroup(tecdoc_name, best_match) # 3. Reverse substring (Nexpart Part Type contains TecDoc) — last resort for pt_key, candidates in _PART_TYPE_INDEX.items(): if name_lower in pt_key and len(name_lower) >= 4: # Min length 4 to avoid false matches on short words like "Cap" return resolve_ambiguous_subgroup(tecdoc_name, candidates) return None # ============================================================================ # DECISION 3 — BILINGUAL VIA translations.py # ============================================================================ # Curated translations for the 14 top-level groups + common subgroups. # These are full-string (not substring) so they always win over the partial # matcher in translations.py and produce clean Spanish display. TAXONOMY_OVERRIDES_ES = { # ─── Top-level groups (14) ─── "Ignition & Filters": "Encendido y Filtros", "Belts, Hoses, Water Pumps & Cooling System Parts": "Bandas, Mangueras, Bombas de Agua y Sistema de Enfriamiento", "Starting & Charging System Parts (Alternators, Batteries & Cables)": "Sistema de Arranque y Carga (Alternadores, Baterías y Cables)", "Brake System, Wheel Bearings, Studs, Nuts & Hardware": "Sistema de Frenos, Baleros, Birlos, Tuercas y Ferretería", "Fuel & Emissions Parts": "Combustible y Emisiones", "Heating & Air Conditioning": "Calefacción y Aire Acondicionado", "Engine Parts": "Partes de Motor", "Drivetrain Parts": "Tren Motriz", "Steering & Suspension Parts": "Dirección y Suspensión", "Exhaust, Clutch & Flywheel Parts": "Escape, Clutch y Volante", "Wipers, Lamps & Fuses": "Limpiaparabrisas, Luces y Fusibles", "Body Parts, Cables, Caps, Electrical Motors, Switches & Other Miscellaneous Parts": "Carrocería, Cables, Tapones, Motores Eléctricos, Switches y Misceláneos", "Chemicals, Waxes & Lubricants": "Químicos, Ceras y Lubricantes", "Tires, Wheels, Tools & Accessory Parts": "Llantas, Rines, Herramientas y Accesorios", # ─── Common subgroups (the most-used ones; expand as needed) ─── "Filters & PCV": "Filtros y PCV", "Spark Plugs & Glow Plugs": "Bujías", "Tune-Up & Ignition Parts": "Afinación y Encendido", "Belts, Tensioners & Pulleys": "Bandas, Tensores y Poleas", "Radiators & Electric Fan Motors": "Radiadores y Motoventiladores", "Thermostats, Housings & Radiator Caps": "Termostatos, Carcasas y Tapones de Radiador", "Water Pumps, Fan Blades & Clutches": "Bombas de Agua, Aspas y Fan Clutches", "Alternators & Voltage Regulators": "Alternadores y Reguladores de Voltaje", "Batteries": "Baterías", "Starters": "Marchas / Arrancadores", "ABS Controls & Parts": "Controles y Partes de ABS", "Front Friction, Drums & Rotors": "Frenos Delanteros: Pastillas, Tambores y Discos", "Rear Friction, Drums & Rotors": "Frenos Traseros: Pastillas, Tambores y Discos", "Front Wheel Bearings & Seals": "Baleros y Sellos de Rueda Delantera", "Rear Wheel Bearings & Seals": "Baleros y Sellos de Rueda Trasera", "Master Cylinders, Boosters & Switches": "Cilindros Maestros, Boosters y Switches", "Fuel Pumps & Tanks": "Bombas y Tanques de Gasolina", "Fuel Injection Parts, Mass Air Flow Sensors": "Inyección, Sensores MAF", "Turbochargers & Superchargers": "Turbos y Compresores", "AC Compressors, Kits & Parts": "Compresores de A/C y Kits", "AC Condensers & Evaporators": "Condensadores y Evaporadores de A/C", "Cams, Lifters & Timing Parts": "Árboles de Levas, Buzos y Distribución", "Crankshafts & Bearings": "Cigüeñales y Metales", "Pistons, Rings & Rods": "Pistones, Anillos y Bielas", "Heads & Manifolds": "Cabezas y Múltiples", "Engine Mounts & Other Miscellaneous Engine Parts": "Soportes de Motor y Otros", "Driveshafts, U-Joints & CV (Constant Velocity) Parts": "Flechas, Crucetas y Juntas Homocinéticas", "Automatic Transmission Seals": "Sellos de Transmisión Automática", "Manual Transmission Seals": "Sellos de Transmisión Manual", "Transmission & Parts": "Transmisión y Partes", "Ball Joints & Control Arms": "Rótulas y Horquillas", "Shock Absorbers & Struts": "Amortiguadores y Strut", "Steering Linkages, Rods & Arms": "Direcciones, Bieletas y Brazos", "Sway Bars, Stabilizer Bars, Strut Rods & Parts": "Barras Estabilizadoras y Tornillos", "All Exhaust & Diagrams": "Sistema de Escape Completo", "Catalytic Converter": "Convertidor Catalítico", "Clutches & Clutch Kits": "Clutches y Kits", "Manifolds & Headers": "Múltiples y Headers", "Arms, Blades & Refills": "Brazos, Plumas y Repuestos", "Headlamps & Flashers": "Faros y Direccionales", "Exterior Lamps": "Luces Exteriores", "Interior Lamps": "Luces Interiores", "Wiper Motors & Washer Pumps": "Motores de Limpia y Bombas de Agua", "Bumpers & License Plates": "Defensas y Placas", "Door, Window & Tailgate Parts": "Puertas, Ventanas y Cajuela", "Engine & Transmission Lubricants & Additives": "Aceites de Motor y Transmisión", "Tires & Wheels": "Llantas y Rines", "Tools, Jacks, Hardware & Manuals": "Herramientas, Gatos y Hardware", # ─── Remaining subgroups (phase 2 translation coverage) ─── "Computers & Relays": "Computadoras y Relés", "Ignition Wires": "Cables de Bujía", "Miscellaneous Ignition Parts": "Conectores y Misceláneos de Encendido", "Engine Coolant & Bypass Hoses": "Mangueras de Refrigerante y Bypass", "Heater & Other Hoses": "Mangueras de Calefacción y Otras", "Sensors, Switches & Relays": "Sensores, Switches y Relés", "Starter Solenoids, Switches & Relays": "Solenoides de Marcha, Switches y Relés", "Brake Cables, Studs, Nuts & Spindle Nuts": "Cables, Birlos y Tuercas de Freno", "Front Brake Hardware & ABS Sensors": "Ferretería y Sensores ABS Delanteros", "Front Calipers, Wheel Cylinders, Hoses": "Calipers, Cilindros y Mangueras Delanteras", "Miscellaneous Disc Hardware": "Ferretería Misceláneo de Disco", "Miscellaneous Drum Hardware": "Ferretería Misceláneo de Tambor", "Miscellaneous Hydraulic Parts & Brake Specifications": "Hidráulica y Especificaciones de Freno", "Rear Brake Hardware & ABS Sensors": "Ferretería y Sensores ABS Traseros", "Rear Calipers, Wheel Cylinders, Hoses": "Calipers, Cilindros y Mangueras Traseras", "Carburetors, Carburetor Kits & Components": "Carburadores, Kits y Componentes", "EGR & Emissions Valves": "EGR y Válvulas de Emisiones", "Emission Sensors, Relays, Solenoids & Switches": "Sensores de Emisiones, Relés, Solenoides y Switches", "Fuel Injection Harnesses, Connectors & Miscellaneous Parts": "Arneses, Conectores e Inyección Misceláneos", "Fuel Injection Sensors, Relays & Switches": "Sensores, Relés y Switches de Inyección", "AC Accumulators, Receiver Driers & Valves": "Acumuladores, Secadores y Válvulas de A/C", "AC Hose Assemblies & Fittings": "Mangueras y Conexiones de A/C", "AC Relays & Switches": "Relés y Switches de A/C", "AC, Heating & Ventilation Gaskets, O-Rings, Kits, Doors & Actuators": "Juntas, O-Rings, Puertas y Actuadores A/C", "Blower Motors & Parts": "Motores de Ventilador y Partes", "Heater Cores & Heater Control Valves": "Radiadores de Calefacción y Válvulas", "Engine Block Parts": "Partes de Bloque de Motor", "Engines & Kits": "Motores y Kits", "Gasket Sets": "Juegos de Juntas", "Individual Gaskets & Seals": "Juntas y Sellos Individuales", "Intake & Exhaust Valves": "Válvulas de Admisión y Escape", "Rockers & Push Rods": "Balancines y Varillas de Empuje", "Vacuum & Oil Pumps": "Bombas de Vacío y Aceite", "Axle & Differential Parts": "Partes de Eje y Diferencial", "Electronics, Sensors, Relays & Miscellaneous Parts": "Electrónica, Sensores y Misceláneos", "Manual Transmission Bearings": "Baleros de Transmisión Manual", "Spindles & Hubs": "Husillos y Mazas", "Transmission Kits & Gaskets": "Kits y Juntas de Transmisión", "Alignment Kits & Tools": "Kits y Herramientas de Alineación", "King Pins, Trailing Arms, Alignment & Other Chassis": "Pivotes, Brazos y Otros de Chasis", "Power Steering Pumps, Hoses & Kits": "Bombas, Mangueras y Kits de Dirección Hidráulica", "Rack & Pinion, Gear Box, Power Cylinder": "Cremallera, Caja de Dirección y Cilindro", "Clutch Hydraulics": "Hidráulica de Clutch", "Individual Exhaust Parts": "Partes de Escape Individuales", "Miscellaneous Clutch Parts": "Partes Misceláneas de Clutch", "Lighting Modules & Switches": "Módulos y Switches de Iluminación", "Lighting Relays & Sensors": "Relés y Sensores de Luces", "Caps": "Tapones", "Cruise Control Parts": "Partes de Control de Crucero", "Electrical Motors": "Motores Eléctricos", "Glass": "Cristales", "Hood & Tailgate Parts": "Partes de Cofre y Cajuela", "Hoods Fenders & Body Parts": "Cofres, Salpicaderas y Carrocería", "Lift Supports": "Amortiguadores de Cofre/Cajuela", "Switches, Relays & Miscellaneous Parts": "Switches, Relés y Misceláneos", "Wheel & Hardware": "Rines y Ferretería", "Bumper & License Plate": "Defensas y Placas", "Electronics Audio/Visual & Mirrors": "Electrónica, Audio y Espejos", "Hood, Fender & Body Parts": "Cofre, Salpicaderas y Carrocería", "Interior & Steering Wheel": "Interior y Volante", # ─── High-value part types (most-searched in real use) ─── # Ignition & Filters "Engine Control Module (ECM)": "Módulo de Control del Motor (ECM)", "Ignition Relay": "Relé de Encendido", "Transmission Control Module": "Módulo de Control de Transmisión", "Engine Air Filter": "Filtro de Aire del Motor", "Engine Oil Filter": "Filtro de Aceite del Motor", "Engine Oil Filter Adapter": "Adaptador de Filtro de Aceite", "Engine Oil Filter Housing": "Carcasa de Filtro de Aceite", "Vapor Canister": "Canister de Vapor", "Vapor Canister Purge Valve": "Válvula de Purga del Canister", "Vapor Canister Purge Solenoid": "Solenoide de Purga del Canister", "Spark Plug Set": "Juego de Bujías", "Direct Ignition Coil": "Bobina de Encendido Directo", "Ignition Coil": "Bobina de Encendido", "Ignition Kit": "Kit de Encendido", # Belts / Cooling "Engine Timing Belt": "Banda de Distribución", "Engine Timing Belt Component Kit": "Kit de Componentes de Distribución", "Engine Timing Belt Kit with Water Pump": "Kit de Distribución con Bomba de Agua", "Engine Timing Chain": "Cadena de Distribución", "Engine Timing Chain Guide": "Guía de Cadena de Distribución", "Engine Timing Chain Tensioner": "Tensor de Cadena de Distribución", "Accessory Drive Belt Tensioner Assembly": "Tensor de Banda Accesoria", "Accessory Drive Belt Tensioner Pulley": "Polea Tensora de Banda Accesoria", "Serpentine Belt": "Banda Serpentina", "Radiator": "Radiador", "Radiator Coolant Hose": "Manguera de Refrigerante del Radiador", "Engine Coolant Reservoir": "Depósito de Refrigerante", "Engine Water Pump": "Bomba de Agua del Motor", "Engine Water Pump Gasket": "Junta de Bomba de Agua", "Engine Water Pump Pulley": "Polea de Bomba de Agua", "Engine Coolant Thermostat": "Termostato de Refrigerante", "Engine Coolant Thermostat Housing": "Carcasa de Termostato", "Engine Coolant Temperature Sensor": "Sensor de Temperatura de Refrigerante", "Engine Cooling Fan": "Ventilador de Enfriamiento", "Engine Cooling Fan Assembly": "Conjunto de Ventilador de Enfriamiento", "HVAC Heater Hose": "Manguera de Calefacción HVAC", # Starting & Charging "Alternator": "Alternador", "Vehicle Battery": "Batería del Vehículo", "Starter": "Marcha / Arrancador", "Ignition Lock Cylinder": "Switch de Encendido (Cilindro)", "Ignition Switch": "Switch de Encendido", # Brake System "ABS Wheel Speed Sensor": "Sensor de Velocidad de Rueda ABS", "Front Disc Brake Pad Set": "Juego de Pastillas Delanteras", "Rear Disc Brake Pad Set": "Juego de Pastillas Traseras", "Front Disc Brake Rotor": "Disco de Freno Delantero", "Rear Disc Brake Rotor": "Disco de Freno Trasero", "Front Disc Brake Caliper": "Caliper de Freno Delantero", "Rear Disc Brake Caliper": "Caliper de Freno Trasero", "Front Brake Hydraulic Hose": "Manguera Hidráulica Delantera", "Rear Brake Hydraulic Hose": "Manguera Hidráulica Trasera", "Brake Master Cylinder": "Cilindro Maestro de Frenos", "Power Brake Booster": "Booster de Frenos", "Front Wheel Bearing": "Balero de Rueda Delantera", "Rear Wheel Bearing": "Balero de Rueda Trasera", "Front Wheel Bearing and Hub Assembly": "Balero y Maza Delantera", "Rear Wheel Bearing and Hub Assembly": "Balero y Maza Trasera", "Wheel Lug Nut": "Tuerca de Rueda (Birlo)", "Wheel Lug Stud": "Birlo de Rueda", # Fuel & Emissions "Electric Fuel Pump": "Bomba Eléctrica de Gasolina", "Fuel Pump Module Assembly": "Conjunto de Módulo de Bomba de Gasolina", "Fuel Level Sensor": "Sensor de Nivel de Gasolina", "Fuel Tank Cap": "Tapón de Tanque de Gasolina", "Fuel Injector": "Inyector de Gasolina", "Fuel Injector Set": "Juego de Inyectores", "Fuel Injection Throttle Body": "Cuerpo de Aceleración", "Mass Air Flow Sensor": "Sensor MAF (Flujo de Aire)", "Oxygen Sensor": "Sensor de Oxígeno", "Engine Camshaft Position Sensor": "Sensor de Posición de Árbol de Levas", "Engine Crankshaft Position Sensor": "Sensor de Posición del Cigüeñal", "Engine Knock Sensor": "Sensor de Detonación", "Manifold Absolute Pressure Sensor": "Sensor MAP (Presión Absoluta)", "Turbocharger": "Turbocargador", # Heating & AC "A/C Compressor": "Compresor de A/C", "A/C Condenser": "Condensador de A/C", "A/C Evaporator Core": "Evaporador de A/C", "A/C Expansion Valve": "Válvula de Expansión de A/C", "A/C Receiver Drier/Desiccant Element": "Filtro Deshidratador de A/C", "A/C Hose Assembly": "Manguera de A/C", "HVAC Blower Motor": "Motor de Ventilador HVAC", "HVAC Blower Motor Resistor": "Resistencia de Ventilador HVAC", "HVAC Heater Core": "Radiador de Calefacción", "HVAC Blend Door Actuator": "Actuador de Puerta de Mezcla", # Engine Parts "Engine Camshaft": "Árbol de Levas", "Engine Harmonic Balancer": "Damper / Polea del Cigüeñal", "Engine Crankshaft Main Bearing Set": "Juego de Metales de Bancada", "Engine Piston": "Pistón", "Engine Piston Ring Set": "Juego de Anillos de Pistón", "Engine Connecting Rod Bearing Set": "Juego de Metales de Biela", "Engine Cylinder Head Gasket": "Junta de Cabeza de Cilindros", "Engine Cylinder Head Bolt Set": "Juego de Tornillos de Cabeza", "Engine Intake Manifold": "Múltiple de Admisión", "Engine Intake Manifold Gasket": "Junta de Múltiple de Admisión", "Engine Valve Cover": "Tapa de Válvulas", "Engine Valve Cover Gasket": "Junta de Tapa de Válvulas", "Engine Oil Pan": "Cárter de Aceite", "Engine Oil Pan Gasket": "Junta de Cárter", "Engine Oil Pump": "Bomba de Aceite", "Engine Oil Pressure Sender": "Sensor de Presión de Aceite", "Engine Oil Pressure Switch": "Switch de Presión de Aceite", "Engine Mount": "Soporte de Motor", "Engine Rocker Arm": "Balancín", "Engine Exhaust Valve": "Válvula de Escape", "Engine Intake Valve": "Válvula de Admisión", "Engine Valve Spring": "Resorte de Válvula", "Engine Valve Stem Oil Seal": "Sello de Válvula", # Drivetrain "CV Axle Assembly": "Flecha Homocinética Completa", "CV Axle Shaft": "Flecha Homocinética", "Automatic Transmission Mount": "Soporte de Transmisión Automática", "Automatic Transmission Oil Cooler": "Enfriador de Aceite de Transmisión", "Automatic Transmission Oil Pan": "Cárter de Transmisión Automática", "Manual Transmission Mount": "Soporte de Transmisión Manual", "Transmission Filter Kit": "Kit de Filtro de Transmisión", "Transmission Oil Pan": "Cárter de Transmisión", "Spindle Nut": "Tuerca de Husillo", "Vehicle Speed Sensor": "Sensor de Velocidad del Vehículo", # Steering & Suspension "Suspension Ball Joint": "Rótula de Suspensión", "Suspension Control Arm Bushing": "Buje de Horquilla", "Suspension Control Arm and Ball Joint Assembly": "Horquilla con Rótula", "Suspension Shock Absorber": "Amortiguador", "Suspension Strut": "Strut de Suspensión", "Suspension Strut Assembly": "Conjunto de Strut", "Suspension Strut Mount": "Base de Strut", "Suspension Stabilizer Bar Link": "Terminal de Barra Estabilizadora", "Steering Tie Rod End": "Terminal de Dirección", "Rack and Pinion Assembly": "Cremallera de Dirección", "Steering Column": "Columna de Dirección", # Exhaust/Clutch "Catalytic Converter": "Convertidor Catalítico", "Catalytic Converter Gasket": "Junta de Convertidor Catalítico", "Exhaust Manifold": "Múltiple de Escape", "Exhaust Manifold Gasket": "Junta de Múltiple de Escape", "Exhaust Muffler": "Mofle", "Exhaust Muffler Assembly": "Conjunto de Mofle", "Exhaust Pipe": "Tubo de Escape", "Exhaust Clamp": "Abrazadera de Escape", "Clutch Slave Cylinder": "Cilindro Esclavo de Clutch", "Transmission Clutch Kit": "Kit de Clutch", # Wipers/Lamps "Wiper Arm": "Brazo de Limpiaparabrisas", "Wiper Blade": "Pluma Limpiaparabrisas", "Wiper Motor": "Motor de Limpiaparabrisas", "Wiper Switch": "Switch de Limpiaparabrisas", "Headlight Bulb": "Foco de Faro", "Tail Light Bulb": "Foco de Calavera", "Brake Light Bulb": "Foco de Freno", "Turn Signal Light Bulb": "Foco Direccional", "Fog Light Bulb": "Foco Antiniebla", "Back Up Light Bulb": "Foco de Reversa", "License Plate Light Bulb": "Foco de Placa", "Dome Light Bulb": "Foco de Domo", "Washer Fluid Reservoir Cap": "Tapón de Depósito de Limpiaparabrisas", "Headlight Switch": "Switch de Luces", "Turn Signal Switch": "Switch de Direccionales", "Multi-Function Switch": "Switch Multifunciones", "Hazard Warning Switch": "Switch de Intermitentes", # Body / Electrical / Misc "Door Lock Actuator": "Actuador de Cerradura", "Door Lock Actuator Motor": "Motor de Actuador de Cerradura", "Window Motor": "Motor de Ventana", "Window Regulator": "Elevador de Ventana", "Window Motor and Regulator Assembly": "Motor y Elevador de Ventana", "Sunroof Motor": "Motor de Quemacocos", "Exterior Door Handle": "Manija Exterior de Puerta", "Interior Door Handle": "Manija Interior de Puerta", "Door Mirror Glass": "Cristal de Espejo", "Horn Relay": "Relé de Claxon", "Liftgate Lift Support": "Amortiguador de Cajuela", "Cruise Control Switch": "Switch de Control de Crucero", "Engine Coolant Reservoir Cap": "Tapón de Depósito de Refrigerante", "Engine Oil Filler Cap": "Tapón de Llenado de Aceite", "Radiator Cap": "Tapón de Radiador", "TPMS Sensor": "Sensor TPMS", "TPMS Programmable Sensor": "Sensor TPMS Programable", # Chemicals / Tools "Automatic Transmission Fluid": "Aceite de Transmisión Automática", "Engine Oil": "Aceite de Motor", } def translate_taxonomy_node(english_name: str) -> str: """Translate a Nexpart group / subgroup / part type to Spanish. STRICT lookup only — no partial substitution. The order: 1. TAXONOMY_OVERRIDES_ES — full-string curated translations. 2. PART_TRANSLATIONS exact match (from services.translations). 3. Fallback: return the English original UNCHANGED. Why strict-only: partial substitution within a compound name produces ugly hybrids ("Front Tambor de Freno", "Engine Filtro de Aceite"). For taxonomy display we'd rather show clean English than dirty Spanish. Untranslated entries are visible reminders to extend the override dict. Args: english_name: the canonical English name (group, subgroup, or part type) Returns: Spanish display string, or the English original if no exact match. """ if not english_name: return english_name # 1. Curated overrides (highest priority) if english_name in TAXONOMY_OVERRIDES_ES: return TAXONOMY_OVERRIDES_ES[english_name] # 2. Exact match in PART_TRANSLATIONS try: from services.translations import PART_TRANSLATIONS if english_name in PART_TRANSLATIONS: return PART_TRANSLATIONS[english_name] except ImportError: pass # 3. Fallback — return English unchanged return english_name def list_untranslated_nodes() -> dict: """Diagnostic helper: list every taxonomy node missing a Spanish entry. Useful for filling in TAXONOMY_OVERRIDES_ES incrementally — run this in a one-off script to see exactly what still needs translation. Returns: {"groups": [...], "subgroups": [...], "part_types": [...]} """ try: from services.translations import PART_TRANSLATIONS known = set(PART_TRANSLATIONS.keys()) | set(TAXONOMY_OVERRIDES_ES.keys()) except ImportError: known = set(TAXONOMY_OVERRIDES_ES.keys()) missing = {"groups": [], "subgroups": [], "part_types": []} for group, subgroups in NEXPART_TAXONOMY.items(): if group not in known: missing["groups"].append(group) for subgroup, part_types in subgroups.items(): if subgroup not in known: missing["subgroups"].append(subgroup) for pt in part_types: if pt not in known: missing["part_types"].append(pt) return missing # ============================================================================ # PUBLIC API — used by catalog_service / blueprints # ============================================================================ def get_groups() -> list: """Return the 14 top-level groups in canonical order. Each item: {"name": english, "name_es": spanish, "subgroup_count": int} """ return [ { "name": group, "name_es": translate_taxonomy_node(group), "subgroup_count": len(subgroups), } for group, subgroups in NEXPART_TAXONOMY.items() ] def get_subgroups(group_name: str) -> list: """Return all subgroups for a given group. Each item: {"name": english, "name_es": spanish, "part_type_count": int} """ subgroups = NEXPART_TAXONOMY.get(group_name, {}) return [ { "name": subgroup, "name_es": translate_taxonomy_node(subgroup), "part_type_count": len(part_types), } for subgroup, part_types in subgroups.items() ] def get_part_types(group_name: str, subgroup_name: str) -> list: """Return all part types within a group + subgroup. Each item: {"name": english, "name_es": spanish} """ subgroups = NEXPART_TAXONOMY.get(group_name, {}) part_types = subgroups.get(subgroup_name, []) return [ { "name": pt, "name_es": translate_taxonomy_node(pt), } for pt in part_types ] def stats() -> dict: """Return totals — useful for healthcheck and debugging.""" total_subgroups = sum(len(sg) for sg in NEXPART_TAXONOMY.values()) total_part_types = sum( len(pts) for sg in NEXPART_TAXONOMY.values() for pts in sg.values() ) return { "groups": len(NEXPART_TAXONOMY), "subgroups": total_subgroups, "part_types": total_part_types, "indexed_keys": len(_PART_TYPE_INDEX), }