feat: i18n

This commit is contained in:
Senko-san
2026-06-06 15:23:07 +03:00
parent bbd59cc225
commit e45bcef3a5
21 changed files with 613 additions and 163 deletions
+26 -24
View File
@@ -1,4 +1,5 @@
import { NavLink, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { Icon, type IconName } from '../common/Icon';
import { useAppDispatch } from '../../hooks/useAppDispatch';
import { usePermissions } from '../../hooks/usePermissions';
@@ -9,24 +10,24 @@ import { getActiveInstance } from '../../config/instances';
interface NavDef {
to: string;
label: string;
labelKey: string;
icon: IconName;
end?: boolean;
}
const MAIN_NAV: NavDef[] = [
{ to: '/', label: 'Home', icon: 'house', end: true },
{ to: '/library', label: 'Library', icon: 'vinyl-record' },
{ to: '/search', label: 'Search & download', icon: 'magnifying-glass' },
{ to: '/downloads', label: 'Downloads', icon: 'arrow-circle-down' },
{ to: '/storage', label: 'Storage', icon: 'hard-drives' },
{ to: '/', labelKey: 'nav.home', icon: 'house', end: true },
{ to: '/library', labelKey: 'nav.library', icon: 'vinyl-record' },
{ to: '/search', labelKey: 'nav.search', icon: 'magnifying-glass' },
{ to: '/downloads', labelKey: 'nav.downloads', icon: 'arrow-circle-down' },
{ to: '/storage', labelKey: 'nav.storage', icon: 'hard-drives' },
];
const CONN_CLASS: Record<string, { cls: string; txt: string }> = {
connected: { cls: 'online', txt: 'Connected' },
connecting: { cls: 'syncing', txt: 'Connecting' },
disconnected: { cls: 'offline', txt: 'Offline' },
error: { cls: 'error', txt: 'Unreachable' },
const CONN_KEY: Record<string, { cls: string; txtKey: string }> = {
connected: { cls: 'online', txtKey: 'conn.connected' },
connecting: { cls: 'syncing', txtKey: 'conn.connecting' },
disconnected: { cls: 'offline', txtKey: 'conn.disconnected' },
error: { cls: 'error', txtKey: 'conn.error' },
};
function navClass({ isActive }: { isActive: boolean }) {
@@ -34,6 +35,7 @@ function navClass({ isActive }: { isActive: boolean }) {
}
export function Sidebar() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { user, isAdmin } = usePermissions();
@@ -41,7 +43,7 @@ export function Sidebar() {
const { data: playlists } = useGetPlaylistsQuery();
const instance = getActiveInstance();
const conn = CONN_CLASS[status] ?? CONN_CLASS.connecting;
const conn = CONN_KEY[status] ?? CONN_KEY.connecting;
const online = status === 'connected';
const handleLogout = (e: React.MouseEvent) => {
@@ -59,16 +61,16 @@ export function Sidebar() {
</div>
<div className="sb-sec">
{MAIN_NAV.map(({ to, label, icon, end }) => (
{MAIN_NAV.map(({ to, labelKey, icon, end }) => (
<NavLink key={to} to={to} end={end} className={navClass}>
<Icon name={icon} />
<span>{label}</span>
<span>{t(labelKey)}</span>
</NavLink>
))}
</div>
<div className="sb-sec">
<span className="msk-label">Playlists</span>
<span className="msk-label">{t('nav.playlists')}</span>
{(playlists?.items ?? []).map((pl) => (
<NavLink
key={pl.id}
@@ -85,27 +87,27 @@ export function Sidebar() {
onClick={() => void navigate('/library')}
>
<Icon name="plus" />
<span className="pl-name">New playlist</span>
<span className="pl-name">{t('nav.newPlaylist')}</span>
</button>
</div>
{isAdmin ? (
<div className="sb-sec">
<span className="msk-label">Administration</span>
<span className="msk-label">{t('nav.administration')}</span>
<NavLink to="/admin" className={navClass}>
<Icon name="shield-check" />
<span>Admin</span>
<span>{t('nav.admin')}</span>
</NavLink>
<NavLink to="/settings" className={navClass}>
<Icon name="gear-six" />
<span>Settings</span>
<span>{t('nav.settings')}</span>
</NavLink>
</div>
) : (
<div className="sb-sec">
<NavLink to="/settings" className={navClass}>
<Icon name="gear-six" />
<span>Settings</span>
<span>{t('nav.settings')}</span>
</NavLink>
</div>
)}
@@ -116,10 +118,10 @@ export function Sidebar() {
type="button"
className={`conn ${conn.cls}`}
onClick={() => void navigate('/connect')}
title="Connection — manage instances"
title={t('conn.manage')}
>
<span className="led" />
{conn.txt}
{t(conn.txtKey)}
</button>
{user && (
<button
@@ -133,10 +135,10 @@ export function Sidebar() {
<div className="user-meta">
<div className="nm">{user.username}</div>
<div className="rl">
{user.role} · {online ? 'online' : 'offline'}
{user.role} · {online ? t('user.online') : t('user.offline')}
</div>
</div>
<span className="uc-action" onClick={handleLogout} title="Sign out">
<span className="uc-action" onClick={handleLogout} title={t('user.signOut')}>
<Icon name="sign-out" />
</span>
</button>