Add internationalization (i18n) support with English and Russian translations
Some checks failed
Validate / validate (push) Failing after 7s

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 17:09:50 +03:00
parent 6a0cc12ca1
commit 38018750ed
4 changed files with 439 additions and 90 deletions

View File

@@ -2,8 +2,130 @@ const API_BASE = '/api/v1';
let refreshInterval = null; let refreshInterval = null;
let apiKey = 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 // Initialize app
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
// Initialize locale first
await initLocale();
// Load API key from localStorage // Load API key from localStorage
apiKey = localStorage.getItem('wled_api_key'); apiKey = localStorage.getItem('wled_api_key');
@@ -97,13 +219,13 @@ async function loadServerInfo() {
const response = await fetch('/health'); const response = await fetch('/health');
const data = await response.json(); 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').textContent = '●';
document.getElementById('server-status').className = 'status-badge online'; document.getElementById('server-status').className = 'status-badge online';
} catch (error) { } catch (error) {
console.error('Failed to load server info:', error); console.error('Failed to load server info:', error);
document.getElementById('server-status').className = 'status-badge offline'; 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'); const container = document.getElementById('displays-list');
if (!data.displays || data.displays.length === 0) { if (!data.displays || data.displays.length === 0) {
container.innerHTML = '<div class="loading">No displays available</div>'; container.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
document.getElementById('display-layout-canvas').innerHTML = '<div class="loading">No displays available</div>'; document.getElementById('display-layout-canvas').innerHTML = `<div class="loading">${t('displays.none')}</div>`;
return; return;
} }
@@ -134,18 +256,18 @@ async function loadDisplays() {
<div class="display-card"> <div class="display-card">
<div class="display-header"> <div class="display-header">
<div class="display-index">${display.name}</div> <div class="display-index">${display.name}</div>
${display.is_primary ? '<span class="badge badge-primary">Primary</span>' : '<span class="badge badge-secondary">Secondary</span>'} ${display.is_primary ? `<span class="badge badge-primary">${t('displays.badge.primary')}</span>` : `<span class="badge badge-secondary">${t('displays.badge.secondary')}</span>`}
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">Resolution:</span> <span class="info-label">${t('displays.resolution')}</span>
<span class="info-value">${display.width} × ${display.height}</span> <span class="info-value">${display.width} × ${display.height}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">Position:</span> <span class="info-label">${t('displays.position')}</span>
<span class="info-value">(${display.x}, ${display.y})</span> <span class="info-value">(${display.x}, ${display.y})</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">Display Index:</span> <span class="info-label">${t('displays.index')}</span>
<span class="info-value">${display.index}</span> <span class="info-value">${display.index}</span>
</div> </div>
</div> </div>
@@ -156,9 +278,9 @@ async function loadDisplays() {
} catch (error) { } catch (error) {
console.error('Failed to load displays:', error); console.error('Failed to load displays:', error);
document.getElementById('displays-list').innerHTML = document.getElementById('displays-list').innerHTML =
'<div class="loading">Failed to load displays</div>'; `<div class="loading">${t('displays.failed')}</div>`;
document.getElementById('display-layout-canvas').innerHTML = document.getElementById('display-layout-canvas').innerHTML =
'<div class="loading">Failed to load layout</div>'; `<div class="loading">${t('displays.failed')}</div>`;
} }
} }
@@ -166,7 +288,7 @@ function renderDisplayLayout(displays) {
const canvas = document.getElementById('display-layout-canvas'); const canvas = document.getElementById('display-layout-canvas');
if (!displays || displays.length === 0) { if (!displays || displays.length === 0) {
canvas.innerHTML = '<div class="loading">No displays to visualize</div>'; canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
return; return;
} }

View File

@@ -10,76 +10,80 @@
<body> <body>
<div class="container"> <div class="container">
<header> <header>
<h1>WLED Screen Controller</h1> <h1 data-i18n="app.title">WLED Screen Controller</h1>
<div class="server-info"> <div class="server-info">
<span id="server-version">Version: Loading...</span> <span id="server-version"><span data-i18n="app.version">Version:</span> <span id="version-number">Loading...</span></span>
<span id="server-status" class="status-badge"></span> <span id="server-status" class="status-badge"></span>
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme"> <button class="theme-toggle" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
<span id="theme-icon">🌙</span> <span id="theme-icon">🌙</span>
</button> </button>
<select id="locale-select" onchange="changeLocale()" data-i18n-title="locale.change" title="Change language" style="margin-left: 10px; padding: 6px 12px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-color); color: var(--text-color); font-size: 0.9rem; cursor: pointer;">
<option value="en">English</option>
<option value="ru">Русский</option>
</select>
<span id="auth-status" style="margin-left: 10px; display: none;"> <span id="auth-status" style="margin-left: 10px; display: none;">
<span id="logged-in-user" style="color: #4CAF50; margin-right: 8px;"></span> <span id="logged-in-user" style="color: #4CAF50; margin-right: 8px;" data-i18n="auth.authenticated">● Authenticated</span>
</span> </span>
<button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;"> <button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
🔑 Login 🔑 <span data-i18n="auth.login">Login</span>
</button> </button>
<button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;"> <button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
🚪 Logout 🚪 <span data-i18n="auth.logout">Logout</span>
</button> </button>
</div> </div>
</header> </header>
<section class="displays-section"> <section class="displays-section">
<h2>Available Displays</h2> <h2 data-i18n="displays.title">Available Displays</h2>
<!-- Visual Layout Preview --> <!-- Visual Layout Preview -->
<div class="display-layout-preview"> <div class="display-layout-preview">
<h3>Display Layout</h3> <h3 data-i18n="displays.layout">Display Layout</h3>
<div id="display-layout-canvas" class="display-layout-canvas"> <div id="display-layout-canvas" class="display-layout-canvas">
<div class="loading">Loading layout...</div> <div class="loading" data-i18n="displays.loading">Loading layout...</div>
</div> </div>
<div class="layout-legend"> <div class="layout-legend">
<span class="legend-item"><span class="legend-dot primary"></span> Primary Display</span> <span class="legend-item"><span class="legend-dot primary"></span> <span data-i18n="displays.legend.primary">Primary Display</span></span>
<span class="legend-item"><span class="legend-dot secondary"></span> Secondary Display</span> <span class="legend-item"><span class="legend-dot secondary"></span> <span data-i18n="displays.legend.secondary">Secondary Display</span></span>
</div> </div>
</div> </div>
<!-- Display Cards --> <!-- Display Cards -->
<h3 style="margin-top: 30px;">Display Information</h3> <h3 style="margin-top: 30px;" data-i18n="displays.information">Display Information</h3>
<div id="displays-list" class="displays-grid"> <div id="displays-list" class="displays-grid">
<div class="loading">Loading displays...</div> <div class="loading" data-i18n="displays.loading">Loading displays...</div>
</div> </div>
</section> </section>
<section class="devices-section"> <section class="devices-section">
<h2>WLED Devices</h2> <h2 data-i18n="devices.title">WLED Devices</h2>
<div id="devices-list" class="devices-grid"> <div id="devices-list" class="devices-grid">
<div class="loading">Loading devices...</div> <div class="loading" data-i18n="devices.loading">Loading devices...</div>
</div> </div>
</section> </section>
<section class="add-device-section"> <section class="add-device-section">
<h2>Add New Device</h2> <h2 data-i18n="devices.add">Add New Device</h2>
<div class="info-banner" style="margin-bottom: 20px; padding: 12px; background: rgba(33, 150, 243, 0.1); border-left: 4px solid #2196F3; border-radius: 4px;"> <div class="info-banner" style="margin-bottom: 20px; padding: 12px; background: rgba(33, 150, 243, 0.1); border-left: 4px solid #2196F3; border-radius: 4px;">
<strong>📱 WLED Configuration:</strong> Configure your WLED device (effects, segments, color order, power limits, etc.) using the <strong><span data-i18n="devices.wled_config">📱 WLED Configuration:</span></strong> <span data-i18n="devices.wled_note">Configure your WLED device (effects, segments, color order, power limits, etc.) using the</span>
<a href="https://kno.wled.ge/" target="_blank" rel="noopener" style="color: #2196F3; text-decoration: underline;">official WLED app</a>. <a href="https://kno.wled.ge/" target="_blank" rel="noopener" style="color: #2196F3; text-decoration: underline;" data-i18n="devices.wled_link">official WLED app</a>.
This controller sends pixel color data and controls brightness per device. <span data-i18n="devices.wled_note2">This controller sends pixel color data and controls brightness per device.</span>
</div> </div>
<form id="add-device-form"> <form id="add-device-form">
<div class="form-group"> <div class="form-group">
<label for="device-name">Device Name:</label> <label for="device-name" data-i18n="device.name">Device Name:</label>
<input type="text" id="device-name" placeholder="Living Room TV" required> <input type="text" id="device-name" data-i18n-placeholder="device.name.placeholder" placeholder="Living Room TV" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="device-url">WLED URL:</label> <label for="device-url" data-i18n="device.url">WLED URL:</label>
<input type="url" id="device-url" placeholder="http://192.168.1.100" required> <input type="url" id="device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="device-led-count">LED Count:</label> <label for="device-led-count" data-i18n="device.led_count">LED Count:</label>
<input type="number" id="device-led-count" value="150" min="1" required> <input type="number" id="device-led-count" value="150" min="1" required>
<small class="input-hint">Number of LEDs configured in your WLED device</small> <small class="input-hint" data-i18n="device.led_count.hint">Number of LEDs configured in your WLED device</small>
</div> </div>
<button type="submit" class="btn btn-primary">Add Device</button> <button type="submit" class="btn btn-primary" data-i18n="device.button.add">Add Device</button>
</form> </form>
</section> </section>
</div> </div>
@@ -90,11 +94,11 @@
<div id="calibration-modal" class="modal"> <div id="calibration-modal" class="modal">
<div class="modal-content" style="max-width: 700px;"> <div class="modal-content" style="max-width: 700px;">
<div class="modal-header"> <div class="modal-header">
<h2>📐 LED Calibration</h2> <h2 data-i18n="calibration.title">📐 LED Calibration</h2>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="hidden" id="calibration-device-id"> <input type="hidden" id="calibration-device-id">
<p style="margin-bottom: 20px; color: var(--text-secondary);"> <p style="margin-bottom: 20px; color: var(--text-secondary);" data-i18n="calibration.description">
Configure how your LED strip is mapped to screen edges. Use test buttons to verify each edge lights up correctly. Configure how your LED strip is mapped to screen edges. Use test buttons to verify each edge lights up correctly.
</p> </p>
@@ -102,22 +106,22 @@
<div style="margin-bottom: 25px;"> <div style="margin-bottom: 25px;">
<div style="position: relative; width: 400px; height: 250px; margin: 0 auto; background: var(--card-bg); border: 2px solid var(--border-color); border-radius: 8px;"> <div style="position: relative; width: 400px; height: 250px; margin: 0 auto; background: var(--card-bg); border: 2px solid var(--border-color); border-radius: 8px;">
<!-- Screen representation --> <!-- Screen representation -->
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; height: 180px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px;"> <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; height: 180px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px;" data-i18n="calibration.preview.screen">
Screen Screen
</div> </div>
<!-- Edge labels --> <!-- Edge labels -->
<div style="position: absolute; top: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);"> <div style="position: absolute; top: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);">
Top: <span id="preview-top-count">0</span> LEDs <span data-i18n="calibration.preview.top">Top:</span> <span id="preview-top-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
</div> </div>
<div style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%) rotate(90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;"> <div style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%) rotate(90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;">
Right: <span id="preview-right-count">0</span> LEDs <span data-i18n="calibration.preview.right">Right:</span> <span id="preview-right-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
</div> </div>
<div style="position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);"> <div style="position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);">
Bottom: <span id="preview-bottom-count">0</span> LEDs <span data-i18n="calibration.preview.bottom">Bottom:</span> <span id="preview-bottom-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
</div> </div>
<div style="position: absolute; left: 5px; top: 50%; transform: translateY(-50%) rotate(-90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;"> <div style="position: absolute; left: 5px; top: 50%; transform: translateY(-50%) rotate(-90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;">
Left: <span id="preview-left-count">0</span> LEDs <span data-i18n="calibration.preview.left">Left:</span> <span id="preview-left-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
</div> </div>
<!-- Starting position indicator --> <!-- Starting position indicator -->
@@ -128,20 +132,20 @@
<!-- Layout Configuration --> <!-- Layout Configuration -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
<div class="form-group"> <div class="form-group">
<label for="cal-start-position">Starting Position:</label> <label for="cal-start-position" data-i18n="calibration.start_position">Starting Position:</label>
<select id="cal-start-position" onchange="updateCalibrationPreview()"> <select id="cal-start-position" onchange="updateCalibrationPreview()">
<option value="bottom_left">Bottom Left</option> <option value="bottom_left" data-i18n="calibration.position.bottom_left">Bottom Left</option>
<option value="bottom_right">Bottom Right</option> <option value="bottom_right" data-i18n="calibration.position.bottom_right">Bottom Right</option>
<option value="top_left">Top Left</option> <option value="top_left" data-i18n="calibration.position.top_left">Top Left</option>
<option value="top_right">Top Right</option> <option value="top_right" data-i18n="calibration.position.top_right">Top Right</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="cal-layout">Direction:</label> <label for="cal-layout" data-i18n="calibration.direction">Direction:</label>
<select id="cal-layout" onchange="updateCalibrationPreview()"> <select id="cal-layout" onchange="updateCalibrationPreview()">
<option value="clockwise">Clockwise</option> <option value="clockwise" data-i18n="calibration.direction.clockwise">Clockwise</option>
<option value="counterclockwise">Counterclockwise</option> <option value="counterclockwise" data-i18n="calibration.direction.counterclockwise">Counterclockwise</option>
</select> </select>
</div> </div>
</div> </div>
@@ -149,45 +153,45 @@
<!-- LED Counts per Edge --> <!-- LED Counts per Edge -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
<div class="form-group"> <div class="form-group">
<label for="cal-top-leds">Top LEDs:</label> <label for="cal-top-leds" data-i18n="calibration.leds.top">Top LEDs:</label>
<input type="number" id="cal-top-leds" min="0" value="0" oninput="updateCalibrationPreview()"> <input type="number" id="cal-top-leds" min="0" value="0" oninput="updateCalibrationPreview()">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="cal-right-leds">Right LEDs:</label> <label for="cal-right-leds" data-i18n="calibration.leds.right">Right LEDs:</label>
<input type="number" id="cal-right-leds" min="0" value="0" oninput="updateCalibrationPreview()"> <input type="number" id="cal-right-leds" min="0" value="0" oninput="updateCalibrationPreview()">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="cal-bottom-leds">Bottom LEDs:</label> <label for="cal-bottom-leds" data-i18n="calibration.leds.bottom">Bottom LEDs:</label>
<input type="number" id="cal-bottom-leds" min="0" value="0" oninput="updateCalibrationPreview()"> <input type="number" id="cal-bottom-leds" min="0" value="0" oninput="updateCalibrationPreview()">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="cal-left-leds">Left LEDs:</label> <label for="cal-left-leds" data-i18n="calibration.leds.left">Left LEDs:</label>
<input type="number" id="cal-left-leds" min="0" value="0" oninput="updateCalibrationPreview()"> <input type="number" id="cal-left-leds" min="0" value="0" oninput="updateCalibrationPreview()">
</div> </div>
</div> </div>
<div style="padding: 10px; background: rgba(255, 193, 7, 0.1); border-left: 4px solid #FFC107; border-radius: 4px; margin-bottom: 20px;"> <div style="padding: 10px; background: rgba(255, 193, 7, 0.1); border-left: 4px solid #FFC107; border-radius: 4px; margin-bottom: 20px;">
<strong>Total LEDs:</strong> <span id="cal-total-leds">0</span> / <span id="cal-device-led-count">0</span> <strong data-i18n="calibration.total">Total LEDs:</strong> <span id="cal-total-leds">0</span> / <span id="cal-device-led-count">0</span>
</div> </div>
<!-- Test Buttons --> <!-- Test Buttons -->
<div style="margin-bottom: 15px;"> <div style="margin-bottom: 15px;">
<p style="font-weight: 600; margin-bottom: 10px;">Test Edges (lights up each edge):</p> <p style="font-weight: 600; margin-bottom: 10px;" data-i18n="calibration.test">Test Edges (lights up each edge):</p>
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;"> <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;">
<button class="btn btn-secondary" onclick="testCalibrationEdge('top')" style="font-size: 0.9rem; padding: 8px;"> <button class="btn btn-secondary" onclick="testCalibrationEdge('top')" style="font-size: 0.9rem; padding: 8px;">
⬆️ Top ⬆️ <span data-i18n="calibration.test.top">Top</span>
</button> </button>
<button class="btn btn-secondary" onclick="testCalibrationEdge('right')" style="font-size: 0.9rem; padding: 8px;"> <button class="btn btn-secondary" onclick="testCalibrationEdge('right')" style="font-size: 0.9rem; padding: 8px;">
➡️ Right ➡️ <span data-i18n="calibration.test.right">Right</span>
</button> </button>
<button class="btn btn-secondary" onclick="testCalibrationEdge('bottom')" style="font-size: 0.9rem; padding: 8px;"> <button class="btn btn-secondary" onclick="testCalibrationEdge('bottom')" style="font-size: 0.9rem; padding: 8px;">
⬇️ Bottom ⬇️ <span data-i18n="calibration.test.bottom">Bottom</span>
</button> </button>
<button class="btn btn-secondary" onclick="testCalibrationEdge('left')" style="font-size: 0.9rem; padding: 8px;"> <button class="btn btn-secondary" onclick="testCalibrationEdge('left')" style="font-size: 0.9rem; padding: 8px;">
⬅️ Left ⬅️ <span data-i18n="calibration.test.left">Left</span>
</button> </button>
</div> </div>
</div> </div>
@@ -195,8 +199,8 @@
<div id="calibration-error" class="error-message" style="display: none;"></div> <div id="calibration-error" class="error-message" style="display: none;"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" onclick="closeCalibrationModal()">Cancel</button> <button class="btn btn-secondary" onclick="closeCalibrationModal()" data-i18n="calibration.button.cancel">Cancel</button>
<button class="btn btn-primary" onclick="saveCalibration()">Save Calibration</button> <button class="btn btn-primary" onclick="saveCalibration()" data-i18n="calibration.button.save">Save Calibration</button>
</div> </div>
</div> </div>
</div> </div>
@@ -205,43 +209,43 @@
<div id="device-settings-modal" class="modal"> <div id="device-settings-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>⚙️ Device Settings</h2> <h2 data-i18n="settings.title">⚙️ Device Settings</h2>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="device-settings-form"> <form id="device-settings-form">
<input type="hidden" id="settings-device-id"> <input type="hidden" id="settings-device-id">
<div class="form-group"> <div class="form-group">
<label for="settings-device-name">Device Name:</label> <label for="settings-device-name" data-i18n="device.name">Device Name:</label>
<input type="text" id="settings-device-name" placeholder="Living Room TV" required> <input type="text" id="settings-device-name" data-i18n-placeholder="device.name.placeholder" placeholder="Living Room TV" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="settings-device-url">WLED URL:</label> <label for="settings-device-url" data-i18n="device.url">WLED URL:</label>
<input type="url" id="settings-device-url" placeholder="http://192.168.1.100" required> <input type="url" id="settings-device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
<small class="input-hint">IP address or hostname of your WLED device</small> <small class="input-hint" data-i18n="settings.url.hint">IP address or hostname of your WLED device</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="settings-device-led-count">LED Count:</label> <label for="settings-device-led-count" data-i18n="device.led_count">LED Count:</label>
<input type="number" id="settings-device-led-count" min="1" required> <input type="number" id="settings-device-led-count" min="1" required>
<small class="input-hint">Number of LEDs configured in your WLED device</small> <small class="input-hint" data-i18n="device.led_count.hint">Number of LEDs configured in your WLED device</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="settings-device-brightness">Brightness: <span id="brightness-value">100%</span></label> <label for="settings-device-brightness"><span data-i18n="settings.brightness">Brightness:</span> <span id="brightness-value">100%</span></label>
<input type="range" id="settings-device-brightness" min="0" max="100" value="100" <input type="range" id="settings-device-brightness" min="0" max="100" value="100"
oninput="document.getElementById('brightness-value').textContent = this.value + '%'" oninput="document.getElementById('brightness-value').textContent = this.value + '%'"
style="width: 100%;"> style="width: 100%;">
<small class="input-hint">Global brightness for this WLED device (0-100%)</small> <small class="input-hint" data-i18n="settings.brightness.hint">Global brightness for this WLED device (0-100%)</small>
</div> </div>
<div id="settings-error" class="error-message" style="display: none;"></div> <div id="settings-error" class="error-message" style="display: none;"></div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" onclick="closeDeviceSettingsModal()">Cancel</button> <button class="btn btn-secondary" onclick="closeDeviceSettingsModal()" data-i18n="settings.button.cancel">Cancel</button>
<button class="btn btn-primary" onclick="saveDeviceSettings()">Save Changes</button> <button class="btn btn-primary" onclick="saveDeviceSettings()" data-i18n="settings.button.save">Save Changes</button>
</div> </div>
</div> </div>
</div> </div>
@@ -250,18 +254,19 @@
<div id="api-key-modal" class="modal"> <div id="api-key-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>🔑 Login to WLED Controller</h2> <h2 data-i18n="auth.title">🔑 Login to WLED Controller</h2>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p class="modal-description"> <p class="modal-description" data-i18n="auth.message">
Please enter your API key to authenticate and access the WLED Screen Controller. Please enter your API key to authenticate and access the WLED Screen Controller.
</p> </p>
<div class="form-group"> <div class="form-group">
<label for="api-key-input">API Key:</label> <label for="api-key-input" data-i18n="auth.label">API Key:</label>
<div class="password-input-wrapper"> <div class="password-input-wrapper">
<input <input
type="password" type="password"
id="api-key-input" id="api-key-input"
data-i18n-placeholder="auth.placeholder"
placeholder="Enter your API key..." placeholder="Enter your API key..."
autocomplete="off" autocomplete="off"
> >
@@ -269,13 +274,13 @@
👁️ 👁️
</button> </button>
</div> </div>
<small class="input-hint">Your API key will be stored securely in your browser's local storage.</small> <small class="input-hint" data-i18n="auth.hint">Your API key will be stored securely in your browser's local storage.</small>
</div> </div>
<div id="api-key-error" class="error-message" style="display: none;"></div> <div id="api-key-error" class="error-message" style="display: none;"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn">Cancel</button> <button class="btn btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn" data-i18n="auth.button.cancel">Cancel</button>
<button class="btn btn-primary" onclick="submitApiKey()">Login</button> <button class="btn btn-primary" onclick="submitApiKey()" data-i18n="auth.button.login">Login</button>
</div> </div>
</div> </div>
</div> </div>
@@ -329,20 +334,20 @@
} }
function showLogin() { function showLogin() {
showApiKeyModal('Enter your API key to login and access the controller.'); showApiKeyModal(t('auth.message'));
document.getElementById('modal-cancel-btn').style.display = 'inline-block'; document.getElementById('modal-cancel-btn').style.display = 'inline-block';
} }
function logout() { function logout() {
if (confirm('Are you sure you want to logout?')) { if (confirm(t('auth.logout.confirm'))) {
localStorage.removeItem('wled_api_key'); localStorage.removeItem('wled_api_key');
apiKey = null; apiKey = null;
updateAuthUI(); updateAuthUI();
showToast('Logged out successfully', 'info'); showToast(t('auth.logout.success'), 'info');
// Clear the UI // Clear the UI
document.getElementById('devices-list').innerHTML = '<div class="loading">Please login to view devices</div>'; document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
document.getElementById('displays-list').innerHTML = '<div class="loading">Please login to view displays</div>'; document.getElementById('displays-list').innerHTML = `<div class="loading">${t('auth.please_login')} displays</div>`;
} }
} }
@@ -395,7 +400,7 @@
const key = input.value.trim(); const key = input.value.trim();
if (!key) { if (!key) {
error.textContent = 'Please enter an API key'; error.textContent = t('auth.error.required');
error.style.display = 'block'; error.style.display = 'block';
return; return;
} }
@@ -406,7 +411,7 @@
updateAuthUI(); updateAuthUI();
closeApiKeyModal(); closeApiKeyModal();
showToast('Logged in successfully!', 'success'); showToast(t('auth.success'), 'success');
// Reload data // Reload data
loadServerInfo(); loadServerInfo();

