feat(login): add language picker on the login page (#328)

The language selector previously lived only in Settings, which is
behind login. New users on a non-English system had no way to switch
to their language before connecting to a server.

Add a compact CustomSelect in the top-right of the login card. Reuses
the existing settings.languageXx labels and i18n.changeLanguage flow
(persists to localStorage as psysonic_language). English remains the
default for first launches — already the case in i18n.ts:12.

Styled to be visually quieter than the Settings variant (transparent
background, smaller font) so it doesn't pull focus from the logo and
form.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frank Stellmacher
2026-04-26 21:58:06 +02:00
committed by GitHub
parent d67db230ad
commit 9fe81ee6f6
2 changed files with 47 additions and 1 deletions
+20 -1
View File
@@ -1,9 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { Wifi, WifiOff, Eye, EyeOff, Server } from 'lucide-react'; import { Wifi, WifiOff, Eye, EyeOff, Server, Globe } from 'lucide-react';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic'; import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import CustomSelect from '../components/CustomSelect';
import { import {
decodeServerMagicString, decodeServerMagicString,
DECODED_PASSWORD_VISUAL_MASK, DECODED_PASSWORD_VISUAL_MASK,
@@ -168,6 +170,23 @@ export default function Login() {
<div className="login-page"> <div className="login-page">
<div className="login-bg" aria-hidden="true" /> <div className="login-bg" aria-hidden="true" />
<div className="login-card animate-fade-in"> <div className="login-card animate-fade-in">
<div className="login-lang-picker" aria-label={t('settings.language')}>
<Globe size={14} aria-hidden="true" />
<CustomSelect
value={i18n.language}
onChange={v => i18n.changeLanguage(v)}
options={[
{ value: 'en', label: t('settings.languageEn') },
{ value: 'de', label: t('settings.languageDe') },
{ value: 'es', label: t('settings.languageEs') },
{ value: 'fr', label: t('settings.languageFr') },
{ value: 'nl', label: t('settings.languageNl') },
{ value: 'nb', label: t('settings.languageNb') },
{ value: 'ru', label: t('settings.languageRu') },
{ value: 'zh', label: t('settings.languageZh') },
]}
/>
</div>
<div className="login-logo"> <div className="login-logo">
<PsysonicLogo /> <PsysonicLogo />
</div> </div>
+27
View File
@@ -4134,6 +4134,33 @@
margin-bottom: var(--space-5); margin-bottom: var(--space-5);
} }
.login-lang-picker {
position: absolute;
top: var(--space-3);
right: var(--space-3);
display: flex;
align-items: center;
gap: var(--space-1);
color: var(--text-muted);
z-index: 2;
}
.login-lang-picker > button {
font-size: 12px;
padding: 4px 8px;
background: transparent;
border: 1px solid transparent;
color: var(--text-muted);
width: auto;
min-width: 110px;
}
.login-lang-picker > button:hover {
background: var(--bg-hover);
border-color: var(--border-subtle);
color: var(--text-primary);
}
.login-title { .login-title {
text-align: center; text-align: center;
font-family: var(--font-display); font-family: var(--font-display);