Add i18n support with English and Spanish locales

- Add svelte-i18n dependency
- Create i18n configuration with locale persistence
- Add complete translation files for EN and ES
- Create root layout with theme CSS variables
- Support 4 themes: dark (default), light, oled, warm
- Add global styles and spacing system
This commit is contained in:
vicrodh committed 2026-01-08 23:39:30 -06:00
1 parent 8a094ef467
commit ef92897d43
6 files changed
+3160 -1

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
import { browser } from '$app/environment';
import { init, register, getLocaleFromNavigator, locale } from 'svelte-i18n';
// Register locales
register('en', () => import('./locales/en.json'));
register('es', () => import('./locales/es.json'));
// Initialize i18n
export function initI18n() {
init({
fallbackLocale: 'en',
initialLocale: browser ? getStoredLocale() || getLocaleFromNavigator() : 'en',
});
}
// Get stored locale from localStorage
function getStoredLocale(): string | null {
if (!browser) return null;
return localStorage.getItem('qbz-locale');
}
// Set and persist locale
export function setLocale(newLocale: string) {
if (browser) {
localStorage.setItem('qbz-locale', newLocale);
}
locale.set(newLocale);
}
// Available locales
export const locales = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
] as const;
export type LocaleCode = (typeof locales)[number]['code'];
// Re-export svelte-i18n utilities for convenience
export { t, locale, locales as availableLocales } from 'svelte-i18n';
+182
View File
@@ -0,0 +1,182 @@
{
"app": {
"name": "QBZ-NIX",
"tagline": "Hi-Fi Qobuz Client for Linux"
},
"nav": {
"home": "Home",
"search": "Search",
"library": "Library",
"albums": "Albums",
"artists": "Artists",
"tracks": "Tracks",
"playlists": "Playlists",
"newReleases": "New Releases",
"radio": "Radio",
"recentlyAdded": "Recently Added"
},
"home": {
"title": "Home",
"featuredForYou": "Featured for you",
"recentlyPlayed": "Recently Played",
"newReleases": "New Releases",
"madeForYou": "Made for You",
"topPicks": "Top Picks"
},
"search": {
"title": "Search",
"placeholder": "Search albums, artists, tracks...",
"all": "All",
"noResults": "No results found",
"searching": "Searching..."
},
"album": {
"tracks": "tracks",
"track": "track",
"by": "by",
"released": "Released",
"label": "Label",
"genre": "Genre"
},
"artist": {
"albums": "Albums",
"popularTracks": "Popular Tracks",
"similarArtists": "Similar Artists",
"followers": "followers"
},
"playlist": {
"tracks": "tracks",
"createNew": "Create New...",
"allPlaylists": "All Playlists",
"myPlaylists": "My Playlists"
},
"player": {
"nowPlaying": "Now Playing",
"queue": "Queue",
"lyrics": "Lyrics",
"shuffle": "Shuffle",
"repeat": "Repeat",
"repeatOne": "Repeat One",
"previous": "Previous",
"next": "Next",
"play": "Play",
"pause": "Pause",
"volume": "Volume",
"mute": "Mute"
},
"quality": {
"mp3": "MP3",
"cdQuality": "CD Quality",
"hiRes": "Hi-Res",
"ultraHiRes": "Ultra Hi-Res",
"flac": "FLAC",
"bitDepth": "{depth}-bit",
"sampleRate": "{rate} kHz",
"lossless": "Lossless"
},
"actions": {
"play": "Play",
"playAll": "Play All",
"shuffle": "Shuffle",
"addToQueue": "Add to Queue",
"addToPlaylist": "Add to Playlist",
"addToFavorites": "Add to Favorites",
"removeFromFavorites": "Remove from Favorites",
"download": "Download",
"share": "Share",
"viewAlbum": "View Album",
"viewArtist": "View Artist",
"more": "More",
"back": "Back",
"seeAll": "See All",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit"
},
"settings": {
"title": "Settings",
"account": {
"title": "Account",
"loggedInAs": "Logged in as",
"subscription": "Subscription",
"logout": "Logout",
"login": "Login"
},
"audio": {
"title": "Audio",
"streamingQuality": "Streaming Quality",
"preferHighest": "Prefer highest available",
"outputDevice": "Output 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"
},
"playback": {
"title": "Playback",
"gapless": "Gapless Playback",
"crossfade": "Crossfade",
"crossfadeSeconds": "{seconds}s",
"normalizeVolume": "Normalize Volume"
},
"appearance": {
"title": "Appearance",
"theme": "Theme",
"themes": {
"dark": "Dark",
"light": "Light",
"oled": "OLED Black",
"warm": "Warm"
},
"accentColor": "Accent Color",
"compactMode": "Compact Mode",
"language": "Language"
},
"integrations": {
"title": "Integrations",
"lastfm": "Last.fm",
"connected": "Connected",
"notConnected": "Not connected",
"connect": "Connect",
"disconnect": "Disconnect",
"scrobbling": "Scrobbling"
},
"storage": {
"title": "Storage",
"cacheSize": "Cache Size",
"clearCache": "Clear Cache"
}
},
"auth": {
"login": "Login",
"email": "Email",
"password": "Password",
"loginButton": "Sign In",
"loggingIn": "Signing in...",
"loginError": "Login failed. Please check your credentials.",
"welcomeBack": "Welcome back"
},
"errors": {
"generic": "Something went wrong",
"networkError": "Network error. Please check your connection.",
"notFound": "Not found",
"unauthorized": "Please log in to continue",
"trackNotAvailable": "This track is not available for streaming",
"qualityNotAvailable": "Requested quality is not available"
},
"time": {
"hours": "h",
"minutes": "min",
"seconds": "s"
},
"empty": {
"noAlbums": "No albums yet",
"noArtists": "No artists yet",
"noTracks": "No tracks yet",
"noPlaylists": "No playlists yet",
"noFavorites": "No favorites yet",
"queueEmpty": "Queue is empty"
}
}
+181
View File
@@ -0,0 +1,181 @@
{
"app": {
"name": "QBZ-NIX",
"tagline": "Cliente Hi-Fi de Qobuz para Linux"
},
"nav": {
"home": "Inicio",
"search": "Buscar",
"library": "Biblioteca",
"albums": "Álbumes",
"artists": "Artistas",
"tracks": "Canciones",
"playlists": "Playlists",
"newReleases": "Novedades",
"radio": "Radio",
"recentlyAdded": "Añadido recientemente"
},
"home": {
"title": "Inicio",
"featuredForYou": "Sugerencias destacadas para ti",
"recentlyPlayed": "Escuchado recientemente",
"newReleases": "Nuevos lanzamientos",
"madeForYou": "Hecho para ti",
"topPicks": "Selecciones destacadas"
},
"search": {
"title": "Buscar",
"placeholder": "Buscar álbumes, artistas, canciones...",
"all": "Todo",
"noResults": "No se encontraron resultados",
"searching": "Buscando..."
},
"album": {
"tracks": "canciones",
"track": "canción",
"by": "de",
"released": "Publicado",
"label": "Sello",
"genre": "Género"
},
"artist": {
"albums": "Álbumes",
"popularTracks": "Canciones populares",
"similarArtists": "Artistas similares",
"followers": "seguidores"
},
"playlist": {
"tracks": "canciones",
"createNew": "Crear nueva...",
"allPlaylists": "Todas las playlists",
"myPlaylists": "Mis playlists"
},
"player": {
"nowPlaying": "Reproduciendo ahora",
"queue": "Cola",
"lyrics": "Letra",
"shuffle": "Aleatorio",
"repeat": "Repetir",
"repeatOne": "Repetir una",
"previous": "Anterior",
"next": "Siguiente",
"play": "Reproducir",
"pause": "Pausar",
"volume": "Volumen",
"mute": "Silenciar"
},
"quality": {
"mp3": "MP3",
"cdQuality": "Calidad CD",
"hiRes": "Hi-Res",
"ultraHiRes": "Ultra Hi-Res",
"flac": "FLAC",
"bitDepth": "{depth}-bit",
"sampleRate": "{rate} kHz",
"lossless": "Sin pérdida"
},
"actions": {
"play": "Reproducir",
"playAll": "Reproducir todo",
"shuffle": "Aleatorio",
"addToQueue": "Añadir a la cola",
"addToPlaylist": "Añadir a playlist",
"addToFavorites": "Añadir a favoritos",
"removeFromFavorites": "Quitar de favoritos",
"download": "Descargar",
"share": "Compartir",
"viewAlbum": "Ver álbum",
"viewArtist": "Ver artista",
"more": "Más",
"back": "Atrás",
"seeAll": "Ver todo",
"cancel": "Cancelar",
"save": "Guardar",
"delete": "Eliminar",
"edit": "Editar"
},
"settings": {
"title": "Configuración",
"account": {
"title": "Cuenta",
"loggedInAs": "Conectado como",
"subscription": "Suscripción",
"logout": "Cerrar sesión",
"login": "Iniciar sesión"
},
"audio": {
"title": "Audio",
"streamingQuality": "Calidad de streaming",
"preferHighest": "Preferir la más alta disponible",
"outputDevice": "Dispositivo de salida",
"exclusiveMode": "Modo exclusivo",
"exclusiveModeDesc": "Bloquear dispositivo de audio para uso exclusivo",
"dacPassthrough": "Passthrough DAC",
"dacPassthroughDesc": "Bypass del mixer del sistema para salida bit-perfect"
},
"playback": {
"title": "Reproducción",
"gapless": "Reproducción sin pausas",
"crossfade": "Crossfade",
"crossfadeSeconds": "{seconds}s",
"normalizeVolume": "Normalizar volumen"
},
"appearance": {
"title": "Apariencia",
"theme": "Tema",
"themes": {
"dark": "Oscuro",
"light": "Claro",
"oled": "OLED Negro",
"warm": "Cálido"
},
"accentColor": "Color de acento",
"compactMode": "Modo compacto",
"language": "Idioma"
},
"integrations": {
"title": "Integraciones",
"lastfm": "Last.fm",
"connected": "Conectado",
"notConnected": "No conectado",
"connect": "Conectar",
"disconnect": "Desconectar",
"scrobbling": "Scrobbling"
},
"storage": {
"title": "Almacenamiento",
"cacheSize": "Tamaño de caché",
"clearCache": "Limpiar caché"
}
},
"auth": {
"login": "Iniciar sesión",
"email": "Correo electrónico",
"password": "Contraseña",
"loginButton": "Entrar",
"loggingIn": "Iniciando sesión...",
"loginError": "Error al iniciar sesión. Verifica tus credenciales.",
"welcomeBack": "Bienvenido de nuevo"
},
"errors": {
"generic": "Algo salió mal",
"networkError": "Error de red. Verifica tu conexión.",
"notFound": "No encontrado",
"unauthorized": "Inicia sesión para continuar",
"trackNotAvailable": "Esta canción no está disponible para streaming",
"qualityNotAvailable": "La calidad solicitada no está disponible"
},
"time": {
"hours": "h",
"minutes": "min",
"seconds": "s"
},
"empty": {
"noAlbums": "Sin álbumes aún",
"noArtists": "Sin artistas aún",
"noTracks": "Sin canciones aún",
"noPlaylists": "Sin playlists aún",
"noFavorites": "Sin favoritos aún",
"queueEmpty": "La cola está vacía"
}
}
+208
View File
@@ -0,0 +1,208 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { initI18n } from '$lib/i18n';
import { isLoading } from 'svelte-i18n';
// Props
let { children }: { children: Snippet } = $props();
// Initialize i18n
initI18n();
// Wait for translations to load
let ready = $derived(!$isLoading);
</script>
{#if ready}
{@render children()}
{:else}
<div class="loading">
<div class="spinner"></div>
</div>
{/if}
<style>
:global(*) {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:global(html, body) {
height: 100%;
width: 100%;
overflow: hidden;
}
:global(body) {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background-color: var(--bg-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* CSS Variables - Dark Theme (Default) */
:global(:root) {
/* Backgrounds */
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-tertiary: #2a2a2a;
--bg-hover: #1f1f1f;
/* Text */
--text-primary: #ffffff;
--text-secondary: #cccccc;
--text-muted: #888888;
--text-disabled: #555555;
/* Accent */
--accent-primary: #4285F4;
--accent-hover: #5a9bf4;
--accent-active: #3275e4;
/* Borders */
--border-subtle: #2a2a2a;
--border-strong: #3a3a3a;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.4);
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 24px;
--spacing-2xl: 32px;
--spacing-3xl: 48px;
/* Layout */
--sidebar-width: 240px;
--now-playing-height: 80px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* Transitions */
--transition-fast: 150ms ease-out;
--transition-normal: 200ms ease-out;
--transition-slow: 300ms ease-out;
}
/* Light Theme */
:global([data-theme="light"]) {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #e8e8e8;
--bg-hover: #f0f0f0;
--text-primary: #0f0f0f;
--text-secondary: #444444;
--text-muted: #666666;
--text-disabled: #999999;
--border-subtle: #e0e0e0;
--border-strong: #cccccc;
}
/* OLED Black Theme */
:global([data-theme="oled"]) {
--bg-primary: #000000;
--bg-secondary: #0a0a0a;
--bg-tertiary: #1a1a1a;
--bg-hover: #111111;
}
/* Warm Theme */
:global([data-theme="warm"]) {
--bg-primary: #1a1814;
--bg-secondary: #242018;
--bg-tertiary: #2e2820;
--bg-hover: #282218;
--accent-primary: #d4a574;
--accent-hover: #e0b888;
--accent-active: #c49460;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background-color: var(--bg-primary);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--bg-tertiary);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Scrollbar styling */
:global(::-webkit-scrollbar) {
width: 8px;
height: 8px;
}
:global(::-webkit-scrollbar-track) {
background: transparent;
}
:global(::-webkit-scrollbar-thumb) {
background: var(--bg-tertiary);
border-radius: var(--radius-full);
}
:global(::-webkit-scrollbar-thumb:hover) {
background: var(--text-muted);
}
/* Selection */
:global(::selection) {
background-color: var(--accent-primary);
color: var(--text-primary);
}
/* Focus visible */
:global(:focus-visible) {
outline: 2px solid var(--accent-primary);
outline-offset: 2px;
}
/* Button reset */
:global(button) {
background: none;
border: none;
cursor: pointer;
font: inherit;
color: inherit;
}
/* Link reset */
:global(a) {
color: inherit;
text-decoration: none;
}
/* Input reset */
:global(input, textarea, select) {
font: inherit;
color: inherit;
}
</style>