/** * Shared helpers for the European / Nordic / APAC auction-house connectors (group g8-auctions-eu-apac). * Multilingual date + money parsing (fr/de/it/es/nl/sv/da/fi/en), bundle detection in those languages, * cautious year extraction for continental title conventions ("1900-tal", "20. Jh.", "1950er"). * Kept inside connectors/api (not the framework). Pure functions, fixture-testable. */ import { parsePrice, SUPPORTED_CURRENCIES, type CurrencyCode } from '@rareindex/shared'; /** Month names / abbreviations → 0-based month. Covers en, fr, de, it, es, nl, sv, da, fi (+ common abbreviations). */ const MONTHS: Record = { // en 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, // fr 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, // de 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, // it 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, // es 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, // nl januari: 0, februari: 1, maart: 2, mrt: 2, mei: 4, augustus: 7, // (juni/juli/september/oktober/november/december shared) // sv / da / no mars_sv: 2, // placeholder never matched; sv "mars" already mapped via fr maj: 4, // sv/da // fi 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, }; function monthIndex(word: string): number | null { const w = word.toLowerCase().replace(/\.$/, ''); if (w in MONTHS && w !== 'mars_sv') return MONTHS[w]!; // truncated forms ("sept", "déc", "okt") const hit = Object.keys(MONTHS).find((k) => k.length >= 3 && w.length >= 3 && k.startsWith(w) && k !== 'mars_sv'); return hit !== undefined ? MONTHS[hit]! : null; } /** * Parse a date written in any of the supported languages. Handles: * "12 juin 2026" · "12. Juni 2026" · "12 giugno 2026" · "12 de junio de 2026" · "12 juni 2026" · "den 12 juni 2026" * "June 12, 2026" · "12/06/2026" · "12.06.2026" · "2026-06-12" · "12 juin 2026 14:00" (time ignored → UTC midnight) * "22–23 juin 2026" / "22-23. Juni 2026" (range → first day). * Returns null instead of guessing; day-first for numeric forms. */ export function parseEuDate(raw: string | null | undefined): Date | null { if (!raw) return null; const s = raw.replace(/[  ]/g, ' ').replace(/\s+/g, ' ').trim(); if (!s) return null; const iso = s.match(/\b(\d{4})-(\d{2})-(\d{2})(?!\d)/); if (iso) return utc(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3])); // day [–day] [de] month [de] year (fr/de/it/es/nl/sv/da/fi/en) 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/); if (dmy) { const mo = monthIndex(dmy[2]!); if (mo !== null) return utc(Number(dmy[3]), mo, Number(dmy[1])); } // month day, year (en) const mdy = s.match(/\b([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})\b/); if (mdy) { const mo = monthIndex(mdy[1]!); if (mo !== null) return utc(Number(mdy[3]), mo, Number(mdy[2])); } // numeric day-first: 12/06/2026, 12.06.2026, 12-06-2026 const num = s.match(/\b(\d{1,2})[./-](\d{1,2})[./-](\d{4})\b/); if (num) { const d = Number(num[1]); const m = Number(num[2]); if (m >= 1 && m <= 12 && d >= 1 && d <= 31) return utc(Number(num[3]), m - 1, d); } // month year only ("juin 2026") → first of month (month precision) const my = s.match(/\b([A-Za-zÀ-ÿ]{3,12})\.?\s+(\d{4})\b/); if (my) { const mo = monthIndex(my[1]!); if (mo !== null) return utc(Number(my[2]), mo, 1); } return null; } function utc(y: number, m: number, d: number): Date | null { const dt = new Date(Date.UTC(y, m, d)); return Number.isNaN(dt.getTime()) || dt.getUTCMonth() !== m ? null : dt; } /** Unix seconds (Auctionet `ends_at`) → Date. */ export function fromUnix(sec: number | null | undefined): Date | null { if (!sec || !Number.isFinite(sec)) return null; const d = new Date(sec * 1000); return Number.isNaN(d.getTime()) ? null : d; } export type NumberLocale = 'eu' | 'en' | 'ch'; /** * Parse a money string written with continental conventions. `locale` disambiguates the trailing-3-digit * case: "1.250" → 1250 under 'eu' (dot = thousands) but 1.25 under 'en'; "1'250" (Swiss) → 1250. * Currency: explicit symbol/code wins ("€", "CHF", "SEK", "kr", "£", "HK$", "¥"), else `fallback`. * Returns null for no number / non-positive amounts. Never invents a currency. */ export function parseEuMoney(text: string | null | undefined, fallback: CurrencyCode | null, locale: NumberLocale = 'eu'): { amount: number; currency: CurrencyCode; confidence: number } | null { if (!text) return null; let s = text.replace(/[  ]/g, ' ').replace(/\s+/g, ' ').trim(); if (!s) return null; // Swiss apostrophe thousands and "Fr." prefix if (locale === 'ch') s = s.replace(/(\d)'(\d{3})/g, '$1$2').replace(/\bFr\.\s?/, 'CHF '); // Danish/Swedish/Norwegian "kr" ambiguity: keep as fallback unless explicit code present const explicit = detectCurrency(s); const currency = explicit ?? fallback; if (!currency) return null; // Normalise EU thousands groups written with spaces or dots when a comma decimal follows or when 3 trailing digits let m = s.match(/-?\d[\d .,']*\d|\d/); if (!m) return null; let n = m[0].replace(/[ ']/g, ''); let confidence = 0.95; if (locale !== 'en') { if (/,\d{1,2}$/.test(n)) n = n.replace(/\./g, '').replace(',', '.'); else if (/^\d{1,3}(\.\d{3})+$/.test(n)) n = n.replace(/\./g, ''); else if (/^\d{1,3}(,\d{3})+$/.test(n)) n = n.replace(/,/g, ''); // some EU sites still print en groups else if (/\.\d{3}$/.test(n) && !/,/.test(n)) { n = n.replace(/\./g, ''); confidence = 0.85; } else if (/,\d{3}$/.test(n)) { n = n.replace(/,/g, ''); confidence = 0.85; } else n = n.replace(',', '.'); } else { const p = parsePrice(m[0], currency); if (!p || p.amount <= 0) return null; return { amount: p.amount, currency, confidence: p.confidence }; } const amount = Number.parseFloat(n); if (!Number.isFinite(amount) || amount <= 0) return null; return { amount, currency, confidence }; } const CURRENCY_TOKENS: Array<[RegExp, CurrencyCode]> = [ [/\bHK\s?\$|\bHKD\b/i, 'HKD'], [/\bA\s?\$|\bAU\s?\$|\bAUD\b/i, 'AUD'], [/\bNZ\s?\$|\bNZD\b/i, 'NZD'], [/\bUS\s?\$|\bUSD\b/i, 'USD'], [/\bCA\s?\$|\bCAD\b/i, 'CAD'], [/\bS\s?\$|\bSGD\b/i, 'SGD'], [/€|\bEUR\b|\beuros?\b/i, 'EUR'], [/£|\bGBP\b/i, 'GBP'], [/¥|¥|\bJPY\b|円/i, 'JPY'], [/\bCHF\b|\bSFr\.?/i, 'CHF'], [/\bSEK\b|\bskr\b/i, 'SEK'], [/\bDKK\b|\bdkr\b/i, 'DKK'], [/\bNOK\b|\bnkr\b/i, 'NOK'], [/\bPLN\b|zł/i, 'PLN'], [/\bCZK\b|Kč/i, 'CZK'], [/\bCNY\b|\bRMB\b/i, 'CNY'], [/\bTWD\b|\bNT\$/i, 'TWD'], ]; /** Explicit currency in a string (code or unambiguous symbol); null when absent or ambiguous ("kr", "$"). */ export function detectCurrency(text: string): CurrencyCode | null { for (const [re, code] of CURRENCY_TOKENS) if (re.test(text)) return code; return null; } export function isSupportedCurrency(code: string | null | undefined): code is CurrencyCode { return !!code && (SUPPORTED_CURRENCIES as readonly string[]).includes(code); } /** * Bundle / multi-item detection across the group's languages. Pairs ("ett par", "Paar", "paire", "coppia", * "a pair") are NOT bundles (a pair of candlesticks is one collectible unit); three or more distinct pieces, * "collection/lot of", "Konvolut", "samling", "parti", "lotto di", "lote de", "partij" are. */ export function isBundleMultilingual(title: string): boolean { const t = title; if (/\b(collection of|group of|lot of|assorted|quantity of|mixed lot|various|job lot)\b/i.test(t)) return true; if (/\b(konvolut|sammlung|posten|nachlass|\d+\s*-?\s*teilig|\d+\s*st(?:ück|k)\.?)\b/i.test(t)) return true; // de if (/\b(samling|parti|diverse|blandat|\d+\s*st\b|\d+\s*delar|\d+\s*stk)\b/i.test(t)) return true; // sv/da/no if (/\b(lot de|ensemble de|réunion de|reunion de|collection de|suite de|lot comprenant)\b/i.test(t)) return true; // fr if (/\b(lotto di|gruppo di|insieme di|collezione di)\b/i.test(t)) return true; // it if (/\b(lote de|conjunto de|colecci[oó]n de)\b/i.test(t)) return true; // es if (/\b(partij|collectie van|set van \d+|diverse)\b/i.test(t)) return true; // nl if (/\b(erä|kokoelma)\b/i.test(t)) return true; // fi const count = parenCount(t); return count !== null && count >= 3; } /** Trailing "(4)" / "(12)" piece count used by Nordic/German houses; null when absent. */ export function parenCount(title: string): number | null { 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); if (!m) return null; const n = Number(m[1]); return n >= 2 && n <= 999 ? n : null; } /** * Year from a continental title, ignoring century/decade conventions: "1900-tal", "1900/2000-tal", "1950er", * "1950s", "20. Jh.", "XIXe siècle", "circa 1900" (kept), "1800-talets". Returns null when nothing safe. */ export function yearFromTitle(title: string): number | null { const now = new Date().getUTCFullYear(); 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; let m: RegExpExecArray | null; while ((m = re.exec(title))) { const y = Number(m[1]); // skip when the token is preceded by "/" (second half of "1900/2000-tal") or followed by "-tal" const before = title[m.index - 1] ?? ''; if (before === '/' || before === '-' || before === '–') continue; if (y >= 1500 && y <= now) return y; } return null; } /** Strip tags, decode common entities, collapse whitespace; truncate to `max` chars. */ export function stripHtml(s: string | null | undefined, max = 600): string | null { if (!s) return null; const t = s .replace(//gi, ' ') .replace(/<\/p>/gi, ' ') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'|'/g, "'") .replace(/&#(\d+);/g, (_, n: string) => String.fromCodePoint(Number(n))) .replace(/\s+/g, ' ') .trim(); if (!t) return null; return t.length > max ? `${t.slice(0, max - 1)}…` : t; } /** Location string → ISO country when obvious (used for `location` display, never for currency). */ export function countryFromLocation(loc: string | null | undefined): string | null { if (!loc) return null; const l = loc.toLowerCase(); if (/stockholm|göteborg|goteborg|malmö|malmo|uppsala|helsingborg|sweden|sverige/.test(l)) return 'SE'; if (/köln|cologne|hamburg|berlin|münchen|munich|frankfurt|düsseldorf|stuttgart|germany|deutschland/.test(l)) return 'DE'; if (/københavn|copenhagen|aarhus|denmark|danmark/.test(l)) return 'DK'; if (/helsinki|helsingfors|turku|finland/.test(l)) return 'FI'; if (/madrid|barcelona|valencia|sevilla|spain|españa/.test(l)) return 'ES'; if (/london|manchester|edinburgh|glasgow|united kingdom|england|scotland/.test(l)) return 'GB'; if (/paris|lyon|marseille|bordeaux|france/.test(l)) return 'FR'; if (/wien|vienna|austria|österreich/.test(l)) return 'AT'; if (/zürich|zurich|genève|geneva|basel|switzerland|schweiz/.test(l)) return 'CH'; if (/oslo|bergen|norway|norge/.test(l)) return 'NO'; if (/amsterdam|rotterdam|den haag|netherlands|nederland/.test(l)) return 'NL'; if (/bruxelles|brussel|antwerpen|belgium|belgique/.test(l)) return 'BE'; if (/milano|roma|torino|genova|italy|italia/.test(l)) return 'IT'; return null; } /** * Translate department / sale-title vocabulary from fr/de/it/es/nl/sv into the English keywords that * `_auction-lib`'s `hintFromLabel` understands, so continental sale titles map to the same department hints. */ const LABEL_VOCAB: Array<[RegExp, string]> = [ [/\b(montres?|horlogerie|uhren|armbanduhren|orologi|relojes|horloges|klockor|armbandsur)\b/i, 'watches'], [/\b(bijoux|joaillerie|schmuck|juwelen|gioielli|joyas|sieraden|smycken|juweelen)\b/i, 'jewellery'], [/\b(vins?|spiritueux|wein|weine|spirituosen|vini|vinos|wijn|whisky|champagne|viner)\b/i, 'wine'], [/\b(monnaies|numismatique|münzen|numismatik|monete|monedas|munten|mynt|medailles|médailles)\b/i, 'coins'], [/\b(timbres|philatélie|briefmarken|philatelie|francobolli|filatelia|sellos|postzegels|frimärken)\b/i, 'stamps'], [/\b(automobiles?|voitures|motorcars|automobilia|autos?|automobili|coches|auto's|bilar)\b/i, 'motor cars'], [/\b(motos?|motocyclettes|motorräder|motociclette|motorfietsen|motorcyklar)\b/i, 'motorcycles'], [/\b(bandes? dessinées?|bd\b|comics|fumetti|cómics|strips|serier)\b/i, 'comics'], [/\b(livres?|manuscrits|bücher|bibliothek|libri|libros|boeken|böcker|autographes|autographen|cartes anciennes|landkarten)\b/i, 'books'], [/\b(photographies?|photographie|fotografie|fotografia|fotografía|foto's|fotografi)\b/i, 'photographs'], [/\b(jouets|spielzeug|giocattoli|juguetes|speelgoed|leksaker|poupées|puppen|trains|modellbahn)\b/i, 'toys'], [/\b(militaria|décorations|orden|ehrenzeichen|armes|waffen|armi|armas|wapens|vapen)\b/i, 'militaria'], [/\b(design|arts? décoratifs? du xxe|kunstgewerbe|arredi di design|diseño|vormgeving)\b/i, 'design'], [/\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'], [/\b(argenterie|orfèvrerie|silber|argenti|plata|zilver|silver)\b/i, 'silver'], [/\b(céramiques?|porcelaine|porzellan|keramik|ceramiche|porcellane|cerámica|porcelana|porselein|keramiek|porslin|keramik)\b/i, 'ceramics'], [/\b(verre|verrerie|glas|vetri|vidrio|glaswerk)\b/i, 'glass'], [/\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'], [/\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'], [/\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'], [/\b(arts premiers|art tribal|art africain|art océanien|arts d'afrique|stammeskunst|arte tribale|arte africano|tribale kunst|afrikansk)\b/i, 'tribal'], [/\b(archéologie|antiquités classiques|antike|archeologia|arqueología|archeologie|antikviteter klassiska)\b/i, 'antiquities'], [/\b(mode|haute couture|maroquinerie|sacs|hermès|vuitton|chanel|taschen|borse|bolsos|tassen|väskor|vintage fashion)\b/i, 'handbags'], [/\b(instruments? scientifiques?|wissenschaftliche instrumente|strumenti scientifici|marine|nautica|technica)\b/i, 'scientific instruments'], [/\b(cinéma|affiches|plakate|manifesti|carteles|affiches|filmplakat)\b/i, 'movie posters'], [/\b(musique|musik|musica|música|muziek|instruments de musique|musikinstrumente|strumenti musicali|vinyles|schallplatten)\b/i, 'music'], [/\b(sport|sports|sportmemorabilia)\b/i, 'sports'], [/\b(stylos|schreibgeräte|penne|plumas|pennen|pennor)\b/i, 'pens'], [/\b(parfums?|parfüm|profumi|perfumes)\b/i, 'perfume'], [/\b(appareils photo|kameras|fotocamere|cámaras|camera's|kameror)\b/i, 'cameras'], [/\b(jeux vidéo|videospiele|videogiochi|videojuegos|videogames|tv-spel)\b/i, 'video games'], [/\b(pendules|horloges|uhren und pendulen|orologi da tavolo|relojes de pared)\b/i, 'clocks'], ]; /** Sale/department label (any supported language) → English keyword string for `hintFromLabel`. */ export function englishLabel(label: string | null | undefined): string { if (!label) return ''; const hits: string[] = []; for (const [re, en] of LABEL_VOCAB) if (re.test(label)) hits.push(en); return hits.length ? hits.join(' ') : label; } /** * Strong per-lot department cues in the group's languages (a Rolex filed under "Moderne Kunst" is still a * watch). Returns a department hint understood by `_auction-lib` or null when the lot text has no strong cue. */ export function strongLotHint(text: string): 'watches' | 'jewelry' | 'wine' | 'coins' | 'stamps' | 'handbags' | 'cars' | 'motorcycles' | 'cameras' | 'comics' | 'books' | 'asian' | 'antiquities' | null { const t = text; 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'; 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'; 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'; 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'; 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'; 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'; 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'; 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'; if (/\b(leica|hasselblad|rolleiflex|nikon f\d?|contax|kamera|appareil photo|fotocamera|cámara|objektiv|objectif)\b/i.test(t)) return 'cameras'; 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'; 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'; 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'; 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'; return null; }