1/** 2 * Site-wide i18n: loads JSON from /locales/{lang}.json and applies [data-i18n*] attributes. 3 */ 4(function () { 5 'use strict'; 6 7 // Apex and www are different origins. Some folders used to stay on rgbjunkie.com while 8 // /locales/*.json 301 to www; fetch() then fails CORS and every data-i18n key shows raw. 9 var host = String(location.hostname || '').toLowerCase(); 10 if (host === 'rgbjunkie.com') { 11 location.replace('https://www.rgbjunkie.com' + location.pathname + location.search + location.hash); 12 return; 13 } 14 15 var STORAGE_KEY = 'rgbjunkie_lang'; 16 var LOCALES_BASE = '/locales/'; 17 18 /** @type {{ code: string, labelKey: string }[]} labelKey resolved via t() so labels localize */ 19 var LANGUAGE_CODES = [ 20 { code: 'en', labelKey: 'lang_en' }, 21 { code: 'es', labelKey: 'lang_es' }, 22 { code: 'fr', labelKey: 'lang_fr' }, 23 { code: 'de', labelKey: 'lang_de' }, 24 { code: 'pt', labelKey: 'lang_pt' }, 25 { code: 'zh-CN', labelKey: 'lang_zh' }, 26 { code: 'hi', labelKey: 'lang_hi' } 27 ]; 28 29 var I18N = { 30 cur: 'en', 31 strings: {}, 32 33 t: function (key, vars) { 34 if (!key) return ''; 35 var s = this.strings[key]; 36 if (s === undefined || s === null || s === '') return key; 37 if (vars && typeof vars === 'object' && !Array.isArray(vars)) { 38 s = String(s).replace(/\{(\w+)\}/g, function (_, name) { 39 return vars[name] !== undefined && vars[name] !== null ? String(vars[name]) : ''; 40 }); 41 } 42 return s; 43 }, 44 45 init: async function () { 46 if (this._initPromise) return this._initPromise; 47 var self = this; 48 this._initPromise = (async function () { 49 var saved = localStorage.getItem(STORAGE_KEY); 50 var lang = saved && LANGUAGE_CODES.some(function (l) { return l.code === saved; }) 51 ? saved 52 : self.detectBrowserLang(); 53 await self.setLanguage(lang); 54 })(); 55 return this._initPromise; 56 }, 57 58 detectBrowserLang: function () { 59 var langs = navigator.languages || [navigator.language || 'en']; 60 for (var i = 0; i < langs.length; i++) { 61 var tag = ((langs[i] || '') + '').trim(); 62 if (!tag) continue; 63 var lower = tag.toLowerCase(); 64 var j; 65 for (j = 0; j < LANGUAGE_CODES.length; j++) { 66 if (LANGUAGE_CODES[j].code.toLowerCase() === lower) return LANGUAGE_CODES[j].code; 67 } 68 var primary = lower.split('-')[0]; 69 for (j = 0; j < LANGUAGE_CODES.length; j++) { 70 var c = LANGUAGE_CODES[j].code.toLowerCase(); 71 if (c === primary || c.split('-')[0] === primary) return LANGUAGE_CODES[j].code; 72 } 73 } 74 return 'en'; 75 }, 76 77 setLanguage: async function (lang) { 78 try { 79 var response = await fetch(LOCALES_BASE + encodeURIComponent(lang) + '.json'); 80 if (!response.ok) throw new Error('Missing locale'); 81 this.strings = await response.json(); 82 this.cur = lang; 83 localStorage.setItem(STORAGE_KEY, lang); 84 } catch (e) { 85 if (lang !== 'en') { 86 await this.setLanguage('en'); 87 return; 88 } 89 console.warn('I18N: failed to load locale, using empty strings', e); 90 this.strings = {}; 91 this.cur = 'en'; 92 } 93 document.documentElement.lang = this.cur.indexOf('-') > -1 ? this.cur : this.cur; 94 this.applyDocumentLanguage(); 95 /** Dispose tooltips *before* updateStaticUI: Bootstrap's dispose() restores the cached (usually English) 96 * title and would overwrite freshly translated titles if we disposed after updating. */ 97 this.disposeBootstrapTooltips(); 98 this.updateStaticUI(); 99 this.populateLangSwitcher(); 100 this.initBootstrapTooltips();
101 window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { lang: this.cur } })); 102 }, 103 104 /** Dropdown toggles must not get Tooltips â they break outside-click close (BS dropdown + Tooltip conflict). */ 105 _skipTooltipElement: function (el) { 106 var t = (el.getAttribute('data-bs-toggle') || '').toLowerCase(); 107 return t.indexOf('dropdown') !== -1; 108 }, 109 110 disposeBootstrapTooltips: function () { 111 if (typeof bootstrap === 'undefined' || !bootstrap.Tooltip) return; 112 var self = this; 113 var seen = new WeakSet(); 114 function disposeOne(el) { 115 if (!el || seen.has(el)) return; 116 seen.add(el); 117 if (self._skipTooltipElement(el)) return; 118 var inst = bootstrap.Tooltip.getInstance(el); 119 if (inst) inst.dispose(); 120 } 121 document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(disposeOne); 122 document.querySelectorAll('[title]').forEach(disposeOne); 123 }, 124 125 initBootstrapTooltips: function () { 126 if (typeof bootstrap === 'undefined' || !bootstrap.Tooltip) return; 127 var self = this; 128 /** Match builder/util.js initializeTooltips (hover-only, no focus trigger). */ 129 var tooltipOpts = { trigger: 'hover' }; 130 document.querySelectorAll('[title]').forEach(function (el) { 131 if (self._skipTooltipElement(el)) return; 132 if (!bootstrap.Tooltip.getInstance(el)) new bootstrap.Tooltip(el, tooltipOpts); 133 }); 134 }, 135 136 applyDocumentLanguage: function () { 137 var t = this.t.bind(this); 138 var docKind = document.body && document.body.getAttribute('data-i18n-doc'); 139 if (docKind === 'showcase') { 140 if (this.strings.showcase_meta_title) document.title = t('showcase_meta_title'); 141 var metaDesc = document.querySelector('meta[name="description"]'); 142 if (metaDesc && this.strings.showcase_meta_description) metaDesc.setAttribute('content', t('showcase_meta_description')); 143 var ogTitle = document.querySelector('meta[property="og:title"]'); 144 if (ogTitle && this.strings.showcase_og_title) ogTitle.setAttribute('content', t('showcase_og_title')); 145 var ogDesc = document.querySelector('meta[property="og:description"]'); 146 if (ogDesc && this.strings.showcase_og_description) ogDesc.setAttribute('content', t('showcase_og_description')); 147 var twTitle = document.querySelector('meta[name="twitter:title"]'); 148 if (twTitle && this.strings.showcase_og_title) twTitle.setAttribute('content', t('showcase_og_title')); 149 var twDesc = document.querySelector('meta[name="twitter:description"]'); 150 if (twDesc && this.strings.showcase_og_description) twDesc.setAttribute('content', t('showcase_og_description')); 151 return; 152 } 153 if (docKind === 'builder') { 154 if (this.strings.builder_page_title) document.title = t('builder_page_title'); 155 var ogTitleB = document.querySelector('meta[property="og:title"]'); 156 if (ogTitleB && this.strings.builder_og_title) ogTitleB.setAttribute('content', t('builder_og_title')); 157 var ogDescB = document.querySelector('meta[property="og:description"]'); 158 if (ogDescB && this.strings.builder_og_description) ogDescB.setAttribute('content', t('builder_og_description')); 159 var twTitleB = document.querySelector('meta[name="twitter:title"]'); 160 if (twTitleB && this.strings.builder_og_title) twTitleB.setAttribute('content', t('builder_og_title')); 161 var twDescB = document.querySelector('meta[name="twitter:description"]'); 162 if (twDescB && this.strings.builder_og_description) twDescB.setAttribute('content', t('builder_og_description')); 163 return; 164 } 165 if (docKind === 'builder-gallery') { 166 if (this.strings.builder_gallery_page_title) document.title = t('builder_gallery_page_title'); 167 var ogTitleBg = document.querySelector('meta[property="og:title"]'); 168 if (ogTitleBg && this.strings.builder_gallery_og_title) ogTitleBg.setAttribute('content', t('builder_gallery_og_title')); 169 var ogDescBg = document.querySelector('meta[property="og:description"]'); 170 if (ogDescBg && this.strings.builder_gallery_og_description) ogDescBg.setAttribute('content', t('builder_gallery_og_description')); 171 var twTitleBg = document.querySelector('meta[name="twitter:title"]'); 172 if (twTitleBg && this.strings.builder_gallery_og_title) twTitleBg.setAttribute('content', t('builder_gallery_og_title')); 173 var twDescBg = document.querySelector('meta[name="twitter:description"]'); 174 if (twDescBg && this.strings.builder_gallery_og_description) twDescBg.setAttribute('content', t('builder_gallery_og_description')); 175 return; 176 } 177 if (docKind === 'gallery-effect') { 178 if (this.strings.effect_gallery_page_title) document.title = t('effect_gallery_page_title'); 179 var ogTitleGe = document.querySelector('meta[property="og:title"]'); 180 if (ogTitleGe && this.strings.effect_gallery_og_title) ogTitleGe.setAttribute('content', t('effect_gallery_og_title')); 181 var ogDescGe = document.querySelector('meta[property="og:description"]'); 182 if (ogDescGe && this.strings.effect_gallery_og_description) ogDescGe.setAttribute('content', t('effect_gallery_og_description')); 183 return; 184 } 185 document.title = t('page_title'); 186 var metaDesc = document.querySelector('meta[name="description"]'); 187 if (metaDesc && this.strings.meta_description) metaDesc.setAttribute('content', t('meta_description')); 188 var ogTitle = document.querySelector('meta[property="og:title"]'); 189 if (ogTitle && this.strings.og_title) ogTitle.setAttribute('content', t('og_title')); 190 var ogDesc = document.querySelector('meta[property="og:description"]'); 191 if (ogDesc && this.strings.og_description) ogDesc.setAttribute('content', t('og_description')); 192 var twTitle = document.querySelector('meta[name="twitter:title"]'); 193 if (twTitle && this.strings.og_title) twTitle.setAttribute('content', t('og_title')); 194 var twDesc = document.querySelector('meta[name="twitter:description"]'); 195 if (twDesc && this.strings.og_description) twDesc.setAttribute('content', t('og_description')); 196 }, 197 198 updateStaticUI: function () { 199 var self = this; 200 document.querySelectorAll('[data-i18n]').forEach(function (el) { 201 var key = el.getAttribute('data-i18n'); 202 if (!key) return; 203 var text = self.t(key); 204 if (el.hasAttribute('data-i18n-html')) { 205 el.innerHTML = text; 206 } else { 207 el.textContent = text; 208 } 209 }); 210 document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) { 211 var key = el.getAttribute('data-i18n-placeholder'); 212 if (key) el.placeholder = self.t(key); 213 }); 214 document.querySelectorAll('[data-i18n-title]').forEach(function (el) { 215 var key = el.getAttribute('data-i18n-title'); 216 if (!key) return; 217 /* Bootstrap keeps a copy here; leaving it forces the old language on the next Tooltip. */ 218 el.removeAttribute('data-bs-original-title');
219 el.title = self.t(key); 220 }); 221 document.querySelectorAll('[data-i18n-aria-label]').forEach(function (el) { 222 var key = el.getAttribute('data-i18n-aria-label'); 223 if (key) el.setAttribute('aria-label', self.t(key)); 224 }); 225 document.querySelectorAll('[data-i18n-alt]').forEach(function (el) { 226 var key = el.getAttribute('data-i18n-alt'); 227 if (key) el.setAttribute('alt', self.t(key)); 228 }); 229 }, 230 231 hideLangDropdown: function () { 232 var toggleBtn = document.getElementById('lang-switcher-btn'); 233 if (!toggleBtn || typeof bootstrap === 'undefined' || !bootstrap.Dropdown) return; 234 try { 235 bootstrap.Dropdown.getOrCreateInstance(toggleBtn).hide(); 236 } catch (e) { /* ignore */ } 237 toggleBtn.blur(); 238 }, 239 240 populateLangSwitcher: function () { 241 this.hideLangDropdown(); 242 var menu = document.getElementById('lang-switcher-menu'); 243 var label = document.getElementById('lang-switcher-label'); 244 var self = this; 245 if (menu) { 246 menu.innerHTML = ''; 247 LANGUAGE_CODES.forEach(function (entry) { 248 var li = document.createElement('li'); 249 /** Use <button> so we do not need preventDefault on <a href="#"> (that can block BS5 dropdown close). */ 250 var btn = document.createElement('button'); 251 btn.type = 'button'; 252 btn.className = 'dropdown-item' + (entry.code === self.cur ? ' active' : ''); 253 btn.setAttribute('data-lang', entry.code); 254 btn.textContent = self.t(entry.labelKey); 255 btn.addEventListener('click', function () { 256 self.hideLangDropdown(); 257 self.setLanguage(entry.code); 258 }); 259 li.appendChild(btn); 260 menu.appendChild(li); 261 }); 262 } 263 if (label) { 264 var curEntry = LANGUAGE_CODES.find(function (l) { return l.code === self.cur; }); 265 label.textContent = curEntry ? self.t(curEntry.labelKey) : self.cur; 266 } 267 } 268 }; 269 270 window.I18N = I18N; 271 window.t = function (key, second) { 272 if (typeof second === 'object' && second !== null && !Array.isArray(second)) { 273 return I18N.t(key, second); 274 } 275 var v = I18N.t(key); 276 if (v === key && typeof second === 'string') return second; 277 return v; 278 }; 279 /** Used by main.js for runtime strings (including code outside DOMContentLoaded). */ 280 window.tr = function (key, vars) { 281 return window.I18N ? window.I18N.t(key, vars) : key; 282 }; 283})();
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.