PageSourceSearch

https://muxtly.pages.dev/assets/js/sidebar.js

js muxtly.pages.dev collected 2026-10-03 05:40:44 UTC 33,302 bytes, 485 lines download raw bytes

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 = '&#9776;';
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 = '&#9776;';
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.