1/** 2 * Language Switcher 3 * 4 * Two tiers: 5 * 1. Signal Pilot translations â the twelve languages the curriculum is 6 * actually written in. Picking one navigates to the real translated page 7 * (/de/education/..., /ja/education/..., and so on) and clears any Google 8 * Translate cookie so nothing gets translated a second time on top. 9 * 2. Google Translate â every other language Google offers, applied to the 10 * English original. 11 */ 12 13(function() { 14 'use strict'; 15 16 // Locales we have hand-translated. Order matters: this is the menu order. 17 const NATIVE = [ 18 { code: 'en', name: 'English', flagCode: 'us' }, 19 { code: 'de', name: 'Deutsch', flagCode: 'de' }, 20 { code: 'es', name: 'Español', flagCode: 'es' }, 21 { code: 'fr', name: 'Français', flagCode: 'fr' }, 22 { code: 'it', name: 'Italiano', flagCode: 'it' }, 23 { code: 'pt', name: 'Português', flagCode: 'pt' }, 24 { code: 'nl', name: 'Nederlands', flagCode: 'nl' }, 25 { code: 'ru', name: 'Ð ÑÑÑкий', flagCode: 'ru' }, 26 { code: 'ja', name: 'æ¥æ¬èª', flagCode: 'jp' }, 27 { code: 'tr', name: 'Türkçe', flagCode: 'tr' }, 28 { code: 'hu', name: 'Magyar', flagCode: 'hu' }, 29 { code: 'ar', name: 'Ø§ÙØ¹Ø±Ø¨ÙØ©', flagCode: 'sa' } 30 ]; 31 32 // Everything else Google Translate can reach, applied to the English page. 33 const MACHINE = [ 34 { code: 'zh-CN', name: '䏿 (ç®ä½)', flagCode: 'cn' }, 35 { code: 'zh-TW', name: '䏿 (ç¹é«)', flagCode: 'tw' }, 36 { code: 'ko', name: 'íêµì´', flagCode: 'kr' }, 37 { code: 'hi', name: 'हिनà¥à¤¦à¥', flagCode: 'in' }, 38 { code: 'pl', name: 'Polski', flagCode: 'pl' }, 39 { code: 'uk', name: 'УкÑаÑнÑÑка', flagCode: 'ua' }, 40 { code: 'sv', name: 'Svenska', flagCode: 'se' }, 41 { code: 'no', name: 'Norsk', flagCode: 'no' }, 42 { code: 'da', name: 'Dansk', flagCode: 'dk' }, 43 { code: 'fi', name: 'Suomi', flagCode: 'fi' }, 44 { code: 'cs', name: 'ÄeÅ¡tina', flagCode: 'cz' }, 45 { code: 'ro', name: 'RomânÄ', flagCode: 'ro' }, 46 { code: 'bg', name: 'ÐÑлгаÑÑки', flagCode: 'bg' }, 47 { code: 'el', name: 'Îλληνικά', flagCode: 'gr' }, 48 { code: 'iw', name: '×¢×ר×ת', flagCode: 'il' }, 49 { code: 'id', name: 'Bahasa Indonesia', flagCode: 'id' }, 50 { code: 'th', name: 'à¹à¸à¸¢', flagCode: 'th' }, 51 { code: 'vi', name: 'Tiếng Viá»t', flagCode: 'vn' } 52 ]; 53 54 const LOCALE_RE = /^\/(de|es|fr|it|pt|nl|ru|ja|tr|hu|ar)(?=\/|$)/; 55 56 // Paths that exist in every locale directory. Anything else has no native 57 // translation, so those pages fall back to Google Translate. 58 const NATIVE_PATHS = [ 59 /^\/education\/curriculum\//, 60 /^\/education\/(index\.html)?$/, 61 /^\/chronicle\//, 62 /^\/(index\.html)?$/, 63 /^\/(affiliates|faq|manage-subscription|privacy|refund|roadmap|terms|trial-thanks)\.html$/ 64 ]; 65 66 function getFlagImg(flagCode, size = 40) { 67 return `<img src="https://flagcdn.com/w${size}/${flagCode}.png" width="26" height="17" alt="${flagCode.toUpperCase()}" loading="lazy" style="vertical-align:middle;border-radius:2px;flex-shrink:0">`; 68 } 69 70 // --- path helpers ------------------------------------------------------- 71 72 // '/de/education/index.html' -> 'de' ; '/education/index.html' -> 'en' 73 function pathLocale() { 74 const m = location.pathname.match(LOCALE_RE); 75 return m ? m[1] : 'en'; 76 } 77 78 // '/de/education/index.html' -> '/education/index.html' 79 function basePath() { 80 const p = location.pathname; 81 const m = p.match(LOCALE_RE); 82 return m ? (p.slice(m[0].length) || '/') : p; 83 } 84 85 function hasNativeVersion(path) { 86 return NATIVE_PATHS.some(re => re.test(path)); 87 } 88 89 function nativeUrl(langCode) { 90 const base = basePath(); 91 const prefix = langCode === 'en' ? '' : '/' + langCode; 92 return prefix + base + location.search + location.hash; 93 } 94 95 // --- cookies ------------------------------------------------------------ 96 97 function baseDomain(host) { 98 const p = host.split('.'); 99 return p.length > 2 ? p.slice(-2).join('.') : host; 100 } 101 102 function setCookie(name, value, days, domain) { 103 let expires = ''; 104 if (days) { 105 const d = new Date(); 106 d.setTime(d.getTime() + days * 864e5); 107 expires = '; expires=' + d.toUTCString(); 108 } 109 const dom = domain ? '; domain=' + domain : ''; 110 document.cookie = name + '=' + value + expires + '; path=/' + dom; 111 } 112 113 function rootDomain() { 114 return '.' + baseDomain(location.hostname.replace(/^www\./, '')); 115 } 116 117 function setGoogTrans(langCode) { 118 const val = '/en/' + (langCode === 'zh' ? 'zh-CN' : langCode);
119 setCookie('googtrans', val, 365); 120 setCookie('googtrans', val, 365, rootDomain()); 121 } 122 123 function clearGoogTrans() { 124 setCookie('googtrans', '', -1); 125 setCookie('googtrans', '', -1, rootDomain()); 126 try { localStorage.removeItem('sp_lang'); } catch (e) {} 127 } 128 129 function googTransCookie() { 130 const match = document.cookie.match(/googtrans=\/en\/([^;]+)/); 131 return match && match[1] ? match[1] : null; 132 } 133 134 function applyDirLang(langCode) { 135 document.documentElement.lang = (langCode === 'zh') ? 'zh-CN' : langCode; 136 document.documentElement.dir = (langCode === 'ar' || langCode === 'iw') ? 'rtl' : 'ltr'; 137 } 138 139 // Which entry should be highlighted. A locale in the URL always wins: we are 140 // reading a real translation, whatever a stale cookie says. 141 function activeCode() { 142 const loc = pathLocale(); 143 if (loc !== 'en') return loc; 144 return googTransCookie() || 'en'; 145 } 146 147 // --- navigation --------------------------------------------------------- 148 149 function goNative(langCode) { 150 clearGoogTrans(); 151 const target = nativeUrl(langCode); 152 if (target === location.pathname + location.search + location.hash) { 153 location.reload(); 154 } else { 155 location.href = target; 156 } 157 } 158 159 function goMachine(langCode) { 160 try { localStorage.setItem('sp_lang', langCode); } catch (e) {} 161 setGoogTrans(langCode); 162 applyDirLang(langCode); 163 // Always machine-translate the English original, never one of our own 164 // translations â layering Google on top of hand-written German is worse 165 // than either on its own. 166 const english = basePath() + location.search + location.hash; 167 if (english !== location.pathname + location.search + location.hash) { 168 location.href = english; 169 } else { 170 location.reload(); 171 } 172 } 173 174 function changeLanguage(langCode, isNative) { 175 if (isNative && hasNativeVersion(basePath())) { 176 goNative(langCode); 177 return; 178 } 179 if (isNative && langCode === 'en') { 180 clearGoogTrans(); 181 document.documentElement.lang = 'en'; 182 document.documentElement.dir = 'ltr'; 183 location.href = basePath() + location.search + location.hash; 184 return; 185 } 186 // No hand-translated version of this particular page: fall back to Google. 187 goMachine(langCode); 188 } 189 190 // --- menu --------------------------------------------------------------- 191 192 function makeHeading(text) { 193 const h = document.createElement('div'); 194 h.className = 'lang-group-label'; 195 h.textContent = text; 196 return h; 197 } 198 199 function makeButton(lang, isNative, current, translated) { 200 const button = document.createElement('button'); 201 button.type = 'button'; 202 button.setAttribute('role', 'menuitem'); 203 button.setAttribute('data-lang', lang.code); 204 button.innerHTML = `${getFlagImg(lang.flagCode)}<span>${lang.name}</span>`; 205 if (isNative && !translated && lang.code !== 'en') { 206 button.classList.add('lang-approx'); 207 button.title = 'This page has no hand-written translation yet â Google Translate will be used.'; 208 } 209 if (lang.code === current) { 210 button.classList.add('active'); 211 button.setAttribute('aria-current', 'true'); 212 } 213 button.addEventListener('click', () => changeLanguage(lang.code, isNative)); 214 return button; 215 } 216 217 function createDropdownMenu() { 218 const menu = document.createElement('div'); 219 menu.className = 'lang-dropdown-menu'; 220 menu.setAttribute('role', 'menu'); 221 menu.setAttribute('aria-label', 'Language selection'); 222 223 const current = activeCode(); 224 const translated = hasNativeVersion(basePath()); 225 226 menu.appendChild(makeHeading('Signal Pilot translations')); 227 NATIVE.forEach(lang => menu.appendChild(makeButton(lang, true, current, translated))); 228 229 menu.appendChild(makeHeading('More languages · Google Translate')); 230 MACHINE.forEach(lang => menu.appendChild(makeButton(lang, false, current, translated))); 231 232 document.addEventListener('click', (e) => { 233 if (!e.target.closest('.lang-dropdown') && menu.classList.contains('active')) { 234 menu.classList.remove('active'); 235 } 236 }); 237 238 return menu; 239 } 240 241 // --- init --------------------------------------------------------------- 242
243 function init() { 244 const container = document.getElementById('google_translate_element'); 245 if (!container) return; 246 247 const current = activeCode(); 248 const all = NATIVE.concat(MACHINE); 249 const currentLangObj = all.find(l => l.code === current) || NATIVE[0]; 250 251 container.id = 'langToggle'; 252 container.setAttribute('role', 'button'); 253 container.setAttribute('tabindex', '0'); 254 container.setAttribute('aria-label', `Select language (current: ${currentLangObj.name})`); 255 container.setAttribute('aria-haspopup', 'true'); 256 container.setAttribute('aria-expanded', 'false'); 257 container.innerHTML = getFlagImg(currentLangObj.flagCode); 258 259 const menu = createDropdownMenu(); 260 document.body.appendChild(menu); 261 262 // The menu used to sit at a hard-coded top:70px, which lands underneath the 263 // header whenever anything above it (the translation notice, a banner) 264 // pushes the header down â and the part hidden behind the header cannot be 265 // scrolled to. Anchor it to the button instead, every time it opens. 266 function position() { 267 const r = container.getBoundingClientRect(); 268 const gap = 10; 269 const margin = 12; 270 menu.style.top = (r.bottom + gap) + 'px'; 271 menu.style.right = Math.max(margin, window.innerWidth - r.right) + 'px'; 272 menu.style.bottom = 'auto'; 273 menu.style.maxHeight = Math.max(180, window.innerHeight - r.bottom - gap - margin) + 'px'; 274 } 275 276 container.addEventListener('click', (e) => { 277 e.stopPropagation(); 278 const isActive = menu.classList.contains('active'); 279 if (!isActive) { 280 position(); 281 menu.scrollTop = 0; 282 } 283 menu.classList.toggle('active'); 284 container.setAttribute('aria-expanded', String(!isActive)); 285 }); 286 287 window.addEventListener('resize', () => { 288 if (menu.classList.contains('active')) position(); 289 }); 290 window.addEventListener('scroll', () => { 291 if (menu.classList.contains('active')) position(); 292 }, { passive: true }); 293 document.addEventListener('keydown', (e) => { 294 if (e.key === 'Escape' && menu.classList.contains('active')) { 295 menu.classList.remove('active'); 296 container.setAttribute('aria-expanded', 'false'); 297 container.focus(); 298 } 299 }); 300 container.addEventListener('keydown', (e) => { 301 if (e.key === 'Enter' || e.key === ' ') { 302 e.preventDefault(); 303 container.click(); 304 } 305 }); 306 307 if (pathLocale() !== 'en') { 308 // We are on one of our own translations. Make sure a leftover cookie 309 // cannot machine-translate it a second time. 310 if (googTransCookie()) clearGoogTrans(); 311 return; 312 } 313 314 if (current !== 'en') { 315 applyDirLang(current); 316 loadGoogleTranslate(current); 317 } 318 } 319 320 function loadGoogleTranslate(lang) { 321 if (!lang || lang === 'en') return; 322 323 const hiddenDiv = document.createElement('div'); 324 hiddenDiv.id = 'google_translate_container'; 325 hiddenDiv.style.position = 'absolute'; 326 hiddenDiv.style.left = '-9999px'; 327 hiddenDiv.setAttribute('aria-hidden', 'true'); 328 document.body.appendChild(hiddenDiv); 329 330 window.googleTranslateElementInit = function() { 331 new google.translate.TranslateElement({ 332 pageLanguage: 'en', 333 includedLanguages: NATIVE.concat(MACHINE).map(l => l.code).join(','), 334 layout: google.translate.TranslateElement.InlineLayout.SIMPLE, 335 autoDisplay: false 336 }, 'google_translate_container'); 337 }; 338 339 const script = document.createElement('script'); 340 script.src = 'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit'; 341 script.async = true; 342 script.onerror = function() { 343 console.warn('Failed to load Google Translate'); 344 }; 345 document.head.appendChild(script); 346 } 347 348 function cleanupGoogleUI() { 349 const frames = document.querySelectorAll('.goog-te-banner-frame, iframe.goog-te-banner-frame, .skiptranslate'); 350 frames.forEach(f => { if (f.parentNode) f.parentNode.removeChild(f); }); 351 document.body.style.top = '0'; 352 document.body.style.position = 'relative'; 353 if (document.body.hasAttribute('style')) { 354 const style = document.body.getAttribute('style'); 355 if (style && style.includes('top:')) { 356 document.body.setAttribute('style', style.replace(/top:\s*[^;]+;?/g, '')); 357 } 358 } 359 } 360 361 window.addEventListener('load', () => { 362 cleanupGoogleUI(); 363 setTimeout(cleanupGoogleUI, 500); 364 setTimeout(cleanupGoogleUI, 1000); 365 setTimeout(cleanupGoogleUI, 2000); 366 }); 367 368 if (document.readyState === 'loading') { 369 document.addEventListener('DOMContentLoaded', init); 370 } else { 371 init(); 372 } 373})();
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.