/** * DC-077: i18n Frontend — Language selector and translation system * * Provides window.DCI18n.t(key) for the dashboard frontend. * Loads translations from /api/v1/i18n/translations/:lang * Language preference stored in localStorage. * Handles RTL for Arabic. */ (function () { 'use strict'; const STORAGE_KEY = 'dashcaddy-language'; const DEFAULT_LANG = 'en'; // Must match the 31 languages in language-selector.js and the backend i18n route. const SUPPORTED_LANGS = [ 'en', 'ar', 'bn', 'cs', 'da', 'de', 'el', 'es', 'fa', 'fi', 'fr', 'hi', 'hu', 'id', 'it', 'ja', 'ko', 'ms', 'nl', 'no', 'pl', 'pt', 'ro', 'ru', 'sv', 'th', 'tr', 'uk', 'ur', 'vi', 'zh', ]; const LANG_NAMES = { en: 'English', ar: 'العربية', bn: 'বাংলা', cs: 'Čeština', da: 'Dansk', de: 'Deutsch', el: 'Ελληνικά', es: 'Español', fa: 'فارسی', fi: 'Suomi', fr: 'Français', hi: 'हिन्दी', hu: 'Magyar', id: 'Bahasa Indonesia', it: 'Italiano', ja: '日本語', ko: '한국어', ms: 'Bahasa Melayu', nl: 'Nederlands', no: 'Norsk', pl: 'Polski', pt: 'Português', ro: 'Română', ru: 'Русский', sv: 'Svenska', th: 'ไทย', tr: 'Türkçe', uk: 'Українська', ur: 'اردو', vi: 'Tiếng Việt', zh: '中文', }; // RTL languages need dir="rtl" on the document element. (No Hebrew per project policy.) const RTL_LANGS = new Set(['ar', 'fa', 'ur']); // Validate the stored language — if it's invalid (old/corrupt), fall back to default. // Wrap in try/catch for environments where localStorage is disabled (private mode). function _readValidLang() { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored && SUPPORTED_LANGS.includes(stored)) return stored; } catch (e) { /* localStorage unavailable */ } return DEFAULT_LANG; } let currentLang = _readValidLang(); let translations = {}; let loaded = false; // Monotonic token to guard against out-of-order async resolution. // Each setLanguage / loadTranslations call captures the current value; if it // changed by the time the fetch resolves, the result is discarded. let _langRequestId = 0; async function loadTranslations(lang, reqId) { // reqId is the monotonic token incremented by the caller (setLanguage/init). // If not provided (direct API call), allocate one for backward compatibility. if (reqId === undefined) reqId = ++_langRequestId; if (lang === DEFAULT_LANG) { translations = {}; // English is the default — no translation needed loaded = true; return; } try { const res = await fetch(`/api/v1/i18n/translations/${lang}`); // Guard against out-of-order resolution: if another loadTranslations // started after this one (or the user switched languages), discard. if (reqId !== _langRequestId) return; if (res.ok) { const data = await res.json(); if (reqId !== _langRequestId) return; // double-check after second await translations = data.translations || {}; loaded = true; } else { // HTTP error — clear stale translations so we don't show the wrong language translations = {}; loaded = false; } } catch (e) { console.warn('[i18n] Failed to load translations for', lang, e); if (reqId === _langRequestId) { translations = {}; loaded = false; } } } function t(key) { if (currentLang === DEFAULT_LANG) return key; // If translations didn't load, fall back to the English key return translations[key] || key; } function setLanguage(lang) { if (!SUPPORTED_LANGS.includes(lang)) return; currentLang = lang; try { localStorage.setItem(STORAGE_KEY, lang); } catch (e) { /* localStorage unavailable */ } // RTL handling — always set dir/lang explicitly so switching back to LTR works. const isRtl = RTL_LANGS.has(lang); document.documentElement.dir = isRtl ? 'rtl' : 'ltr'; document.documentElement.lang = lang; const reqId = ++_langRequestId; // increment BEFORE calling loadTranslations loadTranslations(lang, reqId).then(() => { // Only apply if this is still the latest request. if (reqId === _langRequestId) applyTranslations(); }); } function getLanguage() { return currentLang; } function applyTranslations() { // Apply translations to elements with data-i18n attributes. // Always write the resolved value — when switching back to English or when a // key has no translation, this restores the original English text rather than // leaving the previous language's translated text visible. document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); el.textContent = t(key); }); // Apply to placeholders document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { const key = el.getAttribute('data-i18n-placeholder'); el.placeholder = t(key); }); // Apply to titles document.querySelectorAll('[data-i18n-title]').forEach(el => { const key = el.getAttribute('data-i18n-title'); el.title = t(key); }); } function createLanguageSelector() { // Find the top bar area to insert the selector // Look for the auth-settings area or the header actions const targetContainer = document.querySelector('.header-actions') || document.querySelector('#auth-settings-btn')?.parentElement || document.querySelector('.top-bar-actions'); if (!targetContainer) { // If we can't find a target, try to add it near the settings button const settingsBtn = document.getElementById('auth-settings-btn'); if (settingsBtn && settingsBtn.parentElement) { return createDropdown(settingsBtn.parentElement); } return null; } return createDropdown(targetContainer); } function createDropdown(container) { // Check if selector already exists if (document.getElementById('dc-lang-selector')) return; const wrapper = document.createElement('div'); wrapper.id = 'dc-lang-selector'; wrapper.style.cssText = 'display: inline-flex; align-items: center; gap: 4px; margin: 0 8px; position: relative;'; const btn = document.createElement('button'); btn.id = 'dc-lang-btn'; btn.className = 'lang-selector-btn'; btn.style.cssText = 'background: var(--card-base, #2a2a3e); border: 1px solid var(--border, #3a3a4e); color: var(--text-primary, #e0e0e0); padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; display: flex; align-items: center; gap: 4px;'; btn.innerHTML = `🌐 ${currentLang.toUpperCase()}`; btn.title = 'Select Language'; const dropdown = document.createElement('div'); dropdown.id = 'dc-lang-dropdown'; dropdown.style.cssText = 'display: none; position: absolute; top: 100%; right: 0; margin-top: 4px; background: var(--card-base, #2a2a3e); border: 1px solid var(--border, #3a3a4e); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 9999; min-width: 160px; max-height: 320px; overflow-y: auto;'; SUPPORTED_LANGS.forEach(lang => { const item = document.createElement('div'); item.className = 'lang-option'; item.style.cssText = 'padding: 8px 14px; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--text-primary, #e0e0e0);'; item.onmouseenter = () => item.style.background = 'var(--card-hover, rgba(255,255,255,0.05))'; item.onmouseleave = () => item.style.background = 'transparent'; const flag = document.createElement('span'); flag.textContent = lang === currentLang ? '✓' : ''; flag.style.cssText = 'width: 16px; color: var(--ok-fg, #4ade80);'; const name = document.createElement('span'); name.textContent = LANG_NAMES[lang]; item.appendChild(flag); item.appendChild(name); item.onclick = () => { setLanguage(lang); dropdown.style.display = 'none'; // Update button text btn.querySelector('.lang-current').textContent = lang.toUpperCase(); // Update checkmarks dropdown.querySelectorAll('.lang-option').forEach((opt, i) => { opt.querySelector('span').textContent = SUPPORTED_LANGS[i] === lang ? '✓' : ''; }); // Show notification if (window.showNotification) { window.showNotification(`Language: ${LANG_NAMES[lang]}`, 'info'); } }; dropdown.appendChild(item); }); btn.onclick = (e) => { e.stopPropagation(); dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; }; // Close on outside click document.addEventListener('click', (e) => { if (!wrapper.contains(e.target)) { dropdown.style.display = 'none'; } }); wrapper.appendChild(btn); wrapper.appendChild(dropdown); container.insertBefore(wrapper, container.firstChild); return wrapper; } // Initialize on page load function init() { // Always set dir/lang explicitly — covers LTR reset and RTL setup. const isRtl = RTL_LANGS.has(currentLang); document.documentElement.dir = isRtl ? 'rtl' : 'ltr'; document.documentElement.lang = currentLang; function start() { createLanguageSelector(); if (currentLang !== DEFAULT_LANG) { const reqId = ++_langRequestId; loadTranslations(currentLang, reqId).then(() => { if (reqId === _langRequestId) applyTranslations(); }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } } // Expose globally window.DCI18n = { t, setLanguage, getLanguage, applyTranslations, loadTranslations }; // Auto-init init(); })();