Add internationalization (i18n) support with English and Russian translations
Some checks failed
Validate / validate (push) Failing after 7s
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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
111
server/src/wled_controller/static/locales/en.json
Normal file
111
server/src/wled_controller/static/locales/en.json
Normal 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"
|
||||||
|
}
|
||||||
111
server/src/wled_controller/static/locales/ru.json
Normal file
111
server/src/wled_controller/static/locales/ru.json
Normal 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": "Произошла ошибка"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user