Add internationalization (i18n) support with English and Russian locales
- Add translation JSON files (en.json, ru.json) with 110+ strings each - Implement locale auto-detection from browser settings - Add locale toggle button (EN/RU) with localStorage persistence - Translate all user-facing text: auth, player, scripts, callbacks - Fix dynamic content translation on locale switch (playback state, track title) - Add comprehensive i18n documentation to CLAUDE.md - Follow existing theme toggle pattern for consistency Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
37
CLAUDE.md
37
CLAUDE.md
@@ -34,6 +34,43 @@ The API token is generated on first run and displayed in the console output.
|
||||
|
||||
Default port: `8765`
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
The Web UI supports multiple languages with translations stored in separate JSON files.
|
||||
|
||||
### Locale Files
|
||||
|
||||
Translation files are located in:
|
||||
- `media_server/static/locales/en.json` - English (default)
|
||||
- `media_server/static/locales/ru.json` - Russian
|
||||
|
||||
### Maintaining Translations
|
||||
|
||||
**IMPORTANT:** When adding or modifying user-facing text in the Web UI:
|
||||
|
||||
1. **Update all locale files** - Add or update the translation key in **both** `en.json` and `ru.json`
|
||||
2. **Use consistent keys** - Follow the existing key naming pattern (e.g., `section.element`, `scripts.button.save`)
|
||||
3. **Test both locales** - Verify translations appear correctly by switching between EN/RU
|
||||
|
||||
### Adding New Text
|
||||
|
||||
When adding new UI elements:
|
||||
|
||||
1. Add the English text to `static/locales/en.json`
|
||||
2. Add the Russian translation to `static/locales/ru.json`
|
||||
3. In HTML: use `data-i18n="key.name"` for text content
|
||||
4. In HTML: use `data-i18n-placeholder="key.name"` for input placeholders
|
||||
5. In HTML: use `data-i18n-title="key.name"` for title attributes
|
||||
6. In JavaScript: use `t('key.name')` or `t('key.name', {param: value})` for dynamic text
|
||||
|
||||
### Adding New Locales
|
||||
|
||||
To add support for a new language:
|
||||
|
||||
1. Create `media_server/static/locales/{lang_code}.json` (copy from `en.json`)
|
||||
2. Translate all strings to the new language
|
||||
3. Add the language code to `supportedLocales` array in `index.html`
|
||||
|
||||
## Versioning
|
||||
|
||||
Version is tracked in two files that must be kept in sync:
|
||||
|
||||
@@ -108,6 +108,24 @@
|
||||
fill: var(--text-primary);
|
||||
}
|
||||
|
||||
#locale-toggle {
|
||||
background: none;
|
||||
border: 2px solid var(--text-secondary);
|
||||
color: var(--text-primary);
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#locale-toggle:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.player-container {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
@@ -743,17 +761,17 @@
|
||||
</head>
|
||||
<body>
|
||||
<!-- Clear Token Button -->
|
||||
<button class="clear-token-btn" onclick="clearToken()" title="Clear saved token">Logout</button>
|
||||
<button class="clear-token-btn" onclick="clearToken()" data-i18n-title="auth.logout.title" data-i18n="auth.logout" title="Clear saved token">Logout</button>
|
||||
|
||||
<!-- Auth Modal -->
|
||||
<div id="auth-overlay">
|
||||
<div class="auth-modal">
|
||||
<h2>Media Server</h2>
|
||||
<p>Enter your API token to connect to the media server.</p>
|
||||
<input type="text" id="token-input" placeholder="Enter API Token" autocomplete="off">
|
||||
<button class="btn-connect" onclick="authenticate()">Connect</button>
|
||||
<h2 data-i18n="app.title">Media Server</h2>
|
||||
<p data-i18n="auth.message">Enter your API token to connect to the media server.</p>
|
||||
<input type="text" id="token-input" data-i18n-placeholder="auth.placeholder" placeholder="Enter API Token" autocomplete="off">
|
||||
<button class="btn-connect" onclick="authenticate()" data-i18n="auth.connect">Connect</button>
|
||||
<div class="help-text">
|
||||
<p>To get your token, run:</p>
|
||||
<p data-i18n="auth.help">To get your token, run:</p>
|
||||
<code>media-server --show-token</code>
|
||||
</div>
|
||||
<div class="error-message" id="auth-error"></div>
|
||||
@@ -762,9 +780,9 @@
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Media Server</h1>
|
||||
<h1 data-i18n="app.title">Media Server</h1>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme" id="theme-toggle">
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-i18n-title="player.theme" title="Toggle theme" id="theme-toggle">
|
||||
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
|
||||
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
|
||||
</svg>
|
||||
@@ -772,9 +790,10 @@
|
||||
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="locale-toggle" onclick="toggleLocale()" data-i18n-title="player.locale" title="Change language">EN</button>
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span id="status-text">Disconnected</span>
|
||||
<span id="status-text" data-i18n="player.status.disconnected">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -785,14 +804,14 @@
|
||||
</div>
|
||||
|
||||
<div class="track-info">
|
||||
<div id="track-title">No media playing</div>
|
||||
<div id="track-title" data-i18n="player.no_media">No media playing</div>
|
||||
<div id="artist"></div>
|
||||
<div id="album"></div>
|
||||
<div class="playback-state">
|
||||
<svg class="state-icon" id="state-icon" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>
|
||||
</svg>
|
||||
<span id="playback-state">Idle</span>
|
||||
<span id="playback-state" data-i18n="state.idle">Idle</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -807,17 +826,17 @@
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="previousTrack()" title="Previous" id="btn-previous">
|
||||
<button onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="primary" onclick="togglePlayPause()" title="Play/Pause" id="btn-play-pause">
|
||||
<button class="primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
|
||||
<svg viewBox="0 0 24 24" id="play-pause-icon">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="nextTrack()" title="Next" id="btn-next">
|
||||
<button onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||
</svg>
|
||||
@@ -825,7 +844,7 @@
|
||||
</div>
|
||||
|
||||
<div class="volume-container">
|
||||
<button class="mute-btn" onclick="toggleMute()" title="Mute" id="btn-mute">
|
||||
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
|
||||
<svg viewBox="0 0 24 24" id="mute-icon">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
@@ -835,37 +854,37 @@
|
||||
</div>
|
||||
|
||||
<div class="source-info">
|
||||
Source: <span id="source">Unknown</span>
|
||||
<span data-i18n="player.source">Source:</span> <span id="source" data-i18n="player.unknown_source">Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts Section -->
|
||||
<div class="scripts-container" id="scripts-container" style="display: none;">
|
||||
<h2>Quick Actions</h2>
|
||||
<h2 data-i18n="scripts.quick_actions">Quick Actions</h2>
|
||||
<div class="scripts-grid" id="scripts-grid">
|
||||
<div class="scripts-empty">No scripts configured</div>
|
||||
<div class="scripts-empty" data-i18n="scripts.no_scripts">No scripts configured</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script Management Section -->
|
||||
<div class="script-management">
|
||||
<div class="script-management-header">
|
||||
<h2>Script Management</h2>
|
||||
<button class="add-script-btn" onclick="showAddScriptDialog()">+ Add</button>
|
||||
<h2 data-i18n="scripts.management">Script Management</h2>
|
||||
<button class="add-script-btn" onclick="showAddScriptDialog()" data-i18n="scripts.add">+ Add</button>
|
||||
</div>
|
||||
<table class="scripts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Label</th>
|
||||
<th>Command</th>
|
||||
<th>Timeout</th>
|
||||
<th>Actions</th>
|
||||
<th data-i18n="scripts.table.name">Name</th>
|
||||
<th data-i18n="scripts.table.label">Label</th>
|
||||
<th data-i18n="scripts.table.command">Command</th>
|
||||
<th data-i18n="scripts.table.timeout">Timeout</th>
|
||||
<th data-i18n="scripts.table.actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scriptsTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="empty-state">No scripts configured. Click "Add Script" to create one.</td>
|
||||
<td colspan="5" class="empty-state" data-i18n="scripts.empty">No scripts configured. Click "Add" to create one.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -874,19 +893,19 @@
|
||||
<!-- Callback Management Section -->
|
||||
<div class="script-management">
|
||||
<div class="script-management-header">
|
||||
<h2>Callback Management</h2>
|
||||
<button class="add-script-btn" onclick="showAddCallbackDialog()">+ Add</button>
|
||||
<h2 data-i18n="callbacks.management">Callback Management</h2>
|
||||
<button class="add-script-btn" onclick="showAddCallbackDialog()" data-i18n="callbacks.add">+ Add</button>
|
||||
</div>
|
||||
<p style="color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;">
|
||||
Callbacks are scripts triggered automatically by media control events (play, pause, volume, etc.)
|
||||
<p style="color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;" data-i18n="callbacks.description">
|
||||
Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)
|
||||
</p>
|
||||
<table class="scripts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Command</th>
|
||||
<th>Timeout</th>
|
||||
<th>Actions</th>
|
||||
<th data-i18n="callbacks.table.event">Event</th>
|
||||
<th data-i18n="callbacks.table.command">Command</th>
|
||||
<th data-i18n="callbacks.table.timeout">Timeout</th>
|
||||
<th data-i18n="callbacks.table.actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="callbacksTableBody">
|
||||
@@ -1027,6 +1046,143 @@
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
// Locale management
|
||||
let currentLocale = 'en';
|
||||
let translations = {};
|
||||
const supportedLocales = ['en', 'ru'];
|
||||
|
||||
// Minimal inline fallback for critical UI elements
|
||||
const fallbackTranslations = {
|
||||
'app.title': 'Media Server',
|
||||
'auth.connect': 'Connect',
|
||||
'auth.placeholder': 'Enter API Token',
|
||||
'player.status.connected': 'Connected',
|
||||
'player.status.disconnected': 'Disconnected'
|
||||
};
|
||||
|
||||
// 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.includes(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.includes(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 toggle button (if visible)
|
||||
updateLocaleToggle();
|
||||
}
|
||||
|
||||
// Toggle between locales
|
||||
async function toggleLocale() {
|
||||
const newLocale = currentLocale === 'en' ? 'ru' : 'en';
|
||||
await setLocale(newLocale);
|
||||
}
|
||||
|
||||
// Update locale toggle button
|
||||
function updateLocaleToggle() {
|
||||
const localeButton = document.getElementById('locale-toggle');
|
||||
if (localeButton) {
|
||||
localeButton.textContent = currentLocale === 'en' ? 'RU' : 'EN';
|
||||
localeButton.title = t('player.locale');
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Re-apply dynamic content with new translations
|
||||
// Update playback state
|
||||
updatePlaybackState(currentState);
|
||||
|
||||
// Update connection status
|
||||
const connected = ws && ws.readyState === WebSocket.OPEN;
|
||||
updateConnectionStatus(connected);
|
||||
|
||||
// Re-apply last media status if available
|
||||
if (lastStatus) {
|
||||
document.getElementById('track-title').textContent = lastStatus.title || t('player.no_media');
|
||||
document.getElementById('source').textContent = lastStatus.source || t('player.unknown_source');
|
||||
}
|
||||
|
||||
// Reload tables to get translated content
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (token) {
|
||||
loadScriptsTable();
|
||||
loadCallbacksTable();
|
||||
}
|
||||
}
|
||||
|
||||
let ws = null;
|
||||
let reconnectTimeout = null;
|
||||
let currentState = 'idle';
|
||||
@@ -1034,6 +1190,7 @@
|
||||
let currentPosition = 0;
|
||||
let isUserAdjustingVolume = false;
|
||||
let scripts = [];
|
||||
let lastStatus = null; // Store last status for locale switching
|
||||
|
||||
// Position interpolation
|
||||
let lastPositionUpdate = 0;
|
||||
@@ -1041,10 +1198,13 @@
|
||||
let interpolationInterval = null;
|
||||
|
||||
// Initialize on page load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize theme
|
||||
initTheme();
|
||||
|
||||
// Initialize locale (async - loads JSON file)
|
||||
await initLocale();
|
||||
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (token) {
|
||||
connectWebSocket(token);
|
||||
@@ -1109,7 +1269,7 @@
|
||||
function authenticate() {
|
||||
const token = document.getElementById('token-input').value.trim();
|
||||
if (!token) {
|
||||
showAuthForm('Please enter a token');
|
||||
showAuthForm(t('auth.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1122,7 +1282,7 @@
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
showAuthForm('Token cleared. Please enter a new token.');
|
||||
showAuthForm(t('auth.cleared'));
|
||||
}
|
||||
|
||||
function connectWebSocket(token) {
|
||||
@@ -1167,7 +1327,7 @@
|
||||
if (event.code === 4001) {
|
||||
// Invalid token
|
||||
localStorage.removeItem('media_server_token');
|
||||
showAuthForm('Invalid token. Please try again.');
|
||||
showAuthForm(t('auth.invalid'));
|
||||
} else if (event.code !== 1000) {
|
||||
// Abnormal closure - attempt reconnect
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
@@ -1194,16 +1354,19 @@
|
||||
|
||||
if (connected) {
|
||||
dot.classList.add('connected');
|
||||
text.textContent = 'Connected';
|
||||
text.textContent = t('player.status.connected');
|
||||
} else {
|
||||
dot.classList.remove('connected');
|
||||
text.textContent = 'Disconnected';
|
||||
text.textContent = t('player.status.disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(status) {
|
||||
// Store status for locale switching
|
||||
lastStatus = status;
|
||||
|
||||
// Update track info
|
||||
document.getElementById('track-title').textContent = status.title || 'No media playing';
|
||||
document.getElementById('track-title').textContent = status.title || t('player.no_media');
|
||||
document.getElementById('artist').textContent = status.artist || '';
|
||||
document.getElementById('album').textContent = status.album || '';
|
||||
|
||||
@@ -1243,7 +1406,7 @@
|
||||
updateMuteIcon(status.muted);
|
||||
|
||||
// Update source
|
||||
document.getElementById('source').textContent = status.source || 'Unknown';
|
||||
document.getElementById('source').textContent = status.source || t('player.unknown_source');
|
||||
|
||||
// Enable/disable controls based on state
|
||||
const hasMedia = status.state !== 'idle';
|
||||
@@ -1266,22 +1429,22 @@
|
||||
|
||||
switch(state) {
|
||||
case 'playing':
|
||||
stateText.textContent = 'Playing';
|
||||
stateText.textContent = t('state.playing');
|
||||
stateIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
||||
break;
|
||||
case 'paused':
|
||||
stateText.textContent = 'Paused';
|
||||
stateText.textContent = t('state.paused');
|
||||
stateIcon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
break;
|
||||
case 'stopped':
|
||||
stateText.textContent = 'Stopped';
|
||||
stateText.textContent = t('state.stopped');
|
||||
stateIcon.innerHTML = '<path d="M6 6h12v12H6z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
break;
|
||||
default:
|
||||
stateText.textContent = 'Idle';
|
||||
stateText.textContent = t('state.idle');
|
||||
stateIcon.innerHTML = '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
}
|
||||
|
||||
109
media_server/static/locales/en.json
Normal file
109
media_server/static/locales/en.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"app.title": "Media Server",
|
||||
"auth.message": "Enter your API token to connect to the media server.",
|
||||
"auth.placeholder": "Enter API Token",
|
||||
"auth.connect": "Connect",
|
||||
"auth.help": "To get your token, run:",
|
||||
"auth.logout": "Logout",
|
||||
"auth.logout.title": "Clear saved token",
|
||||
"auth.invalid": "Invalid token. Please try again.",
|
||||
"auth.cleared": "Token cleared. Please enter a new token.",
|
||||
"auth.required": "Please enter a token",
|
||||
"player.theme": "Toggle theme",
|
||||
"player.locale": "Change language",
|
||||
"player.previous": "Previous",
|
||||
"player.play": "Play/Pause",
|
||||
"player.next": "Next",
|
||||
"player.mute": "Mute",
|
||||
"player.status.connected": "Connected",
|
||||
"player.status.disconnected": "Disconnected",
|
||||
"player.no_media": "No media playing",
|
||||
"player.source": "Source:",
|
||||
"player.unknown_source": "Unknown",
|
||||
"state.playing": "Playing",
|
||||
"state.paused": "Paused",
|
||||
"state.stopped": "Stopped",
|
||||
"state.idle": "Idle",
|
||||
"scripts.quick_actions": "Quick Actions",
|
||||
"scripts.no_scripts": "No scripts configured",
|
||||
"scripts.management": "Script Management",
|
||||
"scripts.add": "Add",
|
||||
"scripts.table.name": "Name",
|
||||
"scripts.table.label": "Label",
|
||||
"scripts.table.command": "Command",
|
||||
"scripts.table.timeout": "Timeout",
|
||||
"scripts.table.actions": "Actions",
|
||||
"scripts.empty": "No scripts configured. Click 'Add' to create one.",
|
||||
"scripts.dialog.add": "Add Script",
|
||||
"scripts.dialog.edit": "Edit Script",
|
||||
"scripts.field.name": "Script Name *",
|
||||
"scripts.field.label": "Label",
|
||||
"scripts.field.command": "Command *",
|
||||
"scripts.field.description": "Description",
|
||||
"scripts.field.icon": "Icon (MDI)",
|
||||
"scripts.field.timeout": "Timeout (seconds)",
|
||||
"scripts.placeholder.name": "Only letters, numbers, and underscores allowed",
|
||||
"scripts.placeholder.label": "Human-readable name",
|
||||
"scripts.placeholder.command": "e.g., shutdown /s /t 0",
|
||||
"scripts.placeholder.description": "What does this script do?",
|
||||
"scripts.placeholder.icon": "e.g., mdi:power",
|
||||
"scripts.button.cancel": "Cancel",
|
||||
"scripts.button.save": "Save",
|
||||
"scripts.button.edit": "Edit",
|
||||
"scripts.button.delete": "Delete",
|
||||
"scripts.msg.executed": "{name} executed successfully",
|
||||
"scripts.msg.execute_failed": "Failed to execute {name}",
|
||||
"scripts.msg.execute_error": "Error executing {name}",
|
||||
"scripts.msg.created": "Script created successfully",
|
||||
"scripts.msg.updated": "Script updated successfully",
|
||||
"scripts.msg.create_failed": "Failed to create script",
|
||||
"scripts.msg.update_failed": "Failed to update script",
|
||||
"scripts.msg.deleted": "Script deleted successfully",
|
||||
"scripts.msg.delete_failed": "Failed to delete script",
|
||||
"scripts.msg.not_found": "Script not found",
|
||||
"scripts.msg.load_failed": "Failed to load script details",
|
||||
"scripts.msg.list_failed": "Failed to load scripts",
|
||||
"scripts.confirm.delete": "Are you sure you want to delete the script \"{name}\"?",
|
||||
"callbacks.management": "Callback Management",
|
||||
"callbacks.description": "Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)",
|
||||
"callbacks.add": "Add",
|
||||
"callbacks.table.event": "Event",
|
||||
"callbacks.table.command": "Command",
|
||||
"callbacks.table.timeout": "Timeout",
|
||||
"callbacks.table.actions": "Actions",
|
||||
"callbacks.empty": "No callbacks configured. Click 'Add' to create one.",
|
||||
"callbacks.dialog.add": "Add Callback",
|
||||
"callbacks.dialog.edit": "Edit Callback",
|
||||
"callbacks.field.event": "Event *",
|
||||
"callbacks.field.command": "Command *",
|
||||
"callbacks.field.timeout": "Timeout (seconds)",
|
||||
"callbacks.field.workdir": "Working Directory",
|
||||
"callbacks.placeholder.event": "Select event...",
|
||||
"callbacks.placeholder.command": "e.g., shutdown /s /t 0",
|
||||
"callbacks.placeholder.workdir": "Optional",
|
||||
"callbacks.button.cancel": "Cancel",
|
||||
"callbacks.button.save": "Save",
|
||||
"callbacks.button.edit": "Edit",
|
||||
"callbacks.button.delete": "Delete",
|
||||
"callbacks.event.on_play": "on_play - After play succeeds",
|
||||
"callbacks.event.on_pause": "on_pause - After pause succeeds",
|
||||
"callbacks.event.on_stop": "on_stop - After stop succeeds",
|
||||
"callbacks.event.on_next": "on_next - After next track succeeds",
|
||||
"callbacks.event.on_previous": "on_previous - After previous track succeeds",
|
||||
"callbacks.event.on_volume": "on_volume - After volume change",
|
||||
"callbacks.event.on_mute": "on_mute - After mute toggle",
|
||||
"callbacks.event.on_seek": "on_seek - After seek succeeds",
|
||||
"callbacks.event.on_turn_on": "on_turn_on - Callback-only action",
|
||||
"callbacks.event.on_turn_off": "on_turn_off - Callback-only action",
|
||||
"callbacks.event.on_toggle": "on_toggle - Callback-only action",
|
||||
"callbacks.msg.created": "Callback created successfully",
|
||||
"callbacks.msg.updated": "Callback updated successfully",
|
||||
"callbacks.msg.create_failed": "Failed to create callback",
|
||||
"callbacks.msg.update_failed": "Failed to update callback",
|
||||
"callbacks.msg.deleted": "Callback deleted successfully",
|
||||
"callbacks.msg.delete_failed": "Failed to delete callback",
|
||||
"callbacks.msg.not_found": "Callback not found",
|
||||
"callbacks.msg.load_failed": "Failed to load callback details",
|
||||
"callbacks.msg.list_failed": "Failed to load callbacks",
|
||||
"callbacks.confirm.delete": "Are you sure you want to delete the callback \"{name}\"?"
|
||||
}
|
||||
109
media_server/static/locales/ru.json
Normal file
109
media_server/static/locales/ru.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"app.title": "Медиа Сервер",
|
||||
"auth.message": "Введите API токен для подключения к медиа серверу.",
|
||||
"auth.placeholder": "Введите API токен",
|
||||
"auth.connect": "Подключиться",
|
||||
"auth.help": "Чтобы получить токен, выполните:",
|
||||
"auth.logout": "Выйти",
|
||||
"auth.logout.title": "Очистить сохраненный токен",
|
||||
"auth.invalid": "Неверный токен. Пожалуйста, попробуйте снова.",
|
||||
"auth.cleared": "Токен очищен. Пожалуйста, введите новый токен.",
|
||||
"auth.required": "Пожалуйста, введите токен",
|
||||
"player.theme": "Переключить тему",
|
||||
"player.locale": "Изменить язык",
|
||||
"player.previous": "Предыдущий",
|
||||
"player.play": "Воспроизвести/Пауза",
|
||||
"player.next": "Следующий",
|
||||
"player.mute": "Без звука",
|
||||
"player.status.connected": "Подключено",
|
||||
"player.status.disconnected": "Отключено",
|
||||
"player.no_media": "Медиа не воспроизводится",
|
||||
"player.source": "Источник:",
|
||||
"player.unknown_source": "Неизвестно",
|
||||
"state.playing": "Воспроизведение",
|
||||
"state.paused": "Пауза",
|
||||
"state.stopped": "Остановлено",
|
||||
"state.idle": "Ожидание",
|
||||
"scripts.quick_actions": "Быстрые Действия",
|
||||
"scripts.no_scripts": "Скрипты не настроены",
|
||||
"scripts.management": "Управление Скриптами",
|
||||
"scripts.add": "Добавить",
|
||||
"scripts.table.name": "Имя",
|
||||
"scripts.table.label": "Метка",
|
||||
"scripts.table.command": "Команда",
|
||||
"scripts.table.timeout": "Таймаут",
|
||||
"scripts.table.actions": "Действия",
|
||||
"scripts.empty": "Скрипты не настроены. Нажмите 'Добавить' для создания.",
|
||||
"scripts.dialog.add": "Добавить Скрипт",
|
||||
"scripts.dialog.edit": "Редактировать Скрипт",
|
||||
"scripts.field.name": "Имя Скрипта *",
|
||||
"scripts.field.label": "Метка",
|
||||
"scripts.field.command": "Команда *",
|
||||
"scripts.field.description": "Описание",
|
||||
"scripts.field.icon": "Иконка (MDI)",
|
||||
"scripts.field.timeout": "Таймаут (секунды)",
|
||||
"scripts.placeholder.name": "Только буквы, цифры и подчеркивания",
|
||||
"scripts.placeholder.label": "Человеко-читаемое имя",
|
||||
"scripts.placeholder.command": "например, shutdown /s /t 0",
|
||||
"scripts.placeholder.description": "Что делает этот скрипт?",
|
||||
"scripts.placeholder.icon": "например, mdi:power",
|
||||
"scripts.button.cancel": "Отмена",
|
||||
"scripts.button.save": "Сохранить",
|
||||
"scripts.button.edit": "Редактировать",
|
||||
"scripts.button.delete": "Удалить",
|
||||
"scripts.msg.executed": "{name} выполнен успешно",
|
||||
"scripts.msg.execute_failed": "Не удалось выполнить {name}",
|
||||
"scripts.msg.execute_error": "Ошибка выполнения {name}",
|
||||
"scripts.msg.created": "Скрипт создан успешно",
|
||||
"scripts.msg.updated": "Скрипт обновлен успешно",
|
||||
"scripts.msg.create_failed": "Не удалось создать скрипт",
|
||||
"scripts.msg.update_failed": "Не удалось обновить скрипт",
|
||||
"scripts.msg.deleted": "Скрипт удален успешно",
|
||||
"scripts.msg.delete_failed": "Не удалось удалить скрипт",
|
||||
"scripts.msg.not_found": "Скрипт не найден",
|
||||
"scripts.msg.load_failed": "Не удалось загрузить данные скрипта",
|
||||
"scripts.msg.list_failed": "Не удалось загрузить скрипты",
|
||||
"scripts.confirm.delete": "Вы уверены, что хотите удалить скрипт \"{name}\"?",
|
||||
"callbacks.management": "Управление Обратными Вызовами",
|
||||
"callbacks.description": "Обратные вызовы - это скрипты, автоматически запускаемые при событиях управления медиа (воспроизведение, пауза, остановка и т.д.)",
|
||||
"callbacks.add": "Добавить",
|
||||
"callbacks.table.event": "Событие",
|
||||
"callbacks.table.command": "Команда",
|
||||
"callbacks.table.timeout": "Таймаут",
|
||||
"callbacks.table.actions": "Действия",
|
||||
"callbacks.empty": "Обратные вызовы не настроены. Нажмите 'Добавить' для создания.",
|
||||
"callbacks.dialog.add": "Добавить Обратный Вызов",
|
||||
"callbacks.dialog.edit": "Редактировать Обратный Вызов",
|
||||
"callbacks.field.event": "Событие *",
|
||||
"callbacks.field.command": "Команда *",
|
||||
"callbacks.field.timeout": "Таймаут (секунды)",
|
||||
"callbacks.field.workdir": "Рабочая Директория",
|
||||
"callbacks.placeholder.event": "Выберите событие...",
|
||||
"callbacks.placeholder.command": "например, shutdown /s /t 0",
|
||||
"callbacks.placeholder.workdir": "Опционально",
|
||||
"callbacks.button.cancel": "Отмена",
|
||||
"callbacks.button.save": "Сохранить",
|
||||
"callbacks.button.edit": "Редактировать",
|
||||
"callbacks.button.delete": "Удалить",
|
||||
"callbacks.event.on_play": "on_play - После успешного воспроизведения",
|
||||
"callbacks.event.on_pause": "on_pause - После успешной паузы",
|
||||
"callbacks.event.on_stop": "on_stop - После успешной остановки",
|
||||
"callbacks.event.on_next": "on_next - После успешного перехода к следующему",
|
||||
"callbacks.event.on_previous": "on_previous - После успешного перехода к предыдущему",
|
||||
"callbacks.event.on_volume": "on_volume - После изменения громкости",
|
||||
"callbacks.event.on_mute": "on_mute - После переключения звука",
|
||||
"callbacks.event.on_seek": "on_seek - После успешной перемотки",
|
||||
"callbacks.event.on_turn_on": "on_turn_on - Действие только для обратных вызовов",
|
||||
"callbacks.event.on_turn_off": "on_turn_off - Действие только для обратных вызовов",
|
||||
"callbacks.event.on_toggle": "on_toggle - Действие только для обратных вызовов",
|
||||
"callbacks.msg.created": "Обратный вызов создан успешно",
|
||||
"callbacks.msg.updated": "Обратный вызов обновлен успешно",
|
||||
"callbacks.msg.create_failed": "Не удалось создать обратный вызов",
|
||||
"callbacks.msg.update_failed": "Не удалось обновить обратный вызов",
|
||||
"callbacks.msg.deleted": "Обратный вызов удален успешно",
|
||||
"callbacks.msg.delete_failed": "Не удалось удалить обратный вызов",
|
||||
"callbacks.msg.not_found": "Обратный вызов не найден",
|
||||
"callbacks.msg.load_failed": "Не удалось загрузить данные обратного вызова",
|
||||
"callbacks.msg.list_failed": "Не удалось загрузить обратные вызовы",
|
||||
"callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?"
|
||||
}
|
||||
Reference in New Issue
Block a user