1// Morfoz i18n â multi-language UI from day one. 2// 3// Design: 4// 1. Each language has a JSON dictionary at /public/i18n/<lang>.json. 5// 2. `t(key, vars?)` looks up the key in the current language; falls back to 6// English, then Turkish, then the key itself. 7// 3. Elements with `data-i18n="key"` are auto-translated on boot and on 8// language change. For attribute translation: `data-i18n-attr="aria-label:nav.dashboard"`. 9// 4. Dynamic JS-rendered content uses `t('key')` directly. 10// 5. Language is persisted to localStorage AND to the backend 11// (`PATCH /api/auth/language`) so the user's choice follows them across 12// devices. 13// 14// Order of resolution at boot: 15// 1. user.preferred_language from /api/auth/me (cross-device) 16// 2. localStorage.morfoz_lang (this device, signed-out) 17// 3. navigator.language â 'tr' or 'en' (first visit) 18// 4. fallback to 'tr' 19 20const SUPPORTED = ['tr', 'en']; 21const DEFAULT_LANG = 'tr'; 22const LS_KEY = 'morfoz_lang'; 23 24let _current = DEFAULT_LANG; 25let _dicts = {}; // { tr: {...}, en: {...} } 26const _listeners = new Set(); 27 28// ---------- Public API ---------- 29export function currentLang() { return _current; } 30export function supportedLangs() { return [...SUPPORTED]; } 31 32/** Translate a key. Optional `vars` for placeholder substitution (`{name}`). */ 33export function t(key, vars) { 34 const dict = _dicts[_current] || {}; 35 const fallback = _dicts.en || _dicts.tr || {}; 36 let str = dict[key] ?? fallback[key] ?? key; 37 if (vars && typeof str === 'string') { 38 for (const [k, v] of Object.entries(vars)) { 39 str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), v); 40 } 41 } 42 return str; 43} 44 45/**
46 * Anahtarın YÃKLà BÃTÃN sözlüklerdeki karÅılıkları â tekilleÅtirilmiÅ, yalnız dize. 47 * 48 * `t()` o anki dili verir; oysa kullanıcının DAHA ÃNCE gönderdiÄi bir metni 49 * tanımak gerektiÄinde (onay kartındaki "evet"/"yes" cevabı) metin hangi dil 50 * açıkken yazıldıysa o dildedir ve dil sonradan deÄiÅmiÅ olabilir. initI18n 51 * tr + en'i önden yüklediÄi için çaÄrı anında iki sözlük de burada. 52 */ 53export function tHepsi(key) { 54 const out = []; 55 for (const dict of Object.values(_dicts)) { 56 const v = dict ? dict[key] : undefined; 57 if (typeof v === 'string' && !out.includes(v)) out.push(v); 58 } 59 return out; 60} 61 62/** Change the active language. Re-translates the DOM and persists. */ 63export async function setLanguage(lang, { silent = false } = {}) { 64 if (!SUPPORTED.includes(lang)) return false; 65 if (!_dicts[lang]) await _load(lang); 66 _current = lang; 67 try { localStorage.setItem(LS_KEY, lang); } catch {} 68 document.documentElement.setAttribute('lang', lang); 69 applyTranslations(); 70 _listeners.forEach(fn => { try { fn(lang); } catch {} }); 71 // Sync to backend (don't block UI) 72 if (!silent) { 73 try { 74 const { api } = await import('./api-client.js'); 75 api.patch('/api/auth/language', { language: lang }).catch(() => {}); 76 } catch {} 77 } 78 return true; 79} 80 81/** Subscribe to language change (re-render dynamic content). */ 82export function onLanguageChange(fn) { 83 _listeners.add(fn); 84 return () => _listeners.delete(fn); 85} 86 87/** 88 * Sözlüklerin hiçbirinde karÅılıÄı olmayan anahtar. 89 * 90 * `t()` böyle bir durumda ANAHTARIN KENDİSİNİ döndürüyor â bir metin kutusuna 91 * yazmak için uygun bir deÄer deÄil. Ayrım burada yapılıyor ki `t()`nin 92 * sözleÅmesi (her zaman bir dize döner) bozulmasın. 93 */ 94function _cevirisiYok(key) { 95 return !(key in (_dicts[_current] || {})) && !(key in (_dicts.en || _dicts.tr || {})); 96} 97 98/** Translate every element under `root` (default: document). Idempotent. */ 99export function applyTranslations(root = document) { 100 // textContent translation 101 root.querySelectorAll('[data-i18n]').forEach(el => { 102 const key = el.getAttribute('data-i18n'); 103 if (!key) return; 104 // ÃEVİRİSİ YOKSA ELEMENTE DOKUNMA â HTML'deki Türkçe metin kalsın. 105 // 106 // Eskiden koÅulsuz atanıyordu: `t()` bulamadıÄı anahtar için anahtarın 107 // kendisini döndürdüÄünden, sözlüÄe henüz girmemiÅ her etiket ekranda 108 // "ui.acct.email" gibi görünüyordu. Panel HTML'i data-i18n ile 109 // iÅaretlenip sözlükler doldurulmadan yayına çıksa TÃM panel ham anahtar 110 // yıÄınına dönerdi (1018 etiket). 111 // 112 // DoÄru davranıŠiOS'ta zaten yazılı (Dil.swift): "Ãeviri EKSİKSE ekranda 113 // Türkçe görünür; boÅ kutu ya da anahtar SIZMAZ. Yarım çeviri, bozuk 114 // arayüzden iyidir." Web de artık aynı sözleÅmeye uyuyor â böylece 115 // sözlüÄe taÅıma iÅi parça parça, güvenle ilerleyebiliyor. 116 if (_cevirisiYok(key)) return; 117 el.textContent = t(key); 118 }); 119 // attribute translation: data-i18n-attr="aria-label:nav.dashboard,title:nav.dashboard" 120 root.querySelectorAll('[data-i18n-attr]').forEach(el => { 121 const spec = el.getAttribute('data-i18n-attr') || ''; 122 spec.split(',').forEach(pair => { 123 const [attr, key] = pair.split(':').map(s => s.trim()); 124 // Nitelikte de aynı kural: çevirisi yoksa var olan deÄer korunur 125 // (aria-label="ui.a11y.more" ekran okuyucuya anlamsız gelirdi). 126 if (attr && key && !_cevirisiYok(key)) el.setAttribute(attr, t(key)); 127 }); 128 }); 129} 130 131// ---------- Internal ---------- 132async function _load(lang) { 133 try { 134 const res = await fetch(`/public/i18n/${lang}.json`, { cache: 'no-cache' }); 135 _dicts[lang] = res.ok ? await res.json() : {}; 136 } catch { 137 _dicts[lang] = {}; 138 } 139} 140 141/** One-time bootstrap. Call once at app boot. */ 142export async function initI18n({ userPreferred } = {}) { 143 // Preload Turkish + English so fallback chain always works 144 await Promise.all([_load('tr'), _load('en')]); 145 // Resolve preferred language 146 let lang = DEFAULT_LANG; 147 if (userPreferred && SUPPORTED.includes(userPreferred)) { 148 lang = userPreferred; 149 } else { 150 try { 151 const stored = localStorage.getItem(LS_KEY); 152 if (stored && SUPPORTED.includes(stored)) lang = stored; 153 else { 154 const nav = (navigator.language || '').slice(0, 2).toLowerCase(); 155 if (SUPPORTED.includes(nav)) lang = nav; 156 } 157 } catch {} 158 } 159 _current = lang; 160 document.documentElement.setAttribute('lang', lang); 161 applyTranslations(); 162}
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.