Implement dynamic language switching in web GUI

- Add /api/translations endpoint to return GUI text in current language
- Emit language_changed event when user changes language setting
- Frontend fetches and applies translations on language change
- Update all UI elements dynamically: tabs, headers, labels, buttons
- Load translations on page initialization

Fixes issue where language selection didn't update webpage text.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Fengqing Liu
2025-10-23 22:35:26 +11:00
parent a7d30f516f
commit 9ef08916d9
3 changed files with 198 additions and 1 deletions

View File

@@ -183,6 +183,75 @@ async def get_languages():
return gui_manager.settings.get_languages()
@app.get("/api/translations")
async def get_translations():
"""Get GUI translations for current language"""
from src.i18n.translator import _
# Return only the GUI section of translations
return {
"language": _.current,
"translations": {
"tabs": {
"main": _("gui", "tabs", "main"),
"inventory": _("gui", "tabs", "inventory"),
"settings": _("gui", "tabs", "settings"),
"help": _("gui", "tabs", "help"),
},
"login": {
"title": _("gui", "login", "name"),
"status": _("gui", "login", "labels").split("\n")[0].rstrip(":"),
"logged_in": _("gui", "login", "logged_in"),
"not_logged_in": _("gui", "login", "logged_out"),
},
"progress": {
"title": _("gui", "progress", "name"),
"no_drop": "No active drop", # Not in translations yet
"current_drop": _("gui", "progress", "drop"),
},
"console": {
"title": _("gui", "output"),
},
"channels": {
"title": _("gui", "channels", "name"),
},
"settings": {
"title": _("gui", "tabs", "settings"),
"general": _("gui", "settings", "general", "name"),
"dark_mode": _("gui", "settings", "general", "dark_mode").rstrip(": "),
"games_to_watch": "Games to Watch", # Not in translations yet
"games_help": "Select games to watch. Order matters - drag to reorder priority (top = highest priority).",
"search_games": "Search games...",
"select_all": "Select All",
"deselect_all": "Deselect All",
"selected_games": "Selected Games (drag to reorder)",
"available_games": "Available Games",
"actions": "Actions",
"reload_campaigns": _("gui", "settings", "reload"),
"connection_quality": "Connection Quality:",
"minimum_refresh": "Minimum Refresh Interval (minutes):",
},
"help": {
"title": _("gui", "tabs", "help"),
"about": "About Twitch Drops Miner",
"about_text": "This application automatically mines timed Twitch drops without downloading stream data.",
"how_to_use": "How to Use",
"features": "Features",
"important_notes": "Important Notes",
},
"header": {
"title": "Twitch Drops Miner",
"language": "Language:",
"initializing": "Initializing...",
"connected": "Connected",
"disconnected": "Disconnected",
"auto_mode": "AUTO",
"manual_mode": "MANUAL",
},
},
}
@app.post("/api/settings")
async def update_settings(settings: SettingsUpdate):
"""Update application settings"""

View File

@@ -68,6 +68,8 @@ class SettingsManager:
try:
_.set_language(language)
self._settings.language = language
# Notify clients that translations need to be reloaded
asyncio.create_task(self._broadcaster.emit("language_changed", {"language": language}))
except ValueError as e:
# Invalid language, log warning
import logging

View File

