PageSourceSearch

https://bikeatthebeach.com/js/init-language-picker.js

js bikeatthebeach.com collected 2026-09-24 13:56:44 UTC 8,116 bytes, 215 lines download raw bytes

1/* public/js/init-language-picker.js
2   ══════════════════════════════════════════════════════════════════
3   • Creates a language dropdown that attaches to the Language menu button
4   • When the Language button is clicked, a dropdown panel appears
5     with the configured language options
6   • Remembers last choice in localStorage
7   • Honors ?lang=<code> in the URL (2026-09-02), so a link can open the
8     map in a language the project offers. See js/i18n/initial-language.js
9     for the precedence rule; the URL wins, but only for an offered code.
10   • Guards against duplicates (hot-reload safe)
11   *****************************************************************/
12
13import { resolveInitialLanguage } from './i18n/initial-language.js';
14
15export function initLanguagePicker () {
16
17  /* 0️⃣  project-level flag — also auto-enable when 2+ custom languages exist
18     (handles legacy projects saved before the enableLanguageToggle flag was added) */
19  const ps = window.projectSettings || {};
20  const hasLanguages = Array.isArray(ps.customLanguages) && ps.customLanguages.length >= 2;
21  if (!ps.enableLanguageToggle && !hasLanguages) return;
22
23  /* 1️⃣  duplicate guard */
24  if (document.getElementById('language-picker-dropdown')) return;
25
26  /* 2️⃣  build the language list */
27  const OPTIONS =
28   (Array.isArray(window.projectSettings?.customLanguages) &&
29    window.projectSettings.customLanguages.length)
30     ? window.projectSettings.customLanguages
31     : [
32         { code:'en', label:'English – EN'  },
33         { code:'es', label:'Español – ES'  },
34         { code:'vi', label:'Tiếng Việt – VI'},
35         { code:'zh', label:'中文 – ZH'      },
36         { code:'yi', label:'ייִדיש – YI'  }
37       ];
38
39  // Need at least 2 languages to show a picker
40  if (OPTIONS.length < 2) return;
41
42  /* 3️⃣  create the dropdown panel */
43  const dropdown = document.createElement('div');
44  dropdown.id = 'language-picker-dropdown';
45  dropdown.style.cssText = `
46    display: none;
47    position: fixed;
48    z-index: 2000;
49    background: #fff;
50    border-radius: 8px;
51    box-shadow: 0 4px 20px rgba(0,0,0,0.25);
52    padding: 6px 0;
53    min-width: 180px;
54  `;
55
56  const current = localStorage.getItem('wm_lang') || 'en';
57
58  OPTIONS.forEach(({ code, label }) => {
59    const item = document.createElement('button');
60    item.type = 'button';
61    item.dataset.lang = code;
62    item.textContent = label;
63    item.style.cssText = `
64      display: block;
65      width: 100%;
66      border: none;
67      background: ${code === current ? '#f0f0f0' : 'transparent'};
68      padding: 8px 16px;
69      text-align: left;
70      font-size: 14px;
71      cursor: pointer;
72      font-weight: ${code === current ? '600' : '400'};
73    `;
74    item.addEventListener('mouseenter', () => { item.style.background = '#e9ecef'; });
75    item.addEventListener('mouseleave', () => {
76      item.style.background = item.dataset.lang === current ? '#f0f0f0' : 'transparent';
77    });
78    item.addEventListener('click', () => selectLanguage(code));
79    dropdown.appendChild(item);
80  });
81
82  document.body.appendChild(dropdown);
83
84  /* 4️⃣  find every Language anchor and wire each up.
85   *
86   * The topnav layout renders BOTH a visible topnav button and a
87   * sidebar mobile-fallback button, so there can be multiple elements
88   * with data-mid="languageToggle". Attaching only to the first one
89   * (via querySelector) meant the topnav button clicked by the user
90   * had no handler and appeared broken. Wire every match so whichever
91   * anchor is visible responds. */
92  const langBtns = Array.from(
93    document.querySelectorAll('[data-mid="languageToggle"], [data-role="btn-languageToggle"]')
94  );
95  langBtns.forEach(btn => {
96    btn.addEventListener('click', (e) => {
97      e.stopPropagation();
98      toggleDropdown(btn);
99    });
100  });
101
102  // Also create a hidden <select> so the menu-builder handler can still find it
103  const hiddenSel = document.createElement('select');
104  hiddenSel.id = 'language-picker';
105  hiddenSel.style.display = 'none';
106  OPTIONS.forEach(({ code, label }) => {
107    const opt = document.createElement('option');
108    opt.value = code;
109    opt.textContent = label;
110    hiddenSel.appendChild(opt);
111  });
112  document.body.appendChild(hiddenSel);
113
114  /* 5️⃣  toggle/position logic */
115  function toggleDropdown(anchor) {
116    const isOpen = dropdown.style.display !== 'none';
117    if (isOpen) {
118      dropdown.style.display = 'none';
119      return;
120    }
121
122    // Position relative to the anchor button
123    const rect = anchor.getBoundingClientRect();
124    dropdown.style.display = 'block';
125
126    // Place to the right of the button, aligned to top
127    let left = rect.right + 8;
128    let top = rect.top;
129
130    // If it would go off-screen right, place below instead
131    if (left + 200 > window.innerWidth) {
132      left = rect.left;
133      top = rect.bottom + 4;
134    }
135
136    // Ensure it doesn't go off-screen bottom
137    if (top + dropdown.offsetHeight > window.innerHeight) {
138      top = window.innerHeight - dropdown.offsetHeight - 8;
139    }
140
141    dropdown.style.left = left + 'px';
142    dropdown.style.top = top + 'px';
143  }
144
145  // Close on outside click. Ignore clicks on any language anchor so
146  // the toggle handler above still gets to decide whether to open.
147  document.addEventListener('click', (e) => {
148    if (dropdown.contains(e.target)) return;
149    if (langBtns.some(btn => btn === e.target || btn.contains(e.target))) return;
150    dropdown.style.display = 'none';
151  });
152
153  // Close on Escape
154  document.addEventListener('keydown', (e) => {
155    if (e.key === 'Escape') dropdown.style.display = 'none';
156  });
157
158  /* 6️⃣  language selection handler */
159  async function selectLanguage(code) {
160    dropdown.style.display = 'none';
161    localStorage.setItem('wm_lang', code);
162
163    if (code === 'off' || code === 'en') {
164      location.reload();
165      return;
166    }
167    try {
168      const { translatePageTo } = await import('./global-translate.js');
169      await translatePageTo(code);
170    } catch (err) {
171      console.error('[i18n] translatePageTo failed →', err);
172    }
173
174    // Update active highlight
175    dropdown.querySelectorAll('button').forEach(btn => {
176      const isActive = btn.dataset.lang === code;
177      btn.style.fontWeight = isActive ? '600' : '400';
178      btn.style.background = isActive ? '#f0f0f0' : 'transparent';
179    });
180  }
181
182  /* 7️⃣ pick the opening language: ?lang= first, then the remembered choice */
183  try {
184    const stored = (localStorage.getItem('wm_lang') || '').toLowerCase();
185    const { code, fromUrl } = resolveInitialLanguage({
186      urlLang:    new URLSearchParams(window.location.search).get('lang'),
187      storedLang: stored,
188      offered:    OPTIONS
189    });
190    console.log('[i18n] opening language ' + JSON.stringify({ stored, code, fromUrl, url: location.search }));
191
192    if (fromUrl && stored && stored !== 'en' && stored !== 'off' && stored !== code) {
193      // The link names a different language from the one remembered. Boot
194      // has already started translating subtrees into the remembered one
195      // (retranslateNow() in search, details, map-core all read the value
196      // global-translate.js captured at import), and those calls finish
197      // AFTER anything done here, writing the old language back. Setting
198      // storage and reloading is the only ordering that cannot race; after
199      // the reload stored === code, so this branch is not taken again.
200      localStorage.setItem('wm_lang', code);
201      location.reload();
202      return;
203    }
204
205    // A link that names a language becomes the remembered choice, so the
206    // next page the reader opens on this project stays in it.
207    if (fromUrl) localStorage.setItem('wm_lang', code);
208    if (code !== 'en') {
209      // Auto-translate on page load. English is never passed to
210      // selectLanguage() here: that path reloads the page, and ?lang=en
211      // would reload forever.
212      selectLanguage(code);
213    }
214  } catch {/* ignore */}
215}

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.