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:
2026-02-06 04:27:50 +03:00
parent a0af855846
commit 9bbb8e1bd7
4 changed files with 464 additions and 46 deletions

View File

@@ -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"/>';
}

View 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}\"?"
}

View 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}\"?"
}