PageSourceSearch

https://assets.derekprince.com/JS/language-selector-module/language-selector-module.js

js derekprince.com collected 2026-10-02 12:14:51 UTC 58,372 bytes, 1,420 lines download raw bytes

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=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[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.