From 2dc9989d225199bba963588a708d67a8f321c649 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:45:35 -0600 Subject: [PATCH 1/7] i18n: Add greeting and toast translations - Add time-based greeting translations (goodMorning, goodAfternoon, goodEvening, goodNight) to homeSettings section - Update homeSettingsStore to export getGreetingInfo for i18n support - Update HomeView.svelte to use translated greetings - Add comprehensive toast translations (~60 keys) for all user-facing notifications including queue, playback, offline, favorites, etc. - Update +page.svelte to use $t() for all showToast calls - Complete Spanish translations for all new keys --- src/lib/components/views/HomeView.svelte | 12 +- src/lib/i18n/locales/en.json | 108 ++++++++++++++ src/lib/i18n/locales/es.json | 108 ++++++++++++++ src/lib/stores/homeSettingsStore.ts | 42 +++++- src/routes/+page.svelte | 175 ++++++++++++----------- 5 files changed, 356 insertions(+), 89 deletions(-) diff --git a/src/lib/components/views/HomeView.svelte b/src/lib/components/views/HomeView.svelte index 5d36d1adf..4364c1d1b 100644 --- a/src/lib/components/views/HomeView.svelte +++ b/src/lib/components/views/HomeView.svelte @@ -12,7 +12,7 @@ import { subscribe as subscribeHomeSettings, getSettings, - getGreetingText, + getGreetingInfo, type HomeSettings, type HomeSectionId } from '$lib/stores/homeSettingsStore'; @@ -127,8 +127,14 @@ let homeSettings = $state(getSettings()); let isSettingsModalOpen = $state(false); - // Computed greeting - const greetingText = $derived(getGreetingText(userName)); + // Computed greeting with i18n support + const greetingText = $derived.by(() => { + const info = getGreetingInfo(userName); + if (info.type === 'custom') { + return info.text; + } + return $t(info.key, { name: info.name }); + }); // Check if a section is visible diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index fe08071f6..d58f35bb6 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -562,12 +562,120 @@ "customGreeting": "Custom greeting", "customGreetingDesc": "Use {name} for your name. Leave empty for time-based.", "customGreetingPlaceholder": "Good vibes, {name}!", + "goodMorning": "Good morning, {name}", + "goodAfternoon": "Good afternoon, {name}", + "goodEvening": "Good evening, {name}", + "goodNight": "Good night, {name}", "sections": "Sections", "sectionsDesc": "Reorder with arrows. Toggle visibility with switch.", "moveUp": "Move up", "moveDown": "Move down", "resetToDefaults": "Reset to defaults" }, + "toast": { + "loadingAlbum": "Loading album...", + "loadingArtist": "Loading artist...", + "loadingPlaylist": "Loading playlist...", + "lookingUp": "Looking up {name}...", + "failedLoadAlbum": "Failed to load album", + "failedLoadArtist": "Failed to load artist", + "failedLoadPlaylist": "Failed to load playlist", + "failedLoadMoreAlbums": "Failed to load more albums", + "failedAddToQueue": "Failed to add to queue", + "failedPlayTrack": "Failed to play track", + "queueCleared": "Queue cleared", + "queueEmpty": "Queue is empty", + "queueEnded": "Queue ended", + "failedClearQueue": "Failed to clear queue", + "shuffleEnabled": "Shuffle play enabled", + "shuffleDisabled": "Shuffle disabled", + "repeatOff": "Repeat: Off", + "repeatOne": "Repeat: One", + "repeatAll": "Repeat: All", + "infinitePlayEnabled": "Infinite play enabled", + "infinitePlayDisabled": "Infinite play disabled", + "linkCopied": "Link copied to clipboard", + "albumLinkCopied": "Album link copied to clipboard", + "playlistLinkCopied": "Playlist link copied to clipboard", + "trackLinkCopied": "Track link copied to clipboard", + "fetchingSonglink": "Fetching song.link...", + "fetchingAlbumLink": "Fetching Album.link...", + "songlinkCopied": "song.link copied to clipboard", + "albumLinkCopiedSonglink": "Album.link copied to clipboard", + "songlinkError": "song.link error: {error}", + "albumLinkError": "Album.link error: {error}", + "allTracksOffline": "All tracks already available offline", + "removedFromOffline": "Removed from offline library", + "failedRemoveOffline": "Failed to remove from offline library", + "failedOpenFolder": "Failed to open folder", + "failedOpenAlbumFolder": "Failed to open album folder", + "playlistNoTracks": "Playlist has no tracks", + "copiedToLibrary": "Copied to library", + "copyingToLibrary": "Copying to library...", + "playlistCopied": "Playlist copied to library", + "failedCopyPlaylist": "Failed to copy playlist: {error}", + "removedFromFavorites": "Removed from favorites", + "playlistRemovedFavorites": "Playlist removed from favorites", + "failedRemoveFavorites": "Failed to remove from favorites: {error}", + "failedUpdateFavorites": "Failed to update favorites", + "addedToFavorites": "Added to favorites", + "noAvailableTracks": "No available tracks in queue (offline mode)", + "failedReorderQueue": "Failed to reorder queue", + "trackNotInHistory": "Track not found in history", + "failedNavigateSource": "Failed to navigate to source", + "failedLookupMusician": "Could not look up musician info", + "couldNotFetchDetails": "Could not fetch details", + "failedPrepareOffline": "Failed to prepare for offline", + "failedRefreshOffline": "Failed to refresh offline copy", + "preparingTrackOffline": "Preparing \"{title}\" for offline", + "preparingTracksOffline": "Preparing {count} tracks from \"{album}\" for offline", + "refreshingTrackOffline": "Refreshing \"{title}\" for offline", + "refreshingAlbumOffline": "Refreshing all tracks from \"{album}\" for offline", + "failedLoadAlbumRefresh": "Failed to load album for offline refresh", + "playingTracksNext": "Playing {count} tracks next", + "addedTracksToQueue": "Added {count} tracks to queue", + "failedPreviousTrack": "Failed to go to previous track", + "failedNextTrack": "Failed to go to next track", + "tracksAddedToPlaylist": "Track(s) added to playlist", + "playlistCreated": "Playlist created", + "offlineModeStarted": "Started in offline mode", + "welcomeUser": "Welcome, {name}!", + "restoredTrack": "Restored: {title}", + "logoutSuccess": "Logged out successfully", + "failedLogout": "Failed to logout", + "localTrackSearch": "Local track - search for artist in Search", + "queuedPlayNext": "Queued to play next", + "failedQueueTrack": "Failed to queue track", + "addedToQueue": "Added to queue", + "offlineModeActivated": "Offline mode activated", + "backOnline": "Back online", + "syncedPlaylist": "Synced playlist \"{name}\"", + "failedSyncPlaylist": "Failed to sync playlist \"{name}\": {error}", + "playbackError": "Playback error: {error}", + "trackUnavailable": "\"{title}\" is no longer available", + "playing": "Playing: {title}", + "failedCopyLink": "Failed to copy link", + "failedShareQobuz": "Failed to share Qobuz link: {error}", + "playlistCreatedOffline": "Playlist created offline - will sync when back online", + "playlistImported": "Playlist imported", + "playlistImportFailed": "Playlist import failed", + "enterSearchTerms": "Enter album title or artist to search", + "searchFailed": "Search failed: {error}", + "appliedMetadata": "Applied metadata from {provider}", + "rateLimited": "Rate limited. Please wait a moment and try again.", + "failedFetchMetadata": "Failed to fetch metadata: {error}", + "failedOpenBrowser": "Failed to open browser", + "albumMetadataSaved": "Album metadata saved", + "removedMissingFiles": "Removed {count} tracks with missing files", + "noMissingFilesFound": "No missing files found", + "failedCleanupMissing": "Failed to cleanup missing files", + "metadataRefreshed": "Metadata refreshed from files", + "failedSaveTray": "Failed to save tray setting", + "failedSaveAutoplay": "Failed to save autoplay preference", + "failedSaveIconVisibility": "Failed to save icon visibility preference", + "failedRepairOffline": "Failed to repair offline library: {error}", + "failedOpenCacheFolder": "Failed to open cache folder" + }, "offline": { "title": "Offline Mode", "message": "Connect to the internet to access Qobuz™ content.", diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index b6e8b6191..f54f16f46 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -562,12 +562,120 @@ "customGreeting": "Saludo personalizado", "customGreetingDesc": "Usa {name} para tu nombre. Deja vacío para basado en hora.", "customGreetingPlaceholder": "¡Buenas vibras, {name}!", + "goodMorning": "Buenos días, {name}", + "goodAfternoon": "Buenas tardes, {name}", + "goodEvening": "Buenas noches, {name}", + "goodNight": "Buenas noches, {name}", "sections": "Secciones", "sectionsDesc": "Reordena con flechas. Activa/desactiva con interruptor.", "moveUp": "Subir", "moveDown": "Bajar", "resetToDefaults": "Restablecer valores" }, + "toast": { + "loadingAlbum": "Cargando álbum...", + "loadingArtist": "Cargando artista...", + "loadingPlaylist": "Cargando playlist...", + "lookingUp": "Buscando {name}...", + "failedLoadAlbum": "Error al cargar álbum", + "failedLoadArtist": "Error al cargar artista", + "failedLoadPlaylist": "Error al cargar playlist", + "failedLoadMoreAlbums": "Error al cargar más álbumes", + "failedAddToQueue": "Error al agregar a la cola", + "failedPlayTrack": "Error al reproducir pista", + "queueCleared": "Cola limpiada", + "queueEmpty": "La cola está vacía", + "queueEnded": "Cola terminada", + "failedClearQueue": "Error al limpiar cola", + "shuffleEnabled": "Reproducción aleatoria activada", + "shuffleDisabled": "Reproducción aleatoria desactivada", + "repeatOff": "Repetir: Desactivado", + "repeatOne": "Repetir: Una", + "repeatAll": "Repetir: Todas", + "infinitePlayEnabled": "Reproducción infinita activada", + "infinitePlayDisabled": "Reproducción infinita desactivada", + "linkCopied": "Enlace copiado al portapapeles", + "albumLinkCopied": "Enlace del álbum copiado", + "playlistLinkCopied": "Enlace de la playlist copiado", + "trackLinkCopied": "Enlace de la pista copiado", + "fetchingSonglink": "Obteniendo song.link...", + "fetchingAlbumLink": "Obteniendo Album.link...", + "songlinkCopied": "song.link copiado al portapapeles", + "albumLinkCopiedSonglink": "Album.link copiado al portapapeles", + "songlinkError": "Error en song.link: {error}", + "albumLinkError": "Error en Album.link: {error}", + "allTracksOffline": "Todas las pistas ya están disponibles offline", + "removedFromOffline": "Eliminado de la biblioteca offline", + "failedRemoveOffline": "Error al eliminar de biblioteca offline", + "failedOpenFolder": "Error al abrir carpeta", + "failedOpenAlbumFolder": "Error al abrir carpeta del álbum", + "playlistNoTracks": "La playlist no tiene pistas", + "copiedToLibrary": "Copiado a la biblioteca", + "copyingToLibrary": "Copiando a la biblioteca...", + "playlistCopied": "Playlist copiada a la biblioteca", + "failedCopyPlaylist": "Error al copiar playlist: {error}", + "removedFromFavorites": "Eliminado de favoritos", + "playlistRemovedFavorites": "Playlist eliminada de favoritos", + "failedRemoveFavorites": "Error al eliminar de favoritos: {error}", + "failedUpdateFavorites": "Error al actualizar favoritos", + "addedToFavorites": "Agregado a favoritos", + "noAvailableTracks": "Sin pistas disponibles en la cola (modo offline)", + "failedReorderQueue": "Error al reordenar cola", + "trackNotInHistory": "Pista no encontrada en historial", + "failedNavigateSource": "Error al navegar al origen", + "failedLookupMusician": "No se pudo buscar info del músico", + "couldNotFetchDetails": "No se pudieron obtener los detalles", + "failedPrepareOffline": "Error al preparar para offline", + "failedRefreshOffline": "Error al actualizar copia offline", + "preparingTrackOffline": "Preparando \"{title}\" para offline", + "preparingTracksOffline": "Preparando {count} pistas de \"{album}\" para offline", + "refreshingTrackOffline": "Actualizando \"{title}\" para offline", + "refreshingAlbumOffline": "Actualizando todas las pistas de \"{album}\" para offline", + "failedLoadAlbumRefresh": "Error al cargar álbum para actualizar offline", + "playingTracksNext": "Reproduciendo {count} pistas a continuación", + "addedTracksToQueue": "{count} pistas agregadas a la cola", + "failedPreviousTrack": "Error al ir a pista anterior", + "failedNextTrack": "Error al ir a siguiente pista", + "tracksAddedToPlaylist": "Pista(s) agregada(s) a la playlist", + "playlistCreated": "Playlist creada", + "offlineModeStarted": "Iniciado en modo offline", + "welcomeUser": "¡Bienvenido, {name}!", + "restoredTrack": "Restaurado: {title}", + "logoutSuccess": "Sesión cerrada exitosamente", + "failedLogout": "Error al cerrar sesión", + "localTrackSearch": "Pista local - busca el artista en Búsqueda", + "queuedPlayNext": "En cola para reproducir siguiente", + "failedQueueTrack": "Error al agregar pista a la cola", + "addedToQueue": "Agregado a la cola", + "offlineModeActivated": "Modo offline activado", + "backOnline": "De vuelta en línea", + "syncedPlaylist": "Playlist \"{name}\" sincronizada", + "failedSyncPlaylist": "Error al sincronizar playlist \"{name}\": {error}", + "playbackError": "Error de reproducción: {error}", + "trackUnavailable": "\"{title}\" ya no está disponible", + "playing": "Reproduciendo: {title}", + "failedCopyLink": "Error al copiar enlace", + "failedShareQobuz": "Error al compartir enlace de Qobuz: {error}", + "playlistCreatedOffline": "Playlist creada offline - se sincronizará cuando vuelvas a estar en línea", + "playlistImported": "Playlist importada", + "playlistImportFailed": "Error al importar playlist", + "enterSearchTerms": "Ingresa título del álbum o artista para buscar", + "searchFailed": "Error en búsqueda: {error}", + "appliedMetadata": "Metadatos aplicados de {provider}", + "rateLimited": "Límite de consultas. Por favor espera un momento e intenta de nuevo.", + "failedFetchMetadata": "Error al obtener metadatos: {error}", + "failedOpenBrowser": "Error al abrir navegador", + "albumMetadataSaved": "Metadatos del álbum guardados", + "removedMissingFiles": "{count} pistas con archivos faltantes eliminadas", + "noMissingFilesFound": "No se encontraron archivos faltantes", + "failedCleanupMissing": "Error al limpiar archivos faltantes", + "metadataRefreshed": "Metadatos actualizados desde archivos", + "failedSaveTray": "Error al guardar configuración de bandeja", + "failedSaveAutoplay": "Error al guardar preferencia de reproducción automática", + "failedSaveIconVisibility": "Error al guardar preferencia de visibilidad de iconos", + "failedRepairOffline": "Error al reparar biblioteca offline: {error}", + "failedOpenCacheFolder": "Error al abrir carpeta de caché" + }, "offline": { "title": "Modo Offline", "message": "Conecta a internet para acceder al contenido de Qobuz™.", diff --git a/src/lib/stores/homeSettingsStore.ts b/src/lib/stores/homeSettingsStore.ts index 0a418091d..ccf8bd3db 100644 --- a/src/lib/stores/homeSettingsStore.ts +++ b/src/lib/stores/homeSettingsStore.ts @@ -280,7 +280,7 @@ export function setCustomGreeting(text: string | null): void { } /** - * Get greeting text for display + * Get greeting text for display (legacy - use getGreetingInfo for i18n support) */ export function getGreetingText(userName: string): string { if (settings.greeting.customText) { @@ -290,7 +290,45 @@ export function getGreetingText(userName: string): string { } /** - * Get time-based greeting + * Get greeting info for i18n-aware display + * Returns either custom text or a translation key with params + */ +export function getGreetingInfo(userName: string): { type: 'custom'; text: string } | { type: 'key'; key: string; name: string } { + const firstName = getFirstName(userName); + + if (settings.greeting.customText) { + return { + type: 'custom', + text: settings.greeting.customText.replace('{name}', firstName) + }; + } + + return { + type: 'key', + key: getTimeBasedGreetingKey(), + name: firstName + }; +} + +/** + * Get time-based greeting translation key + */ +export function getTimeBasedGreetingKey(): string { + const hour = new Date().getHours(); + + if (hour >= 5 && hour < 12) { + return 'homeSettings.goodMorning'; + } else if (hour >= 12 && hour < 17) { + return 'homeSettings.goodAfternoon'; + } else if (hour >= 17 && hour < 21) { + return 'homeSettings.goodEvening'; + } else { + return 'homeSettings.goodNight'; + } +} + +/** + * Get time-based greeting (legacy) */ function getTimeBasedGreeting(userName: string): string { const hour = new Date().getHours(); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1abc78043..80ee2dcdc 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -224,6 +224,9 @@ shareSonglinkTrack } from '$lib/services/trackActions'; + // Internationalization + import { t } from '$lib/i18n'; + // App bootstrap import { bootstrapApp } from '$lib/app/bootstrap'; @@ -556,7 +559,7 @@ async function handleAlbumClick(albumId: string) { try { - showToast('Loading album...', 'info'); + showToast($t('toast.loadingAlbum'), 'info'); const album = await invoke('get_album', { albumId }); console.log('Album details:', album); @@ -572,7 +575,7 @@ } } catch (err) { console.error('Failed to load album:', err); - showToast('Failed to load album', 'error'); + showToast($t('toast.failedLoadAlbum'), 'error'); } } @@ -634,7 +637,7 @@ async function handleArtistClick(artistId: number) { try { - showToast('Loading artist...', 'info'); + showToast($t('toast.loadingArtist'), 'info'); const artist = await invoke('get_artist_detail', { artistId }); console.log('Artist details:', artist); @@ -643,7 +646,7 @@ hideToast(); } catch (err) { console.error('Failed to load artist:', err); - showToast('Failed to load artist', 'error'); + showToast($t('toast.failedLoadArtist'), 'error'); } } @@ -660,7 +663,7 @@ * - Weak (1), None (0): Show Informational Modal */ async function handleMusicianClick(name: string, role: string) { - showToast(`Looking up ${name}...`, 'info'); + showToast($t('toast.lookingUp', { name }), 'info'); try { const musician = await invoke('resolve_musician', { name, role }); console.log('Resolved musician:', musician); @@ -691,7 +694,7 @@ } } catch (err) { console.error('Failed to resolve musician:', err); - showToast('Could not look up musician info', 'error'); + showToast($t('toast.failedLookupMusician'), 'error'); // Fallback: open modal with basic info musicianModalData = { name, @@ -796,7 +799,7 @@ } } catch (err) { console.error('[ContextNav] Navigation failed:', err); - showToast('Failed to navigate to source', 'error'); + showToast($t('toast.failedNavigateSource'), 'error'); } } @@ -831,7 +834,7 @@ ); } catch (err) { console.error('Failed to load more artist albums:', err); - showToast('Failed to load more albums', 'error'); + showToast($t('toast.failedLoadMoreAlbums'), 'error'); } finally { isArtistAlbumsLoading = false; } @@ -856,7 +859,7 @@ return convertQobuzAlbum(album); } catch (err) { console.error('Failed to load album:', err); - showToast('Failed to load album', 'error'); + showToast($t('toast.failedLoadAlbum'), 'error'); return null; } } @@ -926,7 +929,7 @@ artist_id: t.artistId ?? album.artistId }); } - showToast(`Playing ${album.tracks.length} tracks next`, 'success'); + showToast($t('toast.playingTracksNext', { count: album.tracks.length }), 'success'); } async function queueAlbumLaterById(albumId: string) { @@ -951,24 +954,24 @@ const success = await addTracksToQueue(queueTracks); if (success) { - showToast(`Added ${queueTracks.length} tracks to queue`, 'success'); + showToast($t('toast.addedTracksToQueue', { count: queueTracks.length }), 'success'); } else { - showToast('Failed to add to queue', 'error'); + showToast($t('toast.failedAddToQueue'), 'error'); } } function shareAlbumQobuzLinkById(albumId: string) { const url = `https://play.qobuz.com/album/${albumId}`; writeText(url); - showToast('Album link copied to clipboard', 'success'); + showToast($t('toast.albumLinkCopied'), 'success'); } async function shareAlbumSonglinkById(albumId: string) { try { - showToast('Fetching Album.link...', 'info'); + showToast($t('toast.fetchingAlbumLink'), 'info'); const album = await fetchAlbumDetail(albumId); if (!album) { - showToast('Could not fetch album details', 'error'); + showToast($t('toast.couldNotFetchDetails'), 'error'); return; } const response = await invoke<{ pageUrl: string }>('share_album_songlink', { @@ -978,10 +981,10 @@ artist: album.artist }); writeText(response.pageUrl); - showToast('Album.link copied to clipboard', 'success'); + showToast($t('toast.albumLinkCopiedSonglink'), 'success'); } catch (err) { console.error('Failed to get Album.link:', err); - showToast(`Album.link error: ${err}`, 'error'); + showToast($t('toast.albumLinkError', { error: String(err) }), 'error'); } } @@ -995,11 +998,11 @@ }); if (tracksToDownload.length === 0) { - showToast('All tracks already available offline', 'info'); + showToast($t('toast.allTracksOffline'), 'info'); return; } - showToast(`Preparing ${tracksToDownload.length} tracks from "${album.title}" for offline`, 'info'); + showToast($t('toast.preparingTracksOffline', { count: tracksToDownload.length, album: album.title }), 'info'); for (const track of tracksToDownload) { try { @@ -1053,7 +1056,7 @@ return playlist; } catch (err) { console.error('Failed to load playlist:', err); - showToast('Failed to load playlist', 'error'); + showToast($t('toast.failedLoadPlaylist'), 'error'); return null; } } @@ -1061,7 +1064,7 @@ async function playPlaylistById(playlistId: number) { const playlist = await fetchPlaylistData(playlistId); if (!playlist?.tracks?.items?.length) { - showToast('Playlist has no tracks', 'info'); + showToast($t('toast.playlistNoTracks'), 'info'); return; } @@ -1109,7 +1112,7 @@ async function queuePlaylistNextById(playlistId: number) { const playlist = await fetchPlaylistData(playlistId); if (!playlist?.tracks?.items?.length) { - showToast('Playlist has no tracks', 'info'); + showToast($t('toast.playlistNoTracks'), 'info'); return; } @@ -1132,13 +1135,13 @@ artist_id: t.performer?.id }); } - showToast(`Playing ${tracks.length} tracks next`, 'success'); + showToast($t('toast.playingTracksNext', { count: tracks.length }), 'success'); } async function queuePlaylistLaterById(playlistId: number) { const playlist = await fetchPlaylistData(playlistId); if (!playlist?.tracks?.items?.length) { - showToast('Playlist has no tracks', 'info'); + showToast($t('toast.playlistNoTracks'), 'info'); return; } @@ -1160,39 +1163,39 @@ const success = await addTracksToQueue(queueTracks); if (success) { - showToast(`Added ${queueTracks.length} tracks to queue`, 'success'); + showToast($t('toast.addedTracksToQueue', { count: queueTracks.length }), 'success'); } else { - showToast('Failed to add to queue', 'error'); + showToast($t('toast.failedAddToQueue'), 'error'); } } async function copyPlaylistToLibraryById(playlistId: number) { try { - showToast('Copying playlist to library...', 'info'); + showToast($t('toast.copyingToLibrary'), 'info'); await invoke('subscribe_playlist', { playlistId }); sidebarRef?.refreshPlaylists(); - showToast('Playlist copied to library', 'success'); + showToast($t('toast.playlistCopied'), 'success'); } catch (err) { console.error('Failed to copy playlist:', err); - showToast(`Failed to copy playlist: ${err}`, 'error'); + showToast($t('toast.failedCopyPlaylist', { error: String(err) }), 'error'); } } function sharePlaylistQobuzLinkById(playlistId: number) { const url = `https://play.qobuz.com/playlist/${playlistId}`; writeText(url); - showToast('Playlist link copied to clipboard', 'success'); + showToast($t('toast.playlistLinkCopied'), 'success'); } async function removePlaylistFavoriteById(playlistId: number) { try { await invoke('playlist_set_favorite', { playlistId, favorite: false }); - showToast('Playlist removed from favorites', 'success'); + showToast($t('toast.playlistRemovedFavorites'), 'success'); sidebarRef?.refreshPlaylists(); sidebarRef?.refreshPlaylistSettings(); } catch (err) { console.error('Failed to remove playlist favorite:', err); - showToast(`Failed to remove from favorites: ${err}`, 'error'); + showToast($t('toast.failedRemoveFavorites', { error: String(err) }), 'error'); } } @@ -1312,7 +1315,7 @@ async function toggleShuffle() { const result = await queueToggleShuffle(); if (result.success) { - showToast(result.enabled ? 'Shuffle enabled' : 'Shuffle disabled', 'info'); + showToast(result.enabled ? $t('toast.shuffleEnabled') : $t('toast.shuffleDisabled'), 'info'); // Persist playback mode to session saveSessionPlaybackMode(result.enabled, repeatMode); } @@ -1321,7 +1324,11 @@ async function toggleRepeat() { const result = await queueToggleRepeat(); if (result.success) { - const messages: Record = { off: 'Repeat off', all: 'Repeat all', one: 'Repeat one' }; + const messages: Record = { + off: $t('toast.repeatOff'), + all: $t('toast.repeatAll'), + one: $t('toast.repeatOne') + }; showToast(messages[result.mode], 'info'); // Persist playback mode to session saveSessionPlaybackMode(isShuffle, result.mode); @@ -1334,9 +1341,9 @@ const result = await toggleTrackFavorite(currentTrack.id, isFavorite); if (result.success) { setIsFavorite(result.isFavorite); - showToast(result.isFavorite ? 'Added to favorites' : 'Removed from favorites', 'success'); + showToast(result.isFavorite ? $t('toast.addedToFavorites') : $t('toast.removedFromFavorites'), 'success'); } else { - showToast('Failed to update favorites', 'error'); + showToast($t('toast.failedUpdateFavorites'), 'error'); } } @@ -1368,7 +1375,7 @@ } } catch (err) { console.error('Failed to go to previous track:', err); - showToast('Failed to go to previous track', 'error'); + showToast($t('toast.failedPreviousTrack'), 'error'); } finally { setIsSkipping(false); } @@ -1387,11 +1394,11 @@ // No next track - stop playback await stopPlayback(); setIsPlaying(false); - showToast('Queue ended', 'info'); + showToast($t('toast.queueEnded'), 'info'); } } catch (err) { console.error('Failed to go to next track:', err); - showToast('Failed to go to next track', 'error'); + showToast($t('toast.failedNextTrack'), 'error'); } finally { setIsSkipping(false); } @@ -1428,7 +1435,7 @@ if (skippedIds.has(track.id)) { // Already tried this track, stop to prevent infinite loop setQueueEnded(true); - showToast('No available tracks in queue (offline mode)', 'info'); + showToast($t('toast.noAvailableTracks'), 'info'); return; } skippedIds.add(track.id); @@ -1480,7 +1487,7 @@ // Find the index in the queue const queueState = await getBackendQueueState(); if (!queueState) { - showToast('Failed to play track', 'error'); + showToast($t('toast.failedPlayTrack'), 'error'); return; } @@ -1506,7 +1513,7 @@ } } catch (err) { console.error('Failed to play queue track:', err); - showToast('Failed to play track', 'error'); + showToast($t('toast.failedPlayTrack'), 'error'); } } @@ -1514,9 +1521,9 @@ async function handleClearQueue() { const success = await clearQueue(); if (success) { - showToast('Queue cleared', 'info'); + showToast($t('toast.queueCleared'), 'info'); } else { - showToast('Failed to clear queue', 'error'); + showToast($t('toast.failedClearQueue'), 'error'); } } @@ -1524,7 +1531,7 @@ async function handleQueueReorder(fromIndex: number, toIndex: number) { const success = await moveQueueTrack(fromIndex, toIndex); if (!success) { - showToast('Failed to reorder queue', 'error'); + showToast($t('toast.failedReorderQueue'), 'error'); } } @@ -1547,7 +1554,7 @@ } if (trackIds.length === 0) { - showToast('Queue is empty', 'info'); + showToast($t('toast.queueEmpty'), 'info'); return; } @@ -1566,7 +1573,7 @@ } catch { // Ignore storage errors } - showToast(infinitePlayEnabled ? 'Infinite play enabled' : 'Infinite play disabled', 'info'); + showToast(infinitePlayEnabled ? $t('toast.infinitePlayEnabled') : $t('toast.infinitePlayDisabled'), 'info'); } // Play a track from history @@ -1575,7 +1582,7 @@ // Get the full queue state to find the track in history const queueState = await getBackendQueueState(); if (!queueState) { - showToast('Failed to play track', 'error'); + showToast($t('toast.failedPlayTrack'), 'error'); return; } @@ -1583,7 +1590,7 @@ const numericId = parseInt(trackId, 10); const historyTrack = queueState.history.find(t => t.id === numericId); if (!historyTrack) { - showToast('Track not found in history', 'error'); + showToast($t('toast.trackNotInHistory'), 'error'); return; } @@ -1601,7 +1608,7 @@ }); } catch (err) { console.error('Failed to play history track:', err); - showToast('Failed to play track', 'error'); + showToast($t('toast.failedPlayTrack'), 'error'); } } @@ -1634,7 +1641,7 @@ // Play from random track (queue will be shuffled by backend) await handleAlbumTrackPlay(randomTrack); - showToast('Shuffle play enabled', 'info'); + showToast($t('toast.shuffleEnabled'), 'info'); } // Add all album tracks next in queue (after current track) @@ -1660,7 +1667,7 @@ artist_id: t.artistId ?? selectedAlbum?.artistId }); } - showToast(`Playing ${selectedAlbum.tracks.length} tracks next`, 'success'); + showToast($t('toast.playingTracksNext', { count: selectedAlbum.tracks.length }), 'success'); } // Add all album tracks to end of queue @@ -1685,9 +1692,9 @@ const success = await addTracksToQueue(queueTracks); if (success) { - showToast(`Added ${queueTracks.length} tracks to queue`, 'success'); + showToast($t('toast.addedTracksToQueue', { count: queueTracks.length }), 'success'); } else { - showToast('Failed to add to queue', 'error'); + showToast($t('toast.failedAddToQueue'), 'error'); } } @@ -1695,7 +1702,7 @@ const album = await fetchAlbumDetail(albumId); addAlbumToPlaylist(album); } - + function addAlbumToPlaylist(album: AlbumDetail | null) { if (!album?.tracks?.length) return; const trackIds = album.tracks.map(t => t.id); @@ -1707,14 +1714,14 @@ if (!selectedAlbum?.id) return; const url = `https://play.qobuz.com/album/${selectedAlbum.id}`; writeText(url); - showToast('Album link copied to clipboard', 'success'); + showToast($t('toast.albumLinkCopied'), 'success'); } // Share album via album.link async function shareAlbumSonglink() { if (!selectedAlbum?.id) return; try { - showToast('Fetching Album.link...', 'info'); + showToast($t('toast.fetchingAlbumLink'), 'info'); const response = await invoke<{ pageUrl: string }>('share_album_songlink', { upc: selectedAlbum.upc || null, albumId: selectedAlbum.id, @@ -1722,10 +1729,10 @@ artist: selectedAlbum.artist }); writeText(response.pageUrl); - showToast('Album.link copied to clipboard', 'success'); + showToast($t('toast.albumLinkCopiedSonglink'), 'success'); } catch (err) { console.error('Failed to get Album.link:', err); - showToast(`Album.link error: ${err}`, 'error'); + showToast($t('toast.albumLinkError', { error: String(err) }), 'error'); } } @@ -1751,20 +1758,20 @@ bitDepth: track.bitDepth, sampleRate: track.samplingRate, }); - showToast(`Preparing "${track.title}" for offline`, 'info'); + showToast($t('toast.preparingTrackOffline', { title: track.title }), 'info'); } catch (err) { console.error('Failed to cache for offline:', err); - showToast('Failed to prepare for offline', 'error'); + showToast($t('toast.failedPrepareOffline'), 'error'); } } async function handleTrackRemoveDownload(trackId: number) { try { await removeCachedTrack(trackId); - showToast('Removed from offline library', 'info'); + showToast($t('toast.removedFromOffline'), 'info'); } catch (err) { console.error('Failed to remove from offline:', err); - showToast('Failed to remove from offline library', 'error'); + showToast($t('toast.failedRemoveOffline'), 'error'); } } @@ -1773,7 +1780,7 @@ await openTrackFolder(trackId); } catch (err) { console.error('Failed to open folder:', err); - showToast('Failed to open folder', 'error'); + showToast($t('toast.failedOpenFolder'), 'error'); } } @@ -1791,10 +1798,10 @@ bitDepth: 'bitDepth' in track ? track.bitDepth : undefined, sampleRate: 'samplingRate' in track ? track.samplingRate : undefined, }); - showToast(`Refreshing "${track.title}" for offline`, 'info'); + showToast($t('toast.refreshingTrackOffline', { title: track.title }), 'info'); } catch (err) { console.error('Failed to refresh offline copy:', err); - showToast('Failed to refresh offline copy', 'error'); + showToast($t('toast.failedRefreshOffline'), 'error'); } } @@ -1811,11 +1818,11 @@ }); if (tracksToDownload.length === 0) { - showToast('All tracks already available offline', 'info'); + showToast($t('toast.allTracksOffline'), 'info'); return; } - showToast(`Preparing ${tracksToDownload.length} tracks from "${selectedAlbum.title}" for offline`, 'info'); + showToast($t('toast.preparingTracksOffline', { count: tracksToDownload.length, album: selectedAlbum.title }), 'info'); for (const track of tracksToDownload) { try { @@ -1838,19 +1845,19 @@ async function handleOpenAlbumFolder() { if (!selectedAlbum) return; - + try { await openAlbumFolder(selectedAlbum.id); } catch (err) { console.error('Failed to open album folder:', err); - showToast('Failed to open album folder', 'error'); + showToast($t('toast.failedOpenAlbumFolder'), 'error'); } } async function handleReDownloadAlbum() { if (!selectedAlbum) return; - showToast(`Refreshing all tracks from "${selectedAlbum.title}" for offline`, 'info'); + showToast($t('toast.refreshingAlbumOffline', { album: selectedAlbum.title }), 'info'); for (const track of selectedAlbum.tracks) { try { @@ -1876,7 +1883,7 @@ await openAlbumFolder(albumId); } catch (err) { console.error('Failed to open album folder:', err); - showToast('Failed to open album folder', 'error'); + showToast($t('toast.failedOpenAlbumFolder'), 'error'); } } @@ -1884,11 +1891,11 @@ try { const album = await invoke('get_album', { albumId }); if (!album || !album.tracks || album.tracks.data.length === 0) { - showToast('Failed to load album for offline refresh', 'error'); + showToast($t('toast.failedLoadAlbumRefresh'), 'error'); return; } - showToast(`Refreshing all tracks from "${album.title}" for offline`, 'info'); + showToast($t('toast.refreshingAlbumOffline', { album: album.title }), 'info'); for (const track of album.tracks.data) { try { @@ -1909,7 +1916,7 @@ } } catch (err) { console.error('Failed to load album:', err); - showToast('Failed to load album for offline refresh', 'error'); + showToast($t('toast.failedLoadAlbumRefresh'), 'error'); } } @@ -1937,10 +1944,10 @@ bitDepth: track.bitDepth, sampleRate: track.samplingRate, }); - showToast(`Preparing "${track.title}" for offline`, 'info'); + showToast($t('toast.preparingTrackOffline', { title: track.title }), 'info'); } catch (err) { console.error('Failed to prepare for offline:', err); - showToast('Failed to prepare for offline', 'error'); + showToast($t('toast.failedPrepareOffline'), 'error'); } } @@ -2053,9 +2060,9 @@ const isLocal = playlistModalTracksAreLocal; if (playlistModalMode === 'addTrack') { - showToast('Track(s) added to playlist', 'success'); + showToast($t('toast.tracksAddedToPlaylist'), 'success'); } else { - showToast('Playlist created', 'success'); + showToast($t('toast.playlistCreated'), 'success'); } sidebarRef?.refreshPlaylists(); sidebarRef?.refreshPlaylistSettings(); @@ -2106,12 +2113,12 @@ subscription: 'Local Library Only' }); navigateTo('library'); - showToast('Started in offline mode', 'info'); + showToast($t('toast.offlineModeStarted'), 'info'); } async function handleLoginSuccess(info: UserInfo) { setLoggedIn(info); - showToast(`Welcome, ${info.userName}!`, 'success'); + showToast($t('toast.welcomeUser', { name: info.userName }), 'success'); // Load favorites now that login is confirmed (sync with Qobuz) loadFavorites(); // Track favorites @@ -2239,10 +2246,10 @@ setLoggedOut(); currentTrack = null; isPlaying = false; - showToast('Logged out successfully', 'info'); + showToast($t('toast.logoutSuccess'), 'info'); } catch (err) { console.error('Logout error:', err); - showToast('Failed to logout', 'error'); + showToast($t('toast.failedLogout'), 'error'); } } @@ -3222,7 +3229,7 @@ lyricsActive={lyricsSidebarVisible} onArtistClick={() => { if (currentTrack?.isLocal) { - showToast('Local track - search for artist in Search', 'info'); + showToast($t('toast.localTrackSearch'), 'info'); } else if (currentTrack?.artistId) { handleArtistClick(currentTrack.artistId); } From aa98b4d4110cc4ceef4f6e5aa3186ddb6d75de41 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:48:05 -0600 Subject: [PATCH 2/7] i18n: Update toast messages in remaining components - Update LocalLibraryTagEditorModal to use toast translations - Update PlaylistImportModal to use toast translations - Update PlaylistModal to use toast translations - Update LocalLibraryView to use toast translations - Update SettingsView to use toast translations for error messages --- .../components/LocalLibraryTagEditorModal.svelte | 15 ++++++++------- src/lib/components/PlaylistImportModal.svelte | 4 ++-- src/lib/components/PlaylistModal.svelte | 5 +++-- src/lib/components/views/LocalLibraryView.svelte | 8 ++++---- src/lib/components/views/SettingsView.svelte | 14 +++++++------- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/lib/components/LocalLibraryTagEditorModal.svelte b/src/lib/components/LocalLibraryTagEditorModal.svelte index d80174b08..fb383ae21 100644 --- a/src/lib/components/LocalLibraryTagEditorModal.svelte +++ b/src/lib/components/LocalLibraryTagEditorModal.svelte @@ -5,6 +5,7 @@ import { ask } from '@tauri-apps/plugin-dialog'; import { openUrl } from '@tauri-apps/plugin-opener'; import { showToast } from '$lib/stores/toastStore'; + import { t } from '$lib/i18n'; interface LocalTrack { id: number; @@ -153,7 +154,7 @@ // Remote metadata functions async function searchRemoteMetadata() { if (!albumTitle.trim() && !albumArtist.trim()) { - showToast('Enter album title or artist to search', 'error'); + showToast($t('toast.enterSearchTerms'), 'error'); return; } @@ -173,7 +174,7 @@ showRemotePanel = results.length > 0; } catch (err) { console.error('Remote search failed:', err); - showToast(`Search failed: ${err}`, 'error'); + showToast($t('toast.searchFailed', { error: String(err) }), 'error'); hasSearched = true; } finally { remoteSearching = false; @@ -224,7 +225,7 @@ 'warning' ); } else { - showToast(`Applied metadata from ${providerName}`, 'success'); + showToast($t('toast.appliedMetadata', { provider: providerName }), 'success'); } showRemotePanel = false; } catch (err) { @@ -232,9 +233,9 @@ // Check for rate limiting const errStr = String(err); if (errStr.includes('429') || errStr.toLowerCase().includes('rate')) { - showToast('Rate limited. Please wait a moment and try again.', 'warning'); + showToast($t('toast.rateLimited'), 'warning'); } else { - showToast(`Failed to fetch metadata: ${err}`, 'error'); + showToast($t('toast.failedFetchMetadata', { error: String(err) }), 'error'); } } finally { remoteLoading = false; @@ -256,7 +257,7 @@ await openUrl(url); } catch (err) { console.error('Failed to open URL:', err); - showToast('Failed to open browser', 'error'); + showToast($t('toast.failedOpenBrowser'), 'error'); } } @@ -360,7 +361,7 @@ }); await invoke('library_write_album_metadata_to_files', { request: payload }); } - showToast('Album metadata saved', 'success'); + showToast($t('toast.albumMetadataSaved'), 'success'); await onSaved(); onClose(); } catch (err) { diff --git a/src/lib/components/PlaylistImportModal.svelte b/src/lib/components/PlaylistImportModal.svelte index d0729566a..a29fae5ff 100644 --- a/src/lib/components/PlaylistImportModal.svelte +++ b/src/lib/components/PlaylistImportModal.svelte @@ -155,12 +155,12 @@ onSuccess?.(result); if (result.matched_tracks > 0) { - showToast('Playlist imported', 'success'); + showToast($t('toast.playlistImported'), 'success'); } } catch (err) { error = String(err); pushLog(`Import failed: ${error}`, 'error'); - showToast('Playlist import failed', 'error'); + showToast($t('toast.playlistImportFailed'), 'error'); } finally { loading = false; } diff --git a/src/lib/components/PlaylistModal.svelte b/src/lib/components/PlaylistModal.svelte index 104f1539c..979634afd 100644 --- a/src/lib/components/PlaylistModal.svelte +++ b/src/lib/components/PlaylistModal.svelte @@ -4,6 +4,7 @@ import { logPlaylistAdd } from '$lib/services/recoService'; import { subscribe as subscribeOffline, getStatus, createPendingPlaylist } from '$lib/stores/offlineStore'; import { showToast } from '$lib/stores/toastStore'; + import { t } from '$lib/i18n'; import { subscribe as subscribeFolders, getVisibleFolders, @@ -241,7 +242,7 @@ [], // No Qobuz tracks for empty playlist [] // No local tracks for empty playlist ); - showToast('Playlist created offline - will sync when back online', 'info'); + showToast($t('toast.playlistCreatedOffline'), 'info'); // Create a temporary playlist object for UI const tempPlaylist: Playlist = { id: -pendingId, // Negative ID to distinguish from real playlists @@ -429,7 +430,7 @@ qobuzTrackIds, localTrackPaths ); - showToast(`Playlist "${name.trim()}" created offline - will sync when back online`, 'info'); + showToast($t('toast.playlistCreatedOffline'), 'info'); // Create a temporary playlist object for UI const tempPlaylist: Playlist = { diff --git a/src/lib/components/views/LocalLibraryView.svelte b/src/lib/components/views/LocalLibraryView.svelte index 1f2fb0858..66d3e0055 100644 --- a/src/lib/components/views/LocalLibraryView.svelte +++ b/src/lib/components/views/LocalLibraryView.svelte @@ -1357,19 +1357,19 @@ if (result.removed > 0) { cleanupStatus = `Removed ${result.removed} of ${result.checked} tracks`; - showToast(`Removed ${result.removed} tracks with missing files`, 'success'); + showToast($t('toast.removedMissingFiles', { count: result.removed }), 'success'); // Reload library data cleanupStatus = 'Refreshing library...'; await loadAlbums(); await loadArtists(); } else { cleanupStatus = `Checked ${result.checked} tracks - all OK`; - showToast('No missing files found', 'info'); + showToast($t('toast.noMissingFilesFound'), 'info'); } } catch (err) { console.error('Failed to cleanup missing files:', err); cleanupStatus = 'Error during cleanup'; - showToast('Failed to cleanup missing files', 'error'); + showToast($t('toast.failedCleanupMissing'), 'error'); } finally { cleaningUpMissingFiles = false; // Clear status after a delay @@ -1761,7 +1761,7 @@ refreshingAlbumMetadata = true; albumMetadataRefreshed = false; await invoke('library_refresh_album_metadata_from_files', { albumGroupKey: selectedAlbum.id }); - showToast('Metadata refreshed from files', 'success'); + showToast($t('toast.metadataRefreshed'), 'success'); albumMetadataRefreshed = true; await handleTagEditorSaved(); } catch (err) { diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index 19c0c5089..026d70c8c 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -1595,7 +1595,7 @@ showToast($t('settings.appearance.tray.enableTrayDesc'), 'info'); } catch (err) { console.error('Failed to set enable tray:', err); - showToast('Failed to save tray setting', 'error'); + showToast($t('toast.failedSaveTray'), 'error'); } } @@ -1605,7 +1605,7 @@ minimizeToTray = value; } catch (err) { console.error('Failed to set minimize to tray:', err); - showToast('Failed to save tray setting', 'error'); + showToast($t('toast.failedSaveTray'), 'error'); } } @@ -1615,7 +1615,7 @@ closeToTray = value; } catch (err) { console.error('Failed to set close to tray:', err); - showToast('Failed to save tray setting', 'error'); + showToast($t('toast.failedSaveTray'), 'error'); } } @@ -1627,7 +1627,7 @@ console.log('[Settings] Autoplay mode saved successfully'); } catch (err) { console.error('[Settings] Failed to set autoplay mode:', err); - showToast('Failed to save autoplay preference', 'error'); + showToast($t('toast.failedSaveAutoplay'), 'error'); } } @@ -1639,7 +1639,7 @@ console.log('[Settings] Show context icon saved successfully'); } catch (err) { console.error('[Settings] Failed to set show context icon:', err); - showToast('Failed to save icon visibility preference', 'error'); + showToast($t('toast.failedSaveIconVisibility'), 'error'); } } @@ -1681,7 +1681,7 @@ notifyDownloadSettingsChanged(); } catch (err) { console.error('Failed to repair downloads:', err); - showToast('Failed to repair offline library: ' + String(err), 'error'); + showToast($t('toast.failedRepairOffline', { error: String(err) }), 'error'); } finally { isRepairingDownloads = false; } @@ -1705,7 +1705,7 @@ await invoke('open_offline_cache_folder'); } catch (err) { console.error('Failed to open cache folder:', err); - showToast('Failed to open cache folder', 'error'); + showToast($t('toast.failedOpenCacheFolder'), 'error'); } } From 764e55d1e8771fb6a584a2ce9489ff32c759ea68 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:17:18 -0600 Subject: [PATCH 3/7] fix: use $derived for networkFsOptions to support i18n reactivity --- src/lib/components/FolderSettingsModal.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/components/FolderSettingsModal.svelte b/src/lib/components/FolderSettingsModal.svelte index 465319cf1..98f047188 100644 --- a/src/lib/components/FolderSettingsModal.svelte +++ b/src/lib/components/FolderSettingsModal.svelte @@ -47,8 +47,8 @@ let accessible = $state(true); let checkingAccessibility = $state(false); - // Network filesystem options - const networkFsOptions = [ + // Network filesystem options (derived to support i18n reactivity) + const networkFsOptions = $derived([ { value: '', label: $t('library.networkFs.auto') }, { value: 'cifs', label: 'SMB/CIFS (Samba)' }, { value: 'nfs', label: 'NFS' }, @@ -58,7 +58,7 @@ { value: 'glusterfs', label: 'GlusterFS' }, { value: 'ceph', label: 'CephFS' }, { value: 'other', label: $t('library.networkFs.other') } - ]; + ]); // Reset form when modal opens $effect(() => { From 55d383c431af36871480d05e3f58a3d02dbf65cf Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:24:52 -0600 Subject: [PATCH 4/7] fix(i18n): correct svelte-i18n interpolation format svelte-i18n requires { values: { key: value } } format for variable interpolation, not { key: value } directly. Fixed across all toast and greeting calls. --- .../LocalLibraryTagEditorModal.svelte | 6 +-- src/lib/components/views/HomeView.svelte | 2 +- .../components/views/LocalLibraryView.svelte | 2 +- src/lib/components/views/SettingsView.svelte | 2 +- src/routes/+page.svelte | 38 +++++++++---------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/lib/components/LocalLibraryTagEditorModal.svelte b/src/lib/components/LocalLibraryTagEditorModal.svelte index fb383ae21..9b7209f62 100644 --- a/src/lib/components/LocalLibraryTagEditorModal.svelte +++ b/src/lib/components/LocalLibraryTagEditorModal.svelte @@ -174,7 +174,7 @@ showRemotePanel = results.length > 0; } catch (err) { console.error('Remote search failed:', err); - showToast($t('toast.searchFailed', { error: String(err) }), 'error'); + showToast($t('toast.searchFailed', { values: { error: String(err) } }), 'error'); hasSearched = true; } finally { remoteSearching = false; @@ -225,7 +225,7 @@ 'warning' ); } else { - showToast($t('toast.appliedMetadata', { provider: providerName }), 'success'); + showToast($t('toast.appliedMetadata', { values: { provider: providerName } }), 'success'); } showRemotePanel = false; } catch (err) { @@ -235,7 +235,7 @@ if (errStr.includes('429') || errStr.toLowerCase().includes('rate')) { showToast($t('toast.rateLimited'), 'warning'); } else { - showToast($t('toast.failedFetchMetadata', { error: String(err) }), 'error'); + showToast($t('toast.failedFetchMetadata', { values: { error: String(err) } }), 'error'); } } finally { remoteLoading = false; diff --git a/src/lib/components/views/HomeView.svelte b/src/lib/components/views/HomeView.svelte index 4364c1d1b..136b45255 100644 --- a/src/lib/components/views/HomeView.svelte +++ b/src/lib/components/views/HomeView.svelte @@ -133,7 +133,7 @@ if (info.type === 'custom') { return info.text; } - return $t(info.key, { name: info.name }); + return $t(info.key, { values: { name: info.name } }); }); diff --git a/src/lib/components/views/LocalLibraryView.svelte b/src/lib/components/views/LocalLibraryView.svelte index 66d3e0055..9b0d8ec75 100644 --- a/src/lib/components/views/LocalLibraryView.svelte +++ b/src/lib/components/views/LocalLibraryView.svelte @@ -1357,7 +1357,7 @@ if (result.removed > 0) { cleanupStatus = `Removed ${result.removed} of ${result.checked} tracks`; - showToast($t('toast.removedMissingFiles', { count: result.removed }), 'success'); + showToast($t('toast.removedMissingFiles', { values: { count: result.removed } }), 'success'); // Reload library data cleanupStatus = 'Refreshing library...'; await loadAlbums(); diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index 026d70c8c..ffbda527f 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -1681,7 +1681,7 @@ notifyDownloadSettingsChanged(); } catch (err) { console.error('Failed to repair downloads:', err); - showToast($t('toast.failedRepairOffline', { error: String(err) }), 'error'); + showToast($t('toast.failedRepairOffline', { values: { error: String(err) } }), 'error'); } finally { isRepairingDownloads = false; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 80ee2dcdc..0ea531aed 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -663,7 +663,7 @@ * - Weak (1), None (0): Show Informational Modal */ async function handleMusicianClick(name: string, role: string) { - showToast($t('toast.lookingUp', { name }), 'info'); + showToast($t('toast.lookingUp', { values: { name } }), 'info'); try { const musician = await invoke('resolve_musician', { name, role }); console.log('Resolved musician:', musician); @@ -929,7 +929,7 @@ artist_id: t.artistId ?? album.artistId }); } - showToast($t('toast.playingTracksNext', { count: album.tracks.length }), 'success'); + showToast($t('toast.playingTracksNext', { values: { count: album.tracks.length } }), 'success'); } async function queueAlbumLaterById(albumId: string) { @@ -954,7 +954,7 @@ const success = await addTracksToQueue(queueTracks); if (success) { - showToast($t('toast.addedTracksToQueue', { count: queueTracks.length }), 'success'); + showToast($t('toast.addedTracksToQueue', { values: { count: queueTracks.length } }), 'success'); } else { showToast($t('toast.failedAddToQueue'), 'error'); } @@ -984,7 +984,7 @@ showToast($t('toast.albumLinkCopiedSonglink'), 'success'); } catch (err) { console.error('Failed to get Album.link:', err); - showToast($t('toast.albumLinkError', { error: String(err) }), 'error'); + showToast($t('toast.albumLinkError', { values: { error: String(err) } }), 'error'); } } @@ -1002,7 +1002,7 @@ return; } - showToast($t('toast.preparingTracksOffline', { count: tracksToDownload.length, album: album.title }), 'info'); + showToast($t('toast.preparingTracksOffline', { values: { count: tracksToDownload.length, album: album.title } }), 'info'); for (const track of tracksToDownload) { try { @@ -1135,7 +1135,7 @@ artist_id: t.performer?.id }); } - showToast($t('toast.playingTracksNext', { count: tracks.length }), 'success'); + showToast($t('toast.playingTracksNext', { values: { count: tracks.length } }), 'success'); } async function queuePlaylistLaterById(playlistId: number) { @@ -1163,7 +1163,7 @@ const success = await addTracksToQueue(queueTracks); if (success) { - showToast($t('toast.addedTracksToQueue', { count: queueTracks.length }), 'success'); + showToast($t('toast.addedTracksToQueue', { values: { count: queueTracks.length } }), 'success'); } else { showToast($t('toast.failedAddToQueue'), 'error'); } @@ -1177,7 +1177,7 @@ showToast($t('toast.playlistCopied'), 'success'); } catch (err) { console.error('Failed to copy playlist:', err); - showToast($t('toast.failedCopyPlaylist', { error: String(err) }), 'error'); + showToast($t('toast.failedCopyPlaylist', { values: { error: String(err) } }), 'error'); } } @@ -1195,7 +1195,7 @@ sidebarRef?.refreshPlaylistSettings(); } catch (err) { console.error('Failed to remove playlist favorite:', err); - showToast($t('toast.failedRemoveFavorites', { error: String(err) }), 'error'); + showToast($t('toast.failedRemoveFavorites', { values: { error: String(err) } }), 'error'); } } @@ -1667,7 +1667,7 @@ artist_id: t.artistId ?? selectedAlbum?.artistId }); } - showToast($t('toast.playingTracksNext', { count: selectedAlbum.tracks.length }), 'success'); + showToast($t('toast.playingTracksNext', { values: { count: selectedAlbum.tracks.length } }), 'success'); } // Add all album tracks to end of queue @@ -1692,7 +1692,7 @@ const success = await addTracksToQueue(queueTracks); if (success) { - showToast($t('toast.addedTracksToQueue', { count: queueTracks.length }), 'success'); + showToast($t('toast.addedTracksToQueue', { values: { count: queueTracks.length } }), 'success'); } else { showToast($t('toast.failedAddToQueue'), 'error'); } @@ -1732,7 +1732,7 @@ showToast($t('toast.albumLinkCopiedSonglink'), 'success'); } catch (err) { console.error('Failed to get Album.link:', err); - showToast($t('toast.albumLinkError', { error: String(err) }), 'error'); + showToast($t('toast.albumLinkError', { values: { error: String(err) } }), 'error'); } } @@ -1758,7 +1758,7 @@ bitDepth: track.bitDepth, sampleRate: track.samplingRate, }); - showToast($t('toast.preparingTrackOffline', { title: track.title }), 'info'); + showToast($t('toast.preparingTrackOffline', { values: { title: track.title } }), 'info'); } catch (err) { console.error('Failed to cache for offline:', err); showToast($t('toast.failedPrepareOffline'), 'error'); @@ -1798,7 +1798,7 @@ bitDepth: 'bitDepth' in track ? track.bitDepth : undefined, sampleRate: 'samplingRate' in track ? track.samplingRate : undefined, }); - showToast($t('toast.refreshingTrackOffline', { title: track.title }), 'info'); + showToast($t('toast.refreshingTrackOffline', { values: { title: track.title } }), 'info'); } catch (err) { console.error('Failed to refresh offline copy:', err); showToast($t('toast.failedRefreshOffline'), 'error'); @@ -1822,7 +1822,7 @@ return; } - showToast($t('toast.preparingTracksOffline', { count: tracksToDownload.length, album: selectedAlbum.title }), 'info'); + showToast($t('toast.preparingTracksOffline', { values: { count: tracksToDownload.length, album: selectedAlbum.title } }), 'info'); for (const track of tracksToDownload) { try { @@ -1857,7 +1857,7 @@ async function handleReDownloadAlbum() { if (!selectedAlbum) return; - showToast($t('toast.refreshingAlbumOffline', { album: selectedAlbum.title }), 'info'); + showToast($t('toast.refreshingAlbumOffline', { values: { album: selectedAlbum.title } }), 'info'); for (const track of selectedAlbum.tracks) { try { @@ -1895,7 +1895,7 @@ return; } - showToast($t('toast.refreshingAlbumOffline', { album: album.title }), 'info'); + showToast($t('toast.refreshingAlbumOffline', { values: { album: album.title } }), 'info'); for (const track of album.tracks.data) { try { @@ -1944,7 +1944,7 @@ bitDepth: track.bitDepth, sampleRate: track.samplingRate, }); - showToast($t('toast.preparingTrackOffline', { title: track.title }), 'info'); + showToast($t('toast.preparingTrackOffline', { values: { title: track.title } }), 'info'); } catch (err) { console.error('Failed to prepare for offline:', err); showToast($t('toast.failedPrepareOffline'), 'error'); @@ -2118,7 +2118,7 @@ async function handleLoginSuccess(info: UserInfo) { setLoggedIn(info); - showToast($t('toast.welcomeUser', { name: info.userName }), 'success'); + showToast($t('toast.welcomeUser', { values: { name: info.userName } }), 'success'); // Load favorites now that login is confirmed (sync with Qobuz) loadFavorites(); // Track favorites From 805fec1529f05068be01a1df42058db8b52089e5 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:29:04 -0600 Subject: [PATCH 5/7] i18n: add Settings audio, offline library, and updates translations Complete Spanish translations for: - Audio settings (backend, ALSA plugin, hardware volume, streaming) - Offline library section (repair, manage, clear) - Updates section (check on launch, show changelog) - Flatpak warning messages --- src/lib/components/views/SettingsView.svelte | 80 ++++++++++---------- src/lib/i18n/locales/en.json | 45 ++++++++++- src/lib/i18n/locales/es.json | 45 ++++++++++- 3 files changed, 126 insertions(+), 44 deletions(-) diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index ffbda527f..dc54a04d4 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -1894,8 +1894,8 @@
-

