PageSourceSearch

https://www.rgbjunkie.com/js/i18n.js

js rgbjunkie.com collected 2026-10-08 11:43:58 UTC 14,998 bytes, 283 lines download raw bytes

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.