1import { createElement } from './create-element.js'; 2import { icons } from './icons-internal.js'; 3import { fetchLanguagePlaceholders } from './fetch-placeholders.js'; 4 5const placeholders = await fetchLanguagePlaceholders(); 6 7/** 8 * Returns two-letter ISO code of first matched preferred language if available 9 * @param {Array<object>} availableLanguages 10 * @returns {String} two letter ISO code 11 */ 12function usePreferredLanguage(availableLanguages) { 13 const availLangIso = availableLanguages.map((lA) => lA.iso); 14 const langMatch = navigator.languages.find((nL) => 15 availLangIso.includes(nL.substring(0, 2)), 16 ); 17 return langMatch?.substring(0, 2); 18} 19 20/** 21 * Constructs new language path for localized version of current site 22 * @param {String} currentPath current language URL path, e.g. '/es' 23 * @param {String} newPath new language URL path e.g. '/' 24 * @param {Location} location the window.location object 25 * @returns {String} new URL 26 * 27 */ 28export function langPathHelper(currentPath, newPath, location) { 29 const basePath = location.origin; 30 const pathname = location.pathname.substring(currentPath.length + 1); 31 const params = location.search || ''; 32 33 // Build the newPathSegment based on newPath and currentPath 34 let newPathSegment = ''; 35 if (newPath.length > 0) { 36 newPathSegment = `/${newPath}`; 37 } 38 if (currentPath.length === 0) { 39 newPathSegment += '/'; 40 } 41 42 // Construct and return the final URL 43 return `${basePath}${newPathSegment}${pathname}${params}`; 44} 45 46/** 47 * Gets available languages as object. 48 * Implementation adapted to stay close to Franklin's fetchPlaceholders function 49 * @returns {Promise<object>} Window languages object 50 */ 51export async function fetchLanguages() { 52 window.languages = window.languages || []; 53 if (window.languages.length < 1) { 54 window.languages = new Promise((resolve, reject) => { 55 fetch('/languages.json') 56 .then((resp) => { 57 if (resp.ok) { 58 return resp.json(); 59 } 60 throw new Error(`${resp.status}: ${resp.statusText}`); 61 }) 62 .then((json) => { 63 window.languages = json.data; 64 resolve(); 65 }) 66 .catch((error) => { 67 // error loading languages 68 window.languages = {}; 69 reject(error); 70 }); 71 }); 72 } 73 await window.languages; 74 return window.languages; 75} 76 77/** 78 * Returns all actually available translations of the current page 79 * @param {Object} currentLang active language object 80 * @param {Array} availableLanguages all languages available from Sharepoint 81 * @returns {Promise<Awaited<* | null>[]>} Promise returning with the available language entries 82 */ 83async function getAvailableLanguagesForCurrentPage( 84 currentLang, 85 availableLanguages, 86) { 87 // Handy for testing purposes: return fetchLanguages(); 88 89 const currentPath = availableLanguages.find( 90 (l) => l.iso === currentLang, 91 ).path; 92 return Promise.all( 93 availableLanguages.map((lang) => { 94 const req = 95 lang.path === currentPath 96 ? window.location 97 : langPathHelper(currentPath, lang.path, window.location); 98 return fetch(req, { method: 'HEAD' }) 99 .then((res) => (res.ok ? lang : null)) 100 .catch((e) => e); 101 }), 102 ).then((links) =>
102 links.filter(Boolean)); 103} 104 105async function switchLanguage(currentLang, newLang) { 106 if (newLang.iso === currentLang) return; 107 // refac once `en` files are in `en` subfolder 108 const availableLanguages = await fetchLanguages(); 109 const current = availableLanguages.find((l) => l.iso === currentLang); 110 localStorage.setItem('i18n', newLang.iso); 111 window.location = langPathHelper(current.path, newLang.path, window.location); 112} 113 114export function getCurrentLanguage() { 115 return window.location.pathname.substring(3, 4) === '/' 116 ? window.location.pathname.substring(1, 3) 117 : 'en'; 118} 119 120export async function detectLanguage() { 121 // get dynamic language list from Sharepoint 122 const availableLanguages = await fetchLanguages(); 123 124 // has to be 'en' if no path subfolder is selected â refac once files are moved to subfolder 125 const currentLang = getCurrentLanguage(); 126 127 // get dynamic list of all languages the current page is available in 128 const langSelectable = await getAvailableLanguagesForCurrentPage( 129 currentLang, 130 availableLanguages, 131 ); 132 133 let prefLang = localStorage.getItem('i18n'); 134 if (!prefLang) { 135 if (currentLang !== 'en') { 136 prefLang = currentLang; 137 } else { 138 // automatically switch page language to preferred language, if available 139 prefLang = usePreferredLanguage(availableLanguages); 140 } 141 localStorage.setItem('i18n', prefLang); 142 } 143 144 if (currentLang !== prefLang) { 145 if (langSelectable.find((l) => l.iso === prefLang)) { 146 switchLanguage( 147 currentLang, 148 langSelectable.find((l) => l.iso === prefLang), 149 ); 150 } 151 } 152 153 window.onlanguagechange = () => { 154 // only change site language if preferred language is available 155 const newLang = availableLanguages.find( 156 (lA) => lA.iso === navigator.language.substring(0, 2), 157 ); 158 if (newLang.iso !== currentLang) switchLanguage(currentLang, newLang); 159 }; 160} 161 162/* Close language switcher dialogue */ 163const closeLangSwitchDialogue = (langSwitch) => { 164 window.removeEventListener('mouseup', closeLangSwitchDialogue); 165 setTimeout(() => { 166 // remove list of languages in switcher dialogue 167 langSwitch.querySelectorAll('ul')?.forEach((ul) => ul.remove()); 168 }, 1); 169}; 170 171function setCurrentLanguageSelection( 172 language, 173 currentLanguageSpan, 174 availableLanguages, 175) { 176 const fullLanguageName = availableLanguages.find( 177 (l) => l.iso === language, 178 ).name; 179 currentLanguageSpan.textContent = fullLanguageName; 180 const langSwitchA11y = `${placeholders.languageSwitcherExplainer} ${placeholders.currentLanguage} ${fullLanguageName}`; 181 const langSwitch = currentLanguageSpan.closest('.language-switcher'); 182 langSwitch.setAttribute('aria-label', langSwitchA11y); 183} 184 185function handleLangSwitchClicked( 186 langSwitch, 187 currentLang, 188 availableLanguages, 189 currentLanguageSpan, 190) { 191 window.addEventListener('mouseup', () => closeLangSwitchDialogue(langSwitch)); 192 const langList = createElement('ul', [ 193 'flex', 194 'flex-col', 195 'items-end', 196 'border', 197 'bg-white', 198 'space-y-2', 199 'absolute', 200 'self-end', 201 'mb-12', 202 'rounded-md', 203 'backdrop-blur-2xl', 204 'shadow-lang-switcher', 205 'py-4', 206 'px-1', 207 ]); 208 langList.ariaLabel = placeholders?.languageSwitcherLanguages; 209 210 // get dynamic list of all languages the current page is available in 211 getAvailableLanguagesForCurrentPage(currentLang, availableLanguages).then( 212 (res) => { 213 res.forEach((lang, index) => { 214 const langRow = createElement('li', [ 215 'flex', 216 'flex-row', 217 'items-center', 218 'space-x-1', 219 'w-full', 220 'pr-2', 221 'pl-2', 222 'rounded-md', 223 'hover:bg-surest-blue', 224 'hover:text-white', 225 'hover:stroke-white', 226 'hover:fill-white', 227 'focus-visible:bg-surest-blue', 228 'focus-visible:text-white', 229 'focus-visible:stroke-white', 230 'focus-visible:fill-white', 231 ]); 232 langRow.ariaLabel = lang.name; 233 langRow.tabIndex = index + 1; 234 const langEl = createElement('span'); 235 langEl.textContent = lang.name; 236 /* display checkmark next to current language */ 237 const checkmark = createElement('span', ['w-2.5']); 238 if (lang.iso === currentLang) { 239 langRow.ariaLabel += ` ${placeholders?.selected}`; 240 checkmark.innerHTML = icons.checkmark; 241 } 242 langRow.onclick = () => { 243 switchLanguage(currentLang, lang); 244 }; 245 langRow.onkeydown = (e) => { 246 switch (e.key) { 247 case 'Tab': 248 if (document.activeElement === langList.lastElementChild) { 249 e.preventDefault(); 250 langList.firstElementChild.focus(); 251 } else if ( 252 e.shiftKey && 253 document.activeElement === langList.firstElementChild 254 ) { 255 e.preventDefault(); 256 langList.lastElementChild.focus(); 257 } 258 break; 259 case 'Enter': 260 e.preventDefault(); 261 switchLanguage(currentLang, lang); 262 break; 263 case 'Escape': 264 closeLangSwitchDialogue(); 265 break; 266 default: 267 e.preventDefault(); 268 } 269 }; 270 langRow.appendChild(checkmark); 271 langRow.appendChild(langEl); 272 langList.appendChild(langRow); 273 }); 274 langSwitch.appendChild(langList); 275 setCurrentLanguageSelection( 276 currentLang, 277 currentLanguageSpan, 278 availableLanguages, 279 ); 280 langList.firstElementChild.focus(); 281 }, 282 ); 283} 284 285/** 286 * Add Language switcher to parent element 287 * @param {Element} parentElement 288 */ 289export const decorateLanguageSwitch = async (parentElement) => { 290 const currentLang = getCurrentLanguage(); 291 292 // Get dynamic language list from Sharepoint 293 const availableLanguages = await fetchLanguages(); 294 295 // Get dynamic list of all languages the current page is available in 296 const langSelectable = await getAvailableLanguagesForCurrentPage( 297 currentLang, 298 availableLanguages, 299 ); 300 301 // If no other languages are available for this page, return 302 if (!langSelectable.find((l) => l.iso !== currentLang)) return; 303 304 const langSwitch = createElement('button', [ 305 'language-switcher', 306 'flex', 307 'space-x-2', 308 'items-center', 309 'cursor-pointer', 310 'stroke-surest-purple-subtle', 311 'hover:underline', 312 'hover:text-surest-indigo', 313 'hover:stroke-surest-indigo', 314 ]); 315 langSwitch.innerHTML = icons.globe; 316 317 const currentLanguageSpan = createElement('span'); 318 319 langSwitch.onclick = () => { 320 handleLangSwitchClicked( 321 langSwitch, 322 currentLang, 323 availableLanguages, 324 currentLanguageSpan, 325 ); 326 }; 327 langSwitch.append(currentLanguageSpan); 328 setCurrentLanguageSelection( 329 currentLang, 330 currentLanguageSpan, 331 availableLanguages, 332 ); 333 parentElement.append(langSwitch); 334};
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.