Loading audio devices...

-

Parsing hardware information

+

{$t('settings.audio.loadingAudioDevices')}

+

{$t('settings.audio.parsingHardware')}

{/if} @@ -1953,15 +1953,15 @@
- Limit Quality to Device - When enabled, overrides the quality setting above if your audio device doesn't support the selected sample rate. Prevents resampling to guarantee bit-perfect playback. + {$t('settings.audio.limitQualityToDevice')} + {$t('settings.audio.limitQualityToDeviceDesc')}
- Audio Backend - Choose audio system: Auto (recommended), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy). + {$t('settings.audio.audioBackend')} + {$t('settings.audio.audioBackendDesc')}
{$t('settings.audio.outputDevice')} - Select your preferred audio output device. Devices shown are from the selected backend. + {$t('settings.audio.outputDeviceDesc')}
{#if isLoadingDevices} - Loading devices... + {$t('settings.audio.loadingDevices')} {:else if selectedBackend === 'ALSA Direct'}
- Repair Offline Library - Fix offline markers lost during library scans + {$t('settings.offlineLibrary.repair')} + {$t('settings.offlineLibrary.repairDesc')}
- Clear Offline Library + {$t('settings.offlineLibrary.clearCache')}
- Manage Offline Cache - Open the cache folder in your file manager + {$t('settings.offlineLibrary.manageCache')} + {$t('settings.offlineLibrary.manageCacheDesc')}
{/if} @@ -2547,11 +2547,11 @@
-

Updates

+

{$t('settings.updates.title')}

- Check for new releases on launch + {$t('settings.updates.checkOnLaunch')}
- Check for updates now + {$t('settings.updates.checkNow')}
- Show what's new on new version launch + {$t('settings.updates.showWhatsNew')}
- Show changelog for current version + {$t('settings.updates.showChangelog')} {#if updatesCurrentVersion} - Current version: v{updatesCurrentVersion} + {$t('settings.updates.currentVersion', { values: { version: updatesCurrentVersion } })} {/if}
@@ -2763,7 +2763,7 @@ onclick={handleClearAllCaches} disabled={isClearingAllCaches} > - {isClearingAllCaches ? $t('settings.storage.clearing') : 'Clear All'} + {isClearingAllCaches ? $t('settings.storage.clearing') : $t('actions.clearAll')}
{/if} diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index d58f35bb6..1ad76d85a 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -344,8 +344,10 @@ "edit": "Edit", "close": "Close", "open": "Open", + "show": "Show", "refresh": "Refresh", "retry": "Retry", + "repair": "Repair", "clear": "Clear", "clearAll": "Clear All", "confirm": "Confirm", @@ -373,13 +375,36 @@ "streamingQualityDesc": "Maximum quality for streaming and playback cache", "qualityChangedCacheCleared": "Quality changed. Playback cache cleared.", "outputDevice": "Output Device", + "outputDeviceDesc": "Select your preferred audio output device. Devices shown are from the selected backend.", "audioDevice": "Audio Device", "exclusiveMode": "Exclusive Mode", "exclusiveModeDesc": "Lock audio device for exclusive use", "dacPassthrough": "DAC Passthrough", "dacPassthroughDesc": "Bypass system mixer for bit-perfect output", "currentSampleRate": "Current Sample Rate", - "noActivePlayback": "No active playback" + "noActivePlayback": "No active playback", + "loadingDevices": "Loading devices...", + "loadingAudioDevices": "Loading audio devices...", + "parsingHardware": "Parsing hardware information", + "limitQualityToDevice": "Limit Quality to Device", + "limitQualityToDeviceDesc": "When enabled, overrides the quality setting above if your audio device doesn't support the selected sample rate. Prevents resampling to guarantee bit-perfect playback.", + "audioBackend": "Audio Backend", + "audioBackendDesc": "Choose audio system: Auto (recommended), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy).", + "alsaPlugin": "ALSA Plugin", + "alsaPluginDesc": "hw: Bit-perfect, exclusive. plughw: Auto-convert. pcm: Most compatible.", + "hardwareVolume": "Enable Hardware Volume Control", + "hardwareVolumeDesc": "Experimental: Controls DAC volume via ALSA mixer. Some DACs don't support this - disable for maximum compatibility. If it fails, playback continues normally.", + "helpBitPerfect": "Help with bit-perfect device detection", + "flatpakWarningTitle": "Flatpak Limitation:", + "flatpakWarningDesc": "PipeWire cannot guarantee bit-perfect playback in sandboxed environments due to daemon access restrictions.", + "flatpakRecommended": "Recommended:", + "flatpakRecommendedDesc": "Switch to ALSA Direct backend for true bit-perfect audio.", + "streamUncached": "Stream Uncached Tracks", + "streamUncachedDesc": "Start playback faster when track is not in cache. Seeking may be limited during initial buffering.", + "initialBuffer": "Initial Buffer Size", + "initialBufferDesc": "Seconds to buffer before starting playback ({seconds}s)", + "streamingOnly": "Streaming Only", + "streamingOnlyDesc": "Disables temporary cache. Not recommended without a fast connection. Offline library tracks always play first." }, "playback": { "title": "Playback", @@ -465,7 +490,23 @@ "disclaimer": "The offline library is a temporary playback cache for offline listening while you have a valid Qobuz subscription. You do not receive a license to keep or redistribute the content. If your subscription becomes invalid, QBZ will remove all cached content after 3 days.", "cachedTracks": "Offline Tracks", "clearCache": "Clear All", - "openFolder": "Open Folder" + "openFolder": "Open Folder", + "repair": "Repair Offline Library", + "repairDesc": "Fix offline markers lost during library scans", + "manageCache": "Manage Offline Cache", + "manageCacheDesc": "Open the cache folder in your file manager", + "sectionSummary": "Cached tracks for offline playback", + "repairing": "Repairing..." + }, + "updates": { + "title": "Updates", + "checkOnLaunch": "Check for new releases on launch", + "checkNow": "Check for updates now", + "checking": "Checking...", + "check": "Check", + "showWhatsNew": "Show what's new on new version launch", + "showChangelog": "Show changelog for current version", + "currentVersion": "Current version: v{version}" }, "networkCacheWarning": "The selected folder is on a network drive.", "networkCacheWarningDetail": "Cached tracks may not be available when the network is disconnected or in offline mode. Are you sure you want to use this location?", diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index f54f16f46..3c6cdaf04 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -344,8 +344,10 @@ "edit": "Editar", "close": "Cerrar", "open": "Abrir", + "show": "Mostrar", "refresh": "Actualizar", "retry": "Reintentar", + "repair": "Reparar", "clear": "Limpiar", "clearAll": "Limpiar Todo", "confirm": "Confirmar", @@ -373,13 +375,36 @@ "streamingQualityDesc": "Calidad máxima para streaming y caché de reproducción", "qualityChangedCacheCleared": "Calidad cambiada. Caché de reproducción limpiado.", "outputDevice": "Dispositivo de Salida", + "outputDeviceDesc": "Selecciona tu dispositivo de audio preferido. Se muestran los dispositivos del backend seleccionado.", "audioDevice": "Dispositivo de Audio", "exclusiveMode": "Modo Exclusivo", "exclusiveModeDesc": "Bloquear dispositivo de audio para uso exclusivo", "dacPassthrough": "Passthrough DAC", "dacPassthroughDesc": "Bypass del mixer del sistema para salida bit-perfect", "currentSampleRate": "Tasa de Muestreo Actual", - "noActivePlayback": "Sin reproducción activa" + "noActivePlayback": "Sin reproducción activa", + "loadingDevices": "Cargando dispositivos...", + "loadingAudioDevices": "Cargando dispositivos de audio...", + "parsingHardware": "Analizando información de hardware", + "limitQualityToDevice": "Limitar Calidad al Dispositivo", + "limitQualityToDeviceDesc": "Cuando está activado, anula la configuración de calidad si tu dispositivo de audio no soporta la tasa de muestreo seleccionada. Previene el remuestreo para garantizar reproducción bit-perfect.", + "audioBackend": "Backend de Audio", + "audioBackendDesc": "Elige el sistema de audio: Auto (recomendado), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo), o PulseAudio (legacy).", + "alsaPlugin": "Plugin ALSA", + "alsaPluginDesc": "hw: Bit-perfect, exclusivo. plughw: Auto-conversión. pcm: Más compatible.", + "hardwareVolume": "Habilitar Control de Volumen por Hardware", + "hardwareVolumeDesc": "Experimental: Controla el volumen del DAC vía mixer ALSA. Algunos DACs no lo soportan - desactívalo para máxima compatibilidad. Si falla, la reproducción continúa normalmente.", + "helpBitPerfect": "Ayuda con detección de dispositivos bit-perfect", + "flatpakWarningTitle": "Limitación de Flatpak:", + "flatpakWarningDesc": "PipeWire no puede garantizar reproducción bit-perfect en entornos sandbox debido a restricciones de acceso al daemon.", + "flatpakRecommended": "Recomendado:", + "flatpakRecommendedDesc": "Cambiar al backend ALSA Direct para audio bit-perfect verdadero.", + "streamUncached": "Transmitir Pistas Sin Caché", + "streamUncachedDesc": "Inicia la reproducción más rápido cuando la pista no está en caché. La búsqueda puede estar limitada durante el buffering inicial.", + "initialBuffer": "Tamaño del Buffer Inicial", + "initialBufferDesc": "Segundos a almacenar en buffer antes de iniciar reproducción ({seconds}s)", + "streamingOnly": "Solo Streaming", + "streamingOnlyDesc": "Desactiva el caché temporal. No recomendado sin una conexión rápida. Las pistas de la biblioteca offline siempre se reproducen primero." }, "playback": { "title": "Reproducción", @@ -465,7 +490,23 @@ "disclaimer": "La biblioteca offline es un caché temporal de reproducción para escucha sin conexión mientras tengas una suscripción válida de Qobuz. No recibes una licencia para conservar o redistribuir el contenido. Si tu suscripción deja de ser válida, QBZ eliminará todo el contenido en caché después de 3 días.", "cachedTracks": "Pistas Offline", "clearCache": "Limpiar Todo", - "openFolder": "Abrir Carpeta" + "openFolder": "Abrir Carpeta", + "repair": "Reparar Biblioteca Offline", + "repairDesc": "Corregir marcadores offline perdidos durante escaneos de biblioteca", + "manageCache": "Administrar Caché Offline", + "manageCacheDesc": "Abrir la carpeta de caché en tu administrador de archivos", + "sectionSummary": "Pistas en caché para reproducción offline", + "repairing": "Reparando..." + }, + "updates": { + "title": "Actualizaciones", + "checkOnLaunch": "Buscar nuevas versiones al iniciar", + "checkNow": "Buscar actualizaciones ahora", + "checking": "Verificando...", + "check": "Verificar", + "showWhatsNew": "Mostrar novedades al lanzar nueva versión", + "showChangelog": "Mostrar registro de cambios para la versión actual", + "currentVersion": "Versión actual: v{version}" }, "networkCacheWarning": "La carpeta seleccionada está en una unidad de red.", "networkCacheWarningDetail": "Las pistas en caché pueden no estar disponibles cuando la red esté desconectada o en modo offline. ¿Estás seguro de que quieres usar esta ubicación?", From 1d2a9f6636807112a23845c74e8b84de6a190db1 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:38:02 -0600 Subject: [PATCH 6/7] i18n: add Settings, Storage, and Blacklist Manager translations - MusicBrainz integration labels - Storage section summary - Complete BlacklistManagerView translations (title, description, search, empty states, confirm modal) --- .../views/BlacklistManagerView.svelte | 39 ++++++++++--------- src/lib/components/views/SettingsView.svelte | 8 ++-- src/lib/i18n/locales/en.json | 26 ++++++++++++- src/lib/i18n/locales/es.json | 26 ++++++++++++- 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/lib/components/views/BlacklistManagerView.svelte b/src/lib/components/views/BlacklistManagerView.svelte index adcd5daed..eb9a49563 100644 --- a/src/lib/components/views/BlacklistManagerView.svelte +++ b/src/lib/components/views/BlacklistManagerView.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { ArrowLeft, Search, X, Trash2, Ban, ToggleLeft, ToggleRight, AlertCircle } from 'lucide-svelte'; import ViewTransition from '../ViewTransition.svelte'; + import { t } from '$lib/i18n'; import { showToast } from '$lib/stores/toastStore'; import { subscribe, @@ -108,17 +109,17 @@
-

Artist Blacklist

+

{$t('settings.blacklist.title')}

- Blacklisted artists are hidden from search results, radio, suggestions, and similar artists. + {$t('settings.blacklist.description')}

@@ -131,10 +132,10 @@ > {#if enabled} - Enabled + {$t('settings.blacklist.enabled')} {:else} - Disabled + {$t('settings.blacklist.disabled')} {/if} @@ -143,7 +144,7 @@ @@ -161,18 +162,18 @@ onclick={() => confirmClearOpen = true} > - Clear All + {$t('settings.blacklist.clearAll')} {/if} - {getCount()} artists + {$t('settings.blacklist.artistCount', { values: { count: getCount() } })} {#if !enabled}
- Blacklist is currently disabled. Artists below will appear in results. + {$t('settings.blacklist.disabledWarning')}
{/if} @@ -180,18 +181,18 @@ {#if loading}
-

Loading blacklist...

+

{$t('settings.blacklist.loadingBlacklist')}

{:else if artists.length === 0}
-

No blacklisted artists

- To blacklist an artist, go to their page and click the ban icon. +

{$t('settings.blacklist.noArtists')}

+ {$t('settings.blacklist.noArtistsHint')}
{:else if filteredArtists.length === 0}
-

No results for "{searchQuery}"

+

{$t('settings.blacklist.noResults', { values: { query: searchQuery } })}

{:else}
@@ -209,7 +210,7 @@
{artist.artist_name} - Added {formatDate(artist.added_at)} + {$t('settings.blacklist.addedOn', { values: { date: formatDate(artist.added_at) } })} {#if artist.notes} {artist.notes} {/if} @@ -218,7 +219,7 @@ @@ -244,16 +245,16 @@ aria-modal="true" onclick={(e) => e.stopPropagation()} > - +
diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index ff35190f3..87214048f 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -2582,9 +2582,9 @@
- MusicBrainz + {$t('settings.integrations.musicbrainz')} - Enable artist relationships and enhanced metadata from MusicBrainz. + {$t('settings.integrations.musicbrainzDesc')}
@@ -2674,7 +2674,7 @@
- MusicBrainz Cache + {$t('settings.integrations.musicbrainzCache')} {#if musicBrainzCacheStats} {musicBrainzCacheStats.artists} artists, {musicBrainzCacheStats.relations} relations, {musicBrainzCacheStats.recordings} recordings diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index f3e6f50d9..e963c5ef3 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -480,10 +480,15 @@ "connect": "Connect", "disconnect": "Disconnect", "scrobbling": "Scrobbling", - "configureApi": "Configure API" + "configureApi": "Configure API", + "musicbrainz": "MusicBrainz", + "musicbrainzDesc": "Enable artist relationships and enhanced metadata from MusicBrainz.", + "musicbrainzCache": "MusicBrainz Cache", + "listenbrainz": "ListenBrainz" }, "storage": { "title": "Storage", + "sectionSummary": "Queue, lyrics, artwork, and metadata caches", "queueCacheNote": "Queue list storage. Clear if you have playback issues. (This is not the Offline Library cache.)", "queueCacheStats": "{tracks} tracks using {used} / {max}", "cachedTracks": "Cached Queue Tracks", @@ -491,6 +496,25 @@ "clearCache": "Clear queue cache", "clearing": "Clearing..." }, + "blacklist": { + "title": "Artist Blacklist", + "description": "Blacklisted artists are hidden from search results, radio, suggestions, and similar artists.", + "enabled": "Enabled", + "disabled": "Disabled", + "clearAll": "Clear All", + "noArtists": "No blacklisted artists", + "noArtistsHint": "To blacklist an artist, go to their page and click the ban icon.", + "artistCount": "{count} artists", + "loadingBlacklist": "Loading blacklist...", + "confirmClear": "Remove all artists from blacklist?", + "disabledWarning": "Blacklist is currently disabled. Artists below will appear in results.", + "searchPlaceholder": "Search blacklisted artists...", + "noResults": "No results for \"{query}\"", + "addedOn": "Added {date}", + "remove": "Remove from blacklist", + "confirmClearTitle": "Clear Blacklist?", + "confirmClearText": "This will remove all {count} artists from your blacklist. This action cannot be undone." + }, "offlineLibrary": { "title": "Offline Library", "disclaimer": "The offline library is a temporary playback cache for offline listening while you have a valid Qobuz subscription. You do not receive a license to keep or redistribute the content. If your subscription becomes invalid, QBZ will remove all cached content after 3 days.", diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index 7b95fa8ed..38ed29006 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -480,10 +480,15 @@ "connect": "Conectar", "disconnect": "Desconectar", "scrobbling": "Scrobbling", - "configureApi": "Configurar API" + "configureApi": "Configurar API", + "musicbrainz": "MusicBrainz", + "musicbrainzDesc": "Habilita relaciones de artistas y metadatos mejorados de MusicBrainz.", + "musicbrainzCache": "Caché de MusicBrainz", + "listenbrainz": "ListenBrainz" }, "storage": { "title": "Almacenamiento", + "sectionSummary": "Cachés de cola, letras, carátulas y metadatos", "queueCacheNote": "Almacenamiento de la cola. Límpialo si tienes problemas de reproducción. (No es la Biblioteca Offline.)", "queueCacheStats": "{tracks} pistas usando {used} / {max}", "cachedTracks": "Pistas de la cola en caché", @@ -491,6 +496,25 @@ "clearCache": "Limpiar caché de la cola", "clearing": "Limpiando..." }, + "blacklist": { + "title": "Lista Negra de Artistas", + "description": "Los artistas en la lista negra están ocultos en resultados de búsqueda, radio, sugerencias y artistas similares.", + "enabled": "Activado", + "disabled": "Desactivado", + "clearAll": "Limpiar Todo", + "noArtists": "No hay artistas en la lista negra", + "noArtistsHint": "Para añadir un artista a la lista negra, ve a su página y haz clic en el ícono de prohibición.", + "artistCount": "{count} artistas", + "loadingBlacklist": "Cargando lista negra...", + "confirmClear": "¿Eliminar todos los artistas de la lista negra?", + "disabledWarning": "La lista negra está desactivada. Los artistas de abajo aparecerán en los resultados.", + "searchPlaceholder": "Buscar artistas en la lista negra...", + "noResults": "Sin resultados para \"{query}\"", + "addedOn": "Añadido {date}", + "remove": "Eliminar de la lista negra", + "confirmClearTitle": "¿Limpiar Lista Negra?", + "confirmClearText": "Esto eliminará los {count} artistas de tu lista negra. Esta acción no se puede deshacer." + }, "offlineLibrary": { "title": "Biblioteca Offline", "disclaimer": "La biblioteca offline es un caché temporal de reproducción para escucha sin conexión mientras tengas una suscripción válida de Qobuz. No recibes una licencia para conservar o redistribuir el contenido. Si tu suscripción deja de ser válida, QBZ eliminará todo el contenido en caché después de 3 días.", From 0c5c0ce1a8e75138cbc5a424ef5b68d1f00a16c7 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:38:53 -0600 Subject: [PATCH 7/7] i18n: add LocalLibraryView translations for empty states and UI --- src/lib/i18n/locales/en.json | 24 +++++++++++++++++++++++- src/lib/i18n/locales/es.json | 24 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index e963c5ef3..e619bed84 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -124,7 +124,29 @@ "networkFs": { "auto": "Auto-detect", "other": "Other" - } + }, + "backToLibrary": "Back to Library", + "yourCollection": "Your local music collection", + "playlistOfflineMode": "Playlist management is disabled in offline mode. You can still play your local tracks.", + "artworkOffline": "Artwork fetching unavailable offline", + "loadingLibrary": "Loading library...", + "failedLoadLibrary": "Failed to load library", + "backToActiveAlbums": "Back to Active Albums", + "noHiddenAlbums": "No hidden albums", + "noAlbumsInLibrary": "No albums in library", + "addFoldersHint": "Add folders and scan to build your library", + "clearAllFilters": "Clear all", + "offlineCache": "Offline cache", + "noAlbumsMatch": "No albums match your search", + "tryDifferentSearch": "Try a different artist or album name", + "noArtistsInLibrary": "No artists in library", + "noArtistsMatch": "No artists match your search", + "noAlbumsFound": "No albums found", + "selectArtist": "Select an artist to view their albums", + "noTracksInLibrary": "No tracks in library", + "editAlbumInfo": "Edit album info", + "hideAlbum": "Hide this album from library", + "hideAlbumHint": "Hidden albums can be viewed from Settings" }, "album": { "tracks": "tracks", diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index 38ed29006..73526cc07 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -124,7 +124,29 @@ "networkFs": { "auto": "Auto-detectar", "other": "Otro" - } + }, + "backToLibrary": "Volver a la Biblioteca", + "yourCollection": "Tu colección de música local", + "playlistOfflineMode": "La gestión de playlists está deshabilitada en modo offline. Aún puedes reproducir tus pistas locales.", + "artworkOffline": "Obtención de carátulas no disponible offline", + "loadingLibrary": "Cargando biblioteca...", + "failedLoadLibrary": "Error al cargar la biblioteca", + "backToActiveAlbums": "Volver a Álbumes Activos", + "noHiddenAlbums": "No hay álbumes ocultos", + "noAlbumsInLibrary": "No hay álbumes en la biblioteca", + "addFoldersHint": "Agrega carpetas y escanea para construir tu biblioteca", + "clearAllFilters": "Limpiar todo", + "offlineCache": "Caché offline", + "noAlbumsMatch": "Ningún álbum coincide con tu búsqueda", + "tryDifferentSearch": "Prueba con un artista o álbum diferente", + "noArtistsInLibrary": "No hay artistas en la biblioteca", + "noArtistsMatch": "Ningún artista coincide con tu búsqueda", + "noAlbumsFound": "No se encontraron álbumes", + "selectArtist": "Selecciona un artista para ver sus álbumes", + "noTracksInLibrary": "No hay pistas en la biblioteca", + "editAlbumInfo": "Editar información del álbum", + "hideAlbum": "Ocultar este álbum de la biblioteca", + "hideAlbumHint": "Los álbumes ocultos se pueden ver desde Configuración" }, "album": { "tracks": "pistas",