1/* ============================================ 2 WHEEL OF NAMES â Internationalization (i18n) 3 ============================================ */ 4 5export class I18n { 6 constructor() { 7 this.currentLang = 'en'; 8 this.translations = {}; 9 this.fallback = {}; 10 this.languages = { 11 en: { name: 'English', flag: 'ð¬ð§' }, 12 ar: { name: 'Ø§ÙØ¹Ø±Ø¨ÙØ©', flag: 'ð¸ð¦' }, 13 bn: { name: 'বাà¦à¦²à¦¾', flag: 'ð§ð©' }, 14 de: { name: 'Deutsch', flag: 'ð©ðª' }, 15 es: { name: 'Español', flag: 'ðªð¸' }, 16 fr: { name: 'Français', flag: 'ð«ð·' }, 17 hi: { name: 'हिनà¥à¤¦à¥', flag: 'ð®ð³' }, 18 id: { name: 'Bahasa Indonesia', flag: 'ð®ð©' }, 19 it: { name: 'Italiano', flag: 'ð®ð¹' }, 20 ja: { name: 'æ¥æ¬èª', flag: 'ð¯ðµ' }, 21 kn: { name: 'à²à²¨à³à²¨à²¡', flag: 'ð®ð³' }, 22 ko: { name: 'íêµì´', flag: 'ð°ð·' }, 23 mr: { name: 'मराठà¥', flag: 'ð®ð³' }, 24 nl: { name: 'Nederlands', flag: 'ð³ð±' }, 25 pl: { name: 'Polski', flag: 'ðµð±' }, 26 pt: { name: 'Português', flag: 'ðµð¹' }, 27 ru: { name: 'Ð ÑÑÑкий', flag: 'ð·ðº' }, 28 sv: { name: 'Svenska', flag: 'ð¸ðª' }, 29 ta: { name: 'தமிழà¯', flag: 'ð®ð³' }, 30 te: { name: 'à°¤à±à°²à±à°à±', flag: 'ð®ð³' }, 31 th: { name: 'à¹à¸à¸¢', flag: 'ð¹ð' }, 32 tr: { name: 'Türkçe', flag: 'ð¹ð·' }, 33 uk: { name: 'УкÑаÑнÑÑка', flag: 'ðºð¦' }, 34 vi: { name: 'Tiếng Viá»t', flag: 'ð»ð³' }, 35 zh: { name: '䏿', flag: 'ð¨ð³' }, 36 }; 37 } 38 39 async init() { 40 // 1. Check URL path first (e.g. /uk/ -> 'uk') 41 const pathParts = window.location.pathname.split('/').filter(Boolean); 42 const pathLang = pathParts[0]; 43 44 let validLang = 'en'; 45 if (pathLang && this.languages[pathLang]) { 46 validLang = pathLang; 47 } else if (window.location.pathname === '/' || window.location.pathname === '') { 48 // 2. If exactly on the root URL, check if there's a saved layout preference 49 const storedLang = localStorage.getItem('wof_lang') || this._getCookie('wof_lang'); 50 if (storedLang && storedLang !== 'en' && this.languages[storedLang]) { 51 // Auto-redirect returning users to their language directory instead of serving English 52 window.location.replace(`/${storedLang}/`); 53 return; 54 } 55 } 56 57 // Load English as fallback 58 this.fallback = await this._loadLang('en'); 59 if (validLang !== 'en') { 60 this.translations = await this._loadLang(validLang); 61 } else { 62 this.translations = this.fallback; 63 } 64 65 this.currentLang = validLang; 66 this._persist(validLang); 67 this.apply(); 68 return validLang; 69 } 70 71 async _loadLang(code) { 72 try { 73 const resp = await fetch(`/lang/${code}.json`); 74 if (!resp.ok) throw new Error(`HTTP ${resp.status}`); 75 return await resp.json(); 76 } catch (e) { 77 console.warn(`Failed to load language: ${code}`, e); 78 return {}; 79 } 80 } 81 82 async setLanguage(code) { 83 if (!this.languages[code]) return; 84 if (code === 'en') { 85 this.translations = this.fallback; 86 } else { 87 this.translations = await this._loadLang(code); 88 } 89 this.currentLang = code; 90 this._persist(code); 91 this.apply(); 92 93 // Update HTML lang attribute 94 document.documentElement.lang = code; 95 } 96 97 t(key, params = {}) { 98 let text = this.translations[key] || this.fallback[key] || key; 99 // Replace {{param}} placeholders 100 Object.keys(params).forEach(k => { 101 text = text.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), params[k]); 102 }); 103 return text; 104 } 105 106 apply() { 107 document.querySelectorAll('[data-i18n]').forEach(el => { 108 const key = el.getAttribute('data-i18n'); 109 const text = this.t(key); 110 if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { 111 el.placeholder = text; 112 } else { 113 el.textContent = text; 114 } 115 }); 116 117 document.querySelectorAll('[data-i18n-title]').forEach(el => { 118 el.title = this.t(el.getAttribute('data-i18n-title')); 119 }); 120 121 document.querySelectorAll('[data-i18n-aria]').forEach(el => { 122 el.setAttribute('aria-label', this.t(el.getAttribute('data-i18n-aria'))); 123 }); 124 125 document.documentElement.lang = this.currentLang; 126 } 127 128 _persist(code) { 129 localStorage.setItem('wof_lang', code); 130 document.cookie = `wof_lang=${code};path=/;max-age=31536000;SameSite=Lax`; 131 } 132 133 _getCookie(name) { 134 const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); 135 return match ? match[2] : null; 136 } 137 138 getLanguages() { return this.languages; } 139 getCurrentLang() { return this.currentLang; } 140}
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.