From 38018750edac10ce700098ec1cce85d86e683062 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Fri, 6 Feb 2026 17:09:50 +0300 Subject: [PATCH] Add internationalization (i18n) support with English and Russian translations Implemented localization system similar to the media-server project pattern: - Created locale JSON files for English (en.json) and Russian (ru.json) - Added complete translations for all UI elements, buttons, labels, and messages - Implemented locale management system with browser locale detection - Added language selector dropdown in header - Applied data-i18n, data-i18n-title, and data-i18n-placeholder attributes - Translations stored in localStorage and persist across sessions - Automatic language detection from browser settings - All dynamic content (displays, devices, modals) now uses translation function Translations cover: - Authentication (login/logout) - Displays (layout visualization, cards, labels) - Devices (management, status, actions) - Settings modal (brightness, device configuration) - Calibration modal (LED mapping, testing) - Error messages and notifications - Server status and version information The implementation uses: - Simple t(key, params) translation function with parameter substitution - Async locale loading from /static/locales/{locale}.json - updateAllText() to refresh all UI elements when language changes - Fallback to English if translation file fails to load Co-Authored-By: Claude Sonnet 4.5 --- server/src/wled_controller/static/app.js | 146 ++++++++++++++-- server/src/wled_controller/static/index.html | 161 +++++++++--------- .../wled_controller/static/locales/en.json | 111 ++++++++++++ .../wled_controller/static/locales/ru.json | 111 ++++++++++++ 4 files changed, 439 insertions(+), 90 deletions(-) create mode 100644 server/src/wled_controller/static/locales/en.json create mode 100644 server/src/wled_controller/static/locales/ru.json 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 @@