feat(i18n): add translations to main UI components

- Sidebar: navigation labels, playlist section, sort options
- SearchView: search placeholder, tabs, results messages
- HomeView: section titles, loading states, empty states
- QueuePanel: headers, search, empty state, actions
- NowPlayingBar: player controls, tooltips, cast buttons
- FocusMode: close button, lyrics loading
- HomeSettingsModal: all settings labels and descriptions

Added new translation keys to en.json and es.json locales.
This commit is contained in:
vicrodh committed 2026-01-14 10:12:38 -06:00
1 parent 5c869eeae9
commit 3cfe3c1333
9 files changed
+209 -102

No files matched your search

+3 -2
View File
@@ -2,6 +2,7 @@
import { SkipBack, Play, Pause, SkipForward, ChevronDown } from 'lucide-svelte';
import LyricsLines from './lyrics/LyricsLines.svelte';
import { startActiveLineUpdates, stopActiveLineUpdates } from '$lib/stores/lyricsStore';
import { t } from '$lib/i18n';
interface LyricsLine {
text: string;
@@ -135,7 +136,7 @@
</div>
<!-- Close Button -->
<button class="close-btn" class:visible={showControls} onclick={onClose} title="Close (Esc)">
<button class="close-btn" class:visible={showControls} onclick={onClose} title={$t('actions.close') + ' (Esc)'}>
<ChevronDown size={28} />
</button>
@@ -154,7 +155,7 @@
{#if lyricsLoading}
<div class="lyrics-state">
<div class="spinner"></div>
<span>Loading lyrics...</span>
<span>{$t('player.fetchingLyrics')}</span>
</div>
{:else if lyricsError}
<div class="lyrics-state">
+13 -12
View File
@@ -13,6 +13,7 @@
resetToDefaults,
type HomeSettings
} from '$lib/stores/homeSettingsStore';
import { t } from '$lib/i18n';
interface Props {
isOpen: boolean;
@@ -58,16 +59,16 @@
}
</script>
<Modal {isOpen} {onClose} title="Customize Home">
<Modal {isOpen} {onClose} title={$t('home.customizeHome')}>
{#snippet children()}
<!-- Greeting Settings -->
<div class="settings-section">
<div class="section-title">Greeting</div>
<div class="section-title">{$t('homeSettings.greeting')}</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Show greeting</span>
<span class="setting-desc">Display personalized greeting at top</span>
<span class="setting-label">{$t('homeSettings.showGreeting')}</span>
<span class="setting-desc">{$t('homeSettings.showGreetingDesc')}</span>
</div>
<label class="toggle">
<input
@@ -82,13 +83,13 @@
{#if settings.greeting.enabled}
<div class="setting-row greeting-input-row">
<div class="setting-info">
<span class="setting-label">Custom greeting</span>
<span class="setting-desc">Use {'{name}'} for your name. Leave empty for time-based.</span>
<span class="setting-label">{$t('homeSettings.customGreeting')}</span>
<span class="setting-desc">{$t('homeSettings.customGreetingDesc')}</span>
</div>
<input
type="text"
class="greeting-input"
placeholder="Good vibes, {name}!"
placeholder={$t('homeSettings.customGreetingPlaceholder')}
bind:value={customGreetingInput}
onblur={handleCustomGreetingChange}
onkeydown={(e) => e.key === 'Enter' && handleCustomGreetingChange()}
@@ -99,8 +100,8 @@
<!-- Sections Order -->
<div class="settings-section">
<div class="section-title">Sections</div>
<p class="section-desc">Reorder with arrows. Toggle visibility with switch.</p>
<div class="section-title">{$t('homeSettings.sections')}</div>
<p class="section-desc">{$t('homeSettings.sectionsDesc')}</p>
<div class="sections-list">
{#each settings.sections as section, index (section.id)}
@@ -110,7 +111,7 @@
class="order-btn"
onclick={() => handleMoveUp(section.id)}
disabled={index === 0}
title="Move up"
title={$t('homeSettings.moveUp')}
>
<ChevronUp size={16} />
</button>
@@ -118,7 +119,7 @@
class="order-btn"
onclick={() => handleMoveDown(section.id)}
disabled={index === settings.sections.length - 1}
title="Move down"
title={$t('homeSettings.moveDown')}
>
<ChevronDown size={16} />
</button>
@@ -150,7 +151,7 @@
{#snippet footer()}
<button class="reset-btn" onclick={handleReset}>
<RotateCcw size={14} />
Reset to defaults
{$t('homeSettings.resetToDefaults')}
</button>
{/snippet}
</Modal>
+17 -16
View File
@@ -20,6 +20,7 @@
} from 'lucide-svelte';
import QualityBadge from './QualityBadge.svelte';
import AudioOutputBadges from './AudioOutputBadges.svelte';
import { t } from '$lib/i18n';
interface Props {
artwork?: string;
@@ -186,16 +187,16 @@
class="control-btn"
class:active={isShuffle}
onclick={onToggleShuffle}
title="Shuffle"
title={$t('player.shuffle')}
>
<Shuffle size={16} />
</button>
<button class="control-btn" onclick={onSkipBack} title="Previous">
<button class="control-btn" onclick={onSkipBack} title={$t('player.previous')}>
<SkipBack size={18} />
</button>
<button class="control-btn play-btn" onclick={onTogglePlay} title={isPlaying ? 'Pause' : 'Play'}>
<button class="control-btn play-btn" onclick={onTogglePlay} title={isPlaying ? $t('player.pause') : $t('player.play')}>
{#if isPlaying}
<Pause size={20} />
{:else}
@@ -203,7 +204,7 @@
{/if}
</button>
<button class="control-btn" onclick={onSkipForward} title="Next">
<button class="control-btn" onclick={onSkipForward} title={$t('player.next')}>
<SkipForward size={18} />
</button>
@@ -211,7 +212,7 @@
class="control-btn"
class:active={repeatMode !== 'off'}
onclick={onToggleRepeat}
title={repeatMode === 'off' ? 'Repeat' : repeatMode === 'all' ? 'Repeat All' : 'Repeat One'}
title={repeatMode === 'off' ? $t('player.repeat') : repeatMode === 'all' ? $t('player.repeatAll') : $t('player.repeatOne')}
>
{#if repeatMode === 'one'}
<Repeat1 size={16} />
@@ -220,7 +221,7 @@
{/if}
</button>
<button class="control-btn" title="Add to Playlist">
<button class="control-btn" title={$t('actions.addToPlaylist')}>
<Plus size={16} />
</button>
@@ -228,7 +229,7 @@
class="control-btn"
class:active={isFavorite}
onclick={onToggleFavorite}
title={isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}
title={isFavorite ? $t('actions.removeFromFavorites') : $t('actions.addToFavorites')}
>
<Heart size={16} fill={isFavorite ? 'currentColor' : 'none'} />
</button>
@@ -262,7 +263,7 @@
<span class="song-title" title={trackTitle}>{trackTitle}</span>
<div class="song-meta">
{#if artist}
<button class="meta-link" onclick={onArtistClick} title="Go to Artist">
<button class="meta-link" onclick={onArtistClick} title={$t('actions.goToArtist')}>
{artist}
</button>
{/if}
@@ -270,7 +271,7 @@
<span class="meta-separator">·</span>
{/if}
{#if album}
<button class="meta-link" onclick={onAlbumClick} title="Go to Album">
<button class="meta-link" onclick={onAlbumClick} title={$t('actions.goToAlbum')}>
{album}
</button>
{/if}
@@ -286,7 +287,7 @@
</div>
{:else}
<div class="empty-state">
<span>No track playing</span>
<span>{$t('player.noTrackPlaying')}</span>
</div>
{/if}
</div>
@@ -297,7 +298,7 @@
class="control-btn"
class:cast-active={isCastConnected}
onclick={onCast}
title={isCastConnected ? 'Casting - Click to manage' : 'Cast to device'}
title={isCastConnected ? $t('player.castingManage') : $t('player.castToDevice')}
>
<Cast size={16} />
</button>
@@ -306,20 +307,20 @@
class="control-btn"
class:active={lyricsActive}
onclick={onToggleLyrics}
title="Lyrics"
title={$t('player.lyrics')}
>
<Mic2 size={16} />
</button>
<button class="control-btn" onclick={onOpenQueue} title="Queue">
<button class="control-btn" onclick={onOpenQueue} title={$t('player.queue')}>
<ListMusic size={16} />
</button>
<button class="control-btn" onclick={onOpenMiniPlayer} title="Mini Player">
<button class="control-btn" onclick={onOpenMiniPlayer} title={$t('player.miniPlayer')}>
<PictureInPicture2 size={16} />
</button>
<button class="control-btn" onclick={onOpenFullScreen} title="Full Screen">
<button class="control-btn" onclick={onOpenFullScreen} title={$t('player.fullScreen')}>
<Maximize2 size={16} />
</button>
@@ -328,7 +329,7 @@
<button
class="control-btn volume-btn"
onclick={() => onVolumeChange?.(volume === 0 ? 70 : 0)}
title={volume === 0 ? 'Unmute' : 'Mute'}
title={volume === 0 ? $t('player.unmute') : $t('player.mute')}
>
{#if volume === 0}
<VolumeX size={16} />
+10 -9
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { X, GripVertical, Play, Search } from 'lucide-svelte';
import GlassSurface from './glass/GlassSurface.svelte';
import { t } from '$lib/i18n';
interface QueueTrack {
id: string;
@@ -114,7 +115,7 @@
<GlassSurface rootClassName="queue-panel" enableRipple={false} enableDistortion={false}>
<!-- Header -->
<div class="header">
<h2>Queue</h2>
<h2>{$t('player.queue')}</h2>
<button class="close-btn" onclick={onClose}>
<X size={20} />
</button>
@@ -131,7 +132,7 @@
<!-- Now Playing -->
{#if currentTrack}
<div class="section now-playing-section">
<div class="section-header">Now Playing</div>
<div class="section-header">{$t('player.nowPlaying')}</div>
<div class="now-playing-card">
<img src={currentTrack.artwork} alt={currentTrack.title} />
<div class="track-info">
@@ -148,7 +149,7 @@
<Search size={14} class="search-icon" />
<input
type="text"
placeholder="Search in queue..."
placeholder={$t('player.searchQueue')}
bind:value={searchQuery}
class="search-input"
/>
@@ -164,7 +165,7 @@
{#if upcomingTracks.length > 0}
<div class="section next-up-section">
<div class="section-header">
Next Up ({filteredTracks.length}{searchQuery ? ` / ${upcomingTracks.length}` : ''})
{$t('player.upNext')} ({filteredTracks.length}{searchQuery ? ` / ${upcomingTracks.length}` : ''})
</div>
<div class="tracks">
{#each filteredTracks as track, index}
@@ -212,7 +213,7 @@
</div>
{/each}
{#if searchQuery && filteredTracks.length === 0}
<div class="no-results">No tracks match "{searchQuery}"</div>
<div class="no-results">{$t('player.noTracksMatch', { values: { query: searchQuery } })}</div>
{/if}
</div>
</div>
@@ -222,8 +223,8 @@
{#if upcomingTracks.length === 0 && !currentTrack}
<div class="empty-state">
<div class="emoji">🎵</div>
<div class="empty-title">Queue is empty</div>
<div class="empty-text">Search for music or browse your library to add tracks.</div>
<div class="empty-title">{$t('player.queueEmpty')}</div>
<div class="empty-text">{$t('player.queueEmptyDescription')}</div>
</div>
{/if}
</div>
@@ -231,8 +232,8 @@
<!-- Footer Actions -->
{#if upcomingTracks.length > 0 || currentTrack}
<div class="footer">
<button class="clear-btn" onclick={onClearQueue}>Clear Queue</button>
<button class="save-btn" onclick={onSaveAsPlaylist}>Save as Playlist</button>
<button class="clear-btn" onclick={onClearQueue}>{$t('player.clearQueue')}</button>
<button class="save-btn" onclick={onSaveAsPlaylist}>{$t('player.saveQueue')}</button>
</div>
{/if}
</GlassSurface>
+22 -21
View File
@@ -5,6 +5,7 @@
import { onMount } from 'svelte';
import NavigationItem from './NavigationItem.svelte';
import UserCard from './UserCard.svelte';
import { t } from '$lib/i18n';
interface Playlist {
id: number;
@@ -374,13 +375,13 @@
}}
>
<Search class="search-icon" size={16} />
<span class="search-placeholder">Search</span>
<span class="search-placeholder">{$t('nav.search')}</span>
</button>
<!-- Navigation Section -->
<nav class="nav-section">
<NavigationItem
label="Home"
label={$t('nav.home')}
active={activeView === 'home'}
onclick={() => handleViewChange('home')}
>
@@ -391,7 +392,7 @@
<!-- Favorites Section (standalone) -->
<nav class="nav-section">
<NavigationItem
label="Favorites"
label={$t('nav.favorites')}
active={activeView === 'favorites'}
onclick={() => handleViewChange('favorites')}
>
@@ -402,20 +403,20 @@
<!-- Playlists Section -->
<div class="section playlists-section">
<div class="playlists-header">
<div class="section-header">Playlists</div>
<div class="section-header">{$t('nav.playlists')}</div>
<div class="header-actions" bind:this={menuRef}>
<button class="icon-btn" onclick={onCreatePlaylist} title="New playlist">
<button class="icon-btn" onclick={onCreatePlaylist} title={$t('playlist.createNew')}>
<Plus size={14} />
</button>
<button
class="icon-btn"
bind:this={triggerRef}
onclick={(e) => { e.stopPropagation(); toggleMenu(); }}
title="Options"
title={$t('actions.more')}
>
<MoreHorizontal size={14} />
</button>
<button class="icon-btn" onclick={() => playlistsCollapsed = !playlistsCollapsed} title={playlistsCollapsed ? "Expand" : "Collapse"}>
<button class="icon-btn" onclick={() => playlistsCollapsed = !playlistsCollapsed} title={playlistsCollapsed ? $t('actions.open') : $t('actions.close')}>
{#if playlistsCollapsed}
<ChevronDown size={14} />
{:else}
@@ -438,7 +439,7 @@
onmouseleave={closeSubmenuDelayed}
>
<ArrowUpDown size={14} />
<span class="menu-label">Sort by</span>
<span class="menu-label">{$t('library.sortBy')}</span>
<ChevronRight size={14} class="submenu-arrow" />
</div>
@@ -452,38 +453,38 @@
onmouseleave={closeSubmenuDelayed}
>
<button class="menu-item" class:selected={sortOption === 'name'} onclick={() => handleSortChange('name')}>
Name (A-Z)
{$t('sort.nameAZ')}
</button>
<button class="menu-item" class:selected={sortOption === 'recent'} onclick={() => handleSortChange('recent')}>
Recent
{$t('sort.recent')}
</button>
<button class="menu-item" class:selected={sortOption === 'tracks'} onclick={() => handleSortChange('tracks')}>
# of Tracks
{$t('sort.trackCount')}
</button>
<button class="menu-item" class:selected={sortOption === 'playcount'} onclick={() => handleSortChange('playcount')}>
Play Count
{$t('sort.playCount')}
</button>
<button class="menu-item" class:selected={sortOption === 'custom'} onclick={() => handleSortChange('custom')}>
Custom
{$t('sort.custom')}
</button>
</div>
{/if}
<button class="menu-item" onclick={() => handleMenuAction(loadUserPlaylists)}>
<RefreshCw size={14} />
<span>Refresh</span>
<span>{$t('actions.refresh')}</span>
</button>
<button class="menu-item" onclick={() => handleMenuAction(onImportPlaylist ?? (() => {}))}>
<Import size={14} />
<span>Import</span>
<span>{$t('playlist.import')}</span>
</button>
<div class="menu-divider"></div>
<button class="menu-item" onclick={() => handleMenuAction(onPlaylistManagerClick ?? (() => {}))}>
<Settings size={14} />
<span>Manage playlists</span>
<span>{$t('playlist.manage')}</span>
</button>
</div>
{/if}
@@ -491,7 +492,7 @@
{#if !playlistsCollapsed}
<div class="playlists-scroll">
{#if playlistsLoading}
<div class="playlists-loading">Loading...</div>
<div class="playlists-loading">{$t('actions.loading')}</div>
{:else if visiblePlaylists.length > 0}
<nav class="playlists-nav">
{#each visiblePlaylists as playlist (playlist.id)}
@@ -507,9 +508,9 @@
{/each}
</nav>
{:else if userPlaylists.length > 0}
<div class="no-playlists">All playlists are hidden</div>
<div class="no-playlists">{$t('playlist.allHidden')}</div>
{:else}
<div class="no-playlists">No playlists yet</div>
<div class="no-playlists">{$t('empty.noPlaylists')}</div>
{/if}
</div>
{/if}
@@ -518,7 +519,7 @@
<!-- Local Library Section -->
<div class="section local-library-section">
<button class="section-header-btn" onclick={() => localLibraryCollapsed = !localLibraryCollapsed}>
<span class="section-header">Local Library</span>
<span class="section-header">{$t('library.title')}</span>
{#if localLibraryCollapsed}
<ChevronDown size={12} />
{:else}
@@ -527,7 +528,7 @@
</button>
{#if !localLibraryCollapsed}
<NavigationItem
label="Browse Library"
label={$t('library.browse')}
active={activeView === 'library'}
onclick={() => handleViewChange('library')}
>
+19 -18
View File
@@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { Music, User, Loader2 } from 'lucide-svelte';
import { t } from '$lib/i18n';
import HorizontalScrollRow from '../HorizontalScrollRow.svelte';
import AlbumCard from '../AlbumCard.svelte';
import TrackRow from '../TrackRow.svelte';
@@ -456,8 +457,8 @@
<div class="loading-icon">
<Loader2 size={36} class="spinner" />
</div>
<h2>Loading your home</h2>
<p>Building recommendations from your listening history.</p>
<h2>{$t('home.loading')}</h2>
<p>{$t('home.loadingDescription')}</p>
</div>
</div>
{/if}
@@ -469,7 +470,7 @@
{:else}
<div></div>
{/if}
<button class="settings-btn" onclick={() => isSettingsModalOpen = true} title="Customize Home">
<button class="settings-btn" onclick={() => isSettingsModalOpen = true} title={$t('home.customizeHome')}>
<img src="/home-gear.svg" alt="Settings" class="settings-icon" />
</button>
</div>
@@ -479,22 +480,22 @@
<div class="state-icon loading">
<Loader2 size={36} class="spinner" />
</div>
<h1>Loading your home</h1>
<p>Building recommendations from your listening history.</p>
<h1>{$t('home.loading')}</h1>
<p>{$t('home.loadingDescription')}</p>
</div>
{:else if error}
<div class="home-state">
<div class="state-icon">
<Music size={36} />
</div>
<h1>Could not load Home</h1>
<h1>{$t('home.loadError')}</h1>
<p>{error}</p>
</div>
{:else if hasContent}
<!-- Render sections in user-defined order -->
{#each visibleSections as sectionId (sectionId)}
{#if sectionId === 'newReleases' && newReleases.length > 0}
<HorizontalScrollRow title="New Releases">
<HorizontalScrollRow title={$t('home.newReleases')}>
{#snippet children()}
{#each newReleases as album}
<AlbumCard
@@ -518,7 +519,7 @@
{/if}
{#if sectionId === 'pressAwards' && pressAwards.length > 0}
<HorizontalScrollRow title="Press Awards">
<HorizontalScrollRow title={$t('home.pressAwards')}>
{#snippet children()}
{#each pressAwards as album}
<AlbumCard
@@ -542,7 +543,7 @@
{/if}
{#if sectionId === 'mostStreamed' && mostStreamed.length > 0}
<HorizontalScrollRow title="Popular Albums">
<HorizontalScrollRow title={$t('home.popularAlbums')}>
{#snippet children()}
{#each mostStreamed as album}
<AlbumCard
@@ -566,7 +567,7 @@
{/if}
{#if sectionId === 'qobuzissimes' && qobuzissimes.length > 0}
<HorizontalScrollRow title="Qobuzissimes">
<HorizontalScrollRow title={$t('home.qobuzissimes')}>
{#snippet children()}
{#each qobuzissimes as album}
<AlbumCard
@@ -590,7 +591,7 @@
{/if}
{#if sectionId === 'editorPicks' && editorPicks.length > 0}
<HorizontalScrollRow title="Editor's Picks">
<HorizontalScrollRow title={$t('home.editorPicks')}>
{#snippet children()}
{#each editorPicks as album}
<AlbumCard
@@ -614,7 +615,7 @@
{/if}
{#if sectionId === 'recentAlbums' && recentAlbums.length > 0}
<HorizontalScrollRow title="Recently Played">
<HorizontalScrollRow title={$t('home.recentlyPlayed')}>
{#snippet children()}
{#each recentAlbums as album}
<AlbumCard
@@ -640,7 +641,7 @@
{#if sectionId === 'continueTracks' && continueTracks.length > 0}
<div class="section">
<div class="section-header">
<h2>Continue Listening</h2>
<h2>{$t('home.continueListening')}</h2>
</div>
<div class="track-list compact">
{#each continueTracks as track, index}
@@ -667,7 +668,7 @@
{/if}
{#if sectionId === 'topArtists' && topArtists.length > 0}
<HorizontalScrollRow title="Your Top Artists">
<HorizontalScrollRow title={$t('home.yourTopArtists')}>
{#snippet children()}
{#each topArtists as artist}
<button class="artist-card" onclick={() => onArtistClick?.(artist.id)}>
@@ -685,7 +686,7 @@
{/if}
<div class="artist-name">{artist.name}</div>
{#if artist.playCount}
<div class="artist-meta">{artist.playCount} plays</div>
<div class="artist-meta">{$t('home.artistPlays', { values: { count: artist.playCount } })}</div>
{/if}
</button>
{/each}
@@ -695,7 +696,7 @@
{/if}
{#if sectionId === 'favoriteAlbums' && favoriteAlbums.length > 0}
<HorizontalScrollRow title="More From Favorites">
<HorizontalScrollRow title={$t('home.moreFromFavorites')}>
{#snippet children()}
{#each favoriteAlbums as album}
<AlbumCard
@@ -723,8 +724,8 @@
<div class="state-icon">
<Music size={48} />
</div>
<h1>Start Listening</h1>
<p>Play music or add favorites to activate recommendations.</p>
<h1>{$t('home.startListening')}</h1>
<p>{$t('home.startListeningDescription')}</p>
</div>
{/if}
+17 -16
View File
@@ -5,6 +5,7 @@
import AlbumCard from '../AlbumCard.svelte';
import TrackMenu from '../TrackMenu.svelte';
import { getSearchState, setSearchState, type SearchResults, type SearchTab } from '$lib/stores/searchState';
import { t } from '$lib/i18n';
let searchInput: HTMLInputElement | null = null;
@@ -278,19 +279,19 @@
<div class="search-view">
<!-- Search Header -->
<div class="search-header">
<h1>Search</h1>
<h1>{$t('search.title')}</h1>
<div class="search-input-container">
<Search size={20} class="search-icon" />
<input
type="text"
placeholder="Search for albums, tracks, or artists..."
placeholder={$t('search.placeholder')}
bind:value={query}
oninput={debounceSearch}
class="search-input"
bind:this={searchInput}
/>
{#if query.trim()}
<button class="search-clear" onclick={clearSearch} aria-label="Clear search">
<button class="search-clear" onclick={clearSearch} aria-label={$t('actions.clear')}>
<X size={18} />
</button>
{/if}
@@ -305,7 +306,7 @@
onclick={() => handleTabChange('albums')}
>
<Disc3 size={18} />
<span>Albums</span>
<span>{$t('search.albums')}</span>
{#if albumResults}
<span class="count">{albumResults.total}</span>
{/if}
@@ -316,7 +317,7 @@
onclick={() => handleTabChange('tracks')}
>
<Music size={18} />
<span>Tracks</span>
<span>{$t('search.tracks')}</span>
{#if trackResults}
<span class="count">{trackResults.total}</span>
{/if}
@@ -327,7 +328,7 @@
onclick={() => handleTabChange('artists')}
>
<Mic2 size={18} />
<span>Artists</span>
<span>{$t('search.artists')}</span>
{#if artistResults}
<span class="count">{artistResults.total}</span>
{/if}
@@ -339,21 +340,21 @@
{#if isSearching}
<div class="loading">
<div class="spinner"></div>
<p>Searching...</p>
<p>{$t('search.searching')}</p>
</div>
{:else if searchError}
<div class="error">
<p>Search failed</p>
<p>{$t('errors.loadFailed')}</p>
<p class="error-detail">{searchError}</p>
</div>
{:else if !query.trim()}
<div class="empty-state">
<Search size={48} />
<p>Start typing to search Qobuz</p>
<p>{$t('search.startTyping')}</p>
</div>
{:else if activeTab === 'albums' && albumResults}
{#if albumResults.items.length === 0}
<div class="no-results">No albums found for "{query}"</div>
<div class="no-results">{$t('search.noAlbumsFor', { values: { query } })}</div>
{:else}
<div class="albums-grid">
{#each albumResults.items as album}
@@ -376,14 +377,14 @@
{#if hasMoreAlbums}
<div class="load-more-container">
<button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
{isLoadingMore ? 'Loading...' : `Load More (${albumResults.items.length} of ${albumResults.total})`}
{isLoadingMore ? $t('actions.loading') : $t('artist.loadMore') + ` (${albumResults.items.length} / ${albumResults.total})`}
</button>
</div>
{/if}
{/if}
{:else if activeTab === 'tracks' && trackResults}
{#if trackResults.items.length === 0}
<div class="no-results">No tracks found for "{query}"</div>
<div class="no-results">{$t('search.noTracksFor', { values: { query } })}</div>
{:else}
<div class="tracks-list">
{#each trackResults.items as track, index}
@@ -440,14 +441,14 @@
{#if hasMoreTracks}
<div class="load-more-container">
<button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
{isLoadingMore ? 'Loading...' : `Load More (${trackResults.items.length} of ${trackResults.total})`}
{isLoadingMore ? $t('actions.loading') : $t('artist.loadMore') + ` (${trackResults.items.length} / ${trackResults.total})`}
</button>
</div>
{/if}
{/if}
{:else if activeTab === 'artists' && artistResults}
{#if artistResults.items.length === 0}
<div class="no-results">No artists found for "{query}"</div>
<div class="no-results">{$t('search.noArtistsFor', { values: { query } })}</div>
{:else}
<div class="artists-grid">
{#each artistResults.items as artist}
@@ -461,7 +462,7 @@
{/if}
<div class="artist-name">{artist.name}</div>
{#if artist.albums_count}
<div class="artist-albums">{artist.albums_count} albums</div>
<div class="artist-albums">{$t('library.albumCount', { values: { count: artist.albums_count } })}</div>
{/if}
</button>
{/each}
@@ -469,7 +470,7 @@
{#if hasMoreArtists}
<div class="load-more-container">
<button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
{isLoadingMore ? 'Loading...' : `Load More (${artistResults.items.length} of ${artistResults.total})`}
{isLoadingMore ? $t('actions.loading') : $t('artist.loadMore') + ` (${artistResults.items.length} / ${artistResults.total})`}
</button>
</div>
{/if}
+54 -4
View File
@@ -28,7 +28,20 @@
"madeForYou": "Made for You",
"topPicks": "Top Picks",
"yourPlaylists": "Your Playlists",
"seeAll": "See All"
"seeAll": "See All",
"loading": "Loading your home",
"loadingDescription": "Building recommendations from your listening history.",
"customizeHome": "Customize Home",
"loadError": "Could not load Home",
"popularAlbums": "Popular Albums",
"qobuzissimes": "Qobuzissimes",
"editorPicks": "Editor's Picks",
"continueListening": "Continue Listening",
"yourTopArtists": "Your Top Artists",
"artistPlays": "{count} plays",
"moreFromFavorites": "More From Favorites",
"startListening": "Start Listening",
"startListeningDescription": "Play music or add favorites to activate recommendations."
},
"search": {
"title": "Search",
@@ -40,10 +53,15 @@
"noResults": "No results found",
"searching": "Searching...",
"recentSearches": "Recent Searches",
"clearHistory": "Clear History"
"clearHistory": "Clear History",
"startTyping": "Start typing to search Qobuz",
"noAlbumsFor": "No albums found for \"{query}\"",
"noTracksFor": "No tracks found for \"{query}\"",
"noArtistsFor": "No artists found for \"{query}\""
},
"library": {
"title": "Local Library",
"browse": "Browse Library",
"albums": "Albums",
"artists": "Artists",
"tracks": "Tracks",
@@ -127,7 +145,17 @@
"playlistName": "Playlist Name",
"description": "Description",
"public": "Public",
"private": "Private"
"private": "Private",
"import": "Import",
"manage": "Manage playlists",
"allHidden": "All playlists are hidden"
},
"sort": {
"nameAZ": "Name (A-Z)",
"recent": "Recent",
"trackCount": "# of Tracks",
"playCount": "Play Count",
"custom": "Custom"
},
"favorites": {
"title": "Favorites",
@@ -148,6 +176,7 @@
"fetchingLyrics": "Fetching lyrics...",
"shuffle": "Shuffle",
"repeat": "Repeat",
"repeatAll": "Repeat All",
"repeatOne": "Repeat One",
"repeatOff": "Repeat Off",
"previous": "Previous",
@@ -159,10 +188,18 @@
"unmute": "Unmute",
"clearQueue": "Clear Queue",
"queueEmpty": "Queue is empty",
"queueEmptyDescription": "Search for music or browse your library to add tracks.",
"playNext": "Play Next",
"addToQueue": "Add to Queue",
"removeFromQueue": "Remove from Queue",
"saveQueue": "Save as Playlist"
"saveQueue": "Save as Playlist",
"searchQueue": "Search in queue...",
"noTracksMatch": "No tracks match \"{query}\"",
"noTrackPlaying": "No track playing",
"castToDevice": "Cast to device",
"castingManage": "Casting - Click to manage",
"miniPlayer": "Mini Player",
"fullScreen": "Full Screen"
},
"quality": {
"mp3": "MP3 320kbps",
@@ -370,5 +407,18 @@
"clearQueue": "Clear all tracks from queue?",
"clearCache": "Clear all cached data?",
"logout": "Are you sure you want to log out?"
},
"homeSettings": {
"greeting": "Greeting",
"showGreeting": "Show greeting",
"showGreetingDesc": "Display personalized greeting at top",
"customGreeting": "Custom greeting",
"customGreetingDesc": "Use {name} for your name. Leave empty for time-based.",
"customGreetingPlaceholder": "Good vibes, {name}!",
"sections": "Sections",
"sectionsDesc": "Reorder with arrows. Toggle visibility with switch.",
"moveUp": "Move up",
"moveDown": "Move down",
"resetToDefaults": "Reset to defaults"
}
}
+54 -4
View File
@@ -28,7 +28,20 @@
"madeForYou": "Hecho para Ti",
"topPicks": "Selecciones Destacadas",
"yourPlaylists": "Tus Listas",
"seeAll": "Ver Todo"
"seeAll": "Ver Todo",
"loading": "Cargando tu inicio",
"loadingDescription": "Construyendo recomendaciones de tu historial de escucha.",
"customizeHome": "Personalizar Inicio",
"loadError": "No se pudo cargar el Inicio",
"popularAlbums": "Álbumes Populares",
"qobuzissimes": "Qobuzissimes",
"editorPicks": "Selección del Editor",
"continueListening": "Continuar Escuchando",
"yourTopArtists": "Tus Artistas Favoritos",
"artistPlays": "{count} reproducciones",
"moreFromFavorites": "Más de Favoritos",
"startListening": "Empieza a Escuchar",
"startListeningDescription": "Reproduce música o agrega favoritos para activar recomendaciones."
},
"search": {
"title": "Buscar",
@@ -40,10 +53,15 @@
"noResults": "Sin resultados",
"searching": "Buscando...",
"recentSearches": "Búsquedas Recientes",
"clearHistory": "Limpiar Historial"
"clearHistory": "Limpiar Historial",
"startTyping": "Escribe para buscar en Qobuz",
"noAlbumsFor": "No se encontraron álbumes para \"{query}\"",
"noTracksFor": "No se encontraron pistas para \"{query}\"",
"noArtistsFor": "No se encontraron artistas para \"{query}\""
},
"library": {
"title": "Biblioteca Local",
"browse": "Explorar Biblioteca",
"albums": "Álbumes",
"artists": "Artistas",
"tracks": "Pistas",
@@ -127,7 +145,17 @@
"playlistName": "Nombre de la Lista",
"description": "Descripción",
"public": "Pública",
"private": "Privada"
"private": "Privada",
"import": "Importar",
"manage": "Administrar listas",
"allHidden": "Todas las listas están ocultas"
},
"sort": {
"nameAZ": "Nombre (A-Z)",
"recent": "Reciente",
"trackCount": "# de Pistas",
"playCount": "Reproducciones",
"custom": "Personalizado"
},
"favorites": {
"title": "Favoritos",
@@ -148,6 +176,7 @@
"fetchingLyrics": "Buscando letras...",
"shuffle": "Aleatorio",
"repeat": "Repetir",
"repeatAll": "Repetir Todo",
"repeatOne": "Repetir Una",
"repeatOff": "No Repetir",
"previous": "Anterior",
@@ -159,10 +188,18 @@
"unmute": "Activar Sonido",
"clearQueue": "Limpiar Cola",
"queueEmpty": "La cola está vacía",
"queueEmptyDescription": "Busca música o explora tu biblioteca para agregar pistas.",
"playNext": "Reproducir Siguiente",
"addToQueue": "Agregar a la Cola",
"removeFromQueue": "Quitar de la Cola",
"saveQueue": "Guardar como Lista"
"saveQueue": "Guardar como Lista",
"searchQueue": "Buscar en la cola...",
"noTracksMatch": "Ninguna pista coincide con \"{query}\"",
"noTrackPlaying": "Sin pista en reproducción",
"castToDevice": "Transmitir a dispositivo",
"castingManage": "Transmitiendo - Clic para administrar",
"miniPlayer": "Mini Reproductor",
"fullScreen": "Pantalla Completa"
},
"quality": {
"mp3": "MP3 320kbps",
@@ -370,5 +407,18 @@
"clearQueue": "¿Limpiar todas las pistas de la cola?",
"clearCache": "¿Limpiar todos los datos en caché?",
"logout": "¿Estás seguro de que quieres cerrar sesión?"
},
"homeSettings": {
"greeting": "Saludo",
"showGreeting": "Mostrar saludo",
"showGreetingDesc": "Mostrar saludo personalizado arriba",
"customGreeting": "Saludo personalizado",
"customGreetingDesc": "Usa {name} para tu nombre. Deja vacío para basado en hora.",
"customGreetingPlaceholder": "¡Buenas vibras, {name}!",
"sections": "Secciones",
"sectionsDesc": "Reordena con flechas. Activa/desactiva con interruptor.",
"moveUp": "Subir",
"moveDown": "Bajar",
"resetToDefaults": "Restablecer valores"
}
}