View File

@@ -0,0 +1,111 @@
{
"app.title": "WLED Screen Controller",
"app.version": "Version:",
"theme.toggle": "Toggle theme",
"locale.change": "Change language",
"auth.login": "Login",
"auth.logout": "Logout",
"auth.authenticated": "Authenticated",
"auth.title": "Login to WLED Controller",
"auth.message": "Please enter your API key to authenticate and access the WLED Screen Controller.",
"auth.label": "API Key:",
"auth.placeholder": "Enter your API key...",
"auth.hint": "Your API key will be stored securely in your browser's local storage.",
"auth.button.cancel": "Cancel",
"auth.button.login": "Login",
"auth.error.required": "Please enter an API key",
"auth.success": "Logged in successfully!",
"auth.logout.confirm": "Are you sure you want to logout?",
"auth.logout.success": "Logged out successfully",
"auth.please_login": "Please login to view",
"displays.title": "Available Displays",
"displays.layout": "Display Layout",
"displays.information": "Display Information",
"displays.legend.primary": "Primary Display",
"displays.legend.secondary": "Secondary Display",
"displays.badge.primary": "Primary",
"displays.badge.secondary": "Secondary",
"displays.resolution": "Resolution:",
"displays.position": "Position:",
"displays.index": "Display Index:",
"displays.loading": "Loading displays...",
"displays.none": "No displays available",
"displays.failed": "Failed to load displays",
"devices.title": "WLED Devices",
"devices.add": "Add New Device",
"devices.loading": "Loading devices...",
"devices.none": "No devices configured",
"devices.failed": "Failed to load devices",
"devices.wled_config": "WLED Configuration:",
"devices.wled_note": "Configure your WLED device (effects, segments, color order, power limits, etc.) using the",
"devices.wled_link": "official WLED app",
"devices.wled_note2": "This controller sends pixel color data and controls brightness per device.",
"device.name": "Device Name:",
"device.name.placeholder": "Living Room TV",
"device.url": "WLED URL:",
"device.url.placeholder": "http://192.168.1.100",
"device.led_count": "LED Count:",
"device.led_count.hint": "Number of LEDs configured in your WLED device",
"device.button.add": "Add Device",
"device.button.start": "Start",
"device.button.stop": "Stop",
"device.button.settings": "Settings",
"device.button.calibrate": "Calibrate",
"device.button.remove": "Remove",
"device.status.connected": "Connected",
"device.status.disconnected": "Disconnected",
"device.status.error": "Error",
"device.status.processing": "Processing",
"device.status.idle": "Idle",
"device.fps": "FPS:",
"device.display": "Display:",
"device.remove.confirm": "Are you sure you want to remove this device?",
"device.added": "Device added successfully",
"device.removed": "Device removed successfully",
"device.started": "Processing started",
"device.stopped": "Processing stopped",
"settings.title": "Device Settings",
"settings.brightness": "Brightness:",
"settings.brightness.hint": "Global brightness for this WLED device (0-100%)",
"settings.url.hint": "IP address or hostname of your WLED device",
"settings.button.cancel": "Cancel",
"settings.button.save": "Save Changes",
"settings.saved": "Settings saved successfully",
"settings.failed": "Failed to save settings",
"calibration.title": "LED Calibration",
"calibration.description": "Configure how your LED strip is mapped to screen edges. Use test buttons to verify each edge lights up correctly.",
"calibration.preview.screen": "Screen",
"calibration.preview.top": "Top:",
"calibration.preview.right": "Right:",
"calibration.preview.bottom": "Bottom:",
"calibration.preview.left": "Left:",
"calibration.preview.leds": "LEDs",
"calibration.start_position": "Starting Position:",
"calibration.position.bottom_left": "Bottom Left",
"calibration.position.bottom_right": "Bottom Right",
"calibration.position.top_left": "Top Left",
"calibration.position.top_right": "Top Right",
"calibration.direction": "Direction:",
"calibration.direction.clockwise": "Clockwise",
"calibration.direction.counterclockwise": "Counterclockwise",
"calibration.leds.top": "Top LEDs:",
"calibration.leds.right": "Right LEDs:",
"calibration.leds.bottom": "Bottom LEDs:",
"calibration.leds.left": "Left LEDs:",
"calibration.total": "Total LEDs:",
"calibration.test": "Test Edges (lights up each edge):",
"calibration.test.top": "Top",
"calibration.test.right": "Right",
"calibration.test.bottom": "Bottom",
"calibration.test.left": "Left",
"calibration.button.cancel": "Cancel",
"calibration.button.save": "Save Calibration",
"calibration.saved": "Calibration saved successfully",
"calibration.failed": "Failed to save calibration",
"calibration.testing": "Testing {edge} edge...",
"server.healthy": "Server online",
"server.offline": "Server offline",
"error.unauthorized": "Unauthorized - please login",
"error.network": "Network error",
"error.unknown": "An error occurred"
}

