PageSourceSearch

https://www.surest.com/scripts/lib-surest/decorate-language-switch.js

js surest.com collected 2026-10-02 04:03:51 UTC 10,241 bytes, 334 lines download raw bytes

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.