| 1 |
/* Country + language helpers used by the Reader Engagement view. |
| 2 |
Country codes are ISO-3166 alpha-2; flag emoji is built from the regional |
| 3 |
indicator code points so we never have to ship a flag-name dictionary. */ |
| 4 |
|
| 5 |
const REGIONAL_INDICATOR_A = 0x1F1E6; |
| 6 |
|
| 7 |
export function countryFlag( code ) { |
| 8 |
if ( ! code || code.length !== 2 ) return "🏳"; |
| 9 |
const a = code.toUpperCase().charCodeAt( 0 ) - 65; |
| 10 |
const b = code.toUpperCase().charCodeAt( 1 ) - 65; |
| 11 |
if ( a < 0 || a > 25 || b < 0 || b > 25 ) return "🏳"; |
| 12 |
return String.fromCodePoint( REGIONAL_INDICATOR_A + a, REGIONAL_INDICATOR_A + b ); |
| 13 |
} |
| 14 |
|
| 15 |
/* Centroid approximations (lat/lng) for ~60 common analytics countries — enough |
| 16 |
for the world-map bubble overlay. Codes outside this set fall back to a |
| 17 |
nullable centroid (skipped on the map, still listed). */ |
| 18 |
export const COUNTRY_DB = { |
| 19 |
US: { name: "United States", lat: 38, lng: -97 }, |
| 20 |
IN: { name: "India", lat: 22, lng: 78 }, |
| 21 |
GB: { name: "United Kingdom", lat: 54, lng: -2 }, |
| 22 |
DE: { name: "Germany", lat: 51, lng: 10 }, |
| 23 |
BR: { name: "Brazil", lat: -14, lng: -51 }, |
| 24 |
FR: { name: "France", lat: 46, lng: 2 }, |
| 25 |
CA: { name: "Canada", lat: 56, lng: -106 }, |
| 26 |
AU: { name: "Australia", lat: -25, lng: 133 }, |
| 27 |
JP: { name: "Japan", lat: 36, lng: 138 }, |
| 28 |
ES: { name: "Spain", lat: 40, lng: -3 }, |
| 29 |
NL: { name: "Netherlands", lat: 52, lng: 5 }, |
| 30 |
SG: { name: "Singapore", lat: 1.4, lng: 103.8 }, |
| 31 |
IT: { name: "Italy", lat: 42, lng: 12 }, |
| 32 |
SE: { name: "Sweden", lat: 60, lng: 18 }, |
| 33 |
NO: { name: "Norway", lat: 61, lng: 8 }, |
| 34 |
DK: { name: "Denmark", lat: 56, lng: 10 }, |
| 35 |
FI: { name: "Finland", lat: 64, lng: 26 }, |
| 36 |
PL: { name: "Poland", lat: 52, lng: 20 }, |
| 37 |
IE: { name: "Ireland", lat: 53, lng: -8 }, |
| 38 |
BE: { name: "Belgium", lat: 50.5, lng: 4.5 }, |
| 39 |
CH: { name: "Switzerland", lat: 47, lng: 8 }, |
| 40 |
AT: { name: "Austria", lat: 47.5, lng: 14 }, |
| 41 |
PT: { name: "Portugal", lat: 39.5, lng: -8 }, |
| 42 |
GR: { name: "Greece", lat: 39, lng: 22 }, |
| 43 |
TR: { name: "Turkey", lat: 39, lng: 35 }, |
| 44 |
RU: { name: "Russia", lat: 62, lng: 95 }, |
| 45 |
UA: { name: "Ukraine", lat: 49, lng: 32 }, |
| 46 |
CZ: { name: "Czechia", lat: 50, lng: 15 }, |
| 47 |
RO: { name: "Romania", lat: 46, lng: 25 }, |
| 48 |
HU: { name: "Hungary", lat: 47, lng: 19 }, |
| 49 |
BG: { name: "Bulgaria", lat: 43, lng: 25 }, |
| 50 |
ZA: { name: "South Africa", lat: -30, lng: 25 }, |
| 51 |
NG: { name: "Nigeria", lat: 10, lng: 8 }, |
| 52 |
EG: { name: "Egypt", lat: 26, lng: 30 }, |
| 53 |
KE: { name: "Kenya", lat: -1, lng: 38 }, |
| 54 |
MA: { name: "Morocco", lat: 32, lng: -7 }, |
| 55 |
CN: { name: "China", lat: 35, lng: 105 }, |
| 56 |
KR: { name: "South Korea", lat: 36, lng: 128 }, |
| 57 |
TW: { name: "Taiwan", lat: 24, lng: 121 }, |
| 58 |
HK: { name: "Hong Kong", lat: 22.3, lng: 114.2 }, |
| 59 |
ID: { name: "Indonesia", lat: -2, lng: 118 }, |
| 60 |
TH: { name: "Thailand", lat: 15, lng: 101 }, |
| 61 |
VN: { name: "Vietnam", lat: 16, lng: 108 }, |
| 62 |
MY: { name: "Malaysia", lat: 4, lng: 102 }, |
| 63 |
PH: { name: "Philippines", lat: 13, lng: 122 }, |
| 64 |
PK: { name: "Pakistan", lat: 30, lng: 70 }, |
| 65 |
BD: { name: "Bangladesh", lat: 24, lng: 90 }, |
| 66 |
LK: { name: "Sri Lanka", lat: 7, lng: 81 }, |
| 67 |
IR: { name: "Iran", lat: 32, lng: 53 }, |
| 68 |
IL: { name: "Israel", lat: 31, lng: 35 }, |
| 69 |
AE: { name: "United Arab Emirates", lat: 24, lng: 54 }, |
| 70 |
SA: { name: "Saudi Arabia", lat: 24, lng: 45 }, |
| 71 |
MX: { name: "Mexico", lat: 23, lng: -102 }, |
| 72 |
AR: { name: "Argentina", lat: -38, lng: -64 }, |
| 73 |
CL: { name: "Chile", lat: -36, lng: -71 }, |
| 74 |
CO: { name: "Colombia", lat: 4, lng: -72 }, |
| 75 |
PE: { name: "Peru", lat: -10, lng: -75 }, |
| 76 |
VE: { name: "Venezuela", lat: 7, lng: -66 }, |
| 77 |
NZ: { name: "New Zealand", lat: -42, lng: 173 } |
| 78 |
}; |
| 79 |
|
| 80 |
export function countryName( code ) { |
| 81 |
if ( ! code ) return ""; |
| 82 |
const upper = code.toUpperCase(); |
| 83 |
if ( COUNTRY_DB[ upper ] ) return COUNTRY_DB[ upper ].name; |
| 84 |
// Browser fallback for codes outside our short table. |
| 85 |
try { |
| 86 |
const n = new Intl.DisplayNames( [ "en" ], { type: "region" } ).of( upper ); |
| 87 |
return n && n !== upper ? n : upper; |
| 88 |
} catch ( e ) { return upper; } |
| 89 |
} |
| 90 |
|
| 91 |
/* Language full name + code label — "English (en)", "Deutsch (de)", etc. |
| 92 |
Uses the browser's Intl.DisplayNames; falls back to a short list, then the |
| 93 |
raw code so we always render something readable. */ |
| 94 |
const LANG_FALLBACK = { |
| 95 |
en: "English", es: "Spanish", fr: "French", de: "German", it: "Italian", |
| 96 |
pt: "Portuguese", nl: "Dutch", sv: "Swedish", no: "Norwegian", da: "Danish", |
| 97 |
fi: "Finnish", pl: "Polish", ru: "Russian", uk: "Ukrainian", cs: "Czech", |
| 98 |
el: "Greek", tr: "Turkish", ar: "Arabic", he: "Hebrew", hi: "Hindi", |
| 99 |
bn: "Bangla", ja: "Japanese", ko: "Korean", zh: "Chinese", id: "Indonesian", |
| 100 |
th: "Thai", vi: "Vietnamese" |
| 101 |
}; |
| 102 |
|
| 103 |
export function languageName( code ) { |
| 104 |
if ( ! code ) return "—"; |
| 105 |
const lower = String( code ).toLowerCase().replace( /[_].*$/, "" ).slice( 0, 5 ); |
| 106 |
try { |
| 107 |
const n = new Intl.DisplayNames( [ "en" ], { type: "language" } ).of( lower ); |
| 108 |
if ( n && n.toLowerCase() !== lower ) return `${ n } (${ lower })`; |
| 109 |
} catch ( e ) {} |
| 110 |
const root = lower.split( "-" )[ 0 ]; |
| 111 |
if ( LANG_FALLBACK[ root ] ) return `${ LANG_FALLBACK[ root ] } (${ lower })`; |
| 112 |
return lower; |
| 113 |
} |
| 114 |
|
| 115 |
/* Client-side CSV download — assemble + trigger via a transient blob URL. */ |
| 116 |
export function downloadCsv( filename, rows ) { |
| 117 |
if ( ! rows || ! rows.length ) return; |
| 118 |
const esc = ( v ) => { |
| 119 |
const s = String( v == null ? "" : v ); |
| 120 |
return /[",\n]/.test( s ) ? `"${ s.replace( /"/g, '""' ) }"` : s; |
| 121 |
}; |
| 122 |
const csv = rows.map( ( r ) => r.map( esc ).join( "," ) ).join( "\n" ); |
| 123 |
const blob = new Blob( [ csv ], { type: "text/csv;charset=utf-8" } ); |
| 124 |
const url = URL.createObjectURL( blob ); |
| 125 |
const a = document.createElement( "a" ); |
| 126 |
a.href = url; a.download = filename; |
| 127 |
document.body.appendChild( a ); a.click(); a.remove(); |
| 128 |
setTimeout( () => URL.revokeObjectURL( url ), 1000 ); |
| 129 |
} |
| 130 |
|