countries.js
1 /** 2 * Country Configuration Registry 3 * 4 * Defines country-specific settings for multi-country support including: 5 * - Google search domains for SERP scraping 6 * - Currency and formatting preferences 7 * - Phone number formats and mobile patterns 8 * - Common cities for local keyword generation 9 * - GDPR compliance requirements 10 * 11 * Countries ordered by GDP (top 25 economies) 12 */ 13 14 export const COUNTRIES = { 15 // 1. United States 16 US: { 17 code: 'US', 18 name: 'United States', 19 googleDomain: 'google.com', 20 language: 'en', 21 currency: 'USD', 22 currencySymbol: '$', 23 dateFormat: 'MM/DD/YYYY', 24 phoneFormat: '+1', 25 mobilePattern: /^(\+1|1)?[2-9]\d{9}$/, // US mobile (hard to distinguish from landline) 26 commonCities: [ 27 'new york', 28 'los angeles', 29 'chicago', 30 'houston', 31 'phoenix', 32 'philadelphia', 33 'san antonio', 34 'san diego', 35 'dallas', 36 'austin', 37 ], 38 requiresGDPRCheck: false, 39 timezone: 'America/New_York', // Eastern (can vary by city) 40 }, 41 42 // 2. China (limited Google access) 43 CN: { 44 code: 'CN', 45 name: 'China', 46 googleDomain: 'google.com.hk', // Google blocked in mainland, use HK 47 language: 'zh', 48 currency: 'CNY', 49 currencySymbol: '¥', 50 dateFormat: 'YYYY/MM/DD', 51 phoneFormat: '+86', 52 mobilePattern: /^(\+86|86)?1[3-9]\d{9}$/, // Chinese mobile 53 commonCities: [ 54 'beijing', 55 'shanghai', 56 'guangzhou', 57 'shenzhen', 58 'chengdu', 59 'hangzhou', 60 'wuhan', 61 'xian', 62 'tianjin', 63 'nanjing', 64 ], 65 requiresGDPRCheck: false, 66 timezone: 'Asia/Shanghai', 67 }, 68 69 // 3. Japan 70 JP: { 71 code: 'JP', 72 name: 'Japan', 73 googleDomain: 'google.co.jp', 74 language: 'ja', 75 currency: 'JPY', 76 currencySymbol: '¥', 77 dateFormat: 'YYYY/MM/DD', 78 phoneFormat: '+81', 79 mobilePattern: /^(\+81|0)(70|80|90)\d{8}$/, // Japanese mobile 80 commonCities: [ 81 'tokyo', 82 'osaka', 83 'yokohama', 84 'nagoya', 85 'sapporo', 86 'fukuoka', 87 'kobe', 88 'kyoto', 89 'kawasaki', 90 'saitama', 91 ], 92 requiresGDPRCheck: false, 93 timezone: 'Asia/Tokyo', 94 }, 95 96 // 4. Germany 97 DE: { 98 code: 'DE', 99 name: 'Germany', 100 googleDomain: 'google.de', 101 language: 'de', 102 currency: 'EUR', 103 currencySymbol: '€', 104 dateFormat: 'DD.MM.YYYY', 105 phoneFormat: '+49', 106 mobilePattern: /^(\+49|0)(15|16|17)\d{8,9}$/, // German mobile 107 commonCities: [ 108 'berlin', 109 'munich', 110 'hamburg', 111 'cologne', 112 'frankfurt', 113 'stuttgart', 114 'dusseldorf', 115 'dortmund', 116 'essen', 117 'leipzig', 118 ], 119 requiresGDPRCheck: true, 120 companyTypes: ['GmbH', 'AG', 'UG (haftungsbeschränkt)', 'UG', 'KG', 'e.V.', 'OHG'], 121 companyKeywords: [ 122 'Handelsregister', 123 'Unternehmensnummer', 124 'USt-IdNr', 125 'Geschäftsführer', 126 'Registergericht', 127 'Amtsgericht', 128 ], 129 keyPageNames: ['impressum', 'kontakt', 'über uns', 'rechtliches', 'datenschutz'], 130 timezone: 'Europe/Berlin', 131 }, 132 133 // 5. United Kingdom 134 UK: { 135 code: 'UK', 136 name: 'United Kingdom', 137 googleDomain: 'google.co.uk', 138 language: 'en', 139 currency: 'GBP', 140 currencySymbol: '£', 141 dateFormat: 'DD/MM/YYYY', 142 phoneFormat: '+44', 143 mobilePattern: /^(\+44|0)(7\d{9}|447\d{8})$/, // UK mobiles start with 07 or +447 144 commonCities: [ 145 'london', 146 'manchester', 147 'birmingham', 148 'glasgow', 149 'liverpool', 150 'edinburgh', 151 'leeds', 152 'bristol', 153 'cardiff', 154 'belfast', 155 ], 156 requiresGDPRCheck: true, 157 // Corporate subscribers — CAN receive unsolicited B2B marketing under UK PECR 158 companyTypes: [ 159 'Ltd', 160 'Limited', 161 'PLC', 162 'LLP', 163 'Limited Liability Partnership', 164 'Incorporated', 165 'CIC', 166 'CIO', 167 ], 168 // Individual subscribers under PECR — CANNOT be contacted without consent. 169 // Sole traders and non-LLP partnerships are legally individuals, not corporate. 170 individualIndicators: ['Sole trader', 'Sole proprietor', 'Partnership'], 171 companyKeywords: [ 172 'Company number', 173 'Company No', 174 'Co. No', 175 'Company registration', 176 'Reg. No', 177 'Registration number', 178 'Registered in England', 179 'Registered in Scotland', 180 'Registered in Wales', 181 'Registered in Northern Ireland', 182 'Registered office', 183 'Registered address', 184 'Companies House', 185 'VAT number', 186 'VAT No', 187 'VAT registration', 188 'Director:', 189 'Directors:', 190 'ICO registration', 191 'ICO number', 192 'Data Protection Registration', 193 ], 194 keyPageNames: ['contact', 'about', 'legal', 'company information', 'privacy policy'], 195 timezone: 'Europe/London', 196 }, 197 198 // 6. India 199 IN: { 200 code: 'IN', 201 name: 'India', 202 googleDomain: 'google.co.in', 203 language: 'en', 204 currency: 'INR', 205 currencySymbol: '₹', 206 dateFormat: 'DD/MM/YYYY', 207 phoneFormat: '+91', 208 mobilePattern: /^(\+91|91|0)?[6-9]\d{9}$/, // Indian mobile 209 commonCities: [ 210 'mumbai', 211 'delhi', 212 'bangalore', 213 'hyderabad', 214 'chennai', 215 'kolkata', 216 'pune', 217 'ahmedabad', 218 'jaipur', 219 'surat', 220 ], 221 requiresGDPRCheck: false, 222 timezone: 'Asia/Kolkata', 223 }, 224 225 // 7. France 226 FR: { 227 code: 'FR', 228 name: 'France', 229 googleDomain: 'google.fr', 230 language: 'fr', 231 currency: 'EUR', 232 currencySymbol: '€', 233 dateFormat: 'DD/MM/YYYY', 234 phoneFormat: '+33', 235 mobilePattern: /^(\+33|0)(6|7)\d{8}$/, // French mobile 236 commonCities: [ 237 'paris', 238 'marseille', 239 'lyon', 240 'toulouse', 241 'nice', 242 'nantes', 243 'montpellier', 244 'strasbourg', 245 'bordeaux', 246 'lille', 247 ], 248 requiresGDPRCheck: true, 249 companyTypes: ['SARL', 'SA', 'SAS', 'EURL', 'SNC', 'SCS'], 250 companyKeywords: [ 251 'Numéro SIREN', 252 'Numéro SIRET', 253 'RCS', 254 'Capital social', 255 'Siège social', 256 'Numéro de TVA', 257 ], 258 keyPageNames: ['contact', 'à propos', 'mentions légales', 'informations légales'], 259 timezone: 'Europe/Paris', 260 }, 261 262 // 8. Italy 263 IT: { 264 code: 'IT', 265 name: 'Italy', 266 googleDomain: 'google.it', 267 language: 'it', 268 currency: 'EUR', 269 currencySymbol: '€', 270 dateFormat: 'DD/MM/YYYY', 271 phoneFormat: '+39', 272 mobilePattern: /^(\+39|39)?3\d{8,9}$/, // Italian mobile 273 commonCities: [ 274 'rome', 275 'milan', 276 'naples', 277 'turin', 278 'palermo', 279 'genoa', 280 'bologna', 281 'florence', 282 'bari', 283 'catania', 284 ], 285 requiresGDPRCheck: true, 286 companyTypes: ['S.r.l.', 'S.p.A.', 'S.n.c.', 'S.a.s.'], 287 companyKeywords: [ 288 'Partita IVA', 289 'Codice Fiscale', 290 'Registro Imprese', 291 'Capitale sociale', 292 'Sede legale', 293 ], 294 keyPageNames: ['contatti', 'chi siamo', 'note legali', 'informazioni legali'], 295 timezone: 'Europe/Rome', 296 }, 297 298 // 9. Canada 299 CA: { 300 code: 'CA', 301 name: 'Canada', 302 googleDomain: 'google.ca', 303 language: 'en', 304 currency: 'CAD', 305 currencySymbol: '$', 306 dateFormat: 'DD/MM/YYYY', 307 phoneFormat: '+1', 308 mobilePattern: /^(\+1|1)?[2-9]\d{9}$/, // Canadian mobile (similar to US) 309 commonCities: [ 310 'toronto', 311 'montreal', 312 'vancouver', 313 'calgary', 314 'ottawa', 315 'edmonton', 316 'winnipeg', 317 'quebec city', 318 'hamilton', 319 'kitchener', 320 ], 321 requiresGDPRCheck: false, 322 timezone: 'America/Toronto', // Eastern (can vary by province) 323 }, 324 325 // 10. South Korea 326 KR: { 327 code: 'KR', 328 name: 'South Korea', 329 googleDomain: 'google.co.kr', 330 language: 'ko', 331 currency: 'KRW', 332 currencySymbol: '₩', 333 dateFormat: 'YYYY.MM.DD', 334 phoneFormat: '+82', 335 mobilePattern: /^(\+82|0)?1[0-9]\d{7,8}$/, // Korean mobile 336 commonCities: [ 337 'seoul', 338 'busan', 339 'incheon', 340 'daegu', 341 'daejeon', 342 'gwangju', 343 'ulsan', 344 'suwon', 345 'changwon', 346 'goyang', 347 ], 348 requiresGDPRCheck: false, 349 timezone: 'Asia/Seoul', 350 }, 351 352 // 11. Spain 353 ES: { 354 code: 'ES', 355 name: 'Spain', 356 googleDomain: 'google.es', 357 language: 'es', 358 currency: 'EUR', 359 currencySymbol: '€', 360 dateFormat: 'DD/MM/YYYY', 361 phoneFormat: '+34', 362 mobilePattern: /^(\+34|34)?[67]\d{8}$/, // Spanish mobile 363 commonCities: [ 364 'madrid', 365 'barcelona', 366 'valencia', 367 'seville', 368 'zaragoza', 369 'malaga', 370 'murcia', 371 'palma', 372 'bilbao', 373 'alicante', 374 ], 375 requiresGDPRCheck: true, 376 companyTypes: ['SL', 'SA', 'SRL', 'SLU'], 377 companyKeywords: ['CIF', 'NIF', 'Registro Mercantil', 'Capital social', 'Domicilio social'], 378 keyPageNames: ['contacto', 'acerca de', 'aviso legal', 'información legal'], 379 timezone: 'Europe/Madrid', 380 }, 381 382 // 12. Australia 383 AU: { 384 code: 'AU', 385 name: 'Australia', 386 googleDomain: 'google.com.au', 387 language: 'en', 388 currency: 'AUD', 389 currencySymbol: '$', 390 dateFormat: 'DD/MM/YYYY', 391 phoneFormat: '+61', 392 mobilePattern: /^(\+61|0)(4\d{8}|614\d{6})$/, // Australian mobiles start with 04 or +614 393 commonCities: [ 394 'sydney', 395 'melbourne', 396 'brisbane', 397 'perth', 398 'adelaide', 399 'gold coast', 400 'canberra', 401 'newcastle', 402 'wollongong', 403 'hobart', 404 ], 405 requiresGDPRCheck: false, 406 timezone: 'Australia/Sydney', // Eastern (can vary by state) 407 }, 408 409 // 13. Mexico 410 MX: { 411 code: 'MX', 412 name: 'Mexico', 413 googleDomain: 'google.com.mx', 414 language: 'es', 415 currency: 'MXN', 416 currencySymbol: '$', 417 dateFormat: 'DD/MM/YYYY', 418 phoneFormat: '+52', 419 mobilePattern: /^(\+52|52)?1?\d{10}$/, // Mexican mobile 420 commonCities: [ 421 'mexico city', 422 'guadalajara', 423 'monterrey', 424 'puebla', 425 'tijuana', 426 'leon', 427 'juarez', 428 'zapopan', 429 'merida', 430 'cancun', 431 ], 432 requiresGDPRCheck: false, 433 timezone: 'America/Mexico_City', 434 }, 435 436 // 14. Indonesia 437 ID: { 438 code: 'ID', 439 name: 'Indonesia', 440 googleDomain: 'google.co.id', 441 language: 'id', 442 currency: 'IDR', 443 currencySymbol: 'Rp', 444 dateFormat: 'DD/MM/YYYY', 445 phoneFormat: '+62', 446 mobilePattern: /^(\+62|0)8\d{8,10}$/, // Indonesian mobile 447 commonCities: [ 448 'jakarta', 449 'surabaya', 450 'bandung', 451 'medan', 452 'semarang', 453 'makassar', 454 'palembang', 455 'tangerang', 456 'depok', 457 'bekasi', 458 ], 459 requiresGDPRCheck: false, 460 timezone: 'Asia/Jakarta', 461 }, 462 463 // 15. Netherlands 464 NL: { 465 code: 'NL', 466 name: 'Netherlands', 467 googleDomain: 'google.nl', 468 language: 'nl', 469 currency: 'EUR', 470 currencySymbol: '€', 471 dateFormat: 'DD-MM-YYYY', 472 phoneFormat: '+31', 473 mobilePattern: /^(\+31|0)6\d{8}$/, // Dutch mobile 474 commonCities: [ 475 'amsterdam', 476 'rotterdam', 477 'the hague', 478 'utrecht', 479 'eindhoven', 480 'groningen', 481 'tilburg', 482 'almere', 483 'breda', 484 'nijmegen', 485 ], 486 requiresGDPRCheck: true, 487 companyTypes: ['BV', 'NV', 'VOF', 'CV'], 488 companyKeywords: ['KvK-nummer', 'BTW-nummer', 'Vestigingsadres', 'Handelsregister'], 489 keyPageNames: ['contact', 'over ons', 'juridisch', 'privacybeleid'], 490 timezone: 'Europe/Amsterdam', 491 }, 492 493 // 16. Switzerland 494 CH: { 495 code: 'CH', 496 name: 'Switzerland', 497 googleDomain: 'google.ch', 498 language: 'de', // Primary, but also fr, it 499 currency: 'CHF', 500 currencySymbol: 'CHF', 501 dateFormat: 'DD.MM.YYYY', 502 phoneFormat: '+41', 503 mobilePattern: /^(\+41|0)7[6-9]\d{7}$/, // Swiss mobile 504 commonCities: [ 505 'zurich', 506 'geneva', 507 'basel', 508 'lausanne', 509 'bern', 510 'winterthur', 511 'lucerne', 512 'st gallen', 513 'lugano', 514 'biel', 515 ], 516 requiresGDPRCheck: false, // Not EU but has similar laws 517 timezone: 'Europe/Zurich', 518 }, 519 520 // 17. Poland 521 PL: { 522 code: 'PL', 523 name: 'Poland', 524 googleDomain: 'google.pl', 525 language: 'pl', 526 currency: 'PLN', 527 currencySymbol: 'zł', 528 dateFormat: 'DD.MM.YYYY', 529 phoneFormat: '+48', 530 mobilePattern: /^(\+48|48)?[5-8]\d{8}$/, // Polish mobile 531 commonCities: [ 532 'warsaw', 533 'krakow', 534 'lodz', 535 'wroclaw', 536 'poznan', 537 'gdansk', 538 'szczecin', 539 'bydgoszcz', 540 'lublin', 541 'katowice', 542 ], 543 requiresGDPRCheck: true, 544 companyTypes: ['Sp. z o.o.', 'S.A.', 'sp.j.', 'sp.k.'], 545 companyKeywords: ['NIP', 'REGON', 'KRS', 'Siedziba'], 546 keyPageNames: ['kontakt', 'o nas', 'informacje prawne', 'polityka prywatności'], 547 timezone: 'Europe/Warsaw', 548 }, 549 550 // 18. Belgium 551 BE: { 552 code: 'BE', 553 name: 'Belgium', 554 googleDomain: 'google.be', 555 language: 'nl', // Also fr, de 556 currency: 'EUR', 557 currencySymbol: '€', 558 dateFormat: 'DD/MM/YYYY', 559 phoneFormat: '+32', 560 mobilePattern: /^(\+32|0)4\d{8}$/, // Belgian mobile 561 commonCities: [ 562 'brussels', 563 'antwerp', 564 'ghent', 565 'charleroi', 566 'liege', 567 'bruges', 568 'namur', 569 'leuven', 570 'mons', 571 'aalst', 572 ], 573 requiresGDPRCheck: true, 574 companyTypes: ['BV', 'NV', 'BVBA', 'SPRL'], 575 companyKeywords: ['Ondernemingsnummer', 'BTW-nummer', 'Maatschappelijke zetel'], 576 keyPageNames: ['contact', 'over ons', 'juridische informatie', 'privacy'], 577 timezone: 'Europe/Brussels', 578 }, 579 580 // 19. Sweden 581 SE: { 582 code: 'SE', 583 name: 'Sweden', 584 googleDomain: 'google.se', 585 language: 'sv', 586 currency: 'SEK', 587 currencySymbol: 'kr', 588 dateFormat: 'YYYY-MM-DD', 589 phoneFormat: '+46', 590 mobilePattern: /^(\+46|0)7[0-9]\d{7}$/, // Swedish mobile 591 commonCities: [ 592 'stockholm', 593 'gothenburg', 594 'malmo', 595 'uppsala', 596 'vasteras', 597 'orebro', 598 'linkoping', 599 'helsingborg', 600 'jonkoping', 601 'norrkoping', 602 ], 603 requiresGDPRCheck: true, 604 companyTypes: ['AB', 'Aktiebolag'], 605 companyKeywords: ['Organisationsnummer', 'Momsregistreringsnummer', 'Säte'], 606 keyPageNames: ['kontakt', 'om oss', 'juridisk information', 'integritetspolicy'], 607 timezone: 'Europe/Stockholm', 608 }, 609 610 // 20. Austria 611 AT: { 612 code: 'AT', 613 name: 'Austria', 614 googleDomain: 'google.at', 615 language: 'de', 616 currency: 'EUR', 617 currencySymbol: '€', 618 dateFormat: 'DD.MM.YYYY', 619 phoneFormat: '+43', 620 mobilePattern: /^(\+43|0)6\d{8,9}$/, // Austrian mobile 621 commonCities: [ 622 'vienna', 623 'graz', 624 'linz', 625 'salzburg', 626 'innsbruck', 627 'klagenfurt', 628 'villach', 629 'wels', 630 'st polten', 631 'dornbirn', 632 ], 633 requiresGDPRCheck: true, 634 companyTypes: ['GmbH', 'AG', 'OG', 'KG'], 635 companyKeywords: ['Firmenbuchnummer', 'UID-Nummer', 'Geschäftsführer', 'Firmensitz'], 636 keyPageNames: ['impressum', 'kontakt', 'über uns', 'rechtliches'], 637 timezone: 'Europe/Vienna', 638 }, 639 640 // 21. Norway 641 NO: { 642 code: 'NO', 643 name: 'Norway', 644 googleDomain: 'google.no', 645 language: 'en', // Temporarily using 'en' - DataForSEO doesn't support 'no' 646 currency: 'NOK', 647 currencySymbol: 'kr', 648 dateFormat: 'DD.MM.YYYY', 649 phoneFormat: '+47', 650 mobilePattern: /^(\+47|47)?[49]\d{7}$/, // Norwegian mobile 651 commonCities: [ 652 'oslo', 653 'bergen', 654 'stavanger', 655 'trondheim', 656 'drammen', 657 'fredrikstad', 658 'kristiansand', 659 'sandnes', 660 'tromso', 661 'sarpsborg', 662 ], 663 requiresGDPRCheck: true, 664 companyTypes: ['AS', 'ASA', 'BA', 'ANS'], 665 companyKeywords: ['Organisasjonsnummer', 'MVA-nummer', 'Forretningsadresse'], 666 keyPageNames: ['kontakt', 'om oss', 'juridisk informasjon', 'personvern'], 667 timezone: 'Europe/Oslo', 668 }, 669 670 // 22. Denmark 671 DK: { 672 code: 'DK', 673 name: 'Denmark', 674 googleDomain: 'google.dk', 675 language: 'da', 676 currency: 'DKK', 677 currencySymbol: 'kr', 678 dateFormat: 'DD-MM-YYYY', 679 phoneFormat: '+45', 680 mobilePattern: /^(\+45|45)?[2-9]\d{7}$/, // Danish mobile 681 commonCities: [ 682 'copenhagen', 683 'aarhus', 684 'odense', 685 'aalborg', 686 'esbjerg', 687 'randers', 688 'kolding', 689 'horsens', 690 'vejle', 691 'roskilde', 692 ], 693 requiresGDPRCheck: true, 694 companyTypes: ['ApS', 'A/S', 'I/S', 'K/S'], 695 companyKeywords: ['CVR-nummer', 'Momsregistreringsnummer', 'Hjemsted'], 696 keyPageNames: ['kontakt', 'om os', 'juridisk information', 'privatlivspolitik'], 697 timezone: 'Europe/Copenhagen', 698 }, 699 700 // 23. Singapore 701 SG: { 702 code: 'SG', 703 name: 'Singapore', 704 googleDomain: 'google.com.sg', 705 language: 'en', 706 currency: 'SGD', 707 currencySymbol: 'S$', 708 dateFormat: 'DD/MM/YYYY', 709 phoneFormat: '+65', 710 mobilePattern: /^(\+65|65)?[89]\d{7}$/, // Singapore mobile 711 commonCities: ['singapore', 'jurong west', 'woodlands', 'tampines', 'bedok'], 712 requiresGDPRCheck: false, 713 timezone: 'Asia/Singapore', 714 }, 715 716 // 24. New Zealand 717 NZ: { 718 code: 'NZ', 719 name: 'New Zealand', 720 googleDomain: 'google.co.nz', 721 language: 'en', 722 currency: 'NZD', 723 currencySymbol: '$', 724 dateFormat: 'DD/MM/YYYY', 725 phoneFormat: '+64', 726 mobilePattern: /^(\+64|0)(2[0-9]\d{7,8})$/, // NZ mobiles start with 02 or +642 727 commonCities: [ 728 'auckland', 729 'wellington', 730 'christchurch', 731 'hamilton', 732 'tauranga', 733 'dunedin', 734 'palmerston north', 735 'napier', 736 'porirua', 737 'new plymouth', 738 ], 739 requiresGDPRCheck: false, 740 timezone: 'Pacific/Auckland', 741 }, 742 743 // 25. Ireland 744 IE: { 745 code: 'IE', 746 name: 'Ireland', 747 googleDomain: 'google.ie', 748 language: 'en', 749 currency: 'EUR', 750 currencySymbol: '€', 751 dateFormat: 'DD/MM/YYYY', 752 phoneFormat: '+353', 753 mobilePattern: /^(\+353|0)8[356789]\d{7}$/, // Irish mobile 754 commonCities: [ 755 'dublin', 756 'cork', 757 'limerick', 758 'galway', 759 'waterford', 760 'drogheda', 761 'dundalk', 762 'swords', 763 'bray', 764 'navan', 765 ], 766 requiresGDPRCheck: true, 767 companyTypes: ['Ltd', 'Limited', 'PLC', 'Teoranta', 'Teo'], 768 individualIndicators: ['Sole trader', 'Sole proprietor', 'Partnership'], 769 companyKeywords: [ 770 'Company number', 771 'Company No', 772 'Co. No', 773 'CRO number', 774 'Company registration', 775 'VAT number', 776 'VAT No', 777 'Registered office', 778 'Registered address', 779 'Director:', 780 'Directors:', 781 ], 782 keyPageNames: ['contact', 'about', 'legal', 'privacy policy'], 783 timezone: 'Europe/Dublin', 784 }, 785 786 // 26. South Africa 787 ZA: { 788 code: 'ZA', 789 name: 'South Africa', 790 googleDomain: 'google.co.za', 791 language: 'en', 792 currency: 'ZAR', 793 currencySymbol: 'R', 794 dateFormat: 'DD/MM/YYYY', 795 phoneFormat: '+27', 796 mobilePattern: /^(\+27|0)[67]\d{8}$/, // ZA mobiles: +27 6x/7x xxxxxxxx 797 commonCities: [ 798 'johannesburg', 799 'cape town', 800 'durban', 801 'pretoria', 802 'port elizabeth', 803 'bloemfontein', 804 'east london', 805 'polokwane', 806 'nelspruit', 807 'kimberley', 808 ], 809 requiresGDPRCheck: false, 810 companyTypes: ['(Pty) Ltd', 'Pty Ltd', 'Ltd', 'CC', 'Inc'], 811 companyKeywords: ['Registration number', 'Reg no', 'VAT number'], 812 keyPageNames: ['contact', 'about', 'privacy policy'], 813 timezone: 'Africa/Johannesburg', 814 }, 815 }; 816 817 /** 818 * Get country configuration by ISO country code 819 * @param {string} code - ISO 3166-1 alpha-2 country code (e.g., 'AU', 'US', 'UK') 820 * @returns {Object} Country configuration object 821 */ 822 export function getCountryByCode(code) { 823 if (!code) { 824 throw new Error('Country code is required'); 825 } 826 827 // Handle country code aliases (GB is the official ISO code for UK) 828 const codeUpper = code.toUpperCase(); 829 const aliases = { 830 GB: 'UK', // Great Britain → United Kingdom 831 }; 832 833 const lookupCode = aliases[codeUpper] || codeUpper; 834 const country = COUNTRIES[lookupCode]; 835 836 if (!country) { 837 return null; // Unknown code (e.g. 'EU') — callers should check for null 838 } 839 840 return country; 841 } 842 843 /** 844 * Get country configuration by Google domain 845 * @param {string} domain - Google domain (e.g., 'google.com.au', 'google.co.uk') 846 * @returns {Object} Country configuration object 847 */ 848 export function getCountryByGoogleDomain(domain) { 849 if (!domain) { 850 throw new Error('Google domain is required'); 851 } 852 853 const country = Object.values(COUNTRIES).find(c => c.googleDomain === domain); 854 if (!country) { 855 const supportedDomains = Object.values(COUNTRIES) 856 .map(c => c.googleDomain) 857 .join(', '); 858 throw new Error(`Unknown Google domain: ${domain}. Supported domains: ${supportedDomains}`); 859 } 860 861 return country; 862 } 863 864 /** 865 * Check if a phone number matches the country's mobile pattern 866 * @param {string} phone - Phone number to validate 867 * @param {Object} country - Country configuration object 868 * @returns {boolean} True if the phone number is a mobile number 869 */ 870 export function isMobileNumber(phone, country) { 871 if (!phone || !country || !country.mobilePattern) { 872 return false; 873 } 874 875 // Remove all whitespace and common separators 876 const cleanPhone = phone.replace(/[\s\-().]/g, ''); 877 878 return country.mobilePattern.test(cleanPhone); 879 } 880 881 /** 882 * Get list of all supported country codes 883 * @returns {string[]} Array of ISO country codes 884 */ 885 export function getSupportedCountries() { 886 return Object.keys(COUNTRIES); 887 } 888 889 /** 890 * Get list of countries that require GDPR compliance checks 891 * @returns {Object[]} Array of country configuration objects 892 */ 893 export function getGDPRCountries() { 894 return Object.values(COUNTRIES).filter(c => c.requiresGDPRCheck); 895 } 896 897 /** 898 * Normalise an inbound country code to the canonical code used in this codebase. 899 * Call this at every DB write point to prevent code drift (e.g. DataForSEO returns 'GB', 900 * we store 'UK'; without normalisation the two accumulate in the same column). 901 * 902 * Canonical codes match the keys in the COUNTRIES map above. 903 * Unknown codes are returned as-is (uppercased) — don't silently drop them. 904 * 905 * @param {string} code - Raw country code from any external source 906 * @returns {string} Canonical country code 907 */ 908 export function normaliseCountryCode(code) { 909 if (!code) return code; 910 const upper = code.toUpperCase(); 911 // Map external ISO codes → our canonical codes 912 const canonicalMap = { 913 GB: 'UK', // ISO 3166-1 alpha-2 for United Kingdom; we use 'UK' 914 }; 915 return canonicalMap[upper] ?? upper; 916 } 917 918 /** 919 * Free email provider domains (used for GDPR filtering) 920 */ 921 export const FREE_EMAIL_PROVIDERS = [ 922 'gmail.com', 923 'googlemail.com', 924 'outlook.com', 925 'hotmail.com', 926 'live.com', 927 'yahoo.com', 928 'yahoo.co.uk', 929 'yahoo.de', 930 'yahoo.fr', 931 'yahoo.es', 932 'yahoo.it', 933 'gmx.com', 934 'gmx.de', 935 'gmx.net', 936 'gmx.at', 937 'gmx.ch', 938 'web.de', 939 'mail.com', 940 'icloud.com', 941 'me.com', 942 'mac.com', 943 'aol.com', 944 'protonmail.com', 945 'proton.me', 946 'tutanota.com', 947 'zoho.com', 948 'yandex.com', 949 'mail.ru', 950 'qq.com', 951 '163.com', 952 '126.com', 953 ]; 954 955 /** 956 * Check if an email address is from a free email provider 957 * @param {string} email - Email address to check 958 * @returns {boolean} True if the email is from a free provider 959 */ 960 export function isFreeEmailProvider(email) { 961 if (!email || typeof email !== 'string') { 962 return false; 963 } 964 965 const domain = email.toLowerCase().split('@')[1]; 966 return FREE_EMAIL_PROVIDERS.includes(domain); 967 }