From 9ef08916d9b6f868aa0a58d14c498b4e2f304cc5 Mon Sep 17 00:00:00 2001 From: Fengqing Liu Date: Thu, 23 Oct 2025 22:35:26 +1100 Subject: [PATCH] Implement dynamic language switching in web GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/web/app.py | 69 +++++++++++++++++++ src/web/managers/settings.py | 2 + web/static/app.js | 128 ++++++++++++++++++++++++++++++++++- 3 files changed, 198 insertions(+), 1 deletion(-) diff --git a/src/web/app.py b/src/web/app.py index 5b95806..cc83c7a 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -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""" diff --git a/src/web/managers/settings.py b/src/web/managers/settings.py index dd7b456..7d45b68 100644 --- a/src/web/managers/settings.py +++ b/src/web/managers/settings.py @@ -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 diff --git a/web/static/app.js b/web/static/app.js index 4ff7247..b3f900f 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -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();