From 08531bd33301784cb8b70fe8be1ea420ee064e69 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Oct 2025 10:06:34 +0000 Subject: [PATCH] Add proper i18n support with language selection in web GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The application already had a complete i18n system with 18+ language translation files, but the web GUI only showed English as an option. Changes: - Add /api/languages endpoint to fetch available languages from translator - Update SettingsUpdate model to include language field - Add SettingsManager.get_languages() method to expose available languages - Update SettingsManager to handle language changes via translator.set_language() - Populate language dropdown dynamically from available translations on page load - Add auto-save for language changes in frontend - Language is persisted to settings.json and loaded on startup The translator is initialized with the saved language at application startup (already implemented in src/__main__.py lines 101-105). Available languages include: English, Français, Deutsch, Español, Italiano, Português, Polski, Русский, Українська, 简体中文, 繁體中文, 日本語, العربية, Türkçe, Română, Nederlandse, Dansk, Čeština, Indonesian 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/web/app.py | 10 +++++++++ src/web/managers/settings.py | 20 ++++++++++++++++++ web/static/app.js | 41 ++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/web/app.py b/src/web/app.py index 747a984..5b95806 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -70,6 +70,7 @@ class ChannelSelectRequest(BaseModel): class SettingsUpdate(BaseModel): games_to_watch: list[str] | None = None dark_mode: bool | None = None + language: str | None = None proxy: str | None = None connection_quality: int | None = None minimum_refresh_interval_minutes: int | None = None @@ -173,6 +174,15 @@ async def get_settings(): return gui_manager.settings.get_settings() +@app.get("/api/languages") +async def get_languages(): + """Get available languages""" + if not gui_manager: + raise HTTPException(status_code=503, detail="GUI not initialized") + + return gui_manager.settings.get_languages() + + @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 f1efc3c..fbe77bd 100644 --- a/src/web/managers/settings.py +++ b/src/web/managers/settings.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from typing import TYPE_CHECKING, Any +from src.i18n.translator import _ from src.models.game import Game @@ -41,6 +42,17 @@ class SettingsManager: "minimum_refresh_interval_minutes": self._settings.minimum_refresh_interval_minutes, } + def get_languages(self) -> dict[str, Any]: + """Get available languages and current selection. + + Returns: + Dictionary with available languages and current language + """ + return { + "available": list(_.languages), + "current": _.current, + } + def update_settings(self, settings_data: dict[str, Any]): """Update settings from user input. @@ -51,6 +63,14 @@ class SettingsManager: self._settings.games_to_watch = settings_data["games_to_watch"] if "dark_mode" in settings_data: self._settings.dark_mode = settings_data["dark_mode"] + if "language" in settings_data: + language = settings_data["language"] + try: + _.set_language(language) + self._settings.language = language + except ValueError as e: + # Invalid language, skip update + pass if "connection_quality" in settings_data: self._settings.connection_quality = settings_data["connection_quality"] if "proxy" in settings_data: diff --git a/web/static/app.js b/web/static/app.js index d4554bb..d9ddb53 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -542,6 +542,14 @@ function updateSettingsUI(settings) { document.getElementById('connection-quality').value = settings.connection_quality || 1; document.getElementById('minimum-refresh-interval').value = settings.minimum_refresh_interval_minutes || 30; + // Update language dropdown if we have the current language + if (settings.language) { + const languageSelect = document.getElementById('language'); + if (languageSelect) { + languageSelect.value = settings.language; + } + } + if (settings.dark_mode) { document.body.classList.add('dark-mode'); } else { @@ -861,6 +869,7 @@ async function confirmOAuth() { async function saveSettings() { const settings = { dark_mode: document.getElementById('dark-mode').checked, + language: document.getElementById('language').value, connection_quality: parseInt(document.getElementById('connection-quality').value), minimum_refresh_interval_minutes: parseInt(document.getElementById('minimum-refresh-interval').value), games_to_watch: state.settings.games_to_watch || [] @@ -878,6 +887,34 @@ async function saveSettings() { } } +async function fetchAndPopulateLanguages() { + try { + const response = await fetch('/api/languages'); + const data = await response.json(); + + const languageSelect = document.getElementById('language'); + if (!languageSelect) return; + + // Clear existing options + languageSelect.innerHTML = ''; + + // Populate with available languages + data.available.forEach(lang => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + languageSelect.appendChild(option); + }); + + // Set current language + if (data.current) { + languageSelect.value = data.current; + } + } catch (error) { + console.error('Failed to fetch languages:', error); + } +} + async function reloadCampaigns() { try { await fetch('/api/reload', {method: 'POST'}); @@ -929,6 +966,7 @@ document.addEventListener('DOMContentLoaded', () => { // Then save settings saveSettings(); }); + document.getElementById('language').addEventListener('change', saveSettings); document.getElementById('connection-quality').addEventListener('change', saveSettings); document.getElementById('minimum-refresh-interval').addEventListener('change', saveSettings); document.getElementById('reload-btn').addEventListener('click', reloadCampaigns); @@ -944,6 +982,9 @@ document.addEventListener('DOMContentLoaded', () => { exitManualBtn.addEventListener('click', exitManualMode); } + // Fetch and populate available languages + fetchAndPopulateLanguages(); + // Request notification permission if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission();