1(function(){ 2/* !=================== GLOBAL SHARED STATE =================== */ 3const LANGUAGE_JSON_URL='https://assets.derekprince.com/test/site-languages.json'; 4const SOFT_COOKIE_NAME='geo-location-soft'; 5const HARD_COOKIE_NAME='geo-location'; 6const DISMISSED_COOKIE_NAME='locale-Closed'; 7const COOKIE_MAX_AGE=60*60*24*730; 8const DISMISSED_COOKIE_MAX_AGE=60*60*24*60; // 60 days 9// Pages where specific content is available in certain languages, otherwise will send home on language switch 10const LANGUAGES_WITH_NEWS = ['en', 'es']; 11// Languages where specific content is available in certain languages, otherwise will send home on language switch 12const LANGUAGES_WITH_COURSES = ['en', 'fr']; // Add more language codes as needed 13 14// Pages that are global-language (no country-specific versions) 15const GLOBAL_LANGUAGE_PAGES = ['outreaches', 'courses', 'outreach-prayer-diary']; 16// Pages that are only available in English 17// 1. Suppress locale notification when switching between English country variants (en-nz â en-global) 18// 2. Redirect to home page when switching to non-English language from these pages 19const GLOBAL_ENGLISH_ONLY_PAGES = [ 20 'sermons', 'videos', 'inspirations', 'sermon-series', 'word', 'shorts', 'books', 21 'devotional-library', 'devotional-series', 'devotions', 'teaching', 'devotionals', 22 'radio', 'contributors', 'radio-series', 'radio-uk', 'radio-usa', 'cards', 'outreach-prayer-diary', 'outlines', 'licenses', 'legal/licenses' 23]; 24// Pages where locale notification should always be suppressed (available in any language) 25const SUPPRESS_NOTIFICATION_PAGES = ['news']; 26 27// Helper: checks if path segments match any GLOBAL_ENGLISH_ONLY_PAGES entry (supports multi-segment slugs like 'legal/licenses') 28function isEnglishOnlySlug(restParts) { 29 const first = restParts[0] || ''; 30 if (GLOBAL_ENGLISH_ONLY_PAGES.includes(first)) return true; 31 const path = restParts.join('/'); 32 return GLOBAL_ENGLISH_ONLY_PAGES.some(s => s.includes('/') && (path === s || path.startsWith(s + '/'))); 33} 34 35let LANG_DATA=[]; 36let SORTED_LANGS=[]; 37const collator=new Intl.Collator(undefined,{sensitivity:'base'}); 38let pickerInstanceCount=0; 39 40/* =================== SHARED UTILITY FUNCTIONS =================== */ 41function writeCookie(name,value){ 42 const secure = location.protocol === 'https:' ? ';Secure' : ''; 43 document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${COOKIE_MAX_AGE};SameSite=Lax${secure}`; 44} 45 46function writeCookieWithExpiry(name,value,maxAge){ 47 const secure = location.protocol === 'https:' ? ';Secure' : ''; 48 document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax${secure}`; 49} 50 51function readCookie(name){ 52 return document.cookie.split(';').map(p=>p.trim()).reduce((acc,p)=>{ 53 if(acc) return acc; 54 const idx=p.indexOf('='); 55 if(idx>-1 && p.slice(0,idx)===name) return decodeURIComponent(p.slice(idx+1)); 56 return acc; 57 },''); 58} 59 60function escapeHTML(str=''){ 61 return str.replace(/[&<>"']/g,s=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[s])); 62} 63function escapeAttr(str=''){ return escapeHTML(str); } 64 65/** 66 * Formats a sentence with variable replacement. 67 * @param {string} text - The text containing variables in the form [[variableName]] 68 * @param {Object} variables - Object mapping variable names to their values 69 * @returns {string} The formatted text with variables replaced 70 */ 71function internationalizedSentence(text, variables){ 72 if(!text) return ''; 73 return text.replace(/\[\[(\w+)\]\]/g, (match, key) => { 74 return variables[key] !== undefined ? variables[key] : match; 75 }); 76} 77 78/* =================== INITIALIZE GLOBAL DATA ONCE =================== */ 79async function initGlobalData(){ 80 if(LANG_DATA.length > 0) return; 81 try{ 82 const IS_PRODUCTION = location.hostname === 'www.derekprince.com' || location.hostname === 'derekprince.com'; 83 const res=await fetch(LANGUAGE_JSON_URL,{cache:'no-store'}); 84 if(!res.ok) throw new Error(res.status); 85 LANG_DATA=await res.json(); 86 87 LANG_DATA=LANG_DATA.map(l=>({ 88 code:l.code, 89 localName:l.localName||l.name||l.code, 90 name:l.name||l.localName||l.code, 91 availableCountries:Array.isArray(l.availableCountries)?l.availableCountries.map(c=>({ 92 code:c.code, 93 name:c.name||c.englishName||c.code, 94 flag:c.flag||'flag_global_circle.svg', 95 useFullLocale: c.useFullLocale === true, 96 isLive: c.isLive, 97 isUDESite: c.isUDESite 98 })):[] 99 })); 100 101 if (IS_PRODUCTION) { 102 LANG_DATA = LANG_DATA.map(l => ({ 103 ...l, 104 availableCountries: l.availableCountries.filter(c => c.isUDESite !== false && c.isLive === true) 105 })).filter(l => l.availableCountries.length > 0); 106 } else { 107 LANG_DATA = LANG_DATA.map(l => ({ 108 ...l, 109 availableCountries: l.availableCountries.filter(c => c.isUDESite !== false) 110 })).filter(l => l.availableCountries.length > 0); 111 } 112 113 SORTED_LANGS=[...LANG_DATA].filter(l=>l.includeInLanguageSwitcher!==false).sort((a,b)=>
113collator.compare(a.localName||a.name,b.localName||b.name)); 114 }catch(err){ 115 // Global data init failed 116 } 117} 118 119/* =================== COMPONENT INITIALIZATION =================== */ 120function initComponent(container){ 121 if(!container) return; 122 123 /* Guard against double initialization */ 124 if(container._localeInitialized) return; 125 container._localeInitialized = true; 126 127 /* Instance-specific state */ 128 let CURRENT_LANGUAGE=null; 129 let CURRENT_COUNTRY=null; 130 131 /* Get props from data attributes */ 132 const props = { 133 langLabel: container.dataset.langLabel || 'Language', 134 langTooltip: container.dataset.langTooltip || '', 135 countryLabel: container.dataset.countryLabel || 'Country', 136 countryTooltip: container.dataset.countryTooltip || '', 137 saveText: container.dataset.saveText || 'Save', 138 viewText: container.dataset.viewText || 'View by Country', 139 viewUrl: container.dataset.viewUrl || '/region' 140 }; 141 142 /* DOM refs - scoped to container, prefix-agnostic */ 143 const langBtn = container.querySelector('[class*="lp-lang-btn"]'); 144 const langMenu = container.querySelector('[class*="lp-lang-menu"]'); 145 const langTextEl = container.querySelector('[class*="lp-lang-text"]'); 146 const countryBtn = container.querySelector('[class*="lp-country-btn"]'); 147 const countryMenu = container.querySelector('[class*="lp-country-menu"]'); 148 const countryFlagEl = countryBtn.querySelector('[class*="lp-flag"]'); 149 const countryTextEl = container.querySelector('[class*="lp-country-text"]'); 150 const saveBtn = container.querySelector('[class*="lp-save"]'); 151 const viewLink = container.querySelector('[class*="lp-view"]'); 152 153 /* Give each control an unambiguous accessible name and relationship. */ 154 const pickerId = `lp-picker-${++pickerInstanceCount}`;
155 const langLabel = langBtn?.closest('[class*="lp-field"]')?.querySelector('label[class*="lp-label"]'); 156 const countryLabel = countryBtn?.closest('[class*="lp-field"]')?.querySelector('label[class*="lp-label"]'); 157 const langTooltip = langBtn?.closest('[class*="lp-field"]')?.querySelector('[class*="lp-tooltip-text"]'); 158 const countryTooltip = countryBtn?.closest('[class*="lp-field"]')?.querySelector('[class*="lp-tooltip-text"]'); 159 160 if(langBtn && langMenu && langTextEl){ 161 langBtn.id = `${pickerId}-language-button`; 162 langMenu.id = `${pickerId}-language-listbox`; 163 langTextEl.id = `${pickerId}-language-value`; 164 if(langLabel){ 165 langLabel.id = `${pickerId}-language-label`; 166 langLabel.htmlFor = langBtn.id; 167 } 168 if(langTooltip) langTooltip.id = `${pickerId}-language-description`; 169 langBtn.setAttribute('aria-haspopup','listbox'); 170 langBtn.setAttribute('aria-controls',langMenu.id); 171 langBtn.setAttribute('aria-expanded',langMenu.hidden?'false':'true'); 172 langBtn.setAttribute('aria-labelledby',`${langLabel?.id || ''} ${langTextEl.id}`.trim()); 173 if(langTooltip) langBtn.setAttribute('aria-describedby',langTooltip.id); 174 langMenu.setAttribute('aria-labelledby',langLabel?.id || langBtn.id); 175 } 176 if(countryBtn && countryMenu && countryTextEl){ 177 countryBtn.id = `${pickerId}-country-button`; 178 countryMenu.id = `${pickerId}-country-listbox`; 179 countryTextEl.id = `${pickerId}-country-value`; 180 if(countryLabel){ 181 countryLabel.id = `${pickerId}-country-label`; 182 countryLabel.htmlFor = countryBtn.id; 183 } 184 if(countryTooltip) countryTooltip.id = `${pickerId}-country-description`; 185 countryBtn.setAttribute('aria-haspopup','listbox'); 186 countryBtn.setAttribute('aria-controls',countryMenu.id); 187 countryBtn.setAttribute('aria-expanded',countryMenu.hidden?'false':'true'); 188 countryBtn.setAttribute('aria-labelledby',`${countryLabel?.id || ''} ${countryTextEl.id}`.trim()); 189 if(countryTooltip) countryBtn.setAttribute('aria-describedby',countryTooltip.id); 190 countryMenu.setAttribute('aria-labelledby',countryLabel?.id || countryBtn.id); 191 } 192 193 /* Populate labels from props */ 194 container.querySelectorAll('[class*="lp-label-text"]').forEach((el, idx) => { 195 if(idx === 0) el.textContent = props.langLabel; 196 else el.textContent = props.countryLabel; 197 }); 198 container.querySelectorAll('[class*="lp-tooltip-text"]').forEach((el, idx) => { 199 if(idx === 0) el.textContent = props.langTooltip; 200 else el.textContent = props.countryTooltip; 201 }); 202 saveBtn.textContent = props.saveText; 203 viewLink.textContent = props.viewText; 204 // Only set href if it hasn't been modified by data-redirect.js 205 // data-redirect may have already added a locale prefix (e.g., /en-ao/region) 206 const currentHref = viewLink.getAttribute('href'); 207 if (!currentHref || currentHref === props.viewUrl) { 208 viewLink.href = props.viewUrl; 209 } 210 211 /* =================== URL HELPERS =================== */ 212 function parseCurrentPath(){ 213 const parts = location.pathname.split('/').filter(Boolean); 214 if(parts.length===0) return { type:'none', lang:null, country:null, restParts:[] }; 215 const first = parts[0]; 216 if(/^[a-z]{2}-[a-z0-9]{2,}$/i.test(first)){ 217 const [l,c] = first.toLowerCase().split('-'); 218 return { type:'full', lang:l, country:c, restParts:parts.slice(1) }; 219 } 220 if(/^[a-z]{2}$/i.test(first)){ 221 return { type:'lang', lang:first.toLowerCase(), country:null, restParts:parts.slice(1) }; 222 } 223 return { type:'none', lang:null, country:null, restParts:parts }; 224 } 225 226 function shouldUseFullLocaleUrl(targetLang, targetCountry){ 227 const lang = LANG_DATA.find(l=>l.code===targetLang); 228 if(!lang) return false; 229 const country = (lang.availableCountries||[]).find(c=>c.code===targetCountry); 230 return country?.useFullLocale === true; 231 } 232 233 function buildPrefix(targetLang, targetCountry, forceFull){ 234 // Special case: en-global goes to root domain (no prefix) 235 if(targetLang === 'en' && targetCountry === 'global') return ''; 236 // Always use just language code for other global countries 237 if(targetCountry === 'global') return `/${targetLang}`; 238 // Use full locale if forced 239 if(forceFull) return `/${targetLang}-${targetCountry}`; 240 return `/${targetLang}`; 241 } 242 243 function needsRedirectToHome(targetLang){ 244 const { restParts } = parseCurrentPath(); 245 const firstSlug = restParts[0] || ''; 246 // Redirect if on /news page or article and language doesn't support news 247 if (firstSlug === 'news' && !LANGUAGES_WITH_NEWS.includes(targetLang)) { 248 return true; 249 } 250 // Redirect if on /courses page or specific course and language doesn't support courses 251 if (firstSlug === 'courses' && !LANGUAGES_WITH_COURSES.includes(targetLang)) { 252 return true; 253 } 254 // Redirect if on English-only page and switching to non-English language 255 if (isEnglishOnlySlug(restParts) && targetLang !== 'en') { 256 return true; 257 } 258 return false;
259 } 260 261 function needsRedirectToNews(targetLang){ 262 const { restParts } = parseCurrentPath(); 263 const firstSlug = restParts[0] || ''; 264 const secondSlug = restParts[1] || ''; 265 // If on news article (news/something) and target language supports news, redirect to /news 266 if (firstSlug === 'news' && secondSlug && LANGUAGES_WITH_NEWS.includes(targetLang)) { 267 return true; 268 } 269 return false; 270 } 271 272 function needsRedirectToCourses(targetLang){ 273 const { restParts } = parseCurrentPath(); 274 const firstSlug = restParts[0] || ''; 275 const secondSlug = restParts[1] || ''; 276 // If on specific course (courses/something) and target language supports courses, redirect to /courses 277 if (firstSlug === 'courses' && secondSlug && LANGUAGES_WITH_COURSES.includes(targetLang)) { 278 return true; 279 } 280 return false; 281 } 282 283 function buildTargetUrl(targetLang, targetCountry){ 284 const { type, restParts } = parseCurrentPath(); 285 const restFirst = restParts[0] || ''; 286 287 // Special handling for global English-only pages 288 if (isEnglishOnlySlug(restParts)) { 289 if (targetLang === 'en') { 290 // Switching between English locales on global English-only pages - stay on same URL 291 return location.pathname + location.search + location.hash; 292 } else { 293 // Switching to non-English on global English-only page - go to home 294 const forceFull = targetCountry !== 'global'; 295 const prefix = buildPrefix(targetLang, targetCountry, forceFull); 296 return prefix || '/'; 297 } 298 } 299 300 if (needsRedirectToHome(targetLang)) { 301 // When redirecting to home, use full locale format (lang-country) for country-specific locales 302 // Only use language-only format for global countries 303 const forceFull = targetCountry !== 'global'; 304 const prefix = buildPrefix(targetLang, targetCountry, forceFull); 305 return prefix || '/'; 306 } 307 if (needsRedirectToNews(targetLang)) { 308 // When redirecting from news article to /news, use full locale format for country-specific locales 309 const forceFull = targetCountry !== 'global'; 310 const prefix = buildPrefix(targetLang, targetCountry, forceFull); 311 return `${prefix}/news${location.search}${location.hash}`; 312 } 313 if (needsRedirectToCourses(targetLang)) { 314 // When redirecting from specific course to /courses, use full locale format for country-specific locales 315 const forceFull = targetCountry !== 'global'; 316 const prefix = buildPrefix(targetLang, targetCountry, forceFull); 317 return `${prefix}/courses${location.search}${location.hash}`; 318 } 319 const restPath = restParts.join('/'); 320 const requiresFull = shouldUseFullLocaleUrl(targetLang, targetCountry); 321 const isGlobalPage = GLOBAL_LANGUAGE_PAGES.includes(restFirst); 322 // Use full locale format when country is not global AND (country requires it OR not on global page) 323 const forceFull = requiresFull || (targetCountry !== 'global' && !isGlobalPage); 324 const prefix = buildPrefix(targetLang, targetCountry, forceFull); 325 const url = `${prefix}${restPath ? `/${restPath}` : ''}${location.search}${location.hash}`; 326 // Ensure we always have at least '/' to trigger actual navigation 327 return url || '/'; 328 } 329 330 function deriveFromUrlIfPresent(){ 331 const info = parseCurrentPath(); 332 if (info.type === 'full') { 333 const langObj = LANG_DATA.find(l => l.code === info.lang); 334 if (!langObj) return { language: 'en', country: 'global', source: 'full' }; 335 const hasCountry = (langObj.availableCountries || []).some(c => c.code === info.country); 336 const country = hasCountry ? info.country : resolveDefaultCountry(langObj);
337 return { language: langObj.code, country, source: 'full' }; 338 } 339 if (info.type === 'lang') { 340 const langObj = LANG_DATA.find(l => l.code === info.lang); 341 if (!langObj) return { language: 'en', country: null, source: 'lang' }; 342 // Don't resolve default country here - let init logic handle it based on page type 343 return { language: langObj.code, country: null, source: 'lang' }; 344 } 345 return null; 346 } 347 348 function resolveDefaultCountry(langObj){ 349 if(!langObj) return 'global'; 350 if(langObj.availableCountries?.some(c=>c.code==='global')) return 'global'; 351 return langObj.availableCountries?.[0]?.code||'global'; 352 } 353 354 /* =================== BUILD MENUS =================== */ 355 function buildLanguageMenu(){ 356 if(!langMenu) return; 357 langMenu.innerHTML=''; 358 const frag=document.createDocumentFragment(); 359 SORTED_LANGS.forEach(l=>{ 360 const btn=document.createElement('button'); 361 btn.type='button'; 362 btn.className='lp-option'; 363 btn.setAttribute('role','option'); 364 btn.setAttribute('aria-selected',l.code===CURRENT_LANGUAGE?'true':'false'); 365 btn.setAttribute('tabindex','-1'); 366 btn.dataset.code=l.code; 367 if(l.code===CURRENT_LANGUAGE) btn.dataset.active='true'; 368 btn.innerHTML=`<span>${escapeHTML(l.localName||l.name)}</span>${l.code===CURRENT_LANGUAGE?'<span class="lp-check" aria-hidden="true">â</span>':''}`; 369 frag.appendChild(btn); 370 }); 371 langMenu.appendChild(frag); 372 setInitialOptionTabStop(langMenu); 373 } 374 375 function buildCountryMenu(){ 376 if(!countryMenu) return; 377 countryMenu.innerHTML=''; 378 if(!CURRENT_LANGUAGE){ 379 countryBtn.setAttribute('disabled', 'true'); 380 countryBtn.dataset.disabled = 'true'; 381 countryBtn.style.pointerEvents = 'none'; 382 countryBtn.style.opacity = '0.5'; 383 return; 384 } 385 const lang=LANG_DATA.find(l=>l.code===CURRENT_LANGUAGE); 386 if(!lang){ 387 countryBtn.setAttribute('disabled', 'true'); 388 countryBtn.dataset.disabled = 'true'; 389 countryBtn.style.pointerEvents = 'none'; 390 countryBtn.style.opacity = '0.5'; 391 return; 392 } 393 const list=[...(lang.availableCountries||[])].sort((a,b)=>collator.compare(a.name,b.name)); 394 countryBtn.removeAttribute('disabled'); 395 countryBtn.dataset.disabled = 'false'; 396 countryBtn.style.pointerEvents = 'auto'; 397 countryBtn.style.opacity = '1'; 398 const frag=document.createDocumentFragment(); 399 list.forEach(c=>{ 400 const btn=document.createElement('button'); 401 btn.type='button'; 402 btn.className='lp-option'; 403 btn.setAttribute('role','option'); 404 btn.setAttribute('aria-selected',c.code===CURRENT_COUNTRY?'true':'false'); 405 btn.setAttribute('tabindex','-1'); 406 btn.dataset.code=c.code; 407 if(c.code===CURRENT_COUNTRY) btn.dataset.active='true'; 408 btn.innerHTML=` 409 <div style="display:flex;align-items:center;gap:.55rem;"> 410 <img class="lp-flag" src="https://images.derekprince.com/flags/${escapeAttr(c.flag)}" alt="" data-nosnippet> 411 <span>${escapeHTML(c.name)}</span> 412 </div>${c.code===CURRENT_COUNTRY?'<span class="lp-check" aria-hidden="true">â</span>':''}`; 413 frag.appendChild(btn); 414 }); 415 countryMenu.appendChild(frag); 416 setInitialOptionTabStop(countryMenu); 417 } 418 419 /* =================== SELECTION =================== */ 420 function selectLanguage(code){ 421 if(!code || code===CURRENT_LANGUAGE) {toggleMenu(langMenu,false);return;} 422 if(!LANG_DATA.some(l=>l.code===code)) return; 423 CURRENT_LANGUAGE=code; 424 const lang=LANG_DATA.find(l=>l.code===code); 425 if(!lang) return; 426 if(!lang.availableCountries.some(c=>c.code===CURRENT_COUNTRY)){ 427 CURRENT_COUNTRY=resolveDefaultCountry(lang); 428 } 429 updateVisuals(); 430 toggleMenu(langMenu,false); 431 } 432 433 function selectCountry(code){ 434 if(!code || code===CURRENT_COUNTRY) {toggleMenu(countryMenu,false);return;} 435 const lang=LANG_DATA.find(l=>l.code===CURRENT_LANGUAGE); 436 if(!lang) return; 437 if(!lang.availableCountries.some(c=>c.code===code)) return; 438 CURRENT_COUNTRY=code; 439 updateVisuals(); 440 toggleMenu(countryMenu,false); 441 } 442 443 /* =================== VISUAL SYNC =================== */ 444 function updateVisuals(){ 445 const lang=LANG_DATA.find(l=>l.code===CURRENT_LANGUAGE); 446 if(lang){ 447 langTextEl.textContent=lang.localName||lang.name; 448 } 449 buildLanguageMenu(); 450 const lang2=LANG_DATA.find(l=>l.code===CURRENT_LANGUAGE); 451 if(lang2){ 452 buildCountryMenu(); 453 const currentCountryObj=lang2.availableCountries.find(c=>c.code===CURRENT_COUNTRY); 454 if(currentCountryObj){ 455 countryTextEl.textContent=currentCountryObj.name; 456 countryFlagEl.src=`https://derekprince.com/cdn-cgi/image/width=40,height=40,quality=75,fit=cover,format=webp/https://images.derekprince.com/flags/${currentCountryObj.flag}`; 457 countryFlagEl.setAttribute('data-nosnippet', ''); 458 }else{ 459 countryTextEl.textContent='Select country'; 460 } 461 } 462 } 463 464 /* =================== MENU TOGGLE =================== */ 465 function getOptions(menuEl){ 466 return Array.from(menuEl?.querySelectorAll('[role="option"]') || []); 467 } 468 469 function setInitialOptionTabStop(menuEl){ 470 const options=getOptions(menuEl); 471 if(!options.length) return; 472 const initial=options.find(option=>option.getAttribute('aria-selected')==='true') || options[0]; 473 options.forEach(option=>option.tabIndex=option===initial?0:-1); 474 } 475 476 function focusOption(menuEl,index){ 477 const options=getOptions(menuEl); 478 if(!options.length) return; 479 const wrappedIndex=(index+options.length)%options.length; 480 options.forEach((option,optionIndex)=>option.tabIndex=optionIndex===wrappedIndex?0:-1); 481 options[wrappedIndex].focus(); 482 options[wrappedIndex].scrollIntoView({block:'nearest'}); 483 } 484 485 const typeaheadState=new WeakMap();
486 function runTypeahead(menuEl,key,currentOption){ 487 const options=getOptions(menuEl); 488 if(!options.length) return; 489 490 const now=Date.now(); 491 const previous=typeaheadState.get(menuEl) || {query:'',time:0}; 492 const query=(now-previous.time>700?'':previous.query)+key.toLocaleLowerCase(); 493 typeaheadState.set(menuEl,{query,time:now}); 494 495 // Repeating one character cycles through all entries beginning with it. 496 const repeated=query.length>1 && Array.from(query).every(char=>char===query[0]); 497 const search=repeated?query[0]:query; 498 const normalize=value=>value.normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim().toLocaleLowerCase(); 499 const currentIndex=currentOption?options.indexOf(currentOption):options.findIndex(option=>option.tabIndex===0); 500 const startIndex=search.length===1?currentIndex+1:Math.max(currentIndex,0); 501 502 for(let offset=0;offset<options.length;offset+=1){ 503 const index=(startIndex+offset+options.length)%options.length; 504 if(normalize(options[index].textContent).startsWith(normalize(search))){ 505 focusOption(menuEl,index); 506 return; 507 } 508 } 509 } 510 511 function focusMenuOption(menuEl,direction){ 512 const options=getOptions(menuEl); 513 if(!options.length) return; 514 const selectedIndex=options.findIndex(option=>option.getAttribute('aria-selected')==='true'); 515 if(direction==='last') focusOption(menuEl,options.length-1); 516 else focusOption(menuEl,selectedIndex>=0?selectedIndex:0); 517 } 518 519 function toggleMenu(menuEl,force){ 520 if(!menuEl) return; 521 const btn = menuEl===langMenu?langBtn:countryBtn; 522 523 if(force !== undefined){ 524 // force parameter provided 525 menuEl.hidden = !force; 526 btn.dataset.open = force ? 'true' : 'false'; 527 btn.setAttribute('aria-expanded',force?'true':'false'); 528 if(force) setInitialOptionTabStop(menuEl); 529 else typeaheadState.delete(menuEl); 530 } else { 531 // toggle 532 const isHidden = menuEl.hidden; 533 menuEl.hidden = !isHidden; 534 btn.dataset.open = isHidden ? 'true' : 'false'; 535 btn.setAttribute('aria-expanded',isHidden?'true':'false'); 536 if(isHidden) setInitialOptionTabStop(menuEl); 537 else typeaheadState.delete(menuEl); 538 } 539 } 540 541 /* =================== EVENT LISTENERS =================== */ 542 if(!langBtn || !langMenu || !countryBtn || !countryMenu || !saveBtn){ 543 return; 544 } 545 546 container.addEventListener('click', e => { 547 548 if(e.target===langBtn || langBtn.contains(e.target)){ 549 e.stopPropagation(); 550 toggleMenu(langMenu, langMenu.hidden); 551 toggleMenu(countryMenu, false); 552 return; 553 } 554 if(e.target===countryBtn || countryBtn.contains(e.target)){ 555 e.stopPropagation(); 556 if(countryBtn.dataset.disabled !== 'true'){ 557 toggleMenu(countryMenu, countryMenu.hidden); 558 toggleMenu(langMenu, false); 559 } 560 return; 561 } 562 }); 563 564 function handleTriggerKeydown(e,menuEl,otherMenu){ 565 if(e.key==='ArrowDown' || e.key==='ArrowUp'){ 566 e.preventDefault(); 567 toggleMenu(otherMenu,false); 568 toggleMenu(menuEl,true); 569 focusMenuOption(menuEl,e.key==='ArrowUp'?'last':'selected'); 570 }else if(e.key==='Escape' && !menuEl.hidden){ 571 e.preventDefault(); 572 toggleMenu(menuEl,false); 573 }else if(e.key.length===1 && !e.ctrlKey && !e.metaKey && !e.altKey && e.key!==' '){ 574 e.preventDefault(); 575 toggleMenu(otherMenu,false); 576 toggleMenu(menuEl,true); 577 runTypeahead(menuEl,e.key,null); 578 } 579 } 580 581 langBtn.addEventListener('keydown',e=>handleTriggerKeydown(e,langMenu,countryMenu)); 582 countryBtn.addEventListener('keydown',e=>handleTriggerKeydown(e,countryMenu,langMenu)); 583 584 function handleMenuKeydown(e,menuEl,trigger,selectOption){ 585 const option=e.target.closest('[role="option"]'); 586 if(!option || !menuEl.contains(option)) return; 587 const options=getOptions(menuEl); 588 const currentIndex=options.indexOf(option); 589 590 if(e.key==='ArrowDown' || e.key==='ArrowUp'){ 591 e.preventDefault(); 592 focusOption(menuEl,currentIndex+(e.key==='ArrowDown'?1:-1)); 593 }else if(e.key==='Home' || e.key==='End'){ 594 e.preventDefault(); 595 focusOption(menuEl,e.key==='Home'?0:options.length-1); 596 }else if(e.key==='Enter' || e.key===' '){ 597 e.preventDefault(); 598 selectOption(option.dataset.code); 599 trigger.focus(); 600 }else if(e.key==='Escape'){ 601 e.preventDefault(); 602 toggleMenu(menuEl,false); 603 trigger.focus(); 604 }else if(e.key.length===1 && !e.ctrlKey && !e.metaKey && !e.altKey){ 605 e.preventDefault(); 606 runTypeahead(menuEl,e.key,option); 607 } 608 } 609 610 langMenu.addEventListener('keydown',e=>handleMenuKeydown(e,langMenu,langBtn,selectLanguage)); 611 countryMenu.addEventListener('keydown',e=>handleMenuKeydown(e,countryMenu,countryBtn,selectCountry)); 612 613 // Close a list once keyboard focus has moved to a different c
613ontrol. 614 container.addEventListener('focusin',e=>{ 615 if(!langMenu.hidden && e.target!==langBtn && !langMenu.contains(e.target)) toggleMenu(langMenu,false); 616 if(!countryMenu.hidden && e.target!==countryBtn && !countryMenu.contains(e.target)) toggleMenu(countryMenu,false); 617 }); 618 619 langMenu.addEventListener('click', e => { 620 e.stopPropagation(); 621 const btn=e.target.closest('.lp-option'); 622 if(!btn) return; 623 selectLanguage(btn.dataset.code); 624 langBtn.focus(); 625 }); 626 627 countryMenu.addEventListener('click', e => { 628 e.stopPropagation(); 629 const btn=e.target.closest('.lp-option'); 630 if(!btn) return; 631 selectCountry(btn.dataset.code); 632 countryBtn.focus(); 633 }); 634 635 saveBtn.addEventListener('click', e => { 636 e.stopPropagation(); 637 if(!CURRENT_LANGUAGE){ 638 return; 639 } 640 const lang=LANG_DATA.find(l=>l.code===CURRENT_LANGUAGE); 641 if(!lang){ 642 return; 643 } 644 if(!lang.availableCountries.some(c=>c.code===CURRENT_COUNTRY)){ 645 CURRENT_COUNTRY=resolveDefaultCountry(lang); 646 } 647 const val=`${CURRENT_LANGUAGE}-${CURRENT_COUNTRY}`; 648 writeCookie(SOFT_COOKIE_NAME,val); 649 writeCookie(HARD_COOKIE_NAME,val); 650 const targetUrl = buildTargetUrl(CURRENT_LANGUAGE, CURRENT_COUNTRY); 651 const currentUrl = location.pathname + location.search + location.hash; 652 653 // If target URL is same as current URL, force reload to rebuild redirect links 654 if (targetUrl === currentUrl) { 655 window.location.reload(); 656 } else { 657 window.location.assign(targetUrl); 658 } 659 }); 660 661 /* Close menus on outside click */ 662 document.addEventListener('click', e => { 663 if(!container.contains(e.target)){ 664 if(!langMenu.hidden) toggleMenu(langMenu, false); 665 if(!countryMenu.hidden) toggleMenu(countryMenu, false); 666 } 667 }); 668 669 /* =================== INITIALIZATION =================== */ 670 (async function componentInit(){ 671 try{ 672 await initGlobalData(); 673 if(LANG_DATA.length === 0){ 674 if(langTextEl) langTextEl.textContent='Failed to load'; 675 return; 676 } 677 678 const urlLocale = deriveFromUrlIfPresent(); 679 if (urlLocale) { 680 CURRENT_LANGUAGE = urlLocale.language; 681 let effectiveCountry = urlLocale.country; 682 if (urlLocale.source === 'lang') { 683 // Check if we're on a global language page (courses, outreaches, etc.) 684 const { restParts } = parseCurrentPath(); 685 const pageSlug = restParts[0] || ''; 686 const isGlobalLanguagePage = GLOBAL_LANGUAGE_PAGES.includes(pageSlug); 687 688 if (isGlobalLanguagePage) { 689 // On global language pages, preserve browsing context (soft > hard > default) 690 const soft = readCookie(SOFT_COOKIE_NAME); 691 if (soft) { 692 const [softLang, softCountry] = soft.split('-'); 693 effectiveCountry = softCountry; 694 } else { 695 const hard = readCookie(HARD_COOKIE_NAME); 696 if (hard) { 697 const [hLang, hCountry] = hard.split('-'); 698 effectiveCountry = hCountry; 699 } 700 } 701 } else { 702 // On non-global pages, use hard cookie if language matches 703 const hard = readCookie(HARD_COOKIE_NAME); 704 if (hard) { 705 const [hLang, hCountry] = hard.split('-'); 706 if (hLang === CURRENT_LANGUAGE) { 707 const langObj = LANG_DATA.find(x=>x.code===CURRENT_LANGUAGE); 708 const validHard = (langObj?.availableCountries || []).some(c=>c.code===hCountry); 709 if (validHard) effectiveCountry = hCountry; 710 } 711 } 712 } 713 } 714 // If country is still null, resolve default 715 if (!effectiveCountry) { 716 const langObj = LANG_DATA.find(x=>x.code===CURRENT_LANGUAGE); 717 effectiveCountry = resolveDefaultCountry(langObj); 718 } 719 CURRENT_COUNTRY = effectiveCountry; 720 const softVal = `${CURRENT_LANGUAGE}-${CURRENT_COUNTRY}`; 721 if (readCookie(SOFT_COOKIE_NAME) !== softVal) writeCookie(SOFT_COOKIE_NAME, softVal); 722 } else { 723 const soft = readCookie(SOFT_COOKIE_NAME); 724 if (soft) { 725 let [l,c] = soft.split('-'); 726 let langObj = LANG_DATA.find(x=>x.code===l); 727 if (!langObj) { l='en'; langObj = LANG_DATA.find(x=>x.code==='en'); } 728 if (!c || !langObj.availableCountries.some(cc=>cc.code===c)) { 729 c = resolveDefaultCountry(langObj); 730 } 731 CURRENT_LANGUAGE = l; 732 CURRENT_COUNTRY = c; 733 const normalized = `${l}-${c}`; 734 if (soft !== normalized) writeCookie(SOFT_COOKIE_NAME, normalized); 735 } else { 736 CURRENT_LANGUAGE='en'; 737 CURRENT_COUNTRY='global'; 738 writeCookie(SOFT_COOKIE_NAME,'en-global'); 739 } 740 } 741 742 buildLanguageMenu(); 743 buildCountryMenu(); 744 updateVisuals(); 745 }catch(err){ 746 if(langTextEl) langTextEl.textContent='Failed to load'; 747 } 748 })(); 749} 750 751/* =================== AUTO-INIT ALL COMPONENTS =================== */ 752document.addEventListener('DOMContentLoaded', async () => { 753 await initGlobalData(); 754 // Use prefix-agnostic selector for Webflow component library 755 document.querySelectorAll('[class*="lp-picker"]').forEach(container => {
756 initComponent(container); 757 }); 758}); 759 760/* Also init components if they're added after DOMContentLoaded */ 761if(document.readyState === 'loading'){ 762 document.addEventListener('DOMContentLoaded', () => { 763 document.querySelectorAll('[class*="lp-picker"]').forEach(container => { 764 if(!container._localeInitialized){ 765 initComponent(container); 766 container._localeInitialized = true; 767 } 768 }); 769 }); 770} 771 772/* =================== LOCALE NOTIFICATION BAR & MODAL =================== */ 773async function initLocaleComponents() { 774 await initGlobalData(); 775 if (LANG_DATA.length === 0) return; 776 777 // Helper to parse URL and derive locale 778 function parseUrlLocale() { 779 const parts = location.pathname.split('/').filter(Boolean); 780 if (parts.length === 0) return { lang: null, country: null }; 781 const first = parts[0]; 782 if (/^[a-z]{2}-[a-z0-9]{2,}$/i.test(first)) { 783 const [l, c] = first.toLowerCase().split('-'); 784 return { lang: l, country: c }; 785 } 786 if (/^[a-z]{2}$/i.test(first)) { 787 return { lang: first.toLowerCase(), country: null }; 788 } 789 return { lang: null, country: null }; 790 } 791 792 // Helper to resolve default country for language 793 function resolveDefaultCountry(langCode) { 794 const langObj = LANG_DATA.find(l => l.code === langCode); 795 if (!langObj) return 'global'; 796 if (langObj.availableCountries?.some(c => c.code === 'global')) return 'global'; 797 return langObj.availableCountries?.[0]?.code || 'global'; 798 } 799 800 let softCookie = readCookie(SOFT_COOKIE_NAME); 801 const hardCookie = readCookie(HARD_COOKIE_NAME); 802 803 // Parse URL to determine current locale 804 const urlLocale = parseUrlLocale(); 805 const parts = location.pathname.split('/').filter(Boolean); 806 const isRootDomain = parts.length === 0 || (parts.length > 0 && !/^[a-z]{2}(-[a-z0-9]{2,})?$/i.test(parts[0])); 807 808 // Only update soft cookie when URL explicitly has a locale prefix 809 // On global pages (no locale prefix), soft cookie remains unchanged to maintain browsing session 810 if (!isRootDomain && urlLocale.lang) { 811 // On localized page, update soft cookie to match URL 812 const langObj = LANG_DATA.find(l => l.code === urlLocale.lang); 813 if (langObj) { 814 let country = urlLocale.country; 815 // Determine the page slug from URL 816 const restParts = parts.slice(1); 817 const pageSlug = restParts[0] || ''; 818 const isGlobalLanguagePage = GLOBAL_LANGUAGE_PAGES.includes(pageSlug); 819 820 // If URL has full locale (lang-country), use it 821 if (country && langObj.availableCountries?.some(c => c.code === country)) { 822 // Valid full locale 823 } else if (country) { 824 // Invalid country in URL, fall back to default 825 country = resolveDefaultCountry(urlLocale.lang); 826 } else { 827 // URL has only language code 828 // For global language pages, preserve browsing context (soft cookie > hard cookie > default) 829 if (isGlobalLanguagePage) { 830 if (softCookie) { 831 const [softLang, softCountry] = softCookie.split('-'); 832 // Always preserve soft cookie country on global pages, regardless of language 833 country = softCountry; 834 } else if (hardCookie) { 835 const [hLang, hCountry] = hardCookie.split('-'); 836 country = hCountry; 837 } else { 838 country = resolveDefaultCountry(urlLocale.lang); 839 } 840 } else { 841 // For non-global pages, check hard cookie for country preference 842 if (hardCookie) { 843 const [hLang, hCountry] = hardCookie.split('-'); 844 if (hLang === urlLocale.lang && langObj.availableCountries?.some(c => c.code === hCountry)) { 845 country = hCountry; 846 } else { 847 country = resolveDefaultCountry(urlLocale.lang); 848 } 849 } else { 850 country = resolveDefaultCountry(urlLocale.lang); 851 } 852 } 853 } 854 const expectedSoft = `${urlLocale.lang}-${country}`; 855 // On global language pages, don't overwrite existing soft cookie if it has a valid country 856 const shouldPreserveSoftCookie = isGlobalLanguagePage && softCookie && (() => { 857 const [existingSoftLang, existingSoftCountry] = softCookie.split('-');
858 return langObj.availableCountries?.some(c => c.code === existingSoftCountry); 859 })(); 860 861 if (!shouldPreserveSoftCookie && softCookie !== expectedSoft) { 862 writeCookie(SOFT_COOKIE_NAME, expectedSoft); 863 softCookie = expectedSoft; 864 } 865 } 866 } else { 867 // We are on a global page (no locale prefix) - e.g. home, about, sermons 868 869 // Check if it's a Global English Only page (e.g. sermons, teaching) 870 // On these pages, we want to maintain the user's browsing session (soft cookie) 871 // so they don't feel "kicked out" of their country context when viewing shared content. 872 const isGlobalEnglishOnly = isEnglishOnlySlug(parts); 873 874 if (isGlobalEnglishOnly) { 875 if (!softCookie) { 876 // No soft cookie exists at all - initialize with hard cookie or en-global 877 if (hardCookie) { 878 writeCookie(SOFT_COOKIE_NAME, hardCookie); 879 softCookie = hardCookie; 880 } else { 881 writeCookie(SOFT_COOKIE_NAME, 'en-global'); 882 softCookie = 'en-global'; 883 } 884 } 885 } else { 886 // It implies a "True" Global page (e.g. home, about) 887 // Ensure soft cookie is set to en-global to reflect that we are viewing the global site 888 // This allows the notification bar to show if the user's hard cookie is different (e.g. en-nz) 889 if (softCookie !== 'en-global') { 890 const previousSoftCookie = softCookie; // Store the old value before updating 891 writeCookie(SOFT_COOKIE_NAME, 'en-global'); 892 softCookie = 'en-global'; 893 894 // Cleanup Links Logic: 895 // Since the 'data-redirect.js' script likely ran earlier with the previous soft cookie (e.g., en-au), 896 // the links on the page might have been prefixed with that locale path (e.g., /en-au/about). 897 // Since we are now forcing the session to 'en-global', we must revert these links to their global targets. 898 // We do this by manually stripping any locale prefix from the previous soft cookie. 899 900 // Determine what prefix data-redirect would have used 901 // data-redirect normalizes "xx-global" to "xx", and keeps "xx-yy" as is. 902 let previousPrefix = previousSoftCookie; 903 if (previousPrefix.endsWith('-global')) { 904 previousPrefix = previousPrefix.split('-')[0]; 905 } 906 const previousPrefixPath = '/' + previousPrefix + '/'; 907 908 // Select all links potentially affected by the redirect script 909 const selectors = ['a[data-redirect]', 'a[data-userpath]', 'a[data-news-redirect]', 'a[data-langpath]']; 910 const links = document.querySelectorAll(selectors.join(',')); 911 912 // Iterate and clean 913 links.forEach(link => { 914 const href = link.getAttribute('href'); 915 if (!href) return; 916 917 // Skip the /region link for non-English locales to allow data-redirect to keep the prefix 918 // For English variants (en-global â en), the region link should stay as /region 919 if (href === previousPrefixPath + 'region' && previousPrefix !== 'en') { 920 return; 921 } 922 923 // Logic for standard redirects: /en-au/slug -> /slug 924 if (href.startsWith(previousPrefixPath)) { 925 // For global pages, links should generally be root-relative standard paths 926 const newHref = href.replace(previousPrefixPath, '/'); 927 link.setAttribute('href', newHref); 928 } 929 // Logic for News Redirects specifically: /en-au/news -> /en/news (Global standard) 930 else if (link.hasAttribute('data-news-redirect') && href.includes(previousPrefixPath)) { 931 link.setAttribute('href', href.replace(previousPrefixPath, '/en/')); 932 } 933 }); 934 } 935 } 936 } 937 938 // Parse cookies 939 const [softLang, softCountry] = (softCookie || 'en-global').split('-'); 940 const [hardLang, hardCountry] = hardCookie ? hardCookie.split('-') : [null, null]; 941 942 // Update 'View by Country' links with soft cookie locale 943 function updateViewLinks() { 944 const viewLinks = document.querySelectorAll('[class*="lp-view"]'); 945 const [lang, country] = softCookie.split('-'); 946 947 // Normalize locale for URL (collapse xx-global to xx) 948 let localePrefix = ''; 949 if (country === 'global') { 950 localePrefix = lang === 'en' ? '' : `/${lang}`; 951 } else { 952 localePrefix = `/${lang}-${country}`; 953 } 954 955 viewLinks.forEach(link => { 956 let href = link.getAttribute('href') || '/region'; 957 // Strip ALL locale prefixes to get base path (e.g., /region) 958 // Keep removing locale prefixes until none remain 959 let basePath = href; 960 let previousPath = ''; 961 while (basePath !== previousPath && /^\/[a-z]{2}(-[a-z0-9]{2,})?\//.test(basePath)) { 962 previousPath = basePath; 963 basePath = basePath.replace(/^\/[a-z]{2}(-[a-z0-9]{2,})?/, ''); 964 } 965 // Ensure basePath starts with / 966 if (!basePath.startsWith('/')) { 967 basePath = '/' + basePath; 968 } 969 link.href = localePrefix + basePath; 970 }); 971 } 972 973 // Call after soft cookie is determined 974 updateViewLinks(); 975 976 // Get language/country details 977 const softLangObj = LANG_DATA.find(l => l.code === softLang); 978 const softCountryObj = softLangObj?.availableCountries?.find(c => c.code === softCountry); 979 const hardLangObj = hardCookie ? LANG_DATA.find(l => l.code === hardLang) : null; 980 const hardCountryObj = hardLangObj?.availableCountries?.find(c => c.code === hardCountry); 981 982 // Cache DOM queries for better performance 983 const languageSelectorFlags = document.querySelectorAll('[class*="language-selector-flags"]'); 984 const languageSelectorModal = document.getElementById('languageSelectorContainer'); 985 const notificationBar = document.getElementById('localeNotificationBar'); 986 const noCookieModal = document.getElementById('noCookieModalContainer'); 987 const footerLanguageSelector = document.getElementById('footerLanguageSelector'); 988 989 // Update language selector flag(s) 990 // Always show soft cookie flag (current browsing location) for consistency 991 const flagToDisplay = softCountryObj; 992 993 if (flagToDisplay && languageSelectorFlags.length > 0) {
994 languageSelectorFlags.forEach(flag => { 995 flag.src = `https://derekprince.com/cdn-cgi/image/width=25,height=20,quality=75,fit=cover,format=webp/https://images.derekprince.com/flags/${flagToDisplay.flag}`; 996 }); 997 } 998 999 // =================== LANGUAGE SELECTOR MODAL HANDLERS =================== 1000 // Only initialize if modal exists (defer non-critical functionality) 1001 if (!languageSelectorModal) return; 1002 1003 const languageSelectorCloseBtn = languageSelectorModal.querySelector('[class*="div-social-25"]'); 1004 const languageSelectorTitle = languageSelectorModal.querySelector('h1, h2, h3, h4, h5, h6'); 1005 let previouslyFocusedElement = null; 1006 1007 languageSelectorModal.setAttribute('role', 'dialog'); 1008 languageSelectorModal.setAttribute('aria-modal', 'true'); 1009 languageSelectorModal.setAttribute('tabindex', '-1'); 1010 if (languageSelectorTitle) { 1011 if (!languageSelectorTitle.id) languageSelectorTitle.id = 'language-selector-title'; 1012 languageSelectorModal.setAttribute('aria-labelledby', languageSelectorTitle.id); 1013 } 1014 if (languageSelectorCloseBtn) { 1015 languageSelectorCloseBtn.setAttribute('role', 'button'); 1016 languageSelectorCloseBtn.setAttribute('tabindex', '0'); 1017 languageSelectorCloseBtn.setAttribute('aria-label', 'Close language and location selector'); 1018 } 1019 1020 function getLanguageSelectorFocusableElements() { 1021 const selector = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; 1022 return Array.from(languageSelectorModal.querySelectorAll(selector)).filter(element => { 1023 return !element.closest('[hidden]') && 1024 getComputedStyle(element).display !== 'none' && 1025 getComputedStyle(element).visibility !== 'hidden'; 1026 }); 1027 } 1028 1029 // Function to open language selector modal 1030 function openLanguageSelector() { 1031 if (languageSelectorModal) { 1032 if (!languageSelectorModal.contains(document.activeElement)) { 1033 previouslyFocusedElement = document.activeElement; 1034 } 1035 languageSelectorModal.style.display = 'flex'; 1036 requestAnimationFrame(() => { 1037 if (getComputedStyle(languageSelectorModal).display === 'none') return; 1038 const languageButton = languageSelectorModal.querySelector('[class*="lp-lang-btn"]:not([disabled])'); 1039 const initialFocus = languageButton || getLanguageSelectorFocusableElements()[0] || languageSelectorModal; 1040 initialFocus.focus(); 1041 }); 1042 } 1043 } 1044 1045 // Function to close language selector modal 1046 function closeLanguageSelector() { 1047 if (languageSelectorModal) { 1048 languageSelectorModal.style.display = 'none'; 1049 if (previouslyFocusedElement?.isConnected && typeof previouslyFocusedElement.focus === 'function') { 1050 previouslyFocusedElement.focus(); 1051 } 1052 previouslyFocusedElement = null; 1053 } 1054 } 1055 1056 // Close button listener 1057 if (languageSelectorCloseBtn) { 1058 languageSelectorCloseBtn.addEventListener('click', (e) => { 1059 e.preventDefault(); 1060 closeLanguageSelector(); 1061 }); 1062 languageSelectorCloseBtn.addEventListener('keydown', (e) => { 1063 if (e.key === 'Enter' || e.key === ' ') { 1064 e.preventDefault(); 1065 closeLanguageSelector(); 1066 } 1067 }); 1068 } 1069 1070 // Keep keyboard focus inside the modal until it is dismissed. 1071 languageSelectorModal.addEventListener('keydown', (e) => { 1072 if (e.key === 'Escape') { 1073 e.preventDefault(); 1074 closeLanguageSelector(); 1075 return; 1076 } 1077 if (e.key !== 'Tab') return; 1078 1079 const focusableElements = getLanguageSelectorFocusableElements(); 1080 if (!focusableElements.length) { 1081 e.preventDefault(); 1082 languageSelectorModal.focus(); 1083 return; 1084 } 1085 1086 const firstElement = focusableElements[0]; 1087 const lastElement = focusableElements[focusableElements.length - 1]; 1088 if (e.shiftKey && (document.activeElement === firstElement || document.activeElement === languageSelectorModal)) { 1089 e.preventDefault(); 1090 lastElement.focus(); 1091 } else if (!e.shiftKey && document.activeElement === lastElement) { 1092 e.preventDefault(); 1093 firstElement.focus(); 1094 } 1095 }); 1096 1097 document.addEventListener('focusin', (e) => { 1098 if (getComputedStyle(languageSelectorModal).display !== 'none' && !languageSelectorModal.contains(e.target)) { 1099 const languageButton = languageSelectorModal.querySelector('[class*="lp-lang-btn"]:not([disabled])'); 1100 (languageButton || getLanguageSelectorFocusableElements()[0] || languageSelectorModal).focus(); 1101 } 1102 }); 1103 1104 // Close on outside click (click on modal background) 1105 if (languageSelectorModal) { 1106 languageSelectorModal.addEventListener('click', (e) => { 1107 if (e.target === languageSelectorModal) { 1108 closeLanguageSelector(); 1109 } 1110 }); 1111 } 1112 1113 // =================== LANGUAGE SELECTOR BUTTON LISTENERS =================== 1114 // Add click listeners to all language selector flag buttons to open modal
1115 languageSelectorFlags.forEach(flag => { 1116 // Make the flag and its parent clickable 1117 const clickableParent = flag.closest('[class*="div-flex-2"]') || flag.parentElement; 1118 if (clickableParent) { 1119 clickableParent.style.cursor = 'pointer'; 1120 clickableParent.addEventListener('click', (e) => { 1121 e.preventDefault(); 1122 openLanguageSelector(); 1123 }); 1124 } 1125 1126 // Also add listener directly to flag in case parent isn't found 1127 flag.style.cursor = 'pointer'; 1128 flag.addEventListener('click', (e) => { 1129 e.preventDefault(); 1130 openLanguageSelector(); 1131 }); 1132 }); 1133 1134 // Add listener for footer language selector 1135 if (footerLanguageSelector) { 1136 footerLanguageSelector.style.cursor = 'pointer'; 1137 footerLanguageSelector.addEventListener('click', (e) => { 1138 e.preventDefault(); 1139 openLanguageSelector(); 1140 }); 1141 } 1142 1143 // Helper to build URL from locale 1144 function buildUrlFromLocale(lang, country) { 1145 const parts = location.pathname.split('/').filter(Boolean); 1146 const first = parts[0]; 1147 1148 // Remove existing locale prefix if present 1149 let restParts = parts; 1150 if (/^[a-z]{2}(-[a-z0-9]{2,})?$/i.test(first)) { 1151 restParts = parts.slice(1); 1152 } 1153 1154 const restPath = restParts.join('/'); 1155 const restFirst = restParts[0] || ''; 1156 1157 // Check if current page is courses and target language doesn't support courses 1158 if (restFirst === 'courses' && !LANGUAGES_WITH_COURSES.includes(lang)) { 1159 // Redirect to home page instead 1160 if (lang === 'en' && country === 'global') { 1161 return '/'; 1162 } else if (country === 'global') { 1163 return `/${lang}`; 1164 } else { 1165 return `/${lang}-${country}`; 1166 } 1167 } 1168 1169 // Check if current page is news and target language doesn't support news 1170 if (restFirst === 'news' && !LANGUAGES_WITH_NEWS.includes(lang)) { 1171 // Redirect to home page instead 1172 if (lang === 'en' && country === 'global') { 1173 return '/'; 1174 } else if (country === 'global') { 1175 return `/${lang}`; 1176 } else { 1177 return `/${lang}-${country}`; 1178 } 1179 } 1180 1181 // Check if current page is global English-only and switching to non-English 1182 if (isEnglishOnlySlug(restParts) && lang !== 'en') { 1183 // Redirect to home page instead 1184 if (country === 'global') { 1185 return `/${lang}`; 1186 } else { 1187 return `/${lang}-${country}`; 1188 } 1189 } 1190 1191 // English-only pages don't have locale-prefixed versions (e.g. /en-us/teaching/96-3 is 404) 1192 // Always serve from root path for any English variant 1193 if (isEnglishOnlySlug(restParts) && lang === 'en') { 1194 return `/${restPath}${location.search}${location.hash}`; 1195 } 1196 1197 // Build new prefix using same logic as picker component 1198 if (lang === 'en' && country === 'global') { 1199 // en-global goes to root 1200 return `/${restPath}${location.search}${location.hash}`; 1201 } 1202 1203 if (country === 'global') { 1204 // Other global countries use just language code 1205 return `/${lang}${restPath ? '/' + restPath : ''}${location.search}${location.hash}`; 1206 } 1207 1208 // For specific countries, check if we need full locale format 1209 const langObj = LANG_DATA.find(l => l.code === lang); 1210 const countryObj = langObj?.availableCountries?.find(c => c.code === country); 1211 const requiresFull = countryObj?.useFullLocale === true; 1212 const isGlobalPage = GLOBAL_LANGUAGE_PAGES.includes(restFirst); 1213 1214 // Use full locale format when country requires it OR not on global page 1215 const forceFull = requiresFull || !isGlobalPage; 1216 1217 if (forceFull) { 1218 return `/${lang}-${country}${restPath ? '/' + restPath : ''}${location.search}${location.hash}`; 1219 } else { 1220 return `/${lang}${restPath ? '/' + restPath : ''}${location.search}${location.hash}`; 1221 } 1222 } 1223 1224 // Helper to check if current page is a global English-only page 1225 function isOnGlobalEnglishOnlyPage() { 1226 const currentPath = location.pathname; 1227 return GLOBAL_ENGLISH_ONLY_PAGES.some(slug => { 1228 // Check if path matches: /slug or /slug/* or /xx/slug or /xx-yy/slug 1229 const pattern = new RegExp(`^(/[a-z]{2}(-[a-z0-9]{2,})?)?/${slug}(/|$)`, 'i'); 1230 return pattern.test(currentPath); 1231 }); 1232 } 1233 1234 // Helper to check if current page should suppress notifications 1235 function isOnSuppressNotificationPage() { 1236 const currentPath = location.pathname; 1237 return SUPPRESS_NOTIFICATION_PAGES.some(slug => { 1238 // Check if path matches: /slug or /slug/* or /xx/slug or /xx-yy/slug 1239 const pattern = new RegExp(`^(/[a-z]{2}(-[a-z0-9]{2,})?)?/${slug}(/|$)`, 'i'); 1240 return pattern.test(currentPath); 1241 }); 1242 } 1243 1244 // =================== LOCALE NOTIFICATION BAR =================== 1245 // Only initialize if notification bar exists (defer non-critical functionality) 1246 if (!notificationBar) { 1247 // Still initialize no-cookie modal if it exists 1248 initNoCookieModal(); 1249 return; 1250 } 1251 1252 // Check if notification has been dismissed for this specific location 1253 const dismissedCookie = readCookie(DISMISSED_COOKIE_NAME); 1254 const isThisNotificationDismissed = dismissedCookie === softCookie; 1255 1256 // Determine if we should suppress the notification 1257 // Suppress when: 1258 // 1. Hard cookie is en-xx, soft is en-global, and on a global English-only page 1259 // 2. This specific notification has been dismissed before 1260 // 3. On a page in the suppress notification list (like news articles) 1261 // 4. Hard cookie is ja-jp and on a global English-only page (Japanese content sometimes links to English pages) 1262 const shouldSuppressNotification = hardCookie && softCookie !== hardCookie && ( 1263 (hardCookie.startsWith('en-') && softCookie === 'en-global' && isOnGlobalEnglishOnlyPage()) || 1264 isThisNotificationDismissed || 1265 isOnSuppressNotificationPage() || 1266 (hardCookie === 'ja-jp' && isOnGlobalEnglishOnlyPage()) 1267 ); 1268 1269 if (notificationBar && hardCookie && softCookie !== hardCookie && !shouldSuppressNotification) { 1270 // Show notification bar (soft â hard) 1271 const switchBackBtn = document.getElementById('switchBackButton'); 1272 const dismissBtn = notificationBar.querySelector('[class*="div-site-6"] a'); // Dismiss button (prefix-agnostic) 1273 const setDefaultBtn = document.getElementById('setDefaultButton'); 1274 const hardCookieFlag = document.getElementById('hardCookieFlag'); 1275 1276 // Update flag for hard cookie country 1277 if (hardCookieFlag && hardLang && hardCountry) { 1278 const hardLangObj = LANG_DATA.find(l => l.code === hardLang); 1279 const hardCountryObj = hardLangObj?.availableCountries?.find(c => c.code === hardCountry); 1280 if (hardCountryObj) { 1281 hardCookieFlag.src = `https://derekprince.com/cdn-cgi/image/width=40,height=40,quality=75,fit=cover,format=webp/https://images.derekprince.com/flags/${hardCountryObj.flag}`; 1282 } 1283 } 1284 1285 // Switch Back: Navigate to hard cookie location 1286 if (switchBackBtn) { 1287 switchBackBtn.addEventListener('click', (e) => { 1288 e.preventDefault(); 1289 writeCookie(SOFT_COOKIE_NAME, hardCookie); 1290 const targetUrl = buildUrlFromLocale(hardLang, hardCountry); 1291 window.location.assign(targetUrl); 1292 }); 1293 } 1294 1295 // Dismiss: Remember this notification was dismissed for current location, hide notification 1296 if (dismissBtn) {
1297 dismissBtn.addEventListener('click', (e) => { 1298 e.preventDefault(); 1299 // Store the current soft cookie value - only dismiss for this specific location 1300 writeCookieWithExpiry(DISMISSED_COOKIE_NAME, softCookie, DISMISSED_COOKIE_MAX_AGE); 1301 notificationBar.style.display = 'none'; 1302 }); 1303 } 1304 1305 // Set Default: Open language selector modal 1306 if (setDefaultBtn) { 1307 setDefaultBtn.addEventListener('click', (e) => { 1308 e.preventDefault(); 1309 openLanguageSelector(); 1310 }); 1311 } 1312 1313 // Show the notification 1314 notificationBar.style.display = 'block'; 1315 } else { 1316 // Notification not shown, check for no-cookie modal 1317 initNoCookieModal(); 1318 } 1319 1320 // =================== NO COOKIE MODAL =================== 1321 function initNoCookieModal() { 1322 if (!noCookieModal || hardCookie) return; 1323 // Show modal (no hard cookie exists) 1324 const stayHereBtn = document.getElementById('stayHereButton'); 1325 const closeBtn = document.getElementById('closeNoCookieModal'); 1326 const connectToBtn = document.getElementById('connectToButton'); 1327 const titleElement = document.getElementById('title'); 1328 1329 // Update title with i18n 1330 if (titleElement && softLangObj && softCountryObj) { 1331 // titleElement.setAttribute('data-template', "You're Browsing [[countryName]] ([[languageName]])"); 1332 titleElement.setAttribute('data-countryname', softCountryObj.name); 1333 titleElement.setAttribute('data-languagename', softLangObj.localName || softLangObj.name); 1334 titleElement.classList.add('i18n-text'); 1335 } 1336 1337 // Stay here: Set hard = soft, close modal 1338 if (stayHereBtn) { 1339 stayHereBtn.addEventListener('click', (e) => { 1340 e.preventDefault(); 1341 writeCookie(HARD_COOKIE_NAME, softCookie); 1342 noCookieModal.style.display = 'none'; 1343 }); 1344 } 1345 1346 // Close X: Set hard = soft, close modal 1347 if (closeBtn) { 1348 closeBtn.addEventListener('click', (e) => { 1349 e.preventDefault(); 1350 writeCookie(HARD_COOKIE_NAME, softCookie); 1351 noCookieModal.style.display = 'none'; 1352 }); 1353 } 1354 1355 // Connect To: Open language selector modal 1356 if (connectToBtn) { 1357 connectToBtn.addEventListener('click', (e) => { 1358 e.preventDefault(); 1359 openLanguageSelector(); 1360 }); 1361 } 1362 1363 // Show the modal 1364 noCookieModal.style.display = 'flex'; 1365 1366 // Initialize i18n texts for modal 1367 initI18nTexts(); 1368 } 1369 1370 // =================== INITIALIZE I18N TEXTS =================== 1371 function internationalizedSentenceWithLowercase(text, variables) { 1372 if (!text) return ''; 1373 return text.replace(/\[\[(\w+)\]\]/g, (match, key) => { 1374 const lowerKey = key.toLowerCase(); 1375 return variables[lowerKey] !== undefined ? variables[lowerKey] : match; 1376 }); 1377 } 1378 1379 function initI18nTexts() { 1380 // Use prefix-agnostic selector for i18n-text class 1381 const elements = document.querySelectorAll('[class*="i18n-text"][data-template]'); 1382 1383 elements.forEach(el => { 1384 const template = el.getAttribute('data-template'); 1385 if (!template) return; 1386 1387 const variables = {}; 1388 Array.from(el.attributes).forEach(attr => { 1389 if (attr.name.startsWith('data-') && attr.name !== 'data-template') { 1390 const key = attr.name.replace('data-', ''); 1391 variables[key] = attr.value; 1392 } 1393 }); 1394 1395 // Override with current soft cookie context if available 1396 // This ensures that on Global English Only pages (where server-side rendered attributes 1397 // might default to "Global"), the text reflects the user's actual browsing context (e.g. "New Zealand") 1398 if (softCountryObj) { 1399 variables['countryname'] = softCountryObj.name; 1400 } 1401 if (softLangObj) { 1402 variables['languagename'] = softLangObj.localName || softLangObj.name; 1403 } 1404 1405 const formatted = internationalizedSentenceWithLowercase(template, variables); 1406 el.textContent = formatted; 1407 }); 1408 } 1409 1410 // Initialize i18n texts if we showed notification or modal 1411 initI18nTexts(); 1412} 1413 1414// Initialize locale components on DOM ready 1415if (document.readyState === 'loading') { 1416 document.addEventListener('DOMContentLoaded', initLocaleComponents); 1417} else { 1418 initLocaleComponents(); 1419} 1420})();
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.