diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index b9e0631..c701edb 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -2,8 +2,130 @@ const API_BASE = '/api/v1'; let refreshInterval = null; let apiKey = null; +// Locale management +let currentLocale = 'en'; +let translations = {}; +const supportedLocales = { + 'en': 'English', + 'ru': 'Русский' +}; + +// Minimal inline fallback for critical UI elements +const fallbackTranslations = { + 'app.title': 'WLED Screen Controller', + 'auth.placeholder': 'Enter your API key...', + 'auth.button.login': 'Login' +}; + +// Translation function +function t(key, params = {}) { + let text = translations[key] || fallbackTranslations[key] || key; + + // Replace parameters like {name}, {value}, etc. + Object.keys(params).forEach(param => { + text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]); + }); + + return text; +} + +// Load translation file +async function loadTranslations(locale) { + try { + const response = await fetch(`/static/locales/${locale}.json`); + if (!response.ok) { + throw new Error(`Failed to load ${locale}.json`); + } + return await response.json(); + } catch (error) { + console.error(`Error loading translations for ${locale}:`, error); + // Fallback to English if loading fails + if (locale !== 'en') { + return await loadTranslations('en'); + } + return {}; + } +} + +// Detect browser locale +function detectBrowserLocale() { + const browserLang = navigator.language || navigator.languages?.[0] || 'en'; + const langCode = browserLang.split('-')[0]; // 'en-US' -> 'en', 'ru-RU' -> 'ru' + + // Only return if we support it + return supportedLocales[langCode] ? langCode : 'en'; +} + +// Initialize locale +async function initLocale() { + const savedLocale = localStorage.getItem('locale') || detectBrowserLocale(); + await setLocale(savedLocale); +} + +// Set locale +async function setLocale(locale) { + if (!supportedLocales[locale]) { + locale = 'en'; + } + + // Load translations for the locale + translations = await loadTranslations(locale); + + currentLocale = locale; + document.documentElement.setAttribute('data-locale', locale); + document.documentElement.setAttribute('lang', locale); + localStorage.setItem('locale', locale); + + // Update all text + updateAllText(); + + // Update locale select dropdown (if visible) + updateLocaleSelect(); +} + +// Change locale from dropdown +function changeLocale() { + const select = document.getElementById('locale-select'); + const newLocale = select.value; + if (newLocale && newLocale !== currentLocale) { + localStorage.setItem('locale', newLocale); + setLocale(newLocale); + } +} + +// Update locale select dropdown +function updateLocaleSelect() { + const select = document.getElementById('locale-select'); + if (select) { + select.value = currentLocale; + } +} + +// Update all text on page +function updateAllText() { + // Update all elements with data-i18n attribute + document.querySelectorAll('[data-i18n]').forEach(el => { + const key = el.getAttribute('data-i18n'); + el.textContent = t(key); + }); + + // Update all elements with data-i18n-placeholder attribute + document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + const key = el.getAttribute('data-i18n-placeholder'); + el.placeholder = t(key); + }); + + // Update all elements with data-i18n-title attribute + document.querySelectorAll('[data-i18n-title]').forEach(el => { + const key = el.getAttribute('data-i18n-title'); + el.title = t(key); + }); +} + // Initialize app -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { + // Initialize locale first + await initLocale(); // Load API key from localStorage apiKey = localStorage.getItem('wled_api_key'); @@ -97,13 +219,13 @@ async function loadServerInfo() { const response = await fetch('/health'); const data = await response.json(); - document.getElementById('server-version').textContent = `Version: ${data.version}`; + document.getElementById('version-number').textContent = data.version; document.getElementById('server-status').textContent = '●'; document.getElementById('server-status').className = 'status-badge online'; } catch (error) { console.error('Failed to load server info:', error); document.getElementById('server-status').className = 'status-badge offline'; - showToast('Server offline', 'error'); + showToast(t('server.offline'), 'error'); } } @@ -124,8 +246,8 @@ async function loadDisplays() { const container = document.getElementById('displays-list'); if (!data.displays || data.displays.length === 0) { - container.innerHTML = '
No displays available
'; - document.getElementById('display-layout-canvas').innerHTML = '
No displays available
'; + container.innerHTML = `
${t('displays.none')}
`; + document.getElementById('display-layout-canvas').innerHTML = `
${t('displays.none')}
`; return; } @@ -134,18 +256,18 @@ async function loadDisplays() {
${display.name}
- ${display.is_primary ? 'Primary' : 'Secondary'} + ${display.is_primary ? `${t('displays.badge.primary')}` : `${t('displays.badge.secondary')}`}
- Resolution: + ${t('displays.resolution')} ${display.width} × ${display.height}
- Position: + ${t('displays.position')} (${display.x}, ${display.y})
- Display Index: + ${t('displays.index')} ${display.index}
@@ -156,9 +278,9 @@ async function loadDisplays() { } catch (error) { console.error('Failed to load displays:', error); document.getElementById('displays-list').innerHTML = - '
Failed to load displays
'; + `
${t('displays.failed')}
`; document.getElementById('display-layout-canvas').innerHTML = - '
Failed to load layout
'; + `
${t('displays.failed')}
`; } } @@ -166,7 +288,7 @@ function renderDisplayLayout(displays) { const canvas = document.getElementById('display-layout-canvas'); if (!displays || displays.length === 0) { - canvas.innerHTML = '
No displays to visualize
'; + canvas.innerHTML = `
${t('displays.none')}
`; return; } diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html index 8205541..0e73c12 100644 --- a/server/src/wled_controller/static/index.html +++ b/server/src/wled_controller/static/index.html @@ -10,76 +10,80 @@
-

WLED Screen Controller

+

WLED Screen Controller

- Version: Loading... + Version: Loading... - +
-

Available Displays

+

Available Displays

-

Display Layout

+

Display Layout

-
Loading layout...
+
Loading layout...
- Primary Display - Secondary Display + Primary Display + Secondary Display
-

Display Information

+

Display Information

-
Loading displays...
+
Loading displays...
-

WLED Devices

+

WLED Devices

-
Loading devices...
+
Loading devices...
-

Add New Device

+

Add New Device

- 📱 WLED Configuration: Configure your WLED device (effects, segments, color order, power limits, etc.) using the - official WLED app. - This controller sends pixel color data and controls brightness per device. + 📱 WLED Configuration: Configure your WLED device (effects, segments, color order, power limits, etc.) using the + official WLED app. + This controller sends pixel color data and controls brightness per device.
- - + +
- - + +
- + - Number of LEDs configured in your WLED device + Number of LEDs configured in your WLED device
- +
@@ -90,11 +94,11 @@