Use real flag icons in language picker and polish locale UX (v4.0.3)

Replace unreliable emoji flags in the native select with a custom language picker that renders real CSS flag icons, improve retry button naming/tooltips, and keep quick/full e2e coverage green after the UI change.
This commit is contained in:
xRangerDE 2026-02-16 12:06:13 +01:00
parent e261ca5281
commit a29c252606
10 changed files with 106 additions and 17 deletions

View File

@ -1,12 +1,12 @@
{
"name": "twitch-vod-manager",
"version": "4.0.2",
"version": "4.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "twitch-vod-manager",
"version": "4.0.2",
"version": "4.0.3",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{
"name": "twitch-vod-manager",
"version": "4.0.2",
"version": "4.0.3",
"description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js",
"author": "xRangerDE",

View File

@ -201,7 +201,9 @@ async function run() {
const deState = {
nav: (document.getElementById('navSettingsText')?.textContent || '').trim(),
retry: (document.getElementById('btnRetryFailed')?.textContent || '').trim(),
deFlag: (document.getElementById('languageDeText')?.textContent || '').trim()
deText: (document.getElementById('languageDeText')?.textContent || '').trim(),
deIcon: !!document.querySelector('#langOptionDe .flag-icon.flag-de'),
deActive: !!document.getElementById('langOptionDe')?.classList.contains('active')
};
lang.value = 'en';
@ -210,14 +212,18 @@ async function run() {
const enState = {
nav: (document.getElementById('navSettingsText')?.textContent || '').trim(),
retry: (document.getElementById('btnRetryFailed')?.textContent || '').trim(),
enFlag: (document.getElementById('languageEnText')?.textContent || '').trim()
enText: (document.getElementById('languageEnText')?.textContent || '').trim(),
enIcon: !!document.querySelector('#langOptionEn .flag-icon.flag-en'),
enActive: !!document.getElementById('langOptionEn')?.classList.contains('active')
};
checks.language = { deState, enState };
assert(deState.nav.includes('Einstellungen'), 'German language switch failed');
assert(enState.nav.includes('Settings'), 'English language switch failed');
assert(deState.deFlag.includes('🇩🇪'), 'German flag missing');
assert(enState.enFlag.includes('🇺🇸'), 'English flag missing');
assert(deState.deIcon, 'German flag icon missing');
assert(enState.enIcon, 'English flag icon missing');
assert(deState.deActive, 'German language button did not activate');
assert(enState.enActive, 'English language button did not activate');
await window.api.saveConfig({ client_id: '', client_secret: '', download_path: tmpDir });
window.showTab('vods');

View File

@ -300,9 +300,19 @@
</div>
<div class="form-group">
<label id="languageLabel">Sprache</label>
<select id="languageSelect" onchange="changeLanguage(this.value)">
<option value="de" id="languageDeText">🇩🇪 Deutsch</option>
<option value="en" id="languageEnText">🇺🇸 English</option>
<div class="language-picker" id="languagePicker">
<button type="button" class="lang-option" id="langOptionDe" onclick="selectLanguageOption('de')" aria-pressed="false">
<span class="flag-icon flag-de" aria-hidden="true"></span>
<span id="languageDeText">Deutsch</span>
</button>
<button type="button" class="lang-option" id="langOptionEn" onclick="selectLanguageOption('en')" aria-pressed="false">
<span class="flag-icon flag-en" aria-hidden="true"></span>
<span id="languageEnText">English</span>
</button>
</div>
<select id="languageSelect" onchange="changeLanguage(this.value)" style="display:none">
<option value="de">de</option>
<option value="en">en</option>
</select>
</div>
</div>
@ -345,7 +355,7 @@
<div class="settings-card">
<h3 id="updateTitle">Updates</h3>
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.2</p>
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.3</p>
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
</div>
@ -377,7 +387,7 @@
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Nicht verbunden</span>
</div>
<span id="versionText">v4.0.2</span>
<span id="versionText">v4.0.3</span>
</div>
</main>
</div>

View File

@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
// ==========================================
// CONFIG & CONSTANTS
// ==========================================
const APP_VERSION = '4.0.2';
const APP_VERSION = '4.0.3';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths

View File

@ -27,8 +27,8 @@ const UI_TEXT_DE = {
designTitle: 'Design',
themeLabel: 'Theme',
languageLabel: 'Sprache',
languageDe: '🇩🇪 Deutsch',
languageEn: '🇺🇸 Englisch',
languageDe: 'Deutsch',
languageEn: 'Englisch',
apiTitle: 'Twitch API',
clientIdLabel: 'Client ID',
clientSecretLabel: 'Client Secret',

View File

@ -27,8 +27,8 @@ const UI_TEXT_EN = {
designTitle: 'Design',
themeLabel: 'Theme',
languageLabel: 'Language',
languageDe: '🇩🇪 German',
languageEn: '🇺🇸 English',
languageDe: 'German',
languageEn: 'English',
apiTitle: 'Twitch API',
clientIdLabel: 'Client ID',
clientSecretLabel: 'Client Secret',

View File

@ -22,6 +22,7 @@ function updateStatus(text: string, connected: boolean): void {
function changeLanguage(lang: string): void {
const normalized = setLanguage(lang);
byId<HTMLSelectElement>('languageSelect').value = normalized;
updateLanguagePicker(normalized);
config.language = normalized;
void window.api.saveConfig({ language: normalized });
@ -40,6 +41,21 @@ function changeLanguage(lang: string): void {
}
}
function updateLanguagePicker(lang: string): void {
const de = byId<HTMLButtonElement>('langOptionDe');
const en = byId<HTMLButtonElement>('langOptionEn');
const isDe = lang === 'de';
de.classList.toggle('active', isDe);
en.classList.toggle('active', !isDe);
de.setAttribute('aria-pressed', String(isDe));
en.setAttribute('aria-pressed', String(!isDe));
}
function selectLanguageOption(lang: string): void {
changeLanguage(lang);
}
function renderPreflightResult(result: PreflightResult): void {
const entries = [
[UI_TEXT.static.preflightInternet, result.checks.internet],

View File

@ -15,6 +15,7 @@ async function init(): Promise<void> {
byId<HTMLInputElement>('downloadPath').value = config.download_path ?? '';
byId<HTMLSelectElement>('themeSelect').value = config.theme ?? 'twitch';
byId<HTMLSelectElement>('languageSelect').value = config.language ?? 'en';
updateLanguagePicker(config.language ?? 'en');
byId<HTMLSelectElement>('downloadMode').value = config.download_mode ?? 'full';
byId<HTMLInputElement>('partMinutes').value = String(config.part_minutes ?? 120);

View File

@ -606,6 +606,62 @@ body {
border-color: var(--accent);
}
.language-picker {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.lang-option {
display: flex;
align-items: center;
gap: 8px;
border: 1px solid rgba(255,255,255,0.14);
border-radius: 6px;
background: var(--bg-main);
color: var(--text);
padding: 9px 10px;
cursor: pointer;
font-size: 13px;
}
.lang-option:hover {
border-color: rgba(255,255,255,0.26);
}
.lang-option.active {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(145, 70, 255, 0.2);
}
.flag-icon {
width: 16px;
height: 12px;
border-radius: 2px;
border: 1px solid rgba(0,0,0,0.35);
flex-shrink: 0;
position: relative;
overflow: hidden;
}
.flag-de {
background: linear-gradient(to bottom, #111 0 33.33%, #dd0000 33.33% 66.66%, #ffce00 66.66% 100%);
}
.flag-en {
background: repeating-linear-gradient(to bottom, #b22234 0 7.7%, #ffffff 7.7% 15.4%);
}
.flag-en::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 45%;
height: 56%;
background: #3c3b6e;
}
.form-row {
display: flex;
gap: 10px;