TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1/**2 * Shared helpers for the European / Nordic / APAC auction-house connectors (group g8-auctions-eu-apac).3 * Multilingual date + money parsing (fr/de/it/es/nl/sv/da/fi/en), bundle detection in those languages,4 * cautious year extraction for continental title conventions ("1900-tal", "20. Jh.", "1950er").5 * Kept inside connectors/api (not the framework). Pure functions, fixture-testable.6 */7import { parsePrice, SUPPORTED_CURRENCIES, type CurrencyCode } from '@rareindex/shared';89/** Month names / abbreviations → 0-based month. Covers en, fr, de, it, es, nl, sv, da, fi (+ common abbreviations). */10const MONTHS: Record<string, number> = {11 // en12 jan: 0, january: 0, feb: 1, february: 1, mar: 2, march: 2, apr: 3, april: 3, may: 4, jun: 5, june: 5, jul: 6, july: 6, aug: 7, august: 7, sep: 8, sept: 8, september: 8, oct: 9, october: 9, nov: 10, november: 10, dec: 11, december: 11,13 // fr14 janvier: 0, janv: 0, février: 1, fevrier: 1, févr: 1, fevr: 1, mars: 2, avril: 3, avr: 3, mai: 4, juin: 5, juillet: 6, juil: 6, août: 7, aout: 7, septembre: 8, octobre: 9, novembre: 10, décembre: 11, decembre: 11, déc: 11,15 // de16 januar: 0, jänner: 0, februar: 1, märz: 2, maerz: 2, mrz: 2, juni: 5, juli: 6, oktober: 9, okt: 9, dezember: 11, dez: 11,17 // it18 gennaio: 0, gen: 0, febbraio: 1, marzo: 2, aprile: 3, maggio: 4, mag: 4, giugno: 5, giu: 5, luglio: 6, lug: 6, agosto: 7, ago: 7, settembre: 8, set: 8, ottobre: 9, ott: 9, dicembre: 11, dic: 11,19 // es20 enero: 0, ene: 0, febrero: 1, abril: 3, abr: 3, mayo: 4, junio: 5, julio: 6, septiembre: 8, setiembre: 8, octubre: 9, noviembre: 10, diciembre: 11,21 // nl22 januari: 0, februari: 1, maart: 2, mrt: 2, mei: 4, augustus: 7, // (juni/juli/september/oktober/november/december shared)23 // sv / da / no24 mars_sv: 2, // placeholder never matched; sv "mars" already mapped via fr25 maj: 4, // sv/da26 // fi27 tammikuu: 0, tammikuuta: 0, helmikuu: 1, helmikuuta: 1, maaliskuu: 2, maaliskuuta: 2, huhtikuu: 3, huhtikuuta: 3, toukokuu: 4, toukokuuta: 4, kesäkuu: 5, kesäkuuta: 5, heinäkuu: 6, heinäkuuta: 6, elokuu: 7, elokuuta: 7, syyskuu: 8, syyskuuta: 8, lokakuu: 9, lokakuuta: 9, marraskuu: 10, marraskuuta: 10, joulukuu: 11, joulukuuta: 11,28};2930function monthIndex(word: string): number | null {31 const w = word.toLowerCase().replace(/\.$/, '');32 if (w in MONTHS && w !== 'mars_sv') return MONTHS[w]!;33 // truncated forms ("sept", "déc", "okt")34 const hit = Object.keys(MONTHS).find((k) => k.length >= 3 && w.length >= 3 && k.startsWith(w) && k !== 'mars_sv');35 return hit !== undefined ? MONTHS[hit]! : null;36}3738/**39 * Parse a date written in any of the supported languages. Handles:40 * "12 juin 2026" · "12. Juni 2026" · "12 giugno 2026" · "12 de junio de 2026" · "12 juni 2026" · "den 12 juni 2026"41 * "June 12, 2026" · "12/06/2026" · "12.06.2026" · "2026-06-12" · "12 juin 2026 14:00" (time ignored → UTC midnight)42 * "22–23 juin 2026" / "22-23. Juni 2026" (range → first day).43 * Returns null instead of guessing; day-first for numeric forms.44 */45export function parseEuDate(raw: string | null | undefined): Date | null {46 if (!raw) return null;47 const s = raw.replace(/[ ]/g, ' ').replace(/\s+/g, ' ').trim();48 if (!s) return null;49 const iso = s.match(/\b(\d{4})-(\d{2})-(\d{2})(?!\d)/);50 if (iso) return utc(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]));51 // day [–day] [de] month [de] year (fr/de/it/es/nl/sv/da/fi/en)52 const dmy = s.match(/\b(\d{1,2})(?:\s?[-–/]\s?\d{1,2})?\.?(?:er|e|º|°|th|st|nd|rd)?\s+(?:de\s+|den\s+)?([A-Za-zÀ-ÿ]{3,12})\.?\s+(?:de\s+)?(\d{4})\b/);53 if (dmy) {54 const mo = monthIndex(dmy[2]!);55 if (mo !== null) return utc(Number(dmy[3]), mo, Number(dmy[1]));56 }57 // month day, year (en)58 const mdy = s.match(/\b([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})\b/);59 if (mdy) {60 const mo = monthIndex(mdy[1]!);61 if (mo !== null) return utc(Number(mdy[3]), mo, Number(mdy[2]));62 }63 // numeric day-first: 12/06/2026, 12.06.2026, 12-06-202664 const num = s.match(/\b(\d{1,2})[./-](\d{1,2})[./-](\d{4})\b/);65 if (num) {66 const d = Number(num[1]);67 const m = Number(num[2]);68 if (m >= 1 && m <= 12 && d >= 1 && d <= 31) return utc(Number(num[3]), m - 1, d);69 }70 // month year only ("juin 2026") → first of month (month precision)71 const my = s.match(/\b([A-Za-zÀ-ÿ]{3,12})\.?\s+(\d{4})\b/);72 if (my) {73 const mo = monthIndex(my[1]!);74 if (mo !== null) return utc(Number(my[2]), mo, 1);75 }76 return null;77}7879function utc(y: number, m: number, d: number): Date | null {80 const dt = new Date(Date.UTC(y, m, d));81 return Number.isNaN(dt.getTime()) || dt.getUTCMonth() !== m ? null : dt;82}8384/** Unix seconds (Auctionet `ends_at`) → Date. */85export function fromUnix(sec: number | null | undefined): Date | null {86 if (!sec || !Number.isFinite(sec)) return null;87 const d = new Date(sec * 1000);88 return Number.isNaN(d.getTime()) ? null : d;89}9091export type NumberLocale = 'eu' | 'en' | 'ch';9293/**94 * Parse a money string written with continental conventions. `locale` disambiguates the trailing-3-digit95 * case: "1.250" → 1250 under 'eu' (dot = thousands) but 1.25 under 'en'; "1'250" (Swiss) → 1250.96 * Currency: explicit symbol/code wins ("€", "CHF", "SEK", "kr", "£", "HK$", "¥"), else `fallback`.97 * Returns null for no number / non-positive amounts. Never invents a currency.98 */99export function parseEuMoney(text: string | null | undefined, fallback: CurrencyCode | null, locale: NumberLocale = 'eu'): { amount: number; currency: CurrencyCode; confidence: number } | null {100 if (!text) return null;101 let s = text.replace(/[ ]/g, ' ').replace(/\s+/g, ' ').trim();102 if (!s) return null;103 // Swiss apostrophe thousands and "Fr." prefix104 if (locale === 'ch') s = s.replace(/(\d)'(\d{3})/g, '$1$2').replace(/\bFr\.\s?/, 'CHF ');105 // Danish/Swedish/Norwegian "kr" ambiguity: keep as fallback unless explicit code present106 const explicit = detectCurrency(s);107 const currency = explicit ?? fallback;108 if (!currency) return null;109 // Normalise EU thousands groups written with spaces or dots when a comma decimal follows or when 3 trailing digits110 let m = s.match(/-?\d[\d .,']*\d|\d/);111 if (!m) return null;112 let n = m[0].replace(/[ ']/g, '');113 let confidence = 0.95;114 if (locale !== 'en') {115 if (/,\d{1,2}$/.test(n)) n = n.replace(/\./g, '').replace(',', '.');116 else if (/^\d{1,3}(\.\d{3})+$/.test(n)) n = n.replace(/\./g, '');117 else if (/^\d{1,3}(,\d{3})+$/.test(n)) n = n.replace(/,/g, ''); // some EU sites still print en groups118 else if (/\.\d{3}$/.test(n) && !/,/.test(n)) {119 n = n.replace(/\./g, '');120 confidence = 0.85;121 } else if (/,\d{3}$/.test(n)) {122 n = n.replace(/,/g, '');123 confidence = 0.85;124 } else n = n.replace(',', '.');125 } else {126 const p = parsePrice(m[0], currency);127 if (!p || p.amount <= 0) return null;128 return { amount: p.amount, currency, confidence: p.confidence };129 }130 const amount = Number.parseFloat(n);131 if (!Number.isFinite(amount) || amount <= 0) return null;132 return { amount, currency, confidence };133}134135const CURRENCY_TOKENS: Array<[RegExp, CurrencyCode]> = [136 [/\bHK\s?\$|\bHKD\b/i, 'HKD'],137 [/\bA\s?\$|\bAU\s?\$|\bAUD\b/i, 'AUD'],138 [/\bNZ\s?\$|\bNZD\b/i, 'NZD'],139 [/\bUS\s?\$|\bUSD\b/i, 'USD'],140 [/\bCA\s?\$|\bCAD\b/i, 'CAD'],141 [/\bS\s?\$|\bSGD\b/i, 'SGD'],142 [/€|\bEUR\b|\beuros?\b/i, 'EUR'],143 [/£|\bGBP\b/i, 'GBP'],144 [/¥|¥|\bJPY\b|円/i, 'JPY'],145 [/\bCHF\b|\bSFr\.?/i, 'CHF'],146 [/\bSEK\b|\bskr\b/i, 'SEK'],147 [/\bDKK\b|\bdkr\b/i, 'DKK'],148 [/\bNOK\b|\bnkr\b/i, 'NOK'],149 [/\bPLN\b|zł/i, 'PLN'],150 [/\bCZK\b|Kč/i, 'CZK'],151 [/\bCNY\b|\bRMB\b/i, 'CNY'],152 [/\bTWD\b|\bNT\$/i, 'TWD'],153];154155/** Explicit currency in a string (code or unambiguous symbol); null when absent or ambiguous ("kr", "$"). */156export function detectCurrency(text: string): CurrencyCode | null {157 for (const [re, code] of CURRENCY_TOKENS) if (re.test(text)) return code;158 return null;159}160161export function isSupportedCurrency(code: string | null | undefined): code is CurrencyCode {162 return !!code && (SUPPORTED_CURRENCIES as readonly string[]).includes(code);163}164165/**166 * Bundle / multi-item detection across the group's languages. Pairs ("ett par", "Paar", "paire", "coppia",167 * "a pair") are NOT bundles (a pair of candlesticks is one collectible unit); three or more distinct pieces,168 * "collection/lot of", "Konvolut", "samling", "parti", "lotto di", "lote de", "partij" are.169 */170export function isBundleMultilingual(title: string): boolean {171 const t = title;172 if (/\b(collection of|group of|lot of|assorted|quantity of|mixed lot|various|job lot)\b/i.test(t)) return true;173 if (/\b(konvolut|sammlung|posten|nachlass|\d+\s*-?\s*teilig|\d+\s*st(?:ück|k)\.?)\b/i.test(t)) return true; // de174 if (/\b(samling|parti|diverse|blandat|\d+\s*st\b|\d+\s*delar|\d+\s*stk)\b/i.test(t)) return true; // sv/da/no175 if (/\b(lot de|ensemble de|réunion de|reunion de|collection de|suite de|lot comprenant)\b/i.test(t)) return true; // fr176 if (/\b(lotto di|gruppo di|insieme di|collezione di)\b/i.test(t)) return true; // it177 if (/\b(lote de|conjunto de|colecci[oó]n de)\b/i.test(t)) return true; // es178 if (/\b(partij|collectie van|set van \d+|diverse)\b/i.test(t)) return true; // nl179 if (/\b(erä|kokoelma)\b/i.test(t)) return true; // fi180 const count = parenCount(t);181 return count !== null && count >= 3;182}183184/** Trailing "(4)" / "(12)" piece count used by Nordic/German houses; null when absent. */185export function parenCount(title: string): number | null {186 const m = title.match(/\((\d{1,3})\)\.?\s*$/) ?? title.match(/\b(\d{1,3})\s*(?:st|stk|pcs|pieces|pièces|pezzi|piezas|stuks|Stück|Teile)\b\.?/i);187 if (!m) return null;188 const n = Number(m[1]);189 return n >= 2 && n <= 999 ? n : null;190}191192/**193 * Year from a continental title, ignoring century/decade conventions: "1900-tal", "1900/2000-tal", "1950er",194 * "1950s", "20. Jh.", "XIXe siècle", "circa 1900" (kept), "1800-talets". Returns null when nothing safe.195 */196export function yearFromTitle(title: string): number | null {197 const now = new Date().getUTCFullYear();198 const re = /\b(1[5-9]\d{2}|20\d{2})\b(?![-–/]\s?\d{2,4}|-?\s?tal|er\b|s\b|-?\s?talet|\/)/g;199 let m: RegExpExecArray | null;200 while ((m = re.exec(title))) {201 const y = Number(m[1]);202 // skip when the token is preceded by "/" (second half of "1900/2000-tal") or followed by "-tal"203 const before = title[m.index - 1] ?? '';204 if (before === '/' || before === '-' || before === '–') continue;205 if (y >= 1500 && y <= now) return y;206 }207 return null;208}209210/** Strip tags, decode common entities, collapse whitespace; truncate to `max` chars. */211export function stripHtml(s: string | null | undefined, max = 600): string | null {212 if (!s) return null;213 const t = s214 .replace(/<br\s*\/?>/gi, ' ')215 .replace(/<\/p>/gi, ' ')216 .replace(/<[^>]+>/g, ' ')217 .replace(/ /g, ' ')218 .replace(/&/g, '&')219 .replace(/</g, '<')220 .replace(/>/g, '>')221 .replace(/"/g, '"')222 .replace(/'|'/g, "'")223 .replace(/&#(\d+);/g, (_, n: string) => String.fromCodePoint(Number(n)))224 .replace(/\s+/g, ' ')225 .trim();226 if (!t) return null;227 return t.length > max ? `${t.slice(0, max - 1)}…` : t;228}229230/** Location string → ISO country when obvious (used for `location` display, never for currency). */231export function countryFromLocation(loc: string | null | undefined): string | null {232 if (!loc) return null;233 const l = loc.toLowerCase();234 if (/stockholm|göteborg|goteborg|malmö|malmo|uppsala|helsingborg|sweden|sverige/.test(l)) return 'SE';235 if (/köln|cologne|hamburg|berlin|münchen|munich|frankfurt|düsseldorf|stuttgart|germany|deutschland/.test(l)) return 'DE';236 if (/københavn|copenhagen|aarhus|denmark|danmark/.test(l)) return 'DK';237 if (/helsinki|helsingfors|turku|finland/.test(l)) return 'FI';238 if (/madrid|barcelona|valencia|sevilla|spain|españa/.test(l)) return 'ES';239 if (/london|manchester|edinburgh|glasgow|united kingdom|england|scotland/.test(l)) return 'GB';240 if (/paris|lyon|marseille|bordeaux|france/.test(l)) return 'FR';241 if (/wien|vienna|austria|österreich/.test(l)) return 'AT';242 if (/zürich|zurich|genève|geneva|basel|switzerland|schweiz/.test(l)) return 'CH';243 if (/oslo|bergen|norway|norge/.test(l)) return 'NO';244 if (/amsterdam|rotterdam|den haag|netherlands|nederland/.test(l)) return 'NL';245 if (/bruxelles|brussel|antwerpen|belgium|belgique/.test(l)) return 'BE';246 if (/milano|roma|torino|genova|italy|italia/.test(l)) return 'IT';247 return null;248}249250/**251 * Translate department / sale-title vocabulary from fr/de/it/es/nl/sv into the English keywords that252 * `_auction-lib`'s `hintFromLabel` understands, so continental sale titles map to the same department hints.253 */254const LABEL_VOCAB: Array<[RegExp, string]> = [255 [/\b(montres?|horlogerie|uhren|armbanduhren|orologi|relojes|horloges|klockor|armbandsur)\b/i, 'watches'],256 [/\b(bijoux|joaillerie|schmuck|juwelen|gioielli|joyas|sieraden|smycken|juweelen)\b/i, 'jewellery'],257 [/\b(vins?|spiritueux|wein|weine|spirituosen|vini|vinos|wijn|whisky|champagne|viner)\b/i, 'wine'],258 [/\b(monnaies|numismatique|münzen|numismatik|monete|monedas|munten|mynt|medailles|médailles)\b/i, 'coins'],259 [/\b(timbres|philatélie|briefmarken|philatelie|francobolli|filatelia|sellos|postzegels|frimärken)\b/i, 'stamps'],260 [/\b(automobiles?|voitures|motorcars|automobilia|autos?|automobili|coches|auto's|bilar)\b/i, 'motor cars'],261 [/\b(motos?|motocyclettes|motorräder|motociclette|motorfietsen|motorcyklar)\b/i, 'motorcycles'],262 [/\b(bandes? dessinées?|bd\b|comics|fumetti|cómics|strips|serier)\b/i, 'comics'],263 [/\b(livres?|manuscrits|bücher|bibliothek|libri|libros|boeken|böcker|autographes|autographen|cartes anciennes|landkarten)\b/i, 'books'],264 [/\b(photographies?|photographie|fotografie|fotografia|fotografía|foto's|fotografi)\b/i, 'photographs'],265 [/\b(jouets|spielzeug|giocattoli|juguetes|speelgoed|leksaker|poupées|puppen|trains|modellbahn)\b/i, 'toys'],266 [/\b(militaria|décorations|orden|ehrenzeichen|armes|waffen|armi|armas|wapens|vapen)\b/i, 'militaria'],267 [/\b(design|arts? décoratifs? du xxe|kunstgewerbe|arredi di design|diseño|vormgeving)\b/i, 'design'],268 [/\b(mobilier|meubles|möbel|arredi|mobili|muebles|meubelen|möbler|objets d'art|kunstgewerbe|antiquités|antiquitäten|antichità|antigüedades|antiek|antikviteter|tapis|teppiche|tappeti|alfombras)\b/i, 'furniture'],269 [/\b(argenterie|orfèvrerie|silber|argenti|plata|zilver|silver)\b/i, 'silver'],270 [/\b(céramiques?|porcelaine|porzellan|keramik|ceramiche|porcellane|cerámica|porcelana|porselein|keramiek|porslin|keramik)\b/i, 'ceramics'],271 [/\b(verre|verrerie|glas|vetri|vidrio|glaswerk)\b/i, 'glass'],272 [/\b(art contemporain|art moderne|après-guerre|post-war|moderne kunst|zeitgenössische kunst|arte moderna|arte contemporanea|arte contemporáneo|moderne en hedendaagse kunst|modern konst|samtida konst|street art|urban art|estampes|graphik|druckgraphik|editions)\b/i, 'contemporary'],273 [/\b(tableaux|peintures|dessins|gemälde|zeichnungen|alte meister|maîtres anciens|dipinti|disegni|pinturas|dibujos|schilderijen|tekeningen|målningar|sculptures|skulpturen|sculture|esculturas|beelden|konst|kunst|arte|art)\b/i, 'fine art'],274 [/\b(art d'asie|arts d'asie|asiatique|asiatica|asiatische kunst|chine|chinois|japon|japonais|arte asiatica|arte asiático|aziatische kunst|asiatisk|china|japan|japanese|chinese)\b/i, 'asian'],275 [/\b(arts premiers|art tribal|art africain|art océanien|arts d'afrique|stammeskunst|arte tribale|arte africano|tribale kunst|afrikansk)\b/i, 'tribal'],276 [/\b(archéologie|antiquités classiques|antike|archeologia|arqueología|archeologie|antikviteter klassiska)\b/i, 'antiquities'],277 [/\b(mode|haute couture|maroquinerie|sacs|hermès|vuitton|chanel|taschen|borse|bolsos|tassen|väskor|vintage fashion)\b/i, 'handbags'],278 [/\b(instruments? scientifiques?|wissenschaftliche instrumente|strumenti scientifici|marine|nautica|technica)\b/i, 'scientific instruments'],279 [/\b(cinéma|affiches|plakate|manifesti|carteles|affiches|filmplakat)\b/i, 'movie posters'],280 [/\b(musique|musik|musica|música|muziek|instruments de musique|musikinstrumente|strumenti musicali|vinyles|schallplatten)\b/i, 'music'],281 [/\b(sport|sports|sportmemorabilia)\b/i, 'sports'],282 [/\b(stylos|schreibgeräte|penne|plumas|pennen|pennor)\b/i, 'pens'],283 [/\b(parfums?|parfüm|profumi|perfumes)\b/i, 'perfume'],284 [/\b(appareils photo|kameras|fotocamere|cámaras|camera's|kameror)\b/i, 'cameras'],285 [/\b(jeux vidéo|videospiele|videogiochi|videojuegos|videogames|tv-spel)\b/i, 'video games'],286 [/\b(pendules|horloges|uhren und pendulen|orologi da tavolo|relojes de pared)\b/i, 'clocks'],287];288289/** Sale/department label (any supported language) → English keyword string for `hintFromLabel`. */290export function englishLabel(label: string | null | undefined): string {291 if (!label) return '';292 const hits: string[] = [];293 for (const [re, en] of LABEL_VOCAB) if (re.test(label)) hits.push(en);294 return hits.length ? hits.join(' ') : label;295}296297/**298 * Strong per-lot department cues in the group's languages (a Rolex filed under "Moderne Kunst" is still a299 * watch). Returns a department hint understood by `_auction-lib` or null when the lot text has no strong cue.300 */301export function strongLotHint(text: string): 'watches' | 'jewelry' | 'wine' | 'coins' | 'stamps' | 'handbags' | 'cars' | 'motorcycles' | 'cameras' | 'comics' | 'books' | 'asian' | 'antiquities' | null {302 const t = text;303 if (/\b(armbanduhr|armbanduhren|taschenuhr|montre|montres|orologio|orologi|reloj|relojes|horloge|polshorloge|armbandsur|fickur|wristwatch|pocket watch|chronograph|chronographe|cronografo|rolex|patek philippe|audemars piguet|omega|breitling|iwc|jaeger[- ]lecoultre|vacheron|cartier tank|tudor|panerai|hublot|zenith|longines|breguet|blancpain)\b/i.test(t) && !/\b(poster|affiche|plakat|book|livre|buch|catalogue|katalog)\b/i.test(t)) return 'watches';304 if (/\b(bague|collier|bracelet|broche|boucles d'oreilles|ring|halskette|armband|brosche|ohrringe|anello|collana|bracciale|spilla|orecchini|anillo|collar|pulsera|pendientes|diamant|diamanten|diamante|brillant|saphir|rubis|émeraude|smaragd|rubin|zaffiro|rubino|smeraldo|diamond|sapphire|emerald|ruby|carats?|\d+(?:[.,]\d+)?\s*ct\b|earrings?|necklace|brooch|pendant|tiara|bangle|cufflinks|halsband|örhängen|armbandsur)\b/i.test(t) && !/\barmbandsur\b/i.test(t)) return 'jewelry';305 if (/\b(whisky|whiskey|bourbon|cognac|armagnac|champagne|bordeaux|bourgogne|burgundy|château|chateau|domaine|magnum|bouteilles?|flaschen?|bottiglie?|botellas?|flaskor|wein|vino|vin\b|riesling|barolo|brunello|romanée|pétrus|petrus|lafite|latour|margaux|mouton|yquem|krug|dom pérignon|macallan|yamazaki|hibiki|springbank|bowmore|ardbeg)\b/i.test(t)) return 'wine';306 if (/\b(münze|münzen|mynt|monnaie|monnaies|moneta|monete|moneda|monedas|munt|munten|coin|coins|dukat|ducat|taler|thaler|riksdaler|sovereign|20 francs or|louis d'or|napoléon|napoleon 20|goldmünze|silbermünze|banknote|geldschein|billet de banque|sedel|sedlar|banconota|billete)\b/i.test(t)) return 'coins';307 if (/\b(briefmarke|briefmarken|timbre|timbres|francobollo|francobolli|sello|sellos|postzegel|postzegels|frimärke|frimärken|stamp|stamps|philatel\w*|postal history)\b/i.test(t)) return 'stamps';308 if (/\b(hermès|hermes birkin|birkin|kelly bag|sac kelly|chanel (?:timeless|classic|flap|2\.55)|louis vuitton|vuitton|goyard|handtasche|sac à main|borsa|bolso|handbag|tote bag)\b/i.test(t)) return 'handbags';309 if (/\b(motorrad|motocyclette|motocicletta|motocicleta|motorfiets|motorcykel|motorcycle|ducati|harley[- ]davidson|vespa|moto guzzi|bsa\b|norton|triumph bonneville)\b/i.test(t)) return 'motorcycles';310 if (/\b(chassis|châssis|fahrgestell|telaio|bastidor|numéro de série|vin\b|coupé|cabriolet|roadster|berline|limousine|spider|spyder|berlinetta)\b/i.test(t) && /\b(ferrari|porsche|mercedes|bmw|jaguar|aston martin|bentley|rolls[- ]royce|alfa romeo|lancia|maserati|bugatti|citroën|citroen|peugeot|renault|volvo|saab|ford|chevrolet|cadillac|lamborghini|mclaren|austin|mg\b|triumph|lotus|fiat|volkswagen|vw\b)\b/i.test(t)) return 'cars';311 if (/\b(leica|hasselblad|rolleiflex|nikon f\d?|contax|kamera|appareil photo|fotocamera|cámara|objektiv|objectif)\b/i.test(t)) return 'cameras';312 if (/\b(bande dessinée|bandes dessinées|planche originale|comic|comics|fumetto|fumetti|serietidning|tintin|astérix|asterix|hergé|uderzo|franquin|manga)\b/i.test(t)) return 'comics';313 if (/\b(dynasty|dynastie|kangxi|qianlong|yongzheng|wanli|ming|qing|song|tang|han\b|edo|meiji|taisho|satsuma|imari|kutani|celadon|cloisonné|netsuke|okimono|inro|tsuba|thangka|khmer|gandhara|famille rose|famille verte|blanc de chine|kakiemon|arita|chinesisch|chinoise|chinese|japanisch|japonais|japanese|tibetan|tibétain)\b/i.test(t)) return 'asian';314 if (/\b(roman|romaine|römisch|romano|greek|grec|griechisch|greco|etruscan|étrusque|egyptian|égyptien|ägyptisch|egizio|mesopotamian|sumerian|bactrian|hellenistic|hellénistique|\d+(?:st|nd|rd|th)? century (?:bc|b\.c\.)|av\. ?j\.-c\.|v\. ?chr\.|a\.c\.|\bbc\b)\b/i.test(t)) return 'antiquities';315 if (/\b(édition originale|first edition|erstausgabe|prima edizione|primera edición|incunable|incunabula|manuscrit|manuscript|handschrift|folio|in-4|in-8|in-12|reliure|einband|legatura|exemplaire numéroté)\b/i.test(t)) return 'books';316 return null;317}318