ceee9b9d12
Three tiers of offline support, all scoped to the active backend's localStorage namespace (mirroring the auth slice): Tier 1 — persist client state. queue + player slices are saved (queue entries/index/source; player track/position/volume/repeat/shuffle) and rehydrated on load, so a reload with no backend restores where the user left off. Playback never auto-resumes (browsers block autoplay). Retires the DEMO_QUEUE and isQueueOpen:true stubs. Tier 2 — persist the RTK Query cache. Last-seen library/albums/artists are snapshotted (fulfilled queries only) and replayed via RTKQ's extractRehydrationInfo at startup, so the library renders read-only when the backend is down. ConnectionStatus tooltip flags cached data offline. No server data is copied into a slice — the cache feeds itself back. Tier 3 — service worker audio + cover cache (PWA). Audio streams are cached keyed by content id (token stripped), range-aware (synthetic 206 slicing), with a 500MB LRU cap, so already-played tracks play fully offline. Cover art uses stale-while-revalidate in its own bounded cache. Module worker (ESM); pure helpers split into sw-core.js and unit-tested. Web app manifest enables "Install app". Player source badge now reflects real cached state. tsc clean, lint clean, 19 new tests pass, production build verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
213 lines
5.2 KiB
TypeScript
213 lines
5.2 KiB
TypeScript
const en = {
|
|
nav: {
|
|
library: 'Library',
|
|
search: 'Search & download',
|
|
downloads: 'Downloads',
|
|
upload: 'Upload',
|
|
storage: 'Storage',
|
|
playlists: 'Playlists',
|
|
newPlaylist: 'New playlist',
|
|
admin: 'Admin',
|
|
settings: 'Settings',
|
|
administration: 'Administration',
|
|
},
|
|
conn: {
|
|
connected: 'Connected',
|
|
connecting: 'Connecting…',
|
|
disconnected: 'Offline',
|
|
error: 'Unreachable',
|
|
manage: 'Connection — manage instances',
|
|
cached: 'Showing last-seen data',
|
|
},
|
|
user: {
|
|
online: 'online',
|
|
offline: 'offline',
|
|
signOut: 'Sign out',
|
|
},
|
|
connect: {
|
|
savedInstances: 'Saved instances',
|
|
active: 'active',
|
|
use: 'Use',
|
|
forgetTitle: 'Forget this instance',
|
|
form: {
|
|
title: 'Connect to a backend',
|
|
serverUrl: 'Server URL',
|
|
username: 'Username',
|
|
password: 'Password',
|
|
submit: 'Connect',
|
|
stubNote:
|
|
'Stub mode — backend not wired. Connect signs in with a fake admin session, scoped to this instance.',
|
|
},
|
|
},
|
|
library: {
|
|
title: 'Library',
|
|
searchPlaceholder: 'Search library…',
|
|
tabs: {
|
|
tracks: 'Tracks',
|
|
albums: 'Albums',
|
|
artists: 'Artists',
|
|
},
|
|
playAll: '▶ Play all ({{count}})',
|
|
empty: {
|
|
tracks: {
|
|
title: 'No tracks',
|
|
description: 'Your library is empty. Start by downloading some music.',
|
|
},
|
|
albums: {
|
|
title: 'No albums',
|
|
description: 'No albums in library.',
|
|
},
|
|
artists: {
|
|
title: 'No artists',
|
|
description: 'No artists in library.',
|
|
},
|
|
},
|
|
albumCard: {
|
|
tracks: '{{count}} tracks',
|
|
tracksDuration: '{{count}} tracks · {{duration}}',
|
|
},
|
|
artistRow: {
|
|
meta: '{{albumCount}} albums · {{trackCount}} tracks',
|
|
},
|
|
},
|
|
album: {
|
|
type: 'Album',
|
|
play: '▶ Play',
|
|
error: 'Failed to load album',
|
|
tracksError: 'Failed to load tracks',
|
|
empty: {
|
|
title: 'No tracks',
|
|
description: 'This album has no tracks.',
|
|
},
|
|
},
|
|
playlist: {
|
|
type: 'Playlist',
|
|
play: '▶ Play',
|
|
error: 'Failed to load playlist',
|
|
tracksError: 'Failed to load tracks',
|
|
empty: {
|
|
title: 'Empty playlist',
|
|
description: 'This playlist has no tracks yet.',
|
|
},
|
|
},
|
|
player: {
|
|
nothingPlaying: 'Nothing playing',
|
|
shuffle: 'Shuffle',
|
|
previous: 'Previous',
|
|
next: 'Next',
|
|
pause: 'Pause',
|
|
play: 'Play',
|
|
repeat: 'Repeat: {{mode}}',
|
|
streaming: 'Streaming · 320 kbps',
|
|
local: 'Local · FLAC',
|
|
queue: 'Play queue',
|
|
mute: 'Mute',
|
|
unmute: 'Unmute',
|
|
},
|
|
queue: {
|
|
title: 'Play queue',
|
|
clear: 'Clear queue',
|
|
close: 'Close',
|
|
from: 'From {{source}}',
|
|
radio: 'Radio · {{source}}',
|
|
nowPlaying: 'Now playing',
|
|
nextUp: 'Next up',
|
|
nothingNext: 'Nothing queued next',
|
|
empty: 'Queue is empty',
|
|
radioActive: 'Radio active',
|
|
mixing: '∞ mixing',
|
|
familiar: 'Familiar',
|
|
new: 'New',
|
|
loadingMore: 'Loading more from radio…',
|
|
doubleClickPlay: 'Double-click to play',
|
|
removeFromQueue: 'Remove from queue',
|
|
},
|
|
track: {
|
|
menu: {
|
|
options: 'Track options',
|
|
playNow: 'Play now',
|
|
playNext: 'Play next',
|
|
addToQueue: 'Add to queue',
|
|
addToPlaylist: 'Add to playlist…',
|
|
editMetadata: 'Edit metadata',
|
|
download: 'Download',
|
|
delete: 'Delete',
|
|
},
|
|
},
|
|
common: {
|
|
error: 'Something went wrong',
|
|
retry: 'Retry',
|
|
comingSoon: 'Coming soon',
|
|
back: 'Back',
|
|
},
|
|
pages: {
|
|
admin: 'Admin',
|
|
settings: 'Settings',
|
|
downloads: 'Downloads',
|
|
search: 'Search & Download',
|
|
storage: 'Storage',
|
|
login: 'Sign in',
|
|
artist: 'Artist',
|
|
playlists: 'Playlists',
|
|
upload: 'Upload files',
|
|
metadata: 'Edit metadata',
|
|
metadataBatch: 'Edit metadata (batch)',
|
|
storageMaintenance: 'Storage maintenance',
|
|
queue: 'Play queue',
|
|
},
|
|
settings: {
|
|
language: 'Language',
|
|
theme: 'Theme',
|
|
themeDark: 'Dark',
|
|
themeLight: 'Light',
|
|
tabs: {
|
|
profile: 'Profile',
|
|
playback: 'Playback',
|
|
scrobbling: 'Scrobbling',
|
|
instance: 'Instance',
|
|
},
|
|
},
|
|
admin: {
|
|
userDetail: 'User',
|
|
tabs: {
|
|
users: 'Users',
|
|
sources: 'Sources',
|
|
instance: 'Instance',
|
|
},
|
|
},
|
|
notFound: {
|
|
title: 'Page not found',
|
|
description: "This screen doesn't exist yet.",
|
|
backToLibrary: 'Back to library',
|
|
},
|
|
upload: {
|
|
title: 'Upload files',
|
|
dropzone: {
|
|
title: 'Drag & drop audio files here',
|
|
hint: 'or click to choose files — one or many at a time',
|
|
button: 'Choose files',
|
|
},
|
|
queueTitle: 'Uploads ({{completed}}/{{total}})',
|
|
clearCompleted: 'Clear completed',
|
|
retry: 'Retry',
|
|
editMetadata: 'Edit metadata',
|
|
metadataPending:
|
|
'Uploaded tracks land as “Unknown Artist” with metadata pending — enrich them afterwards.',
|
|
unknownArtist: 'Unknown Artist · metadata pending',
|
|
status: {
|
|
queued: 'Queued',
|
|
uploading: 'Uploading',
|
|
done: 'Uploaded',
|
|
duplicate: 'Already in library',
|
|
error: 'Failed',
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
export default en;
|
|
|
|
type DeepString<T> = {
|
|
[K in keyof T]: T[K] extends Record<string, unknown> ? DeepString<T[K]> : string;
|
|
};
|
|
export type Translations = DeepString<typeof en>;
|