mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
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.
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -20,7 +20,7 @@ function interpolate(raw: string, vars?: Record<string, string | number>): 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, string | number>): 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, string | number>): string {
|
||||
|
||||
@@ -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<Phase>({ kind: 'idle' })
|
||||
const startBtnRef = useRef<HTMLButtonElement>(null)
|
||||
const langEnRef = useRef<HTMLButtonElement>(null)
|
||||
const langDeRef = useRef<HTMLButtonElement>(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')}
|
||||
</button>
|
||||
<div className="login__lang-select">
|
||||
<span className="login__lang-label">{t('login.languageSelect')}</span>
|
||||
<div className="login__lang-buttons">
|
||||
<button
|
||||
ref={langEnRef}
|
||||
className={`login__lang-btn${settings.language === 'en' ? ' login__lang-btn--active' : ''}`}
|
||||
onClick={() => setLanguage('en')}
|
||||
aria-label="English"
|
||||
>
|
||||
🇺🇸
|
||||
</button>
|
||||
<button
|
||||
ref={langDeRef}
|
||||
className={`login__lang-btn${settings.language === 'de' ? ' login__lang-btn--active' : ''}`}
|
||||
onClick={() => setLanguage('de')}
|
||||
aria-label="Deutsch"
|
||||
>
|
||||
🇩🇪
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user