diff --git a/src/web/app.py b/src/web/app.py
index cc83c7a..888851c 100644
--- a/src/web/app.py
+++ b/src/web/app.py
@@ -188,7 +188,7 @@ async def get_translations():
"""Get GUI translations for current language"""
from src.i18n.translator import _
- # Return only the GUI section of translations
+ # Return comprehensive GUI translations
return {
"language": _.current,
"translations": {
@@ -200,32 +200,69 @@ async def get_translations():
},
"login": {
"title": _("gui", "login", "name"),
- "status": _("gui", "login", "labels").split("\n")[0].rstrip(":"),
+ "status_label": _("gui", "login", "labels").split("\n")[0].rstrip(":"),
"logged_in": _("gui", "login", "logged_in"),
"not_logged_in": _("gui", "login", "logged_out"),
+ "username": _("gui", "login", "username"),
+ "password": _("gui", "login", "password"),
+ "twofa_code": _("gui", "login", "twofa_code"),
+ "button": _("gui", "login", "button"),
+ "oauth_prompt": "Enter this code at:",
+ "oauth_activate": "Twitch Activate",
+ "oauth_confirm": "I've entered the code",
},
"progress": {
"title": _("gui", "progress", "name"),
- "no_drop": "No active drop", # Not in translations yet
- "current_drop": _("gui", "progress", "drop"),
+ "no_drop": "No active drop",
+ "drop": _("gui", "progress", "drop").rstrip(":"),
+ "game": _("gui", "progress", "game").rstrip(":"),
+ "campaign": _("gui", "progress", "campaign").rstrip(":"),
+ "remaining": _("gui", "progress", "remaining"),
+ "progress_label": _("gui", "progress", "drop_progress").rstrip(":"),
+ "return_to_auto": "Return to Auto Mode",
+ "manual_mode_info": "Manual Mode: Mining",
},
"console": {
"title": _("gui", "output"),
},
"channels": {
"title": _("gui", "channels", "name"),
+ "online": _("gui", "channels", "online"),
+ "pending": _("gui", "channels", "pending"),
+ "offline": _("gui", "channels", "offline"),
+ "no_channels": "No channels tracked yet...",
+ "no_channels_for_games": "No channels found for selected games...",
+ "channel_count": "channel",
+ "channel_count_plural": "channels",
+ "viewers": "viewers",
+ },
+ "inventory": {
+ "title": _("gui", "tabs", "inventory"),
+ "no_campaigns": "No campaigns loaded yet...",
+ "status": {
+ "active": _("gui", "inventory", "status", "active"),
+ "upcoming": _("gui", "inventory", "status", "upcoming"),
+ "expired": _("gui", "inventory", "status", "expired"),
+ "claimed": _("gui", "inventory", "status", "claimed"),
+ },
+ "starts": _("gui", "inventory", "starts"),
+ "ends": _("gui", "inventory", "ends"),
+ "claimed_drops": "claimed",
},
"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_to_watch": "Games to Watch",
"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",
+ "no_games_selected": "No games selected. Check games below to add them.",
+ "no_games_match": "No games match your search.",
+ "all_games_selected": "All games are selected or no games available.",
"actions": "Actions",
"reload_campaigns": _("gui", "settings", "reload"),
"connection_quality": "Connection Quality:",
@@ -236,15 +273,21 @@ async def get_translations():
"about": "About Twitch Drops Miner",
"about_text": "This application automatically mines timed Twitch drops without downloading stream data.",
"how_to_use": "How to Use",
+ "how_it_works": _("gui", "help", "how_it_works"),
+ "how_it_works_text": _("gui", "help", "how_it_works_text"),
+ "getting_started": _("gui", "help", "getting_started"),
+ "getting_started_text": _("gui", "help", "getting_started_text"),
"features": "Features",
"important_notes": "Important Notes",
+ "useful_links": _("gui", "help", "links", "name"),
+ "github_repo": "GitHub Repository",
},
"header": {
"title": "Twitch Drops Miner",
"language": "Language:",
"initializing": "Initializing...",
- "connected": "Connected",
- "disconnected": "Disconnected",
+ "connected": _("gui", "websocket", "connected"),
+ "disconnected": _("gui", "websocket", "disconnected"),
"auto_mode": "AUTO",
"manual_mode": "MANUAL",
},
diff --git a/web/static/app.js b/web/static/app.js
index b3f900f..3bdb60d 100644
--- a/web/static/app.js
+++ b/web/static/app.js
@@ -244,9 +244,11 @@ function renderChannels() {
const container = document.getElementById('channels-list');
container.innerHTML = '';
+ const t = state.translations;
const channels = Object.values(state.channels);
if (channels.length === 0) {
- container.innerHTML = '
No channels tracked yet...
';
+ const emptyMsg = t.channels?.no_channels || 'No channels tracked yet...';
+ container.innerHTML = `${emptyMsg}
`;
return;
}
@@ -263,7 +265,8 @@ function renderChannels() {
});
if (filteredChannels.length === 0) {
- container.innerHTML = 'No channels found for selected games...
';
+ const emptyMsg = t.channels?.no_channels_for_games || 'No channels found for selected games...';
+ container.innerHTML = `${emptyMsg}
`;
return;
}
@@ -314,11 +317,16 @@ function renderChannels() {
const channelCount = group.channels.length;
const totalViewers = group.channels.reduce((sum, ch) => sum + (ch.viewers || 0), 0);
+ const channelText = channelCount === 1
+ ? (t.channels?.channel_count || 'channel')
+ : (t.channels?.channel_count_plural || 'channels');
+ const viewersText = t.channels?.viewers || 'viewers';
+
gameHeader.innerHTML = `
${iconHtml}
${group.name}
-
${channelCount} channel${channelCount !== 1 ? 's' : ''} • ${totalViewers.toLocaleString()} viewers
+
${channelCount} ${channelText} • ${totalViewers.toLocaleString()} ${viewersText}
`;
@@ -458,9 +466,11 @@ function renderInventory() {
const container = document.getElementById('inventory-grid');
container.innerHTML = '';
+ const t = state.translations;
const campaigns = Object.values(state.campaigns);
if (campaigns.length === 0) {
- container.innerHTML = 'No campaigns loaded yet...
';
+ const emptyMsg = t.inventory?.no_campaigns || 'No campaigns loaded yet...';
+ container.innerHTML = `${emptyMsg}
`;
return;
}
@@ -472,21 +482,22 @@ function renderInventory() {
let statusText = '';
if (campaign.active) {
statusClass = 'active';
- statusText = 'Active';
+ statusText = t.inventory?.status?.active || 'Active';
} else if (campaign.upcoming) {
statusClass = 'upcoming';
- statusText = 'Upcoming';
+ statusText = t.inventory?.status?.upcoming || 'Upcoming';
} else if (campaign.expired) {
statusClass = 'expired';
- statusText = 'Expired';
+ statusText = t.inventory?.status?.expired || 'Expired';
}
+ const claimedText = t.inventory?.status?.claimed || 'Claimed';
const dropsHtml = campaign.drops.map(drop => `
${drop.name}
${drop.rewards}
${drop.current_minutes} / ${drop.required_minutes} minutes (${Math.round(drop.progress * 100)}%)
- ${drop.is_claimed ? '
✓ Claimed
' : ''}
+ ${drop.is_claimed ? `
✓ ${claimedText}
` : ''}
`).join('');
@@ -495,6 +506,7 @@ function renderInventory() {
? `${campaign.name} 🔗`
: `${campaign.name}
`;
+ const claimedCountText = t.inventory?.claimed_drops || 'claimed';
card.innerHTML = `
${statusText}
- ${campaign.claimed_drops} / ${campaign.total_drops} claimed
+ ${campaign.claimed_drops} / ${campaign.total_drops} ${claimedCountText}
${dropsHtml}
@@ -636,10 +648,12 @@ function renderSelectedGames(games) {
const container = document.getElementById('selected-games-list');
if (!container) return;
+ const t = state.translations;
container.innerHTML = '';
if (games.length === 0) {
- container.innerHTML = '
No games selected. Check games below to add them.
';
+ const emptyMsg = t.settings?.no_games_selected || 'No games selected. Check games below to add them.';
+ container.innerHTML = `
${emptyMsg}
`;
return;
}
@@ -669,13 +683,16 @@ function renderAvailableGames(games, filterText) {
const container = document.getElementById('available-games-list');
if (!container) return;
+ const t = state.translations;
container.innerHTML = '';
if (games.length === 0) {
if (filterText) {
- container.innerHTML = '
No games match your search.
';
+ const emptyMsg = t.settings?.no_games_match || 'No games match your search.';
+ container.innerHTML = `
${emptyMsg}
`;
} else {
- container.innerHTML = '
All games are selected or no games available.
';
+ const emptyMsg = t.settings?.all_games_selected || 'All games are selected or no games available.';
+ container.innerHTML = `
${emptyMsg}
`;
}
return;
}
@@ -956,26 +973,74 @@ function applyTranslations(t) {
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
+ // Update Main tab - Login section
const mainTab = document.getElementById('main-tab');
if (mainTab && t.login) {
const loginHeader = mainTab.querySelector('.login-panel h2');
if (loginHeader) loginHeader.textContent = t.login.title;
+
+ // Update login form placeholders
+ const usernameInput = document.getElementById('username');
+ if (usernameInput) usernameInput.placeholder = t.login.username;
+
+ const passwordInput = document.getElementById('password');
+ if (passwordInput) passwordInput.placeholder = t.login.password;
+
+ const twofaInput = document.getElementById('2fa-token');
+ if (twofaInput) twofaInput.placeholder = t.login.twofa_code;
+
+ const loginButton = document.getElementById('login-button');
+ if (loginButton) loginButton.textContent = t.login.button;
+
+ // Update OAuth display text
+ const oauthDisplay = document.getElementById('oauth-code-display');
+ if (oauthDisplay) {
+ const oauthP = oauthDisplay.querySelector('p');
+ if (oauthP) {
+ const link = oauthP.querySelector('a');
+ if (link) {
+ oauthP.textContent = t.login.oauth_prompt + ' ';
+ link.textContent = t.login.oauth_activate;
+ oauthP.appendChild(link);
+ }
+ }
+
+ const oauthConfirmBtn = document.getElementById('oauth-confirm');
+ if (oauthConfirmBtn) oauthConfirmBtn.textContent = t.login.oauth_confirm;
+ }
}
+
+ // Update Progress section
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;
+
+ const exitManualBtn = document.getElementById('exit-manual-btn');
+ if (exitManualBtn) exitManualBtn.textContent = t.progress.return_to_auto;
}
+
+ // Update Console section
if (mainTab && t.console) {
const consoleHeader = mainTab.querySelector('.console-panel h2');
if (consoleHeader) consoleHeader.textContent = t.console.title;
}
+
+ // Update Channels section
if (mainTab && t.channels) {
const channelsHeader = mainTab.querySelector('.channels-panel h2');
if (channelsHeader) channelsHeader.textContent = t.channels.title;
+ // Channel list will re-render with translated empty messages
+ renderChannels();
+ }
+
+ // Update Inventory tab
+ const inventoryTab = document.getElementById('inventory-tab');
+ if (inventoryTab && t.inventory) {
+ // Inventory will re-render with translated status and empty messages
+ renderInventory();
}
// Update Settings tab
@@ -988,7 +1053,6 @@ function applyTranslations(t) {
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);
@@ -1029,20 +1093,57 @@ function applyTranslations(t) {
const reloadBtn = document.getElementById('reload-btn');
if (reloadBtn) reloadBtn.textContent = t.settings.reload_campaigns;
+
+ // Re-render games to watch with translated empty messages
+ renderGamesToWatch();
}
// 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
+ const helpContent = helpTab.querySelector('.help-content');
+ if (helpContent) {
+ const headers = helpContent.querySelectorAll('h2, h3');
+ if (headers[0]) headers[0].textContent = t.help.about;
+ if (headers[1]) headers[1].textContent = t.help.how_to_use;
+ if (headers[2]) headers[2].textContent = t.help.features;
+ if (headers[3]) headers[3].textContent = t.help.important_notes;
+
+ // Update help text paragraphs if we have translations
+ const aboutP = helpContent.querySelector('p');
+ if (aboutP && t.help.about_text) {
+ aboutP.textContent = t.help.about_text;
+ }
+
+ // Add how it works section if it exists in translations
+ if (t.help.how_it_works_text) {
+ // Find or create the how it works section
+ let howItWorksH3 = Array.from(helpContent.querySelectorAll('h3')).find(h => h.textContent.includes('How It Works') || h.textContent === t.help.how_it_works);
+ if (!howItWorksH3) {
+ // Insert How It Works section after the first list
+ const firstList = helpContent.querySelector('ol');
+ if (firstList) {
+ howItWorksH3 = document.createElement('h3');
+ howItWorksH3.textContent = t.help.how_it_works;
+ const howItWorksP = document.createElement('p');
+ howItWorksP.textContent = t.help.how_it_works_text;
+ firstList.parentNode.insertBefore(howItWorksP, firstList.nextSibling);
+ firstList.parentNode.insertBefore(howItWorksH3, howItWorksP);
+ }
+ }
+ }
+ }
}
// Update header elements
if (t.header) {
const languageLabel = document.querySelector('.language-selector span');
if (languageLabel) languageLabel.textContent = t.header.language;
+
+ const statusText = document.getElementById('status-text');
+ if (statusText && statusText.textContent === 'Initializing...') {
+ statusText.textContent = t.header.initializing;
+ }
}
}