@@ -8,7 +8,8 @@ const state = {
campaigns: {},
settings: {},
currentDrop: null,
countdownTimer: null // Track the active countdown timer
countdownTimer: null, // Track the active countdown timer
translations: {} // Store current translations
};
// Initialize Socket.IO connection
@@ -180,6 +181,11 @@ socket.on('manual_mode_update', (data) => {
updateManualModeUI(data);
});
socket.on('language_changed', (data) => {
console.log('Language changed to:', data.language);
fetchAndApplyTranslations();
});
// ==================== UI Update Functions ====================
function updateStatus(status) {
@@ -923,6 +929,123 @@ async function fetchAndPopulateLanguages() {
}
}
async function fetchAndApplyTranslations() {
try {
const response = await fetch('/api/translations');
const data = await response.json();
state.translations = data.translations;
applyTranslations(data.translations);
console.log('Translations applied for language:', data.language);
} catch (error) {
console.error('Failed to fetch translations:', error);
}
}
function applyTranslations(t) {
// Update tab buttons
const tabButtons = {
'main': document.querySelector('[data-tab="main"]'),
'inventory': document.querySelector('[data-tab="inventory"]'),
'settings': document.querySelector('[data-tab="settings"]'),
'help': document.querySelector('[data-tab="help"]')
};
if (tabButtons.main && t.tabs) tabButtons.main.textContent = t.tabs.main;
if (tabButtons.inventory && t.tabs) tabButtons.inventory.textContent = t.tabs.inventory;
if (tabButtons.settings && t.tabs) tabButtons.settings.textContent = t.tabs.settings;
if (tabButtons.help && t.tabs) tabButtons.help.textContent = t.tabs.help;
// Update panel headers in Main tab
const mainTab = document.getElementById('main-tab');
if (mainTab && t.login) {
const loginHeader = mainTab.querySelector('.login-panel h2');
if (loginHeader) loginHeader.textContent = t.login.title;
}
if (mainTab && t.progress) {
const progressHeader = mainTab.querySelector('.progress-panel h2');
if (progressHeader) progressHeader.textContent = t.progress.title;
const noDropMsg = document.getElementById('no-drop-message');
if (noDropMsg) noDropMsg.textContent = t.progress.no_drop;
}
if (mainTab && t.console) {
const consoleHeader = mainTab.querySelector('.console-panel h2');
if (consoleHeader) consoleHeader.textContent = t.console.title;
}
if (mainTab && t.channels) {
const channelsHeader = mainTab.querySelector('.channels-panel h2');
if (channelsHeader) channelsHeader.textContent = t.channels.title;
}
// Update Settings tab
const settingsTab = document.getElementById('settings-tab');
if (settingsTab && t.settings) {
const headers = settingsTab.querySelectorAll('h2');
if (headers[0]) headers[0].textContent = t.settings.general;
if (headers[1]) headers[1].textContent = t.settings.games_to_watch;
if (headers[2]) headers[2].textContent = t.settings.actions;
const darkModeLabel = settingsTab.querySelector('label:has(#dark-mode)');
if (darkModeLabel) {
// Preserve the checkbox, just update text
const checkbox = darkModeLabel.querySelector('input');
darkModeLabel.textContent = '';
darkModeLabel.appendChild(checkbox);
darkModeLabel.appendChild(document.createTextNode(' ' + t.settings.dark_mode));
}
const connQualityLabel = settingsTab.querySelector('label:has(#connection-quality)');
if (connQualityLabel) {
const input = connQualityLabel.querySelector('input');
connQualityLabel.textContent = t.settings.connection_quality + ' ';
connQualityLabel.appendChild(input);
}
const refreshLabel = settingsTab.querySelector('label:has(#minimum-refresh-interval)');
if (refreshLabel) {
const input = refreshLabel.querySelector('input');
refreshLabel.textContent = t.settings.minimum_refresh + ' ';
refreshLabel.appendChild(input);
}
const helpText = settingsTab.querySelector('.help-text');
if (helpText) helpText.textContent = t.settings.games_help;
const searchInput = document.getElementById('games-filter');
if (searchInput) searchInput.placeholder = t.settings.search_games;
const selectAllBtn = document.getElementById('select-all-btn');
if (selectAllBtn) selectAllBtn.textContent = t.settings.select_all;
const deselectAllBtn = document.getElementById('deselect-all-btn');
if (deselectAllBtn) deselectAllBtn.textContent = t.settings.deselect_all;
const selectedGamesHeader = settingsTab.querySelector('.selected-games h3');
if (selectedGamesHeader) selectedGamesHeader.textContent = t.settings.selected_games;
const availableGamesHeader = settingsTab.querySelector('.available-games h3');
if (availableGamesHeader) availableGamesHeader.textContent = t.settings.available_games;
const reloadBtn = document.getElementById('reload-btn');
if (reloadBtn) reloadBtn.textContent = t.settings.reload_campaigns;
}
// Update Help tab
const helpTab = document.getElementById('help-tab');
if (helpTab && t.help) {
const headers = helpTab.querySelectorAll('h2, h3');
if (headers[0]) headers[0].textContent = t.help.about;
// Keep other help text in English for now as they're not in the translation object
}
// Update header elements
if (t.header) {
const languageLabel = document.querySelector('.language-selector span');
if (languageLabel) languageLabel.textContent = t.header.language;
}
}
async function reloadCampaigns() {
try {
await fetch('/api/reload', {method: 'POST'});
@@ -993,6 +1116,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Fetch and populate available languages
fetchAndPopulateLanguages();
// Fetch and apply translations for the current language
fetchAndApplyTranslations();
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();