View File

@@ -0,0 +1,111 @@
{
"app.title": "WLED Контроллер Экрана",
"app.version": "Версия:",
"theme.toggle": "Переключить тему",
"locale.change": "Изменить язык",
"auth.login": "Войти",
"auth.logout": "Выйти",
"auth.authenticated": "Авторизован",
"auth.title": "Вход в WLED Контроллер",
"auth.message": "Пожалуйста, введите ваш API ключ для аутентификации и доступа к WLED Контроллеру Экрана.",
"auth.label": "API Ключ:",
"auth.placeholder": "Введите ваш API ключ...",
"auth.hint": "Ваш API ключ будет безопасно сохранен в локальном хранилище браузера.",
"auth.button.cancel": "Отмена",
"auth.button.login": "Войти",
"auth.error.required": "Пожалуйста, введите API ключ",
"auth.success": "Вход выполнен успешно!",
"auth.logout.confirm": "Вы уверены, что хотите выйти?",
"auth.logout.success": "Выход выполнен успешно",
"auth.please_login": "Пожалуйста, войдите для просмотра",
"displays.title": "Доступные Дисплеи",
"displays.layout": "Расположение Дисплеев",
"displays.information": "Информация о Дисплеях",
"displays.legend.primary": "Основной Дисплей",
"displays.legend.secondary": "Вторичный Дисплей",
"displays.badge.primary": "Основной",
"displays.badge.secondary": "Вторичный",
"displays.resolution": "Разрешение:",
"displays.position": "Позиция:",
"displays.index": "Индекс Дисплея:",
"displays.loading": "Загрузка дисплеев...",
"displays.none": "Нет доступных дисплеев",
"displays.failed": "Не удалось загрузить дисплеи",
"devices.title": "WLED Устройства",
"devices.add": "Добавить Новое Устройство",
"devices.loading": "Загрузка устройств...",
"devices.none": "Устройства не настроены",
"devices.failed": "Не удалось загрузить устройства",
"devices.wled_config": "Конфигурация WLED:",
"devices.wled_note": "Настройте ваше WLED устройство (эффекты, сегменты, порядок цветов, ограничения питания и т.д.) используя",
"devices.wled_link": "официальное приложение WLED",
"devices.wled_note2": "Этот контроллер отправляет данные о цвете пикселей и управляет яркостью для каждого устройства.",
"device.name": "Имя Устройства:",
"device.name.placeholder": "ТВ в Гостиной",
"device.url": "WLED URL:",
"device.url.placeholder": "http://192.168.1.100",
"device.led_count": "Количество Светодиодов:",
"device.led_count.hint": "Количество светодиодов, настроенных в вашем WLED устройстве",
"device.button.add": "Добавить Устройство",
"device.button.start": "Запустить",
"device.button.stop": "Остановить",
"device.button.settings": "Настройки",
"device.button.calibrate": "Калибровка",
"device.button.remove": "Удалить",
"device.status.connected": "Подключено",
"device.status.disconnected": "Отключено",
"device.status.error": "Ошибка",
"device.status.processing": "Обработка",
"device.status.idle": "Ожидание",
"device.fps": "FPS:",
"device.display": "Дисплей:",
"device.remove.confirm": "Вы уверены, что хотите удалить это устройство?",
"device.added": "Устройство успешно добавлено",
"device.removed": "Устройство успешно удалено",
"device.started": "Обработка запущена",
"device.stopped": "Обработка остановлена",
"settings.title": "Настройки Устройства",
"settings.brightness": "Яркость:",
"settings.brightness.hint": "Общая яркость для этого WLED устройства (0-100%)",
"settings.url.hint": "IP адрес или имя хоста вашего WLED устройства",
"settings.button.cancel": "Отмена",
"settings.button.save": "Сохранить Изменения",
"settings.saved": "Настройки успешно сохранены",
"settings.failed": "Не удалось сохранить настройки",
"calibration.title": "Калибровка Светодиодов",
"calibration.description": "Настройте как ваша светодиодная лента сопоставляется с краями экрана. Используйте кнопки тестирования чтобы проверить что каждый край светится правильно.",
"calibration.preview.screen": "Экран",
"calibration.preview.top": "Сверху:",
"calibration.preview.right": "Справа:",
"calibration.preview.bottom": "Снизу:",
"calibration.preview.left": "Слева:",
"calibration.preview.leds": "Светодиодов",
"calibration.start_position": "Начальная Позиция:",
"calibration.position.bottom_left": "Нижний Левый",
"calibration.position.bottom_right": "Нижний Правый",
"calibration.position.top_left": "Верхний Левый",
"calibration.position.top_right": "Верхний Правый",
"calibration.direction": "Направление:",
"calibration.direction.clockwise": "По Часовой Стрелке",
"calibration.direction.counterclockwise": "Против Часовой Стрелки",
"calibration.leds.top": "Светодиодов Сверху:",
"calibration.leds.right": "Светодиодов Справа:",
"calibration.leds.bottom": "Светодиодов Снизу:",
"calibration.leds.left": "Светодиодов Слева:",
"calibration.total": "Всего Светодиодов:",
"calibration.test": "Тест Краев (подсвечивает каждый край):",
"calibration.test.top": "Сверху",
"calibration.test.right": "Справа",
"calibration.test.bottom": "Снизу",
"calibration.test.left": "Слева",
"calibration.button.cancel": "Отмена",
"calibration.button.save": "Сохранить Калибровку",
"calibration.saved": "Калибровка успешно сохранена",
"calibration.failed": "Не удалось сохранить калибровку",
"calibration.testing": "Тестирование {edge} края...",
"server.healthy": "Сервер онлайн",
"server.offline": "Сервер офлайн",
"error.unauthorized": "Не авторизован - пожалуйста, войдите",
"error.network": "Сетевая ошибка",
"error.unknown": "Произошла ошибка"
}