From d89b9f2e82830e29472a8fad3739f0f82b199d77 Mon Sep 17 00:00:00 2001 From: Andre <200930841+Andre-Bzk@users.noreply.github.com> Date: Sat, 16 May 2026 17:43:42 +0200 Subject: [PATCH] feat: add language selector to login screen, default to English Without a stored preference, the app always started in German even for non-German speakers who had never seen a settings screen. English as the fallback and a visible selector on the login screen mean the UI is navigable before any account connection. --- src/renderer/src/context/SettingsContext.tsx | 2 +- src/renderer/src/i18n/de.ts | 1 + src/renderer/src/i18n/en.ts | 1 + src/renderer/src/i18n/useT.ts | 4 +- src/renderer/src/screens/LoginScreen.tsx | 43 ++++++++++++++-- src/renderer/src/styles/global.css | 52 ++++++++++++++++++++ 6 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/context/SettingsContext.tsx b/src/renderer/src/context/SettingsContext.tsx index 32fbc14..8bbe11d 100644 --- a/src/renderer/src/context/SettingsContext.tsx +++ b/src/renderer/src/context/SettingsContext.tsx @@ -25,7 +25,7 @@ const DEFAULTS: AppSettings = { badgeGap: BADGE_GAP_DEFAULT, hlsCacheEnabled: false, fileLoggingEnabled: false, - language: 'de' + language: 'en' } export { SIDEBAR_MIN, SIDEBAR_MAX, SIDEBAR_DEFAULT, BADGE_GAP_MIN, BADGE_GAP_MAX, BADGE_GAP_DEFAULT } diff --git a/src/renderer/src/i18n/de.ts b/src/renderer/src/i18n/de.ts index 4d7d81b..da65564 100644 --- a/src/renderer/src/i18n/de.ts +++ b/src/renderer/src/i18n/de.ts @@ -48,6 +48,7 @@ export const de = { 'login.expiresIn': 'Code läuft in {time} ab.', 'login.error.accessDenied': 'Zugriff abgelehnt.', 'login.error.expired': 'Code abgelaufen — bitte erneut versuchen.', + 'login.languageSelect': 'Sprachauswahl', // ─── Following ────────────────────────────────────────────────────────── 'following.loading': 'Lade Kanäle…', diff --git a/src/renderer/src/i18n/en.ts b/src/renderer/src/i18n/en.ts index 7ed755c..db398cb 100644 --- a/src/renderer/src/i18n/en.ts +++ b/src/renderer/src/i18n/en.ts @@ -49,6 +49,7 @@ export const en: Messages = { 'login.expiresIn': 'Code expires in {time}.', 'login.error.accessDenied': 'Access denied.', 'login.error.expired': 'Code expired — please try again.', + 'login.languageSelect': 'Language', // ─── Following ────────────────────────────────────────────────────────── 'following.loading': 'Loading channels…', diff --git a/src/renderer/src/i18n/useT.ts b/src/renderer/src/i18n/useT.ts index ac99ba0..16123b3 100644 --- a/src/renderer/src/i18n/useT.ts +++ b/src/renderer/src/i18n/useT.ts @@ -20,7 +20,7 @@ function interpolate(raw: string, vars?: Record): strin export function useT(): TranslateFn { const { settings } = useSettings() - const table = TABLES[settings.language] ?? de + const table = TABLES[settings.language] ?? en return useCallback((key: MessageKey, vars?: Record): string => interpolate(table[key], vars), [table]) } @@ -36,7 +36,7 @@ export function getActiveLanguage(): Language { if (parsed.language === 'en' || parsed.language === 'de') return parsed.language } } catch { /* ignore */ } - return 'de' + return 'en' } export function tStatic(key: MessageKey, vars?: Record): string { diff --git a/src/renderer/src/screens/LoginScreen.tsx b/src/renderer/src/screens/LoginScreen.tsx index 961e2d3..f9661a0 100644 --- a/src/renderer/src/screens/LoginScreen.tsx +++ b/src/renderer/src/screens/LoginScreen.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react' import QRCode from 'qrcode' import type { DeviceFlowStartInfo } from '../types/t4sd' import { useT } from '../i18n/useT' +import { useSettings } from '../context/SettingsContext' interface Props { onAuthorized: () => void @@ -16,8 +17,11 @@ type Phase = export default function LoginScreen({ onAuthorized }: Props): JSX.Element { const t = useT() + const { settings, setLanguage } = useSettings() const [phase, setPhase] = useState({ kind: 'idle' }) const startBtnRef = useRef(null) + const langEnRef = useRef(null) + const langDeRef = useRef(null) // On mount: check whether the client ID is configured. useEffect(() => { @@ -29,9 +33,21 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element { useEffect(() => { const onKey = (e: KeyboardEvent): void => { - if (e.key !== 'Enter') return - if (phase.kind === 'idle' || phase.kind === 'error') void start() - else if (phase.kind === 'awaiting') void cancel() + const active = document.activeElement + if (e.key === 'ArrowRight' && active === langEnRef.current) { + langDeRef.current?.focus() + } else if (e.key === 'ArrowLeft' && active === langDeRef.current) { + langEnRef.current?.focus() + } else if (e.key === 'ArrowDown' && active === startBtnRef.current) { + langEnRef.current?.focus() + } else if (e.key === 'ArrowUp' && (active === langEnRef.current || active === langDeRef.current)) { + startBtnRef.current?.focus() + } else if (e.key === 'Enter') { + if (active === langEnRef.current) { setLanguage('en'); return } + if (active === langDeRef.current) { setLanguage('de'); return } + if (phase.kind === 'idle' || phase.kind === 'error') void start() + else if (phase.kind === 'awaiting') void cancel() + } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) @@ -134,6 +150,27 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element { > {phase.kind === 'starting' ? t('login.starting') : t('login.connect')} +
+ {t('login.languageSelect')} +
+ + +
+
) } diff --git a/src/renderer/src/styles/global.css b/src/renderer/src/styles/global.css index 305f9ff..e8a1c12 100644 --- a/src/renderer/src/styles/global.css +++ b/src/renderer/src/styles/global.css @@ -149,6 +149,58 @@ body, color: #ff6868; } +.login__lang-select { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + margin-top: 4px; +} + +.login__lang-label { + font-size: 13px; + color: var(--color-fg-dim); + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.login__lang-buttons { + display: flex; + gap: 12px; +} + +.login__lang-btn { + width: 80px; + height: 56px; + font-size: 36px; + line-height: 1; + border-radius: 10px; + border: 2px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: border-color 0.15s, background 0.15s; + outline: none; +} + +.login__lang-btn:hover, +.login__lang-btn:focus { + border-color: rgba(255, 255, 255, 0.4); + background: rgba(255, 255, 255, 0.08); +} + +.login__lang-btn--active { + border-color: var(--color-accent); + background: rgba(145, 71, 255, 0.15); +} + +.login__lang-btn--active:focus { + border-color: var(--color-accent); + background: rgba(145, 71, 255, 0.25); +} + /* ── App Layout (post-auth) ───────────────────────── */ .app-layout { display: grid;