Add proper i18n support with language selection in web GUI

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 <noreply@anthropic.com>
This commit is contained in:
Claude
2025-10-23 10:06:34 +00:00
parent 3e38e9eba1
commit 08531bd333
3 changed files with 71 additions and 0 deletions

View File

@@ -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"""

View File

@@ -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:

View File

@@ -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();