1/* Muxtly global hub sidebar â injected into every page. 2 * Desktop â¥1024px: persistent left column (CSS grid on body.has-sidebar). 3 * Mobile <1024px: hidden, slides out via floating toggle button + backdrop. 4 * 5 * Hub list + subcategories are data-driven below. Subcategory pages are 6 * optional (only generated when a hub has >40 tools); links gracefully 404 7 * until those pages exist, so we only emit subcategory links for hubs that 8 * actually have subcategory pages yet. Set HUBS_WITH_SUBPAGES to expand. 9 */ 10(function () { 11 'use strict'; 12 13 // (hubDir, emoji) + subcategories [subDir, labelKey] 14 // labels resolved per-language via LABELS below; fallback to en. 15 var HUBS = [ 16 { dir: 'health', icon: 'ð¥', subs: [ 17 ['fitness','Fitness'], ['nutrition','Nutrition'], ['pregnancy','Pregnancy'], ['wellness','Wellness'] 18 ]}, 19 { dir: 'finance', icon: 'ð°', subs: [ 20 ['loans','Loans'], ['mortgage','Mortgage'], ['tax','Tax'], ['investing','Investing'], 21 ['salary','Salary'], ['budget','Budget'], ['business','Business'] 22 ]}, 23 { dir: 'math', icon: 'ð¢', subs: [ 24 ['basic','Basic Math'], ['algebra','Algebra & Geometry'], ['statistics','Statistics'], 25 ['electronics','Electronics & RF'], ['science','Physics & Chemistry'], ['engineering','Mechanical'] 26 ]}, 27 { dir: 'date-time', icon: 'ð ', subs: [ 28 ['dates','Dates & Age'], ['time','Time & Zones'], ['japan','Japanese Calendar'] 29 ]}, 30 { dir: 'text-tools', icon: 'ð', subs: [ 31 ['case','Case & Format'], ['count','Counters'], ['convert','Text Converters'] 32 ]}, 33 { dir: 'generators', icon: 'ð ï¸', subs: [ 34 ['password','Password & Security'], ['qr','QR & Barcode'], ['name','Name Generators'] 35 ]}, 36 { dir: 'converters', icon: 'ð', subs: [ 37 ['units','Unit Conversions'], ['digital','Data & Files'], ['japan-units','ShakkanhÅ'], 38 ['japan-daily','Washoku & Rice'], ['japan-lookup','Lookups & Reference'] 39 ]}, 40 { dir: 'construction', icon: 'ðï¸', subs: [ 41 ['concrete','Concrete & Materials'], ['paint','Paint & Flooring'], ['roofing','Roofing & Structure'] 42 ]}, 43 { dir: 'academic', icon: 'ð', subs: [ 44 ['science','Science'], ['writing','Writing & Citation'], ['study','Study Tools'] 45 ]}, 46 { dir: 'fun', icon: 'ð²', subs: [ 47 ['planets','Planet Physics'], ['pets','Pet Ages'], ['celebrations','Holidays'], ['extras','Everyday Fun'] 48 ]} 49 ]; 50 51 // Subcategory links are shown as collapsible <details> for hubs whose 52 // subcategory PAGES exist. Only enable a hub here after its subcat pages 53 // are generated (generate_subcats.py + subcat_defs.py taxonomy). 54 var HUBS_WITH_SUBPAGES = { 55 health:1, finance:1, math:1, 'date-time':1, converters:1, fun:1 56 }; 57 58 // Localized subcategory labels, keyed 'hub/sub'. Mirrors SUBCATS in 59 // tools/ssg/subcat_defs.py (integrator, hub-subcats phase 2). 60 var SUB_LABELS = { 61 'health/fitness': {en:'Fitness & Body Metrics',it:'Fitness e Metriche Corporali',de:'Fitness & Körperwerte',es:'Fitness y métricas',fr:'Fitness et mesures',ja:'ä½çµæã»ãã£ãããã¹',ko:'체격 ë° ì´ë',pt:'Fitness e medidas','zh-CN':'ä½åä¸å¥èº«','zh-TW':'髿 èå¥èº«'}, 62 'health/nutrition': {en:'Nutrition & Diet',it:'Nutrizione e Dieta',de:'Ernährung & Diät',es:'Nutrición y dieta',fr:'Nutrition et régime',ja:'æ é¤ã»é£äº',ko:'ìì ë° ìë¨',pt:'Nutrição e dieta','zh-CN':'è¥å »ä¸é¥®é£','zh-TW':'çé¤è飲é£'}, 63 'health/pregnancy': {en:'Pregnancy & Parenting',it:'Gravidanza e Genitorialità ',de:'Schwangerschaft & Familie',es:'Embarazo y crianza',fr:'Grossesse et parentalité',ja:'å¦å¨ ã»åè²ã¦',ko:'ìì ë° ì¡ì',pt:'Gravidez e parentalidade','zh-CN':'æåä¸è²å¿','zh-TW':'æ·åèè²å '}, 64 'health/wellness': {en:'Everyday Wellness',it:'Benessere Quotidiano',de:'Alltag & Wellness',es:'Bienestar diario',fr:'Bien-être quotidien',ja:'æ¥å¸¸ã®å¥åº·',ko:'ì¼ì ê±´ê°',pt:'Bem-estar diário','zh-CN':'æ¥å¸¸å¥åº·','zh-TW':'æ¥å¸¸å¥åº·'}, 65 'finance/loans': {en:'Loans & Credit',it:'Prestiti e Finanziamenti',de:'Kredite & Darlehen',es:'Préstamos y crédito',fr:'Prêts et crédit',ja:'ãã¼ã³ã»åãå ¥ã',ko:'ëì¶ ë° ì ì©',pt:'Empréstimos e crédito','zh-CN':'贷款ä¸ä¿¡è´·','zh-TW':'貸款è信貸'}, 66 'finance/mortgage': {en:'Mortgages & Home',it:'Mutui e Casa',de:'Hypotheken & Immobilien',es:'Hipotecas y vivienda',fr:'Hypothèques et logement',ja:'ä½å® ãã¼ã³ã»ä½å® è³¼å ¥',ko:'ëª¨ê¸°ì§ ë° ì£¼í',pt:'Hipotecas e moradia','zh-CN':'æ¿è´·ä¸è´æ¿','zh-TW':'æ¿è²¸èè³¼å±'}, 67 'finance/tax': {en:'Taxes & VAT',it:'Tasse e IVA',de:'Steuern & MwSt.',es:'Impuestos e IVA',fr:'Impôts et TVA',ja:'ç¨é',ko:'ì¸ê¸',pt:'Impostos','zh-CN':'ç¨å¡','zh-TW':'ç¨ å'}, 68 'finance/investing':{en:'Investing & Retirement',it:'Investimenti e Pensione',de:'Investieren & Rente',es:'Inversión y jubilación',fr:'Investir et retraite',ja:'æè³ã»å¹´é',ko:'í¬ì ë° ìí´',pt:'Investimento e aposentadoria','zh-CN':'æèµä¸éä¼','zh-TW':'æè³èéä¼'}, 69 'finance/salary': {en:'Salary & Bonuses',it:'Stipendio e Bonus',de:'Gehalt & Boni',es:'Salario y bonos',fr:'Salaire et primes',ja:'給ä¸ã»è³ä¸',ko:'ê¸ì¬ ë° ìì¬',pt:'Salário e bônus','zh-CN':'èªèµä¸å¥é','zh-TW':'èªè³èçé'}, 70 'finance/budget': {en:'Budget & Living Costs',it:'Budget e Costi di Sostentamento',de:'Budget & Lebenshaltung',es:'Presupuesto y gastos',fr:'Budget et dépenses',ja:'å®¶è¨ã»ç活費',ko:'ìì° ë° ìíë¹',pt:'Orçamento e custos','zh-CN':'é¢ç®ä¸çæ´»å¼æ¯','zh-TW':'é ç®èçæ´»éæ¯'}, 71 'finance/business': {en:'Business & Pricing',it:'Attività e Prezzi',de:'Geschäft & Preise',es:'Negocio y precios',fr:'Affaires et prix',ja:'ãã¸ãã¹ã»ä¾¡æ ¼è¨å®',ko:'ë¹ì¦ëì¤ ë° ê°ê²©',pt:'Negócios e preços','zh-CN':'åä¸ä¸å®ä»·','zh-TW':'忥èå®å¹'}, 72 'math/basic': {en:'Basic Math & Numbers',it:'Aritmetica di Base e Numeri',de:'Grundrechnen & Zahlen',es:'Matemáticas básicas',fr:'Maths de base',ja:'åºæ¬è¨ç®ã»æ°',ko:'기본 ìí',pt:'Matemática básica','zh-CN':'åºç¡æ°å¦ä¸æ°å','zh-TW':'åºç¤æ¸å¸èæ¸å'}, 73 'math/algebra': {en:'Algebra & Geometry',it:'Algebra e Geometri
73a',de:'Algebra & Geometrie',es:'Ãlgebra y geometrÃa',fr:'Algèbre et géométrie',ja:'代æ°ã»å¹¾ä½',ko:'ëì ë° ê¸°í',pt:'Ãlgebra e geometria','zh-CN':'代æ°ä¸å ä½','zh-TW':'代æ¸èå¹¾ä½'}, 74 'math/statistics': {en:'Statistics & Probability',it:'Statistica e Probabilità ',de:'Statistik & Wahrscheinlichkeit',es:'EstadÃstica y probabilidad',fr:'Statistiques et probabilités',ja:'çµ±è¨ã»ç¢ºç',ko:'íµê³ ë° íë¥ ',pt:'EstatÃstica e probabilidade','zh-CN':'ç»è®¡ä¸æ¦ç','zh-TW':'çµ±è¨èæ©ç'}, 75 'math/electronics': {en:'Electronics & RF',it:'Elettronica e Radiofrequenza',de:'Elektronik & HF',es:'Electrónica y RF',fr:'Ãlectronique et RF',ja:'é»ååè·¯ã»RF',ko:'ì ì ë° RF',pt:'Eletrônica e RF','zh-CN':'çµåä¸å°é¢','zh-TW':'é»åèå°é »'}, 76 'math/science': {en:'Physics & Chemistry',it:'Fisica e Chimica',de:'Physik & Chemie',es:'FÃsica y quÃmica',fr:'Physique et chimie',ja:'ç©çã»åå¦',ko:'물리 ë° íí',pt:'FÃsica e quÃmica','zh-CN':'ç©çä¸åå¦','zh-TW':'ç©çèåå¸'}, 77 'math/engineering': {en:'Mechanical & Manufacturing',it:'Meccanica e Produzione',de:'Maschinenbau & Fertigung',es:'Mecánica y fabricación',fr:'Mécanique et fabrication',ja:'æ©æ¢°ã»è£½é ',ko:'ê¸°ê³ ë° ì ì¡°',pt:'Mecânica e fabricação','zh-CN':'æºæ¢°ä¸å¶é ','zh-TW':'æ©æ¢°è製é '}, 78 'date-time/dates': {en:'Date & Age Calculators',it:'Calcoli di Date ed Età ',de:'Datum- und Altersrechner',es:'Fechas y edad',fr:'Dates et âge',ja:'æ¥ä»ã»å¹´é½¢è¨ç®',ko:'ë ì§ ë° ëì´',pt:'Datas e idade','zh-CN':'æ¥æä¸å¹´é¾','zh-TW':'æ¥æè年齡'}, 79 'date-time/time': {en:'Time, Zones & World Clock',it:'Ora, Fusi Orari e Orologio Mondiale',de:'Zeit, Zonen & Weltzeit',es:'Hora, zonas y reloj mundial',fr:'Heure, fuseaux et horloge',ja:'æå»ã»ã¿ã¤ã ã¾ã¼ã³',ko:'ìê° ë° ìê°ë',pt:'Hora, fusos e relógio','zh-CN':'æ¶é´ä¸æ¶åº','zh-TW':'æéèæå'}, 80 'date-time/japan': {en:'Japanese Calendar & Traditions',it:'Calendario e Tradizioni Giapponesi',de:'Japanischer Kalender',es:'Calendario japonés',fr:'Calendrier japonais',ja:'æ¥æ¬ã®æ¦ã»ä¼çµ±',ko:'ì¼ë³¸ ë¬ë ¥',pt:'Calendário japonês','zh-CN':'æ¥æ¬åæ³ä¸ä¼ ç»','zh-TW':'æ¥æ¬ææ³èå³çµ±'}, 81 'converters/units': {en:'Everyday Unit Conversions',it:'Conversioni di Unità Quotidiane',de:'Alltagseinheiten',es:'Unidades cotidianas',fr:'Unités du quotidien',ja:'åä½å¤æ',ko:'ë¨ì ë³í',pt:'Unidades cotidianas','zh-CN':'常ç¨åä½è½¬æ¢','zh-TW':'常ç¨å®ä½è½æ'}, 82 'converters/digital':{en:'Data & File Conversion',it:'Dati e Conversione File',de:'Daten & Dateien',es:'Datos y archivos',fr:'Données et fichiers',ja:'ãã¼ã¿ã»ãã¡ã¤ã«',ko:'ë°ì´í° ë° íì¼',pt:'Dados e arquivos','zh-CN':'æ°æ®ä¸æä»¶','zh-TW':'è³æèæªæ¡'}, 83 'converters/japan-units':{en:'ShakkanhÅ Japanese Units',it:'Shakkanho (unità giapponesi)',de:'ShakkanhÅ (japan. Einheiten)',es:'Unidades japonesas',fr:'Unités japonaises',ja:'尺貫æ³',ko:'ì¤ì¹¸í¸(ì¼ë³¸ ë¨ì)',pt:'Unidades japonesas','zh-CN':'æ¥æ¬å°ºè´¯æ³åä½','zh-TW':'æ¥æ¬å°ºè²«æ³å®ä½'}, 84 'converters/japan-daily':{en:'Washoku & Rice',it:'Washoku e Riso',de:'Washoku & Reis',es:'Washoku y arroz',fr:'Washoku et riz',ja:'åé£ã»ç±³',ko:'ìì¼ì¿ ì ì',pt:'Washoku e arroz','zh-CN':'åé£ä¸ç±³','zh-TW':'åé£èç±³'}, 85 'converters/japan-lookup':{en:'Lookups & Reference',it:'Ricerche e Reference',de:'Nachschlage-Tools',es:'Búsquedas y referencia',fr:'Recherches et référence',ja:'æ¤ç´¢ã»åç §',ko:'ì¡°í ë° ì°¸ì¡°',pt:'Buscas e referência','zh-CN':'æ¥è¯¢ä¸åè','zh-TW':'æ¥è©¢èåè'}, 86 'fun/planets': {en:'Planet Physics',it:'Fisica dei Pianeti',de:'Planetenphysik',es:'FÃsica planetaria',fr:'Physique planétaire',ja:'ææç©ç',ko:'íì± ë¬¼ë¦¬',pt:'FÃsica planetária','zh-CN':'æçç©ç','zh-TW':'æçç©ç'}, 87 'fun/pets': {en:'Pet Age Calculators',it:'Età degli Animali Domestici',de:'Haustieralter',es:'Edad de mascotas',fr:'Ãge des animaux',ja:'ãããã®å¹´é½¢',ko:'ë°ë ¤ë물 ëì´',pt:'Idade de pets','zh-CN':'å® ç©å¹´é¾','zh-TW':'寵ç©å¹´é½¡'}, 88 'fun/celebrations': {en:'Holidays & Milestones',it:'Feste e Traguardi',de:'Feiertage & Meilensteine',es:'Fiestas y hitos',fr:'Fêtes et jalons',ja:'ç¥æ¥ã»è¨å¿µæ¥',ko:'기ë ì¼ ë° ì¶í',pt:'Feriados e mar
88cos','zh-CN':'èæ¥ä¸çºªå¿µ','zh-TW':'ç¯æ¥èç´å¿µ'}, 89 'fun/extras': {en:'Everyday Fun',it:'Svago Quotidiano',de:'AlltagsspaÃ',es:'Diversión diaria',fr:'Amusement quotidien',ja:'æ¥å¸¸ã®æ¥½ãã¿',ko:'ì¼ì ì¬ë¯¸',pt:'Diversão diária','zh-CN':'æ¥å¸¸è¶£å³','zh-TW':'æ¥å¸¸è¶£å³'} 90 }; 91 92 // Localized hub names (mirrors CAT_NAMES in generate_v2_hubs.py) 93 var HUB_LABELS = { 94 health: {en:'Health',it:'Salute',de:'Gesundheit',es:'Salud',fr:'Santé',ja:'å¥åº·',ko:'ê±´ê°',pt:'Saúde','zh-CN':'å¥åº·','zh-TW':'å¥åº·'}, 95 finance: {en:'Finance',it:'Finanza',de:'Finanzen',es:'Finanzas',fr:'Finance',ja:'éè',ko:'ê¸ìµ',pt:'Finanças','zh-CN':'è´¢å¡','zh-TW':'財å'}, 96 math: {en:'Math',it:'Matematica',de:'Mathe',es:'Matemáticas',fr:'Maths',ja:'æ°å¦',ko:'ìí',pt:'Matemática','zh-CN':'æ°å¦','zh-TW':'æ¸å¸'}, 97 'date-time':{en:'Date & Time',it:'Data e ora',de:'Datum & Zeit',es:'Fecha y Hora',fr:'Date et Heure',ja:'æ¥ä»ã¨æå»',ko:'ë ì§ì ìê°',pt:'Data e Hora','zh-CN':'æ¥æä¸æ¶é´','zh-TW':'æ¥æèæé'}, 98 'text-tools':{en:'Text Tools',it:'Strumenti di testo',de:'Textwerkzeuge',es:'Herramientas de Texto',fr:'Outils Texte',ja:'ããã¹ããã¼ã«',ko:'í ì¤í¸ ë구',pt:'Ferramentas de Texto','zh-CN':'ææ¬å·¥å ·','zh-TW':'æåå·¥å ·'}, 99 generators:{en:'Generators',it:'Generatori',de:'Generatoren',es:'Generadores',fr:'Générateurs',ja:'ã¸ã§ãã¬ã¼ã¿ã¼',ko:'ìì±ê¸°',pt:'Geradores','zh-CN':'çæå¨','zh-TW':'ç¢çå¨'}, 100 converters:{en:'Converters',it:'Convertitori',de:'Konverter',es:'Convertidores',fr:'Convertisseurs',ja:'ã³ã³ãã¼ã¿ã¼',ko:'ë³í기',pt:'Conversores','zh-CN':'转æ¢å¨','zh-TW':'è½æå¨'}, 101 construction:{en:'Construction',it:'Costruzione',de:'Bauwesen',es:'Construcción',fr:'Construction',ja:'建ç¯',ko:'ê±´ì¤',pt:'Construção','zh-CN':'建ç','zh-TW':'建ç¯'}, 102 academic:{en:'Academic',it:'Studio',de:'Akademisch',es:'Académico',fr:'Académique',ja:'å¦è¡',ko:'íì ',pt:'Acadêmico','zh-CN':'妿¯','zh-TW':'å¸è¡'}, 103 fun: {en:'Fun & Games',it:'Divertimento',de:'SpaÃ',es:'Juegos',fr:'Jeux',ja:'楽ãããã¼ã«',ko:'ì¬ë¯¸',pt:'Diversão','zh-CN':'è¶£å³','zh-TW':'è¶£å³'} 104 }; 105 106 // Country-specific tool hubs (only those with a real landing page) 107 var COUNTRIES = [ 108 { dir: 'japan-tools', icon: 'ð¯ðµ' }, 109 { dir: 'uk-tools', icon: 'ð¬ð§' }, 110 { dir: 'spain-tools', icon: 'ðªð¸' }, 111 { dir: 'germany-tools', icon: 'ð©ðª' }, 112 { dir: 'portugal-tools', icon: 'ðµð¹' }, 113 { dir: 'australia-tools', icon: 'ð¦ðº' }, 114 { dir: 'us-tools', icon: 'ðºð¸' }, 115 { dir: 'canada-tools', icon: 'ð¨ð¦' }, 116 { dir: 'korea-tools', icon: 'ð°ð·' }, 117 { dir: 'france-tools', icon: 'ð«ð·' } 118 ]; 119 120 // Per-language page availability for staged PILOT languages. Languages not 121 // listed here (en + the 9 core languages) have every page, so the sidebar 122 // renders full navigation. For a pilot language the sidebar hides any hub / 123 // country / collection whose page does not exist yet instead of rendering 124 // 404 links, and keeps subcategory expanders off until subcat pages are 125 // generated for it. 126 // MAINTENANCE: when a pilot language gains new hub / country / collection 127 // pages, update its entry (see docs/projects/toolhub/ 128 // next-language-rollout-playbook.md §4). Last updated 2026-09-10 (nl pilot). 129 var LANG_PAGES = { 130 it: { 131 hubs: ['health','finance','math','date-time','text-tools','generators', 132 'converters','construction','academic','fun'], 133 countries: [ // all it country hubs now generated 134 { dir: 'japan-tools', icon: 'ð¯ðµ' }, { dir: 'uk-tools', icon: 'ð¬ð§' }, 135 { dir: 'spain-tools', icon: 'ðªð¸' }, { dir: 'germany-tools', icon: 'ð©ðª' }, 136 { dir: 'portugal-tools', icon: 'ðµð¹' }, { dir: 'australia-tools', icon: 'ð¦ðº' }, 137 { dir: 'us-tools', icon: 'ðºð¸' }, { dir: 'canada-tools', icon: 'ð¨ð¦' }, 138 { dir: 'korea-tools', icon: 'ð°ð·' }, { dir: 'france-tools', icon: 'ð«ð·' } 139 ], 140 collections: [ // all it collection pages now generated 141 { slug: 'exam-prep', icon: 'ð' }, 142 { slug: 'home-renovation', icon: 'ðï¸' }, 143 { slug: 'health-check', icon: 'ð' } 144 ], 145 subs: true // it subcategory pages generated (2026-09-15) 146 }, 147 nl: { 148 hubs: ['health','finance','math','date-time','generators', 149 'converters','academic','fun'], 150 countries: [], 151 collections: [], 152 subs: false 153 } 154 }; 155 156 var COUNTRY_LABELS = { 157 'japan-tools': {en:'Japan Tools',it:'Strumenti Giappone',de:'Japan-Tools',es:'Herramientas de Japón',fr:'Outils Japon',ja:'æ¥æ¬åããã¼ã«',ko:'ì¼ë³¸ ë구',pt:'Ferramentas do Japão','zh-CN':'æ¥æ¬å·¥å ·','zh-TW':'æ¥æ¬å·¥å ·'}, 158 'uk-tools': {en:'UK Tools',it:'Strumenti Regno Unito',de:'UK-Tools',es:'Herramientas de Reino Unido',fr:'Outils Royaume-Uni',ja:'è±å½åããã¼ã«',ko:'ìêµ ë구',pt:'Ferramentas do Reino Unido','zh-CN':'è±å½å·¥å ·','zh-TW':'è±åå·¥å ·'}, 159 'spain-tools': {en:'Spain Tools',it:'Strumenti Spagna',de:'Spanien-Tools',es:'Herramientas de España',fr:'Outils Espagne',ja:'ã¹ãã¤ã³ã®ãã¼ã«',ko:'ì¤íì¸ ë구',pt:'Ferramentas da Esp
159anha','zh-CN':'西ççå·¥å ·','zh-TW':'西ççå·¥å ·'}, 160 'germany-tools': {en:'Germany Tools',it:'Strumenti Germania',de:'Deutschland-Tools',es:'Herramientas de Alemania',fr:'Outils Allemagne',ja:'ãã¤ãã®ãã¼ã«',ko:'ë ì¼ ë구',pt:'Ferramentas da Alemanha','zh-CN':'å¾·å½å·¥å ·','zh-TW':'å¾·åå·¥å ·'}, 161 'portugal-tools': {en:'Portugal Tools',it:'Strumenti Portogallo',de:'Portugal-Tools',es:'Herramientas de Portugal',fr:'Outils Portugal',ja:'ãã«ãã¬ã«ã®ãã¼ã«',ko:'í¬ë¥´í¬ê° ë구',pt:'Ferramentas de Portugal','zh-CN':'è¡èçå·¥å ·','zh-TW':'è¡èçå·¥å ·'}, 162 'australia-tools': {en:'Australia Tools',it:'Strumenti Australia',de:'Australien-Tools',es:'Herramientas de Australia',fr:'Outils Australie',ja:'ãªã¼ã¹ãã©ãªã¢ã®ãã¼ã«',ko:'í¸ì£¼ ë구',pt:'Ferramentas da Austrália','zh-CN':'澳大å©äºå·¥å ·','zh-TW':'æ¾³æ´²å·¥å ·'}, 163 'us-tools': {en:'United States Tools',it:'Strumenti Stati Uniti',de:'USA-Tools',es:'Herramientas de Estados Unidos',fr:'Outils Etats-Unis',ja:'ã¢ã¡ãªã«ã®ãã¼ã«',ko:'ë¯¸êµ ë구',pt:'Ferramentas dos Estados Unidos','zh-CN':'ç¾å½å·¥å ·','zh-TW':'ç¾åå·¥å ·'}, 164 'canada-tools': {en:'Canada Tools',it:'Strumenti Canada',de:'Kanada-Tools',es:'Herramientas de Canadá',fr:'Outils Canada',ja:'ã«ããã®ãã¼ã«',ko:'ìºëë¤ ë구',pt:'Ferramentas do Canadá','zh-CN':'å æ¿å¤§å·¥å ·','zh-TW':'å æ¿å¤§å·¥å ·'}, 165 'korea-tools': {en:'Korea Tools',it:'Strumenti Corea',de:'Korea-Tools',es:'Herramientas de Corea',fr:'Outils Corée',ja:'éå½ã®ãã¼ã«',ko:'íêµ ë구',pt:'Ferramentas da Coreia','zh-CN':'é©å½å·¥å ·','zh-TW':'éåå·¥å ·'}, 166 'france-tools': {en:'France Tools',it:'Strumenti Francia',de:'Frankreich-Tools',es:'Herramientas de Francia',fr:'Outils France',ja:'ãã©ã³ã¹ã®ãã¼ã«',ko:'íëì¤ ë구',pt:'Ferramentas da França','zh-CN':'æ³å½å·¥å ·','zh-TW':'æ³åå·¥å ·'} 167 }; 168 169 // Curated collections (mirror HOMEPAGE_COLLECTIONS in generate_v2_hubs.py) 170 var COLLECTIONS = [ 171 { slug: 'exam-prep', icon: 'ð' }, 172 { slug: 'home-renovation', icon: 'ðï¸' }, 173 { slug: 'health-check', icon: 'ð' } 174 ]; 175 var COLLECTION_LABELS = { 176 'exam-prep': {en:'Exam Preparation',it:'Preparazione agli Esami',de:'Prüfungsvorbereitung',es:'Preparación de Exámenes',fr:'Préparation aux Examens',ja:'試é¨å¯¾ç',ko:'ìí ì¤ë¹',pt:'Preparação de Provas','zh-CN':'èè¯åå¤','zh-TW':'è試æºå'}, 177 'home-renovation': {en:'Home Renovation',it:'Ristrutturazione della Casa',de:'Hausrenovierung',es:'Renovación del Hogar',fr:'Rénovation de la Maison',ja:'ãã¼ã ãªããã¼ã·ã§ã³',ko:'ì§ ì리',pt:'Reforma da Casa','zh-CN':'å®¶å± è£ ä¿®','zh-TW':'å®¶å± è£ä¿®'}, 178 'health-check': {en:'Health Check',it:'Controllo della Salute',de:'Gesundheitscheck',es:'Chequeo de Salud',fr:'Bilan de Santé',ja:'å¥åº·è¨ºæ',ko:'ê±´ê° ì¡°í',pt:'Checagem de Saúde','zh-CN':'å¥åº·æ£æ¥','zh-TW':'å¥åº·æª¢æ¥'} 179 }; 180 181 var SUB_EMOJIS = { 182 'health/fitness':'ðª','health/nutrition':'ð¥','health/pregnancy':'ð¤°','health/wellness':'ð', 183 'finance/loans':'ð³','finance/mortgage':'ð ','finance/tax':'ð§¾','finance/investing':'ð', 184 'finance/salary':'ð¼','finance/budget':'ð','finance/business':'ðª', 185 'math/basic':'â','math/algebra':'ð','math/statistics':'ð','math/electronics':'â¡', 186 'math/science':'ð¬','math/engineering':'âï¸', 187 'date-time/dates':'ðï¸','date-time/time':'ð','date-time/japan':'ð', 188 'converters/units':'ð','converters/digital':'ð¾','converters/japan-units':'ð¯', 189 'converters/japan-daily':'ð','converters/japan-lookup':'ð¾', 190 'fun/planets':'ðª','fun/pets':'ð¾','fun/celebrations':'ð','fun/extras':'ð²' 191 }; 192 193 var UI = { 194 categories: {en:'Categories',it:'Categorie',de:'Kategorien',es:'CategorÃas',fr:'Catégories',ja:'ã«ãã´ãªã¼',ko:'ì¹´í ê³ ë¦¬',pt:'Categorias','zh-CN':'åç±»','zh-TW':'åé¡'}, 195 by_country: {en:'By Country / Region',it:'Per paese / regione',de:'Nach Land / Region',es:'Por PaÃ
195s / Región',fr:'Par Pays / Région',ja:'å½ã»å°åå¥',ko:'êµê°ë³',pt:'Por PaÃs / Região','zh-CN':'æå½å®¶/å°åº','zh-TW':'æåå®¶/å°å'}, 196 collections:{en:'Curated Collections',it:'Collezioni selezionate',de:'Kuratierte Sammlungen',es:'Colecciones Curadas',fr:'Collections Sélectionnées',ja:'å³é¸ã³ã¬ã¯ã·ã§ã³',ko:'ìì 컬ë ì ',pt:'Coleções Curadas','zh-CN':'ç²¾éå·¥å ·é','zh-TW':'ç²¾é¸å·¥å ·é'} 197 }; 198 199 function langOf() { 200 var h = String(document.documentElement.lang || 'en'); 201 if (h.indexOf('zh-Hant') === 0 || h === 'zh-TW') return 'zh-TW'; 202 if (h.indexOf('zh-Hans') === 0 || h === 'zh-CN') return 'zh-CN'; 203 return h.split('-')[0]; 204 } 205 function L(dict, lang) { return dict[lang] || dict.en; } 206 207 // Detect current hub + sub from URL: /{lang?}/{hub}/({sub}/)?({slug}/)? 208 function currentContext(langPrefix, lang) { 209 var p = location.pathname; 210 var rest = langPrefix ? p.replace(/^\//, '') : p.replace(/^\//, ''); 211 // strip lang prefix 212 if (langPrefix && rest.indexOf(langPrefix + '/') === 0) rest = rest.slice(langPrefix.length + 1); 213 var parts = rest.split('/').filter(Boolean); 214 var hub = null, sub = null; 215 for (var i = 0; i < HUBS.length; i++) { 216 if (parts[0] === HUBS[i].dir) { 217 hub = HUBS[i].dir; 218 if (parts[1] && HUBS[i].subs.some(function (s) { return s[0] === parts[1]; })) sub = parts[1]; 219 break; 220 } 221 } 222 // tool page: no hub in URL â derive from meta breadcrumb_cat if present 223 if (!hub) { 224 var bc = document.querySelector('nav[aria-label="Breadcrumb"] li[aria-current="page"]'); 225 // fallback: read data-category from a tool page's breadcrumb_cat via body data attr 226 var dc = document.body.getAttribute('data-category'); 227 if (dc) hub = dc; 228 } 229 return { hub: hub, sub: sub }; 230 } 231 232 /* perf(batch46-B, 2026-08-17): mobile lazy-build. 233 * The sidebar is display:none below 1024px (fixed drawer, hidden until the 234 * floating toggle is tapped) â building 60+ nav nodes at DOMContentLoaded was 235 * pure main-thread waste on every mobile page view (PageSpeed TBT regression 236 * after the 2026-08-14/15 site-wide sidebar + subcats rollout). 237 * Mobile now creates only the toggle button + backdrop; the full sidebar DOM 238 * is built on first toggle tap (one frame) or when the viewport crosses 239 * 1024px. Desktop builds immediately, unchanged. */ 240 241 function build() { 242 if (document.getElementById('muxtly-sidebar-wrap')) return; 243 var lang = langOf(); 244 var langPrefix = (lang === 'en') ? '' : '/' + lang; 245 246 var wrap = document.createElement('aside'); 247 wrap.className = 'muxtly-sidebar-wrap'; 248 wrap.id = 'muxtly-sidebar-wrap'; 249 wrap.setAttribute('aria-label', 'Categories'); 250 251 var nav = document.createElement('nav'); 252 nav.className = 'muxtly-sidebar'; 253 var heading = document.createElement('div'); 254 heading.className = 'sb-heading'; 255 heading.textContent = L(UI.categories, lang); 256 nav.appendChild(heading); 257 258 var ctx = currentContext(langPrefix, lang); 259 260 // Staged pilot languages: hide nav entries whose pages don't exist yet 261 // (see LANG_PAGES above). Core languages get the full navigation. 262 var avail = LANG_PAGES[lang] || null; 263 var availHubs = (avail && avail.hubs) ? avail.hubs : null; 264 var availCountries = avail ? (avail.countries || []) : COUNTRIES; 265 var availCollections = avail ? (avail.collections || []) : COLLECTIONS; 266 var subsEnabled = !avail || avail.subs !== false; 267 268 HUBS.forEach(function (hub) { 269 if (availHubs && availHubs.indexOf(hub.dir) < 0) return; 270 var hasSubs = HUBS_WITH_SUBPAGES[hub.dir] && hub.subs.length && subsEnabled; 271 var hubHref = langPrefix + '/' + hub.dir + '/'; 272 var hubActive = (ctx.hub === hub.dir && !ctx.sub) ? ' active' : ''; 273 var li = document.createElement('div'); 274 li.className = 'sb-section'; 275 276 if (hasSubs) { 277 var det = document.createElement('details'); 278 if (ctx.hub === hub.dir) det.setAttribute('open', ''); 279 var sum = document.createElement('summary'); 280 // The category title is the summary header itself. Clicking the 281 // label navigates to the hub landing page; the separate caret 282 // toggles the subcategory list. This avoids rendering the hub 283 // title twice (header + a duplicate first row). 284 var sumLink = document.createElement('span'); 285 sumLink.className = 'sb-hub-link' + hubActive; 286 sumLink.innerHTML = '<span class="sb-hub-icon">' + hub.icon + '</span><span>' + L(HUB_LABELS[hub.dir] || {}, lang) + '</span>'; 287 var caret = document.createElement('span'); 288 caret.className = 'sb-caret'; 289 caret.textContent = 'â¾'; 290 sum.appendChild(sumLink); 291 sum.appendChild(caret); 292 det.appendChild(sum); 293 sum.addEventListener('click', function (e) { 294 if (e.target.closest('.sb-hub-link')) { 295 e.preventDefault(); // suppress native details toggle 296 location.href = hubHref; // go to the hub landing page 297 } 298 // clicking the caret (outside .sb-hub-link) â native toggle 299 }); 300 var ul = document.createElement('ul'); 301 ul.className = 'sb-sub';
302 hub.subs.forEach(function (s) { 303 var subLi = document.createElement('li'); 304 var subA = document.createElement('a'); 305 subA.className = 'sb-sub-link' + (ctx.sub === s[0] ? ' active' : ''); 306 subA.href = hubHref + s[0] + '/'; 307 var skey = hub.dir + '/' + s[0]; 308 if (SUB_LABELS[skey]) { 309 subA.innerHTML = '<span class="sb-hub-icon">' + (SUB_EMOJIS[skey] || '') + '</span>' + L(SUB_LABELS[skey], lang); 310 } else { 311 subA.textContent = s[1]; 312 } 313 subLi.appendChild(subA); 314 ul.appendChild(subLi); 315 }); 316 det.appendChild(ul); 317 li.appendChild(det); 318 } else { 319 var flat = document.createElement('a'); 320 flat.className = 'sb-hub-link' + hubActive; 321 flat.href = hubHref; 322 flat.innerHTML = '<span class="sb-hub-icon">' + hub.icon + '</span><span>' + L(HUB_LABELS[hub.dir] || {}, lang) + '</span>'; 323 li.appendChild(flat); 324 } 325 nav.appendChild(li); 326 }); 327 328 // Helper: build a collapsible <details> group (summary + child links). 329 // `isOpen` controls default expand state. The <summary> toggles 330 // expand/collapse natively; child <a> links handle navigation. 331 function mkCollapsible(icon, label, children, isOpen) { 332 var sec = document.createElement('div'); 333 sec.className = 'sb-section'; 334 var det = document.createElement('details'); 335 if (isOpen) det.setAttribute('open', ''); 336 var sum = document.createElement('summary'); 337 sum.innerHTML = '<span class="sb-hub-link"><span class="sb-hub-icon">' + icon + '</span><span>' + label + '</span></span>'; 338 det.appendChild(sum); 339 var ul = document.createElement('ul'); 340 ul.className = 'sb-sub'; 341 children.forEach(function (ch) { 342 var li2 = document.createElement('li'); 343 var a2 = document.createElement('a'); 344 a2.className = 'sb-sub-link' + (ch.active ? ' active' : ''); 345 a2.href = ch.href; 346 a2.innerHTML = '<span class="sb-hub-icon">' + ch.icon + '</span>' + ch.label; 347 li2.appendChild(a2); 348 ul.appendChild(li2); 349 }); 350 det.appendChild(ul); 351 sec.appendChild(det); 352 return sec; 353 } 354 355 var path = location.pathname; 356 357 // --- Country-specific tools section (collapsible, default folded) --- 358 if (availCountries.length) { 359 var cKids = availCountries.map(function (c) { 360 var href = langPrefix + '/' + c.dir + '/'; 361 return { href: href, icon: c.icon, label: L(COUNTRY_LABELS[c.dir] || {}, lang), active: path.indexOf('/' + c.dir + '/') > -1 }; 362 }); 363 var cAnyActive = cKids.some(function (k) { return k.active; }); 364 nav.appendChild(mkCollapsible('ð', L(UI.by_country, lang), cKids, cAnyActive)); 365 } 366 367 // --- Curated collections section (collapsible, default folded) --- 368 if (availCollections.length) { 369 var colKids = availCollections.map(function (col) { 370 var href = langPrefix + '/collections/' + col.slug + '/'; 371 return { href: href, icon: col.icon, label: L(COLLECTION_LABELS[col.slug] || {}, lang), active: path.indexOf('/collections/' + col.slug + '/') > -1 }; 372 }); 373 var colAnyActive = colKids.some(function (k) { return k.active; }); 374 nav.appendChild(mkCollapsible('â', L(UI.collections, lang), colKids, colAnyActive)); 375 } 376 377 // Accordion: opening one group (category / country / collections) 378 // collapses the currently open group, keeping the sidebar short. 379 var groupDets = nav.querySelectorAll('details'); 380 Array.prototype.forEach.call(groupDets, function (det) { 381 det.addEventListener('toggle', function () { 382 if (!det.open) return; 383 Array.prototype.forEach.call(groupDets, function (other) { 384 if (other !== det && other.open) other.removeAttribute('open'); 385 }); 386 }); 387 }); 388 389 wrap.appendChild(nav); 390 391 // backdrop + toggle (mobile) â reuse the lazy-created chrome when present. 392 // When reusing, do NOT attach another click listener: the lazy path's own 393 // handler (which calls build() first, then toggles) keeps working because 394 // build() is idempotent. Attaching twice would double-fire on one tap. 395 var toggleCreated = false;
396 var backdrop = document.getElementById('muxtly-sidebar-backdrop'); 397 if (!backdrop) { 398 backdrop = document.createElement('div'); 399 backdrop.className = 'muxtly-sidebar-backdrop'; 400 backdrop.id = 'muxtly-sidebar-backdrop'; 401 document.body.appendChild(backdrop); 402 } 403 var toggle = document.getElementById('muxtly-sidebar-toggle'); 404 if (!toggle) { 405 toggle = document.createElement('button'); 406 toggle.className = 'muxtly-sidebar-toggle'; 407 toggle.id = 'muxtly-sidebar-toggle'; 408 toggle.type = 'button'; 409 toggle.setAttribute('aria-label', 'Toggle categories'); 410 toggle.innerHTML = '☰'; 411 document.body.appendChild(toggle); 412 toggleCreated = true; 413 } 414 415 function openDrawer(o) { 416 wrap.classList.toggle('open', o); 417 backdrop.classList.toggle('open', o); 418 } 419 if (toggleCreated) { 420 toggle.addEventListener('click', function () { openDrawer(!wrap.classList.contains('open')); }); 421 backdrop.addEventListener('click', function () { openDrawer(false); }); 422 } 423 // close drawer on nav (clicks inside sidebar navigate away anyway, but for hash/anchors) 424 wrap.addEventListener('click', function (e) { 425 if (e.target.closest('a') && window.matchMedia('(max-width:1023px)').matches) openDrawer(false); 426 }); 427 428 // Insert sidebar as first body child so CSS grid area applies 429 document.body.insertBefore(wrap, document.body.firstChild); 430 document.body.classList.add('has-sidebar'); 431 } 432 433 function init() { 434 var mqWide = (window.matchMedia && window.matchMedia('(min-width:1024px)')) || null; 435 436 if (!mqWide || mqWide.matches) { 437 build(); // desktop: sidebar is visible layout, build now (unchanged path) 438 if (mqWide && mqWide.addEventListener) { 439 mqWide.addEventListener('change', function (e) { if (e.matches) build(); }); 440 } 441 return; 442 } 443 444 // Mobile: create only the floating toggle + backdrop now (2 nodes); 445 // the full sidebar DOM is built on first toggle tap. 446 if (document.getElementById('muxtly-sidebar-toggle')) return; // idempotence 447 var toggle = document.createElement('button'); 448 toggle.className = 'muxtly-sidebar-toggle'; 449 toggle.id = 'muxtly-sidebar-toggle'; 450 toggle.type = 'button'; 451 toggle.setAttribute('aria-label', 'Toggle categories'); 452 toggle.innerHTML = '☰'; 453 var backdrop = document.createElement('div'); 454 backdrop.className = 'muxtly-sidebar-backdrop'; 455 backdrop.id = 'muxtly-sidebar-backdrop'; 456 457 toggle.addEventListener('click', function () { 458 build(); // one-time, synchronous; then toggle the drawer 459 var wrap = document.getElementById('muxtly-sidebar-wrap'); 460 if (!wrap) return; 461 var open = !wrap.classList.contains('open'); 462 wrap.classList.toggle('open', open); 463 backdrop.classList.toggle('open', open); 464 }); 465 backdrop.addEventListener('click', function () { 466 var wrap = document.getElementById('muxtly-sidebar-wrap'); 467 if (wrap) wrap.classList.remove('open'); 468 backdrop.classList.remove('open'); 469 }); 470 471 document.body.appendChild(backdrop); 472 document.body.appendChild(toggle); 473 474 // Rotate/resize to desktop: build the sidebar so the grid column appears. 475 if (mqWide.addEventListener) { 476 mqWide.addEventListener('change', function (e) { if (e.matches) build(); }); 477 } 478 } 479 480 if (document.readyState === 'loading') { 481 document.addEventListener('DOMContentLoaded', init); 482 } else { 483 init(); 484 } 485})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.